2 Copyright (c) 2010, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.com/yui/license.html
8 YUI.add('transition-native', function(Y) {
11 * Provides the transition method for Node.
12 * Transition has no API of its own, but adds the transition method to Node.
15 * @requires node-style
18 var TRANSITION = '-webkit-transition',
19 TRANSITION_CAMEL = 'WebkitTransition',
20 TRANSITION_PROPERTY_CAMEL = 'WebkitTransitionProperty',
21 TRANSITION_PROPERTY = '-webkit-transition-property',
22 TRANSITION_DURATION = '-webkit-transition-duration',
23 TRANSITION_TIMING_FUNCTION = '-webkit-transition-timing-function',
24 TRANSITION_DELAY = '-webkit-transition-delay',
25 TRANSITION_END = 'webkitTransitionEnd',
26 ON_TRANSITION_END = 'onwebkittransitionend',
27 TRANSFORM_CAMEL = 'WebkitTransform',
32 * A class for constructing transition instances.
33 * Adds the "transition" method to Node.
38 Transition = function() {
39 this.init.apply(this, arguments);
43 Transition.toggles = {};
45 Transition._hasEnd = {};
47 Transition._toCamel = function(property) {
48 property = property.replace(/-([a-z])/gi, function(m0, m1) {
49 return m1.toUpperCase();
55 Transition._toHyphen = function(property) {
56 property = property.replace(/([A-Z]?)([a-z]+)([A-Z]?)/g, function(m0, m1, m2, m3) {
59 str += '-' + m1.toLowerCase();
64 str += '-' + m3.toLowerCase();
74 Transition._reKeywords = /^(?:node|duration|iterations|easing|delay|on|onstart|onend)$/i;
76 Transition.useNative = false;
78 if (TRANSITION in Y.config.doc.documentElement.style) {
79 Transition.useNative = true;
80 Transition.supported = true; // TODO: remove
83 Y.Node.DOM_EVENTS[TRANSITION_END] = 1;
85 Transition.NAME = 'transition';
87 Transition.DEFAULT_EASING = 'ease';
88 Transition.DEFAULT_DURATION = 0.5;
89 Transition.DEFAULT_DELAY = 0;
91 Transition._nodeAttrs = {};
93 Transition.prototype = {
94 constructor: Transition,
95 init: function(node, config) {
98 if (!anim._running && config) {
99 anim._config = config;
100 node._transition = anim; // cache for reuse
102 anim._duration = ('duration' in config) ?
103 config.duration: anim.constructor.DEFAULT_DURATION;
105 anim._delay = ('delay' in config) ?
106 config.delay: anim.constructor.DEFAULT_DELAY;
108 anim._easing = config.easing || anim.constructor.DEFAULT_EASING;
109 anim._count = 0; // track number of animated properties
110 anim._running = false;
117 addProperty: function(prop, config) {
121 nodeInstance = Y.one(node),
122 attrs = Transition._nodeAttrs[uid],
130 attrs = Transition._nodeAttrs[uid] = {};
135 // might just be a value
136 if (config && config.value !== undefined) {
138 } else if (config !== undefined) {
143 if (typeof val === 'function') {
144 val = val.call(nodeInstance, nodeInstance);
147 if (attr && attr.transition) {
148 // take control if another transition owns this property
149 if (attr.transition !== anim) {
150 attr.transition._count--; // remapping attr to this transition
154 anim._count++; // properties per transition
156 // make 0 async and fire events
157 dur = ((typeof config.duration != 'undefined') ? config.duration :
158 anim._duration) || 0.0001;
163 delay: (typeof config.delay != 'undefined') ? config.delay :
166 easing: config.easing || anim._easing,
171 // native end event doesnt fire when setting to same value
172 // supplementing with timer
173 // val may be a string or number (height: 0, etc), but computedStyle is always string
174 computed = Y.DOM.getComputedStyle(node, prop);
175 compareVal = (typeof val === 'string') ? computed : parseFloat(computed);
177 if (Transition.useNative && compareVal === val) {
178 setTimeout(function() {
179 anim._onNativeEnd.call(node, {
187 removeProperty: function(prop) {
189 attrs = Transition._nodeAttrs[Y.stamp(anim._node)];
191 if (attrs && attrs[prop]) {
198 initAttrs: function(config) {
202 if (config.transform && !config[TRANSFORM_CAMEL]) {
203 config[TRANSFORM_CAMEL] = config.transform;
204 delete config.transform; // TODO: copy
207 for (attr in config) {
208 if (config.hasOwnProperty(attr) && !Transition._reKeywords.test(attr)) {
209 this.addProperty(attr, config[attr]);
211 // when size is auto or % webkit starts from zero instead of computed
212 // (https://bugs.webkit.org/show_bug.cgi?id=16020)
213 // TODO: selective set
214 if (node.style[attr] === '') {
215 Y.DOM.setStyle(node, attr, Y.DOM.getComputedStyle(node, attr));
222 * Starts or an animation.
227 run: function(callback) {
230 config = anim._config,
232 type: 'transition:start',
237 if (!anim._running) {
238 anim._running = true;
240 //anim._node.fire('transition:start', data);
242 if (config.on && config.on.start) {
243 config.on.start.call(Y.one(node), data);
246 anim.initAttrs(anim._config);
248 anim._callback = callback;
260 _prepDur: function(dur) {
261 dur = parseFloat(dur);
266 _runNative: function(time) {
271 computed = getComputedStyle(node),
272 attrs = Transition._nodeAttrs[uid],
274 cssTransition = computed[TRANSITION_PROPERTY],
276 transitionText = TRANSITION_PROPERTY + ': ',
277 duration = TRANSITION_DURATION + ': ',
278 easing = TRANSITION_TIMING_FUNCTION + ': ',
279 delay = TRANSITION_DELAY + ': ',
284 // preserve existing transitions
285 if (cssTransition !== 'all') {
286 transitionText += cssTransition + ',';
287 duration += computed[TRANSITION_DURATION] + ',';
288 easing += computed[TRANSITION_TIMING_FUNCTION] + ',';
289 delay += computed[TRANSITION_DELAY] + ',';
293 // run transitions mapped to this instance
294 for (name in attrs) {
295 hyphy = Transition._toHyphen(name);
297 if (attrs.hasOwnProperty(name) && attr.transition === anim) {
298 if (name in node.style) { // only native styles allowed
299 duration += anim._prepDur(attr.duration) + ',';
300 delay += anim._prepDur(attr.delay) + ',';
301 easing += (attr.easing) + ',';
303 transitionText += hyphy + ',';
304 cssText += hyphy + ': ' + attr.value + '; ';
306 this.removeProperty(name);
311 transitionText = transitionText.replace(/,$/, ';');
312 duration = duration.replace(/,$/, ';');
313 easing = easing.replace(/,$/, ';');
314 delay = delay.replace(/,$/, ';');
316 // only one native end event per node
317 if (!Transition._hasEnd[uid]) {
318 //anim._detach = Y.on(TRANSITION_END, anim._onNativeEnd, node);
319 //node[ON_TRANSITION_END] = anim._onNativeEnd;
320 node.addEventListener(TRANSITION_END, anim._onNativeEnd, false);
321 Transition._hasEnd[uid] = true;
325 //setTimeout(function() { // allow updates to apply (size fix, onstart, etc)
326 style.cssText += transitionText + duration + easing + delay + cssText;
331 _end: function(elapsed) {
334 callback = anim._callback,
335 config = anim._config,
337 type: 'transition:end',
342 nodeInstance = Y.one(node);
344 anim._running = false;
345 anim._callback = null;
348 if (config.on && config.on.end) {
349 setTimeout(function() { // IE: allow previous update to finish
350 config.on.end.call(nodeInstance, data);
352 // nested to ensure proper fire order
354 callback.call(nodeInstance, data);
358 } else if (callback) {
359 setTimeout(function() { // IE: allow previous update to finish
360 callback.call(nodeInstance, data);
363 //node.fire('transition:end', data);
368 _endNative: function(name) {
369 var node = this._node,
370 value = node.ownerDocument.defaultView.getComputedStyle(node, '')[TRANSITION_PROPERTY];
372 if (typeof value === 'string') {
373 value = value.replace(new RegExp('(?:^|,\\s)' + name + ',?'), ',');
374 value = value.replace(/^,|,$/, '');
375 node.style[TRANSITION_CAMEL] = value;
379 _onNativeEnd: function(e) {
382 event = e,//e._event,
383 name = Transition._toCamel(event.propertyName),
384 elapsed = event.elapsedTime,
385 attrs = Transition._nodeAttrs[uid],
387 anim = (attr) ? attr.transition : null,
392 anim.removeProperty(name);
393 anim._endNative(name);
394 config = anim._config[name];
399 elapsedTime: elapsed,
403 if (config && config.on && config.on.end) {
404 config.on.end.call(Y.one(node), data);
407 //node.fire('transition:propertyEnd', data);
409 if (anim._count <= 0) { // after propertyEnd fires
415 destroy: function() {
419 anim._detach.detach();
422 //anim._node[ON_TRANSITION_END] = null;
423 node.removeEventListener(TRANSITION_END, anim._onNativeEnd, false);
428 Y.Transition = Transition;
429 Y.TransitionNative = Transition; // TODO: remove
432 * Animate one or more css properties to a given value. Requires the "transition" module.
433 * <pre>example usage:
434 * Y.one('#demo').transition({
435 * duration: 1, // in seconds, default is 0.5
436 * easing: 'ease-out', // default is 'ease'
437 * delay: '1', // delay start for 1 second, default is 0
442 * opacity: { // per property
452 * @param {Object} config An object containing one or more style properties, a duration and an easing.
453 * @param {Function} callback A function to run after the transition has completed.
456 Y.Node.prototype.transition = function(name, config, callback) {
458 transitionAttrs = Transition._nodeAttrs[Y.stamp(this._node)],
459 anim = (transitionAttrs) ? transitionAttrs.transition || null : null,
463 if (typeof name === 'string') { // named effect, pull config from registry
464 if (typeof config === 'function') {
469 fxConfig = Transition.fx[name];
471 if (config && typeof config !== 'boolean') {
472 config = Y.clone(config);
474 for (prop in fxConfig) {
475 if (fxConfig.hasOwnProperty(prop)) {
476 if (! (prop in config)) {
477 config[prop] = fxConfig[prop];
485 } else { // name is a config, config is a callback or undefined
490 if (anim && !anim._running) {
491 anim.init(this, config);
493 anim = new Transition(this._node, config);
500 Y.Node.prototype.show = function(name, config, callback) {
501 this._show(); // show prior to transition
502 if (name && Y.Transition) {
503 if (typeof name !== 'string' && !name.push) { // named effect or array of effects supercedes default
504 if (typeof config === 'function') {
508 name = this.SHOW_TRANSITION;
510 this.transition(name, config, callback);
515 var _wrapCallBack = function(anim, fn, callback) {
521 callback.apply(anim._node, arguments);
526 Y.Node.prototype.hide = function(name, config, callback) {
527 if (name && Y.Transition) {
528 if (typeof config === 'function') {
533 callback = _wrapCallBack(this, this._hide, callback); // wrap with existing callback
534 if (typeof name !== 'string' && !name.push) { // named effect or array of effects supercedes default
535 if (typeof config === 'function') {
539 name = this.HIDE_TRANSITION;
541 this.transition(name, config, callback);
549 * Animate one or more css properties to a given value. Requires the "transition" module.
550 * <pre>example usage:
551 * Y.all('.demo').transition({
552 * duration: 1, // in seconds, default is 0.5
553 * easing: 'ease-out', // default is 'ease'
554 * delay: '1', // delay start for 1 second, default is 0
559 * opacity: { // per property
569 * @param {Object} config An object containing one or more style properties, a duration and an easing.
570 * @param {Function} callback A function to run after the transition has completed. The callback fires
571 * once per item in the NodeList.
574 Y.NodeList.prototype.transition = function(config, callback) {
575 var nodes = this._nodes,
579 while ((node = nodes[i++])) {
580 Y.one(node).transition(config, callback);
586 Y.Node.prototype.toggleView = function(name, on) {
588 this._toggles = this._toggles || [];
590 if (typeof name == 'boolean') { // no transition, just toggle
593 if (typeof on === 'undefined' && name in this._toggles) {
594 on = ! this._toggles[name];
602 callback = _wrapCallBack(anim, this._hide);
605 this._toggles[name] = on;
606 this.transition(Y.Transition.toggles[name][on], callback);
609 Y.NodeList.prototype.toggleView = function(config, callback) {
610 var nodes = this._nodes,
614 while ((node = nodes[i++])) {
615 Y.one(node).toggleView(config, callback);
621 Y.mix(Transition.fx, {
642 height: function(node) {
643 return node.get('scrollHeight') + 'px';
645 width: function(node) {
646 return node.get('scrollWidth') + 'px';
653 var overflow = this.getStyle('overflow');
654 if (overflow !== 'hidden') { // enable scrollHeight/Width
655 this.setStyle('overflow', 'hidden');
656 this._transitionOverflow = overflow;
661 if (this._transitionOverflow) { // revert overridden value
662 this.setStyle('overflow', this._transitionOverflow);
669 Y.mix(Transition.toggles, {
670 size: ['sizeIn', 'sizeOut'],
671 fade: ['fadeOut', 'fadeIn']
675 }, '3.3.0' ,{requires:['node-base']});
676 YUI.add('transition-timer', function(Y) {
679 * The Transition Utility provides an API for creating advanced transitions.
684 * Provides the base Transition class, for animating numeric properties.
687 * @submodule transition-timer
691 var Transition = Y.Transition;
693 Y.mix(Transition.prototype, {
695 if (Transition.useNative) {
702 _runTimer: function() {
706 Transition._running[Y.stamp(anim)] = anim;
707 anim._startTime = new Date();
708 Transition._startTimer();
711 _endTimer: function() {
713 delete Transition._running[Y.stamp(anim)];
714 anim._startTime = null;
717 _runFrame: function() {
718 var t = new Date() - this._startTime;
722 _runAttrs: function(time) {
725 config = anim._config,
727 attrs = Transition._nodeAttrs[uid],
728 customAttr = Transition.behaviors,
741 for (name in attrs) {
742 attribute = attrs[name];
743 if ((attribute && attribute.transition === anim)) {
744 d = attribute.duration;
745 delay = attribute.delay;
746 elapsed = (time - delay) / 1000;
755 setter = (i in customAttr && 'set' in customAttr[i]) ?
756 customAttr[i].set : Transition.DEFAULT_SETTER;
764 if (!delay || time >= delay) {
765 setter(anim, name, attribute.from, attribute.to, t - delay, d - delay,
766 attribute.easing, attribute.unit);
772 if (config[name] && config[name].on && config[name].on.end) {
773 config[name].on.end.call(Y.one(node), data);
776 //node.fire('transition:propertyEnd', data);
778 if (!allDone && anim._count <= 0) {
790 _initAttrs: function() {
792 customAttr = Transition.behaviors,
793 uid = Y.stamp(anim._node),
794 attrs = Transition._nodeAttrs[uid],
805 for (name in attrs) {
806 attribute = attrs[name];
807 if (attrs.hasOwnProperty(name) && (attribute && attribute.transition === anim)) {
808 duration = attribute.duration * 1000;
809 delay = attribute.delay * 1000;
810 easing = attribute.easing;
811 val = attribute.value;
813 // only allow supported properties
814 if (name in anim._node.style || name in Y.DOM.CUSTOM_STYLES) {
815 begin = (name in customAttr && 'get' in customAttr[name]) ?
816 customAttr[name].get(anim, name) : Transition.DEFAULT_GETTER(anim, name);
818 mFrom = Transition.RE_UNITS.exec(begin);
819 mTo = Transition.RE_UNITS.exec(val);
821 begin = mFrom ? mFrom[1] : begin;
822 end = mTo ? mTo[1] : val;
823 unit = mTo ? mTo[2] : mFrom ? mFrom[2] : ''; // one might be zero TODO: mixed units
825 if (!unit && Transition.RE_DEFAULT_UNIT.test(name)) {
826 unit = Transition.DEFAULT_UNIT;
829 if (typeof easing === 'string') {
830 if (easing.indexOf('cubic-bezier') > -1) {
831 easing = easing.substring(13, easing.length - 1).split(',');
832 } else if (Transition.easings[easing]) {
833 easing = Transition.easings[easing];
837 attribute.from = Number(begin);
838 attribute.to = Number(end);
839 attribute.unit = unit;
840 attribute.easing = easing;
841 attribute.duration = duration + delay;
842 attribute.delay = delay;
851 destroy: function() {
857 Y.mix(Y.Transition, {
860 * Regex of properties that should use the default unit.
862 * @property RE_DEFAULT_UNIT
865 RE_DEFAULT_UNIT: /^width|height|top|right|bottom|left|margin.*|padding.*|border.*$/i,
868 * The default unit to use with properties that pass the RE_DEFAULT_UNIT test.
870 * @property DEFAULT_UNIT
876 * Time in milliseconds passed to setInterval for frame processing
878 * @property intervalTime
885 * Bucket for custom getters and setters
887 * @property behaviors
892 get: function(anim, attr) {
893 return Y.DOM._getAttrOffset(anim._node, attr);
899 * The default setter to use when setting object properties.
901 * @property DEFAULT_SETTER
904 DEFAULT_SETTER: function(anim, att, from, to, elapsed, duration, fn, unit) {
908 var node = anim._node,
909 val = Transition.cubicBezier(fn, elapsed / duration);
911 val = from + val[0] * (to - from);
914 if (att in node.style || att in Y.DOM.CUSTOM_STYLES) {
916 Y.DOM.setStyle(node, att, val + unit);
924 * The default getter to use when getting object properties.
926 * @property DEFAULT_GETTER
929 DEFAULT_GETTER: function(anim, att) {
930 var node = anim._node,
933 if (att in node.style || att in Y.DOM.CUSTOM_STYLES) {
934 val = Y.DOM.getComputedStyle(node, att);
940 _startTimer: function() {
941 if (!Transition._timer) {
942 Transition._timer = setInterval(Transition._runFrame, Transition.intervalTime);
946 _stopTimer: function() {
947 clearInterval(Transition._timer);
948 Transition._timer = null;
952 * Called per Interval to handle each animation frame.
957 _runFrame: function() {
960 for (anim in Transition._running) {
961 if (Transition._running[anim]._runFrame) {
963 Transition._running[anim]._runFrame();
968 Transition._stopTimer();
972 cubicBezier: function(p, t) {
982 A = x3 - 3 * x2 + 3 * x1 - x0,
983 B = 3 * x2 - 6 * x1 + 3 * x0,
986 E = y3 - 3 * y2 + 3 * y1 - y0,
987 F = 3 * y2 - 6 * y1 + 3 * y0,
991 x = (((A*t) + B)*t + C)*t + D,
992 y = (((E*t) + F)*t + G)*t + H;
998 ease: [0.25, 0, 1, 0.25],
999 linear: [0, 0, 1, 1],
1000 'ease-in': [0.42, 0, 1, 1],
1001 'ease-out': [0, 0, 0.58, 1],
1002 'ease-in-out': [0.42, 0, 0.58, 1]
1008 RE_UNITS: /^(-?\d*\.?\d*){1}(em|ex|px|in|cm|mm|pt|pc|%)*$/
1011 Transition.behaviors.top = Transition.behaviors.bottom = Transition.behaviors.right = Transition.behaviors.left;
1013 Y.Transition = Transition;
1016 }, '3.3.0' ,{requires:['transition-native', 'node-style']});
1019 YUI.add('transition', function(Y){}, '3.3.0' ,{use:['transition-native', 'transition-timer']});