2 * jQuery Transit - CSS3 transitions and transformations
3 * Copyright(c) 2011 Rico Sta. Cruz <rico@ricostacruz.com>
6 * http://ricostacruz.com/jquery.transit
7 * http://github.com/rstacruz/jquery.transit
16 // Map of $.css() keys to values for 'transitionProperty'.
17 // See https://developer.mozilla.org/en/CSS/CSS_transitions#Properties_that_can_be_animated
19 marginLeft : 'margin',
20 marginRight : 'margin',
21 marginBottom : 'margin',
23 paddingLeft : 'padding',
24 paddingRight : 'padding',
25 paddingBottom : 'padding',
26 paddingTop : 'padding'
29 // Will simply transition "instantly" if false
32 // Set this to false if you don't want to use the transition end property.
33 useTransitionEnd: false
36 var div = document.createElement('div');
39 // Helper function to get the proper vendor property name.
40 // (`transition` => `WebkitTransition`)
41 function getVendorPropertyName(prop) {
42 var prefixes = ['Moz', 'Webkit', 'O', 'ms'];
43 var prop_ = prop.charAt(0).toUpperCase() + prop.substr(1);
45 if (prop in div.style) { return prop; }
47 for (var i=0; i<prefixes.length; ++i) {
48 var vendorProp = prefixes[i] + prop_;
49 if (vendorProp in div.style) { return vendorProp; }
54 * matchMedia() polyfill - test whether a CSS media type or media query applies
55 * primary author: Scott Jehl
56 * Copyright (c) 2010 Filament Group, Inc
58 * adapted by Paul Irish to use the matchMedia API
59 * http://dev.w3.org/csswg/cssom-view/#dom-window-matchmedia
60 * which webkit now supports: http://trac.webkit.org/changeset/72552
62 * Doesn't implement media.type as there's no way for crossbrowser property
63 * getters. instead of media.type == 'tv' just use media.matchMedium('tv')
65 if ( !(window.matchMedia) ){
66 window.matchMedia = (function(doc,undefined){
69 docElem = doc.documentElement,
70 fakeBody = doc.createElement('body'),
71 testDiv = doc.createElement('div');
73 testDiv.setAttribute('id','ejs-qtest');
74 fakeBody.appendChild(testDiv);
77 if (cache[q] === undefined) {
78 var styleBlock = doc.createElement('style'),
79 cssrule = '@media '+q+' { #ejs-qtest { position: absolute; } }';
80 //must set type for IE!
81 styleBlock.type = "text/css";
82 if (styleBlock.styleSheet){
83 styleBlock.styleSheet.cssText = cssrule;
86 styleBlock.appendChild(doc.createTextNode(cssrule));
88 docElem.insertBefore(fakeBody, docElem.firstChild);
89 docElem.insertBefore(styleBlock, docElem.firstChild);
90 cache[q] = ((window.getComputedStyle ? window.getComputedStyle(testDiv,null) : testDiv.currentStyle)['position'] == 'absolute');
91 docElem.removeChild(fakeBody);
92 docElem.removeChild(styleBlock);
100 // Helper function to check if transform3D is supported.
101 // Should return true for Webkits and Firefox 10+.
102 function checkTransform3dSupport() {
103 // Borrowed Modernizr implementation which detects support for 3D transforms on Android < 4
104 var ret = !!getVendorPropertyName('perspective');
105 if (ret && 'WebkitPerspective' in div.style) {
106 return matchMedia("(-webkit-transform-3d)").matches;
111 var isChrome = navigator.userAgent.toLowerCase().indexOf('chrome') > -1;
113 // Check for the browser's transitions support.
114 // You can access this in jQuery's `$.support.transition`.
115 // As per [jQuery's cssHooks documentation](http://api.jquery.com/jQuery.cssHooks/),
116 // we set $.support.transition to a string of the actual property name used.
117 support.transition = getVendorPropertyName('transition');
118 support.transitionDelay = getVendorPropertyName('transitionDelay');
119 support.transform = getVendorPropertyName('transform');
120 support.transformOrigin = getVendorPropertyName('transformOrigin');
121 support.transform3d = checkTransform3dSupport();
123 $.extend($.support, support);
126 'MozTransition': 'transitionend',
127 'OTransition': 'oTransitionEnd',
128 'WebkitTransition': 'webkitTransitionEnd',
129 'msTransition': 'MSTransitionEnd'
132 // Detect the 'transitionend' event needed.
133 var transitionEnd = support.transitionEnd = eventNames[support.transition] || null;
135 // Avoid memory leak in IE.
139 // List of easing aliases that you can use with `$.fn.transition`.
144 'in-out': 'ease-in-out',
145 'snap': 'cubic-bezier(0,1,.5,1)'
148 // ## 'transform' CSS hook
149 // Allows you to use the `transform` property in CSS.
151 // $("#hello").css({ transform: "rotate(90deg)" });
153 // $("#hello").css('transform');
154 // //=> { rotate: '90deg' }
156 $.cssHooks.transform = {
157 // The getter returns a `Transform` object.
158 get: function(elem) {
159 return $(elem).data('transform') || new Transform();
162 // The setter accepts a `Transform` object or a string.
163 set: function(elem, v) {
166 if (!(value instanceof Transform)) {
167 value = new Transform(value);
170 // We've seen the 3D version of Scale() not work in Chrome when the
171 // element being scaled extends outside of the viewport. Thus, we're
172 // forcing Chrome to not use the 3d transforms as well. Not sure if
173 // translate is affectede, but not risking it. Detection code from
174 // http://davidwalsh.name/detecting-google-chrome-javascript
176 // Also eliminate WebKit-browsers withouth 3d transforms support
177 // such as Android < 4 default browser. Fixes #34
178 if (support.transform3d && !isChrome) {
179 elem.style[support.transform] = value.toString(true);
181 elem.style[support.transform] = value.toString();
184 $(elem).data('transform', value);
188 // ## 'transformOrigin' CSS hook
189 // Allows the use for `transformOrigin` to define where scaling and rotation
192 // $("#hello").css({ transformOrigin: '0 0' });
194 $.cssHooks.transformOrigin = {
195 get: function(elem) {
196 return elem.style[support.transformOrigin];
198 set: function(elem, value) {
199 elem.style[support.transformOrigin] = value;
203 // ## Other CSS hooks
204 // Allows you to rotate, scale and translate.
205 registerCssHook('scale');
206 registerCssHook('translate');
207 registerCssHook('rotate');
208 registerCssHook('rotateX');
209 registerCssHook('rotateY');
210 registerCssHook('rotate3d');
211 registerCssHook('perspective');
212 registerCssHook('skewX');
213 registerCssHook('skewY');
214 registerCssHook('x', true);
215 registerCssHook('y', true);
217 // ## Transform class
218 // This is the main class of a transformation property that powers
219 // `$.fn.css({ transform: '...' })`.
221 // This is, in essence, a dictionary object with key/values as `-transform`
224 // var t = new Transform("rotate(90) scale(4)");
226 // t.rotate //=> "90deg"
227 // t.scale //=> "4,4"
229 // Setters are accounted for.
231 // t.set('rotate', 4)
232 // t.rotate //=> "4deg"
234 // Convert it to a CSS string using the `toString()` and `toString(true)` (for WebKit)
237 // t.toString() //=> "rotate(90deg) scale(4,4)"
238 // t.toString(true) //=> "rotate(90deg) scale3d(4,4,0)" (WebKit version)
240 function Transform(str) {
241 if (typeof str === 'string') { this.parse(str); }
245 Transform.prototype = {
246 // ### setFromString()
247 // Sets a property from a string.
249 // t.setFromString('scale', '2,4');
250 // // Same as set('scale', '2', '4');
252 setFromString: function(prop, val) {
254 (typeof val === 'string') ? val.split(',') :
255 (val.constructor === Array) ? val :
260 Transform.prototype.set.apply(this, args);
266 // t.set('scale', 2, 4);
268 set: function(prop) {
269 var args = Array.prototype.slice.apply(arguments, [1]);
270 if (this.setter[prop]) {
271 this.setter[prop].apply(this, args);
273 this[prop] = args.join(',');
277 get: function(prop) {
278 if (this.getter[prop]) {
279 return this.getter[prop].apply(this);
281 return this[prop] || 0;
288 // .css({ rotate: 30 })
289 // .css({ rotate: "30" })
290 // .css({ rotate: "30deg" })
291 // .css({ rotate: "30deg" })
293 rotate: function(theta) {
294 this.rotate = unit(theta, 'deg');
297 rotateX: function(theta) {
298 this.rotateX = unit(theta, 'deg');
301 rotateY: function(theta) {
302 this.rotateY = unit(theta, 'deg');
307 // .css({ scale: 9 }) //=> "scale(9,9)"
308 // .css({ scale: '3,2' }) //=> "scale(3,2)"
310 scale: function(x, y) {
311 if (y === undefined) { y = x; }
312 this.scale = x + "," + y;
317 this.skewX = unit(x, 'deg');
321 this.skewY = unit(y, 'deg');
325 perspective: function(dist) {
326 this.perspective = unit(dist, 'px');
330 // Translations. Notice how this keeps the other value.
332 // .css({ x: 4 }) //=> "translate(4px, 0)"
333 // .css({ y: 10 }) //=> "translate(4px, 10px)"
336 this.set('translate', x, null);
340 this.set('translate', null, y);
344 // Notice how this keeps the other value.
346 // .css({ translate: '2, 5' }) //=> "translate(2px, 5px)"
348 translate: function(x, y) {
349 if (this._translateX === undefined) { this._translateX = 0; }
350 if (this._translateY === undefined) { this._translateY = 0; }
352 if (x !== null) { this._translateX = unit(x, 'px'); }
353 if (y !== null) { this._translateY = unit(y, 'px'); }
355 this.translate = this._translateX + "," + this._translateY;
361 return this._translateX || 0;
365 return this._translateY || 0;
369 var s = (this.scale || "1,1").split(',');
370 if (s[0]) { s[0] = parseFloat(s[0]); }
371 if (s[1]) { s[1] = parseFloat(s[1]); }
374 // "2.5,1" => [2.5,1]
375 return (s[0] === s[1]) ? s[0] : s;
378 rotate3d: function() {
379 var s = (this.rotate3d || "0,0,0,0deg").split(',');
380 for (var i=0; i<=3; ++i) {
381 if (s[i]) { s[i] = parseFloat(s[i]); }
383 if (s[3]) { s[3] = unit(s[3], 'deg'); }
390 // Parses from a string. Called on constructor.
391 parse: function(str) {
393 str.replace(/([a-zA-Z0-9]+)\((.*?)\)/g, function(x, prop, val) {
394 self.setFromString(prop, val);
399 // Converts to a `transition` CSS property string. If `use3d` is given,
400 // it converts to a `-webkit-transition` CSS property string instead.
401 toString: function(use3d) {
404 for (var i in this) {
405 if (this.hasOwnProperty(i)) {
406 // Don't use 3D transformations if the browser can't support it.
407 if ((!support.transform3d) && (
410 (i === 'perspective') ||
411 (i === 'transformOrigin'))) { continue; }
414 if (use3d && (i === 'scale')) {
415 re.push(i + "3d(" + this[i] + ",1)");
416 } else if (use3d && (i === 'translate')) {
417 re.push(i + "3d(" + this[i] + ",0)");
419 re.push(i + "(" + this[i] + ")");
429 function callOrQueue(self, queue, fn) {
430 if (queue === true) {
433 self.queue(queue, fn);
439 // ### getProperties(dict)
440 // Returns properties (for `transition-property`) for dictionary `props`. The
441 // value of `props` is what you would expect in `$.css(...)`.
442 function getProperties(props) {
445 $.each(props, function(key) {
446 key = $.camelCase(key); // Convert "text-align" => "textAlign"
447 key = $.transit.propertyMap[key] || key;
448 key = uncamel(key); // Convert back to dasherized
450 if ($.inArray(key, re) === -1) { re.push(key); }
456 // ### getTransition()
457 // Returns the transition string to be used for the `transition` CSS property.
461 // getTransition({ opacity: 1, rotate: 30 }, 500, 'ease');
462 // //=> 'opacity 500ms ease, -webkit-transform 500ms ease'
464 function getTransition(properties, duration, easing, delay) {
465 // Get the CSS properties needed.
466 var props = getProperties(properties);
468 // Account for aliases (`in` => `ease-in`).
469 if ($.cssEase[easing]) { easing = $.cssEase[easing]; }
471 // Build the duration/easing/delay attributes for it.
472 var attribs = '' + toMS(duration) + ' ' + easing;
473 if (parseInt(delay, 10) > 0) { attribs += ' ' + toMS(delay); }
475 // For more properties, add them this way:
476 // "margin 200ms ease, padding 200ms ease, ..."
477 var transitions = [];
478 $.each(props, function(i, name) {
479 transitions.push(name + ' ' + attribs);
482 return transitions.join(', ');
485 // ## $.fn.transition
486 // Works like $.fn.animate(), but uses CSS transitions.
488 // $("...").transition({ opacity: 0.1, scale: 0.3 });
490 // // Specific duration
491 // $("...").transition({ opacity: 0.1, scale: 0.3 }, 500);
493 // // With duration and easing
494 // $("...").transition({ opacity: 0.1, scale: 0.3 }, 500, 'in');
497 // $("...").transition({ opacity: 0.1, scale: 0.3 }, function() { ... });
499 // // With everything
500 // $("...").transition({ opacity: 0.1, scale: 0.3 }, 500, 'in', function() { ... });
502 // // Alternate syntax
503 // $("...").transition({
508 // complete: function() { /* ... */ }
511 $.fn.transition = $.fn.transit = function(properties, duration, easing, callback) {
516 // Account for `.transition(properties, callback)`.
517 if (typeof duration === 'function') {
519 duration = undefined;
522 // Account for `.transition(properties, duration, callback)`.
523 if (typeof easing === 'function') {
529 if (typeof properties.easing !== 'undefined') {
530 easing = properties.easing;
531 delete properties.easing;
534 if (typeof properties.duration !== 'undefined') {
535 duration = properties.duration;
536 delete properties.duration;
539 if (typeof properties.complete !== 'undefined') {
540 callback = properties.complete;
541 delete properties.complete;
544 if (typeof properties.queue !== 'undefined') {
545 queue = properties.queue;
546 delete properties.queue;
549 if (typeof properties.delay !== 'undefined') {
550 delay = properties.delay;
551 delete properties.delay;
554 // Set defaults. (`400` duration, `ease` easing)
555 if (typeof duration === 'undefined') { duration = $.fx.speeds._default; }
556 if (typeof easing === 'undefined') { easing = $.cssEase._default; }
558 duration = $.fx.off ? 0 : toMS(duration);
560 // Build the `transition` property.
561 var transitionValue = getTransition(properties, duration, easing, delay);
563 // Compute delay until callback.
564 // If this becomes 0, don't bother setting the transition property.
565 var work = $.transit.enabled && support.transition;
566 var i = work ? (parseInt(duration, 10) + parseInt(delay, 10)) : 0;
568 // If there's nothing to do...
570 var fn = function(next) {
571 self.css(properties);
572 if (callback) { callback(); }
576 callOrQueue(self, queue, fn);
580 // Save the old transitions of each element so we can restore it later.
581 var oldTransitions = {};
583 var run = function(nextCall) {
586 // Prepare the callback.
587 var cb = function() {
588 if (bound) { self.unbind(transitionEnd, cb); }
591 self.each(function() {
592 this.style[support.transition] = (oldTransitions[this] || null);
596 if (typeof callback === 'function') { callback.apply(self); }
597 if (typeof nextCall === 'function') { nextCall(); }
600 if ((i > 0) && (transitionEnd) && ($.transit.useTransitionEnd)) {
601 // Use the 'transitionend' event if it's available.
603 self.bind(transitionEnd, cb);
605 // Fallback to timers if the 'transitionend' event isn't supported.
606 window.setTimeout(cb, i);
609 // Apply transitions.
610 self.each(function() {
612 this.style[support.transition] = transitionValue;
614 $(this).css(properties);
618 // Defer running. This allows the browser to paint any pending CSS it hasn't
619 // painted yet before doing the transitions.
620 var deferredRun = function(next) {
623 // Durations that are too slow will get transitions mixed up.
624 // (Tested on Mac/FF 7.0.1)
625 if ((support.transition === 'MozTransition') && (i < 25)) { i = 25; }
627 window.setTimeout(function() { run(next); }, i);
630 // Use jQuery's fx queue.
631 callOrQueue(self, queue, deferredRun);
637 function registerCssHook(prop, isPixels) {
638 // For certain properties, the 'px' should not be implied.
639 if (!isPixels) { $.cssNumber[prop] = true; }
641 $.transit.propertyMap[prop] = support.transform;
644 get: function(elem) {
645 var t = $(elem).css('transform');
649 set: function(elem, value) {
650 var t = $(elem).css('transform');
651 t.setFromString(prop, value);
653 $(elem).css({ transform: t });
659 // Converts a camelcase string to a dasherized string.
660 // (`marginLeft` => `margin-left`)
661 function uncamel(str) {
662 return str.replace(/([A-Z])/g, function(letter) { return '-' + letter.toLowerCase(); });
665 // ### unit(number, unit)
666 // Ensures that number `number` has a unit. If no unit is found, assume the
667 // default is `unit`.
669 // unit(2, 'px') //=> "2px"
670 // unit("30deg", 'rad') //=> "30deg"
672 function unit(i, units) {
673 if ((typeof i === "string") && (!i.match(/^[\-0-9\.]+$/))) {
676 return "" + i + units;
680 // ### toMS(duration)
681 // Converts given `duration` to a millisecond string.
683 // toMS('fast') //=> '400ms'
684 // toMS(10) //=> '10ms'
686 function toMS(duration) {
689 // Allow for string durations like 'fast'.
690 if ($.fx.speeds[i]) { i = $.fx.speeds[i]; }
692 return unit(i, 'ms');
695 // Export some functions for testable-ness.
696 $.transit.getTransitionValue = getTransition;