Merge branch 'MDL-32509' of git://github.com/danpoltawski/moodle
[moodle.git] / lib / yui / 3.5.0 / build / transition / transition.js
blobb72d5a6c2d7331dc9436f2d3bc6a68cf658210c0
1 /*
2 YUI 3.5.0 (build 5089)
3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
6 */
7 YUI.add('transition', function(Y) {
9 /**
10 * Provides the transition method for Node.
11 * Transition has no API of its own, but adds the transition method to Node.
13 * @module transition
14 * @requires node-style
17 var CAMEL_VENDOR_PREFIX = '',
18     VENDOR_PREFIX = '',
19     DOCUMENT = Y.config.doc,
20     DOCUMENT_ELEMENT = 'documentElement',
21     TRANSITION = 'transition',
22     TRANSITION_CAMEL = 'Transition',
23     TRANSITION_PROPERTY_CAMEL,
24     TRANSITION_PROPERTY,
25     TRANSITION_DURATION,
26     TRANSITION_TIMING_FUNCTION,
27     TRANSITION_DELAY,
28     TRANSITION_END,
29     ON_TRANSITION_END,
30     TRANSFORM_CAMEL,
32     EMPTY_OBJ = {},
34     VENDORS = [
35         'Webkit',
36         'Moz'
37     ],
39     VENDOR_TRANSITION_END = {
40         Webkit: 'webkitTransitionEnd'
41     },
43 /**
44  * A class for constructing transition instances.
45  * Adds the "transition" method to Node.
46  * @class Transition
47  * @constructor
48  */
50 Transition = function() {
51     this.init.apply(this, arguments);
54 Transition._toCamel = function(property) {
55     property = property.replace(/-([a-z])/gi, function(m0, m1) {
56         return m1.toUpperCase();
57     });
59     return property;
62 Transition._toHyphen = function(property) {
63     property = property.replace(/([A-Z]?)([a-z]+)([A-Z]?)/g, function(m0, m1, m2, m3) {
64         var str = ((m1) ? '-' + m1.toLowerCase() : '') + m2;
65         
66         if (m3) {
67             str += '-' + m3.toLowerCase();
68         }
70         return str;
71     }); 
73     return property;
76 Transition.SHOW_TRANSITION = 'fadeIn';
77 Transition.HIDE_TRANSITION = 'fadeOut';
79 Transition.useNative = false;
81 Y.Array.each(VENDORS, function(val) { // then vendor specific
82     var property = val + TRANSITION_CAMEL;
83     if (property in DOCUMENT[DOCUMENT_ELEMENT].style) {
84         CAMEL_VENDOR_PREFIX = val;
85         VENDOR_PREFIX = Transition._toHyphen(val) + '-';
87         Transition.useNative = true;
88         Transition.supported = true; // TODO: remove
89         Transition._VENDOR_PREFIX = val;
90     }
91 });
93 TRANSITION_CAMEL = CAMEL_VENDOR_PREFIX + TRANSITION_CAMEL;
94 TRANSITION_PROPERTY_CAMEL = CAMEL_VENDOR_PREFIX + 'TransitionProperty';
95 TRANSITION_PROPERTY = VENDOR_PREFIX + 'transition-property';
96 TRANSITION_DURATION = VENDOR_PREFIX + 'transition-duration';
97 TRANSITION_TIMING_FUNCTION = VENDOR_PREFIX + 'transition-timing-function';
98 TRANSITION_DELAY = VENDOR_PREFIX + 'transition-delay';
99 TRANSITION_END = 'transitionend';
100 ON_TRANSITION_END = 'on' + CAMEL_VENDOR_PREFIX.toLowerCase() + 'transitionend';
102 TRANSITION_END = VENDOR_TRANSITION_END[CAMEL_VENDOR_PREFIX] || TRANSITION_END;
104 TRANSFORM_CAMEL = CAMEL_VENDOR_PREFIX + 'Transform';
106 Transition.fx = {};
107 Transition.toggles = {};
109 Transition._hasEnd = {};
111 Transition._reKeywords = /^(?:node|duration|iterations|easing|delay|on|onstart|onend)$/i;
113 Y.Node.DOM_EVENTS[TRANSITION_END] = 1; 
115 Transition.NAME = 'transition';
117 Transition.DEFAULT_EASING = 'ease';
118 Transition.DEFAULT_DURATION = 0.5;
119 Transition.DEFAULT_DELAY = 0;
121 Transition._nodeAttrs = {};
123 Transition.prototype = {
124     constructor: Transition,
125     init: function(node, config) {
126         var anim = this;
127         anim._node = node;
128         if (!anim._running && config) {
129             anim._config = config;
130             node._transition = anim; // cache for reuse
132             anim._duration = ('duration' in config) ?
133                 config.duration: anim.constructor.DEFAULT_DURATION;
135             anim._delay = ('delay' in config) ?
136                 config.delay: anim.constructor.DEFAULT_DELAY;
138             anim._easing = config.easing || anim.constructor.DEFAULT_EASING;
139             anim._count = 0; // track number of animated properties
140             anim._running = false;
142         }
144         return anim;
145     },
147     addProperty: function(prop, config) {
148         var anim = this,
149             node = this._node,
150             uid = Y.stamp(node),
151             nodeInstance = Y.one(node),
152             attrs = Transition._nodeAttrs[uid],
153             computed,
154             compareVal,
155             dur,
156             attr,
157             val;
159         if (!attrs) {
160             attrs = Transition._nodeAttrs[uid] = {};
161         }
163         attr = attrs[prop];
165         // might just be a value
166         if (config && config.value !== undefined) {
167             val = config.value;
168         } else if (config !== undefined) {
169             val = config; 
170             config = EMPTY_OBJ;
171         }
173         if (typeof val === 'function') {
174             val = val.call(nodeInstance, nodeInstance);
175         }
177         if (attr && attr.transition) {
178             // take control if another transition owns this property
179             if (attr.transition !== anim) {
180                 attr.transition._count--; // remapping attr to this transition
181             }
182         } 
184         anim._count++; // properties per transition
186         // make 0 async and fire events
187         dur = ((typeof config.duration != 'undefined') ? config.duration :
188                     anim._duration) || 0.0001;
190         attrs[prop] = {
191             value: val,
192             duration: dur,
193             delay: (typeof config.delay != 'undefined') ? config.delay :
194                     anim._delay,
196             easing: config.easing || anim._easing,
198             transition: anim
199         };
201         // native end event doesnt fire when setting to same value
202         // supplementing with timer
203         // val may be a string or number (height: 0, etc), but computedStyle is always string
204         computed = Y.DOM.getComputedStyle(node, prop);
205         compareVal = (typeof val === 'string') ? computed : parseFloat(computed);
207         if (Transition.useNative && compareVal === val) {
208             setTimeout(function() {
209                 anim._onNativeEnd.call(node, {
210                     propertyName: prop,
211                     elapsedTime: dur
212                 });
213             }, dur * 1000);
214         }
215     },
217     removeProperty: function(prop) {
218         var anim = this,
219             attrs = Transition._nodeAttrs[Y.stamp(anim._node)];
221         if (attrs && attrs[prop]) {
222             delete attrs[prop];
223             anim._count--;
224         }
226     },
228     initAttrs: function(config) {
229         var attr,
230             node = this._node;
232         if (config.transform && !config[TRANSFORM_CAMEL]) {
233             config[TRANSFORM_CAMEL] = config.transform;
234             delete config.transform; // TODO: copy
235         }
237         for (attr in config) {
238             if (config.hasOwnProperty(attr) && !Transition._reKeywords.test(attr)) {
239                 this.addProperty(attr, config[attr]);
241                 // when size is auto or % webkit starts from zero instead of computed 
242                 // (https://bugs.webkit.org/show_bug.cgi?id=16020)
243                 // TODO: selective set
244                 if (node.style[attr] === '') {
245                     Y.DOM.setStyle(node, attr, Y.DOM.getComputedStyle(node, attr));
246                 }
247             }
248         }
249     },
251     /**
252      * Starts or an animation.
253      * @method run
254      * @chainable
255      * @private
256      */    
257     run: function(callback) {
258         var anim = this,
259             node = anim._node,
260             config = anim._config,
261             data = {
262                 type: 'transition:start',
263                 config: config
264             };
267         if (!anim._running) {
268             anim._running = true;
270             if (config.on && config.on.start) {
271                 config.on.start.call(Y.one(node), data);
272             }
274             anim.initAttrs(anim._config);
276             anim._callback = callback;
277             anim._start();
278         }
281         return anim;
282     },
284     _start: function() {
285         this._runNative();
286     },
288     _prepDur: function(dur) {
289         dur = parseFloat(dur);
291         return dur + 's';
292     },
294     _runNative: function(time) {
295         var anim = this,
296             node = anim._node,
297             uid = Y.stamp(node),
298             style = node.style,
299             computed = node.ownerDocument.defaultView.getComputedStyle(node),
300             attrs = Transition._nodeAttrs[uid],
301             cssText = '',
302             cssTransition = computed[Transition._toCamel(TRANSITION_PROPERTY)],
304             transitionText = TRANSITION_PROPERTY + ': ',
305             duration = TRANSITION_DURATION + ': ',
306             easing = TRANSITION_TIMING_FUNCTION + ': ',
307             delay = TRANSITION_DELAY + ': ',
308             hyphy,
309             attr,
310             name;
312         // preserve existing transitions
313         if (cssTransition !== 'all') {
314             transitionText += cssTransition + ',';
315             duration += computed[Transition._toCamel(TRANSITION_DURATION)] + ',';
316             easing += computed[Transition._toCamel(TRANSITION_TIMING_FUNCTION)] + ',';
317             delay += computed[Transition._toCamel(TRANSITION_DELAY)] + ',';
319         }
321         // run transitions mapped to this instance
322         for (name in attrs) {
323             hyphy = Transition._toHyphen(name);
324             attr = attrs[name];
325             if ((attr = attrs[name]) && attr.transition === anim) {
326                 if (name in node.style) { // only native styles allowed
327                     duration += anim._prepDur(attr.duration) + ',';
328                     delay += anim._prepDur(attr.delay) + ',';
329                     easing += (attr.easing) + ',';
331                     transitionText += hyphy + ',';
332                     cssText += hyphy + ': ' + attr.value + '; ';
333                 } else {
334                     this.removeProperty(name);
335                 }
336             }
337         }
339         transitionText = transitionText.replace(/,$/, ';');
340         duration = duration.replace(/,$/, ';');
341         easing = easing.replace(/,$/, ';');
342         delay = delay.replace(/,$/, ';');
344         // only one native end event per node
345         if (!Transition._hasEnd[uid]) {
346             node.addEventListener(TRANSITION_END, anim._onNativeEnd, '');
347             Transition._hasEnd[uid] = true;
349         }
350         
351         style.cssText += transitionText + duration + easing + delay + cssText;
353     },
355     _end: function(elapsed) {
356         var anim = this,
357             node = anim._node,
358             callback = anim._callback,
359             config = anim._config,
360             data = {
361                 type: 'transition:end',
362                 config: config,
363                 elapsedTime: elapsed 
364             },
366             nodeInstance = Y.one(node); 
368         anim._running = false;
369         anim._callback = null;
371         if (node) {
372             if (config.on && config.on.end) {
373                 setTimeout(function() { // IE: allow previous update to finish
374                     config.on.end.call(nodeInstance, data);
376                     // nested to ensure proper fire order
377                     if (callback) {
378                         callback.call(nodeInstance, data);
379                     }
381                 }, 1);
382             } else if (callback) {
383                 setTimeout(function() { // IE: allow previous update to finish
384                     callback.call(nodeInstance, data);
385                 }, 1);
386             }
387         }
389     },
391     _endNative: function(name) {
392         var node = this._node,
393             value = node.ownerDocument.defaultView.getComputedStyle(node, '')[Transition._toCamel(TRANSITION_PROPERTY)];
395         name = Transition._toHyphen(name);
396         if (typeof value === 'string') {
397             value = value.replace(new RegExp('(?:^|,\\s)' + name + ',?'), ',');
398             value = value.replace(/^,|,$/, '');
399             node.style[TRANSITION_CAMEL] = value;
400         }
401     },
403     _onNativeEnd: function(e) {
404         var node = this,
405             uid = Y.stamp(node),
406             event = e,//e._event,
407             name = Transition._toCamel(event.propertyName),
408             elapsed = event.elapsedTime,
409             attrs = Transition._nodeAttrs[uid],
410             attr = attrs[name],
411             anim = (attr) ? attr.transition : null,
412             data,
413             config;
415         if (anim) {
416             anim.removeProperty(name);
417             anim._endNative(name);
418             config = anim._config[name];
420             data = {
421                 type: 'propertyEnd',
422                 propertyName: name,
423                 elapsedTime: elapsed,
424                 config: config
425             };
427             if (config && config.on && config.on.end) {
428                 config.on.end.call(Y.one(node), data);
429             }
431             if (anim._count <= 0)  { // after propertyEnd fires
432                 anim._end(elapsed);
433                 node.style[TRANSITION_PROPERTY_CAMEL] = ''; // clean up style
434             }
435         }
436     },
438     destroy: function() {
439         var anim = this,
440             node = anim._node;
442         if (node) {
443             node.removeEventListener(TRANSITION_END, anim._onNativeEnd, false);
444             anim._node = null;
445         }
446     }
449 Y.Transition = Transition;
450 Y.TransitionNative = Transition; // TODO: remove
452 /** 
453  *   Animate one or more css properties to a given value. Requires the "transition" module.
454  *   <pre>example usage:
455  *       Y.one('#demo').transition({
456  *           duration: 1, // in seconds, default is 0.5
457  *           easing: 'ease-out', // default is 'ease'
458  *           delay: '1', // delay start for 1 second, default is 0
460  *           height: '10px',
461  *           width: '10px',
463  *           opacity: { // per property
464  *               value: 0,
465  *               duration: 2,
466  *               delay: 2,
467  *               easing: 'ease-in'
468  *           }
469  *       });
470  *   </pre>
471  *   @for Node
472  *   @method transition
473  *   @param {Object} config An object containing one or more style properties, a duration and an easing.
474  *   @param {Function} callback A function to run after the transition has completed. 
475  *   @chainable
477 Y.Node.prototype.transition = function(name, config, callback) {
478     var 
479         transitionAttrs = Transition._nodeAttrs[Y.stamp(this._node)],
480         anim = (transitionAttrs) ? transitionAttrs.transition || null : null,
481         fxConfig,
482         prop;
483     
484     if (typeof name === 'string') { // named effect, pull config from registry
485         if (typeof config === 'function') {
486             callback = config;
487             config = null;
488         }
490         fxConfig = Transition.fx[name];
492         if (config && typeof config !== 'boolean') {
493             config = Y.clone(config);
495             for (prop in fxConfig) {
496                 if (fxConfig.hasOwnProperty(prop)) {
497                     if (! (prop in config)) {
498                         config[prop] = fxConfig[prop]; 
499                     }
500                 }
501             }
502         } else {
503             config = fxConfig;
504         }
506     } else { // name is a config, config is a callback or undefined
507         callback = config;
508         config = name;
509     }
511     if (anim && !anim._running) {
512         anim.init(this, config);
513     } else {
514         anim = new Transition(this._node, config);
515     }
517     anim.run(callback);
518     return this;
521 Y.Node.prototype.show = function(name, config, callback) {
522     this._show(); // show prior to transition
523     if (name && Y.Transition) {
524         if (typeof name !== 'string' && !name.push) { // named effect or array of effects supercedes default
525             if (typeof config === 'function') {
526                 callback = config;
527                 config = name;
528             }
529             name = Transition.SHOW_TRANSITION; 
530         }    
531         this.transition(name, config, callback);
532     }    
533     return this;
536 var _wrapCallBack = function(anim, fn, callback) {
537     return function() {
538         if (fn) {
539             fn.call(anim);
540         }
541         if (callback) {
542             callback.apply(anim._node, arguments);
543         }
544     };
547 Y.Node.prototype.hide = function(name, config, callback) {
548     if (name && Y.Transition) {
549         if (typeof config === 'function') {
550             callback = config;
551             config = null;
552         }
554         callback = _wrapCallBack(this, this._hide, callback); // wrap with existing callback
555         if (typeof name !== 'string' && !name.push) { // named effect or array of effects supercedes default
556             if (typeof config === 'function') {
557                 callback = config;
558                 config = name;
559             }
560             name = Transition.HIDE_TRANSITION; 
561         }    
562         this.transition(name, config, callback);
563     } else {
564         this._hide();
565     }    
566     return this;
567 }; 
569 /** 
570  *   Animate one or more css properties to a given value. Requires the "transition" module.
571  *   <pre>example usage:
572  *       Y.all('.demo').transition({
573  *           duration: 1, // in seconds, default is 0.5
574  *           easing: 'ease-out', // default is 'ease'
575  *           delay: '1', // delay start for 1 second, default is 0
577  *           height: '10px',
578  *           width: '10px',
580  *           opacity: { // per property
581  *               value: 0,
582  *               duration: 2,
583  *               delay: 2,
584  *               easing: 'ease-in'
585  *           }
586  *       });
587  *   </pre>
588  *   @for NodeList
589  *   @method transition
590  *   @param {Object} config An object containing one or more style properties, a duration and an easing.
591  *   @param {Function} callback A function to run after the transition has completed. The callback fires
592  *       once per item in the NodeList.
593  *   @chainable
595 Y.NodeList.prototype.transition = function(config, callback) {
596     var nodes = this._nodes,
597         i = 0,
598         node;
600     while ((node = nodes[i++])) {
601         Y.one(node).transition(config, callback);
602     }
604     return this;
607 Y.Node.prototype.toggleView = function(name, on, callback) {
608     this._toggles = this._toggles || [];
609     callback = arguments[arguments.length - 1];
611     if (typeof name == 'boolean') { // no transition, just toggle
612         on = name;
613         name = null;
614     }
616     name = name || Y.Transition.DEFAULT_TOGGLE;
618     if (typeof on == 'undefined' && name in this._toggles) { // reverse current toggle
619         on = ! this._toggles[name];
620     }
622     on = (on) ? 1 : 0;
623     if (on) {
624         this._show();
625     }  else {
626         callback = _wrapCallBack(this, this._hide, callback);
627     }
629     this._toggles[name] = on;
630     this.transition(Y.Transition.toggles[name][on], callback);
632     return this;
635 Y.NodeList.prototype.toggleView = function(name, on, callback) {
636     var nodes = this._nodes,
637         i = 0,
638         node;
640     while ((node = nodes[i++])) {
641         Y.one(node).toggleView(name, on, callback);
642     }
644     return this;
647 Y.mix(Transition.fx, {
648     fadeOut: {
649         opacity: 0,
650         duration: 0.5,
651         easing: 'ease-out'
652     },
654     fadeIn: {
655         opacity: 1,
656         duration: 0.5,
657         easing: 'ease-in'
658     },
660     sizeOut: {
661         height: 0,
662         width: 0,
663         duration: 0.75,
664         easing: 'ease-out'
665     },
667     sizeIn: {
668         height: function(node) {
669             return node.get('scrollHeight') + 'px';
670         },
671         width: function(node) {
672             return node.get('scrollWidth') + 'px';
673         },
674         duration: 0.5,
675         easing: 'ease-in',
676         
677         on: {
678             start: function() {
679                 var overflow = this.getStyle('overflow');
680                 if (overflow !== 'hidden') { // enable scrollHeight/Width
681                     this.setStyle('overflow', 'hidden');
682                     this._transitionOverflow = overflow;
683                 }
684             },
686             end: function() {
687                 if (this._transitionOverflow) { // revert overridden value
688                     this.setStyle('overflow', this._transitionOverflow);
689                     delete this._transitionOverflow;
690                 }
691             }
692         } 
693     }
696 Y.mix(Transition.toggles, {
697     size: ['sizeOut', 'sizeIn'],
698     fade: ['fadeOut', 'fadeIn']
701 Transition.DEFAULT_TOGGLE = 'fade';
705 }, '3.5.0' ,{requires:['node-style']});