define(["dojo/_base/lang", "dojo/_base/array", "dojo/Deferred", "dojo/when", "dojo/promise/all", "dojo/on", "dojo/sniff"], function(lang, array, Deferred, when, all, on, has){ // module: // dojox/css3/transition //create cross platform animation/transition effects //TODO enable opera mobile when it is hardware accelerated // IE10 is using standard names so this is working without any modifications var transitionEndEventName = "transitionend"; var transitionPrefix = "t"; //by default use "t" prefix and "ransition" to make word "transition" var translateMethodStart = "translate3d(";//Android 2.x does not support translateX in CSS Transition, we need to use translate3d in webkit browsers var translateMethodEnd = ",0,0)"; if(has("webkit")){ transitionPrefix = "WebkitT"; transitionEndEventName = "webkitTransitionEnd"; }else if(has("mozilla")){ transitionPrefix = "MozT"; translateMethodStart = "translateX("; translateMethodEnd = ")"; } //TODO find a way to lock the animation and prevent animation conflict //Use the simple object inheritance var transition = function(/*Object?*/args){ // summary: // This module defines the transition utilities which can be used // to perform transition effects based on the CSS Transition standard. // args: // The arguments which will be mixed into this transition object. //default config should be in animation object itself instead of its prototype //otherwise, it might be easy for making mistake of modifying prototype var defaultConfig = { startState: {}, endState: {}, node: null, duration: 250, "in": true, direction: 1, autoClear: true }; lang.mixin(this, defaultConfig); lang.mixin(this, args); //create the deferred object which will resolve after the animation is finished. //We can rely on "onAfterEnd" function to notify the end of a single animation, //but using a deferred object is easier to wait for multiple animations end. if(!this.deferred){ this.deferred = new Deferred(); } }; lang.extend(transition, { play: function(){ // summary: // Plays the transition effect defined by this transition object. transition.groupedPlay([this]); }, //method to apply the state of the transition _applyState: function(state){ var style = this.node.style; for(var property in state){ if(state.hasOwnProperty(property)){ style[property] = state[property]; } } }, initState: function(){ // summary: // Method to initialize the state for a transition. //apply the immediate style change for initial state. this.node.style[transitionPrefix + "ransitionProperty"] = "none"; this.node.style[transitionPrefix + "ransitionDuration"] = "0ms"; this._applyState(this.startState); }, _beforeStart: function(){ if (this.node.style.display === "none"){ this.node.style.display = ""; } this.beforeStart(); }, _beforeClear: function(){ this.node.style[transitionPrefix + "ransitionProperty"] = ""; this.node.style[transitionPrefix + "ransitionDuration"] = ""; if(this["in"] !== true){ this.node.style.display = "none"; } this.beforeClear(); }, _onAfterEnd: function(){ this.deferred.resolve(this.node); if(this.node.id && transition.playing[this.node.id]===this.deferred){ delete transition.playing[this.node.id]; } this.onAfterEnd(); }, beforeStart: function(){ // summary: // The callback which will be called right before the start // of the transition effect. }, beforeClear: function(){ // summary: // The callback which will be called right after the end // of the transition effect and before the final state is // cleared. }, onAfterEnd: function(){ // summary: // The callback which will be called right after the end // of the transition effect and after the final state is // cleared. }, start: function(){ // summary: // Method to start the transition. this._beforeStart(); this._startTime = new Date().getTime(); // set transition start timestamp this._cleared = false; // set clear flag to false var self = this; //change the transition duration self.node.style[transitionPrefix + "ransitionProperty"] = "all"; self.node.style[transitionPrefix + "ransitionDuration"] = self.duration + "ms"; //connect to clear the transition state after the transition end. //Since the transition is conducted asynchronously, we need to //connect to transition end event to clear the state on.once(self.node, transitionEndEventName, function(){ self.clear(); }); this._applyState(this.endState); }, clear: function(){ // summary: // Method to clear the state after a transition. if(this._cleared) { return; } this._cleared = true; // set clear flag to true this._beforeClear(); this._removeState(this.endState); // console.log(this.node.id + " clear."); this._onAfterEnd(); }, //create removeState method _removeState: function(state){ var style = this.node.style; for(var property in state){ if(state.hasOwnProperty(property)){ style[property] = ""; } } } }); //TODO add the lock mechanism for all of the transition effects // consider using only one object for one type of transition. transition.slide = function(node, config){ // summary: // Method which is used to create the transition object of a slide effect. // node: // The node that the slide transition effect will be applied on. // config: // The cofig arguments which will be mixed into this transition object. //create the return object and set the startState, endState of the return var ret = new transition(config); ret.node = node; var startX = "0"; var endX = "0"; if(ret["in"]){ if(ret.direction === 1){ startX = "100%"; }else{ startX = "-100%"; } }else{ if(ret.direction === 1){ endX = "-100%"; }else{ endX = "100%"; } } ret.startState[transitionPrefix + "ransform"]=translateMethodStart+startX+translateMethodEnd; ret.endState[transitionPrefix + "ransform"]=translateMethodStart+endX+translateMethodEnd; return ret; }; transition.fade = function(node, config){ // summary: // Method which is used to create the transition object of fade effect. // node: // The node that the fade transition effect will be applied on. // config: // The cofig arguments which will be mixed into this transition object. var ret = new transition(config); ret.node = node; var startOpacity = "0"; var endOpacity = "0"; if(ret["in"]){ endOpacity = "1"; }else{ startOpacity = "1"; } lang.mixin(ret, { startState:{ "opacity": startOpacity }, endState:{ "opacity": endOpacity } }); return ret; }; transition.flip = function(node, config){ // summary: // Method which is used to create the transition object of flip effect. // node: // The node that the flip transition effect will be applied on. // config: // The cofig arguments which will be mixed into this transition object. var ret = new transition(config); ret.node = node; if(ret["in"]){ //Need to set opacity here because Android 2.2 has bug that //scale(...) in transform does not persist status lang.mixin(ret,{ startState:{ "opacity": "0" }, endState:{ "opacity": "1" } }); ret.startState[transitionPrefix + "ransform"]="scale(0,0.8) skew(0,-30deg)"; ret.endState[transitionPrefix + "ransform"]="scale(1,1) skew(0,0)"; }else{ lang.mixin(ret,{ startState:{ "opacity": "1" }, endState:{ "opacity": "0" } }); ret.startState[transitionPrefix + "ransform"]="scale(1,1) skew(0,0)"; ret.endState[transitionPrefix + "ransform"]="scale(0,0.8) skew(0,30deg)"; } return ret; }; var getWaitingList = function(/*Array*/ nodes){ var defs = []; array.forEach(nodes, function(node){ //check whether the node is under other animation if(node.id && transition.playing[node.id]){ //hook on deferred object in transition.playing defs.push(transition.playing[node.id]); } }); return all(defs); }; transition.getWaitingList = getWaitingList; transition.groupedPlay = function(/*Array*/args){ // summary: // The method which groups multiple transitions and plays // them together. // args: // The array of transition objects which will be played together. var animNodes = array.filter(args, function(item){ return item.node; }); var waitingList = getWaitingList(animNodes); //update registry with deferred objects in animations of args. array.forEach(args, function(item){ if(item.node.id){ transition.playing[item.node.id] = item.deferred; } }); //wait for all deferred object in deferred list to resolve when(waitingList, function(){ array.forEach(args, function(item){ //set the start state item.initState(); }); //Assume the fps of the animation should be higher than 30 fps and //allow the browser to use one frame's time to redraw so that //the transition can be started setTimeout(function(){ array.forEach(args, function(item){ item.start(); }); // check and clear node if the node not cleared. // 1. on Android2.2/2.3, the "fade out" transitionEnd event will be lost if the soft keyboard popup, so we need to check nodes' clear status. // 2. The "fade in" transitionEnd event will before or after "fade out" transitionEnd event and it always occurs. // We can check fade out node status in the last "fade in" node transitionEnd event callback, if node transition timeout, we clear it. // NOTE: the last "fade in" transitionEnd event will always fired, so we bind on this event and check other nodes. on.once(args[args.length-1].node, transitionEndEventName, function(){ var timeout; for(var i=0; i= args[i].duration){ args[i].clear(); } } } }); setTimeout(function(){ var timeout; for(var i=0; i= args[i].duration){ args[i].clear(); } } } }, args[0].duration+50); }, 33); }); }; transition.chainedPlay = function(/*Array*/args){ // summary: // The method which plays multiple transitions one by one. // args: // The array of transition objects which will be played in a chain. var animNodes = array.filter(args, function(item){ return item.node; }); var waitingList = getWaitingList(animNodes); //update registry with deferred objects in animations of args. array.forEach(args, function(item){ if(item.node.id){ transition.playing[item.node.id] = item.deferred; } }); when(waitingList, function(){ array.forEach(args, function(item){ //set the start state item.initState(); }); //chain animations together for (var i=1, len=args.length; i < len; i++){ args[i-1].deferred.then(lang.hitch(args[i], function(){ this.start(); })); } //Assume the fps of the animation should be higher than 30 fps and //allow the browser to use one frame's time to redraw so that //the transition can be started setTimeout(function(){ args[0].start(); }, 33); }); }; //TODO complete the registry mechanism for animation handling and prevent animation conflicts transition.playing = {}; return transition; });