1 /*
  2  *  Copyright © 2009 Apple Inc. All rights reserved.
  3  */
  4 
  5 /**
  6  *  @class
  7  *  @name TKTransitionDelegate
  8  *  @since TuneKit 1.0
  9  */
 10 
 11 /**
 12  *  Invoked when a transition has completed.
 13  *
 14  *  @name transitionDidComplete
 15  *  @function
 16  *
 17  *  @param {TKTransition} theTransition The transition that just completed.
 18  *  @memberOf TKTransitionDelegate.prototype
 19  */
 20 
 21 const TKTransitionDidCompleteDelegate = 'transitionDidComplete';
 22 
 23 const TKTransitionDefaults = {
 24   duration  : 0.5,
 25   delay     : 0,
 26   removesTargetUponCompletion : false,
 27   revertsToOriginalValues : false
 28 };
 29 
 30 const TKTransitionStyles = ['-webkit-transition-property', '-webkit-transition-duration', '-webkit-transition-timing-function', '-webkit-transition-delay', '-webkit-transition'];
 31 
 32 /* ==================== TKTransition ==================== */
 33 
 34 /**
 35  *  @class
 36  *
 37  *  <p>The TKTransition class allows to create a synchronized change of one or more property values on a
 38  *  {@link Element} instance or any DOM element.</p>
 39  *
 40  *  <p>First, the user sets the {@link #target} for the transition,
 41  *  identifying the target element, and second the set of {@link #properties}, in the form of an <code>Array</code>
 42  *  of CSS properties for the element. Then the {@link #to} and optional
 43  *  {@link #from} value arrays are provided to define what values will be used for the transition. Each item in the
 44  *  {@link #from} and {@link #to} arrays matches the item at the same index in the {@link #properties} array, and the same
 45  *  applies to the {@link #duration} property, which can also be a single value shared for all transitioned properties.</p>
 46  *
 47  *  <p>Finally, once the properties on the transition are all set, the {@link #start} method must be called so that the
 48  *  transition is started. Note that it is possible to group transitions within a {@link TKTransaction} and set a default
 49  *  set of transition properties within a transaction using the {@link TKTransaction.defaults} static property.</p>
 50  *  
 51  *  <p>The following example shows how to make a transition that fades an element in as it animates from the right side of the screen:</p>
 52  *
 53 <pre class="example">
 54 new TKTransition({
 55   target : anElement,
 56   properties : ['opacity', '-webkit-transform'];
 57   from : [0, 'translate(320px, 0)'],
 58   to : [1, 'translate(0, 0)']
 59   duration : 0.5;
 60 }).start();
 61 </pre>
 62  *
 63  *  <p>Note that properties of a {@link TKTransition} object can be passed as a batch to the {@link TKTransition} constructor
 64  *  as an anonymous object. This also allows reuse of a set of parameters across several transitions. Also, a set of pre-baked
 65  *  transitions exist can be easily applied to an element with the {@link Element#applyTransition} method.</p>
 66  *
 67  *  @since TuneKit 1.0
 68  */
 69 function TKTransition (params) {
 70   // set up defaults
 71   /**
 72    *  The transition target.
 73    *  @type Element 
 74    */
 75   this.target = null;
 76   /**
 77    *  The set of properties that will be transitioned. The properties are CSS properties of the targeted <code>Element</code>.
 78    *  @type Array 
 79    */
 80   this.properties = null;
 81   /**
 82    *  The set of durations in seconds, each duration matching a property in the {@link #properties} array.
 83    *  Note that it's possible to specify a single value instead of an array to use the same duration for 
 84    *  all properties.
 85    *  @type Object 
 86    */
 87   this.duration = null;
 88   /**
 89    *  The set of delays in seconds, each delay matching a property in the {@link #properties} array.
 90    *  Note that it's possible to specify a single delay instead of an array to use the same delay for 
 91    *  all properties.
 92    *  @type Object 
 93    */
 94   this.delay = null;
 95   /**
 96    *  Optional list of values to start the transition from. Each value in this array must match the property
 97    *  at the same index in the {@link #properties} array.
 98    *  @type Array
 99    */
100   this.from = null;
101   /**
102    *  Required list of values to transition to. Each value in this array must match the property
103    *  at the same index in the {@link #properties} array.
104    *  @type Array
105    */
106   this.to = null;
107   /**
108    *  The set of timing functions, each timing function matching a property in the {@link #properties}
109    *  array. Note that it's possible to specify a single timing function instead of an array to use the
110    *  same timing function for all properties.
111    *  @type Object 
112    */
113   this.timingFunction = null;
114   /**
115    *  The delegate object that implements the {@link TKTransitionDelegate} protocol.
116    *  @type Object
117    */
118   this.delegate = null;
119   /**
120    *  Indicates whether the target needs to be removed once the transition completes. Defaults to <code>false</code>.
121    *  @type bool
122    */
123   this.removesTargetUponCompletion = null;
124   /**
125    *  Indicates whether the target needs to reset the property that are transitioned to their original values
126    *  once the transition completes. Defaults to <code>false</code>.
127    *  @type bool
128    */
129   this.revertsToOriginalValues = null;
130   //
131   this.defaultsApplied = false;
132   this.archivedStyles = null;
133   this.archivedValues = [];
134   // import params
135   TKUtils.copyPropertiesFromSourceToTarget(params, this);
136 };
137 
138 /* ==================== Dealing with defaults ==================== */
139 
140 TKTransition.prototype.applyDefaults = function () {
141   if (this.defaultsApplied) {
142     return;
143   }
144   //
145   for (var i in TKTransitionDefaults) {
146     if (this[i] === null) {
147       this[i] = TKTransitionDefaults[i];
148     }
149   }
150   //
151   this.defaultsApplied = true;
152 };
153 
154 /* ==================== Archiving the transition styles ==================== */
155 
156 TKTransition.prototype.getTargetStyle = function () {
157   // if (this.target.style === null) {
158   //   this.target.setAttribute('style', 'foo: bar');
159   // }
160   return this.target.style;
161 };
162 
163 TKTransition.prototype.archiveTransitionStyles = function () {
164   // do nothing if we already have archived styles in this run session
165   if (this.archivedStyles !== null) {
166     return;
167   }
168   // iterate all TKTransitionStyles and archive them
169   this.archivedStyles = [];
170   var style = this.getTargetStyle();
171   for (var i = 0; i < TKTransitionStyles.length; i++) {
172     this.archivedStyles.push(style.getPropertyValue(TKTransitionStyles[i]));
173   }
174 };
175 
176 TKTransition.prototype.restoreTransitionStyles = function () {
177   var style = this.getTargetStyle();
178   // iterate all TKTransitionStyles and restore them
179   for (var i = 0; i < TKTransitionStyles.length; i++) {
180     style.setProperty(TKTransitionStyles[i], this.archivedStyles[i], '');
181   }
182   // reset archived styles
183   this.archivedStyles = null;
184 };
185 
186 /* ==================== Archiving the base values ==================== */
187 
188 TKTransition.prototype.archiveBaseValues = function () {
189   // do nothing if we don't need to archive base values
190   if (!this.revertsToOriginalValues) {
191     return;
192   }
193   var style = this.getTargetStyle();
194   for (var i = 0; i < this.properties.length; i++) {
195     this.archivedValues.push(style.getPropertyValue(this.properties[i]));
196   }
197 };
198 
199 TKTransition.prototype.restoreBaseValues = function () {
200   var style = this.getTargetStyle();
201   for (var i = 0; i < this.properties.length; i++) {
202     style.setProperty(this.properties[i], this.archivedValues[i], null);
203   }
204 };
205 
206 /* ==================== Starting the transition ==================== */
207 
208 /**
209  *  Starts the transition.
210  */
211 TKTransition.prototype.start = function () {
212   // if we have an active transaction, just add to it
213   if (TKTransaction.openTransactions > 0) {
214     TKTransaction.addTransition(this);
215     return;
216   }  
217   // otherwise, we'll just get it going
218   this.applyDefaults();
219   if (this.from === null) {
220     this.applyToState();
221   }
222   else {
223     this.applyFromState();
224     var _this = this;
225     setTimeout(function () {
226       _this.applyToState();
227     }, 0);  
228   }
229 };
230 
231 /* ==================== Applying the "from" state ==================== */
232 
233 TKTransition.prototype.applyFromState = function () {
234   // do nothing if we have no "from" state specified
235   if (this.from === null) {
236     return;
237   }
238   //
239   this.applyDefaults();
240   this.archiveTransitionStyles();
241   var style = this.getTargetStyle();
242   style.webkitTransitionDuration = 0;
243   for (var i = 0; i < this.properties.length; i++) {
244     style.setProperty(this.properties[i], this.from[i], '');
245   }
246 };
247 
248 /* ==================== Applying the "to" state ==================== */
249 
250 TKTransition.prototype.applyToState = function () {
251   // first, make sure we have defaults applied if some
252   // properties are not explicitely set on our transition
253   this.applyDefaults();
254 
255   // and that we archive the transition styles to be reverted
256   this.archiveTransitionStyles();
257 
258   // and that we archive the original values
259   this.archiveBaseValues();
260   
261   // now compile the styles needed for this transition
262   this.cssProperties = [];
263   var transition_styles = [];
264   for (var i = 0; i < this.properties.length; i++) {
265     var property = this.properties[i];
266     // do nothing if we already have an animation defined for this
267     if (this.cssProperties.indexOf(property) > -1) {
268       continue;
269     }
270     // else, set up the CSS style for this transition
271     var duration = (TKUtils.objectIsArray(this.duration)) ? this.duration[i] : this.duration;
272     var timing = (TKUtils.objectIsArray(this.timingFunction)) ? this.timingFunction[i] : this.timingFunction;
273     var delay = (TKUtils.objectIsArray(this.delay)) ? this.delay[i] : this.delay;
274     transition_styles.push([property, duration + 's', timing, delay + 's'].join(' '));
275     // and remember we are animating this property
276     this.cssProperties.push(property);
277   }
278 
279   var style = this.getTargetStyle();
280   for (var i = 0; i < this.properties.length; i++) {
281     style.setProperty(this.properties[i], this.to[i], '');
282   }
283 
284   // set up the transition styles
285   style.webkitTransition = transition_styles.join(', ');
286 
287   // register for events to track transition completions
288   this.target.addEventListener('webkitTransitionEnd', this, false);
289   this.completedTransitions = 0;
290 };
291 
292 /* ==================== Tracking transition completion ==================== */
293 
294 // XXX: we won't be getting an event for properties that have the same value in the to
295 // state, so we'll need to do a little work to track css properties that won't really transition
296 TKTransition.prototype.handleEvent = function (event) {
297   // do nothing if that event just bubbled from our target's sub-tree
298   if (event.currentTarget !== this.target) {
299     return;
300   }
301 
302   // update the completion counter
303   this.completedTransitions++;
304 
305   // do nothing if we still have running transitions
306   if (this.completedTransitions != this.cssProperties.length) {
307     return;
308   }
309 
310   // the counter reached our properties count, fire up the delegate
311   if (TKUtils.objectHasMethod(this.delegate, TKTransitionDidCompleteDelegate)) {
312     this.delegate[TKTransitionDidCompleteDelegate](this);
313   }
314 
315   // remove from the tree if instructed to do so
316   if (this.removesTargetUponCompletion) {
317     this.target.parentNode.removeChild(this.target);
318   }
319   // otherwise, restore transition styles
320   else {
321     this.restoreTransitionStyles();
322   }
323   
324   // restore original values if instructed to do so
325   if (this.revertsToOriginalValues) {
326     this.restoreBaseValues();
327   }
328 };
329 
330 TKClass(TKTransition);
331