');
var shown = false;
@@ -424,7 +432,7 @@ describe('$$interimElement service', function() {
expect(shown).toBe(true);
}));
- it('allows parent getter', inject(function($rootScope) {
+ it('allows parent getter', inject(function() {
var parent = angular.element('
');
var parentGetter = jasmine.createSpy('parentGetter').and.returnValue(parent);
@@ -496,7 +504,7 @@ describe('$$interimElement service', function() {
}
}));
- it('resolves the show promise', inject(function($animate, $rootScope) {
+ it('resolves the show promise', inject(function( ) {
var resolved = false;
Service.show().then(function(arg) {
@@ -614,13 +622,13 @@ describe('$$interimElement service', function() {
$provide.value('$mdCompiler', $mdCompiler);
$provide.value('$mdTheming', $themingSpy);
});
- inject(function($q, $compile, _$rootScope_, _$animate_, _$timeout_) {
+ inject(function($q, $compile, _$rootScope_, _$$rAF_, _$timeout_) {
$rootScope = _$rootScope_;
- $animate = _$animate_;
+ $$rAF = _$$rAF_;
$timeout = _$timeout_;
$compilerSpy.and.callFake(function(opts) {
- var el = $compile(opts.template);
+ var el = $compile(opts.template || "
");
return $q(function(resolve){
resolve({
link: el,
@@ -644,9 +652,13 @@ describe('$$interimElement service', function() {
}
function flush() {
- $rootScope.$digest();
- $animate.triggerCallbacks();
- $timeout.flush();
+ try {
+ $timeout.flush();
+ $rootScope.$apply();
+ $$rAF.flush();
+ } finally {
+ $timeout.flush();
+ }
}
function tailHook( sourceFn, hookFn ) {
diff --git a/src/core/util/animate.js b/src/core/util/animation/animate.js
similarity index 68%
rename from src/core/util/animate.js
rename to src/core/util/animation/animate.js
index 3abb0dc3b39..b8e2dda4601 100644
--- a/src/core/util/animate.js
+++ b/src/core/util/animation/animate.js
@@ -1,49 +1,46 @@
angular
.module('material.core')
- .factory('$$mdAnimate', function($$rAF, $q, $timeout, $mdConstant){
+ .factory('$$mdAnimate', function($q, $timeout, $mdConstant, $animateCss){
// Since $$mdAnimate is injected into $mdUtil... use a wrapper function
// to subsequently inject $mdUtil as an argument to the AnimateDomUtils
return function($mdUtil) {
- return AnimateDomUtils( $mdUtil, $$rAF, $q, $timeout, $mdConstant);
+ return AnimateDomUtils( $mdUtil, $q, $timeout, $mdConstant, $animateCss);
};
});
/**
* Factory function that requires special injections
*/
-function AnimateDomUtils($mdUtil, $$rAF, $q, $timeout, $mdConstant) {
+function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) {
var self;
return self = {
/**
*
*/
translate3d : function( target, from, to, options ) {
- // Set translate3d style to start at the `from` origin
- target.css(from);
-
- // Wait while CSS takes affect
- // Set the `to` styles and run the transition-in styles
- $$rAF(function () {
- target.css(to).addClass(options.transitionInClass);
+ return $animateCss(target,{
+ from:from,
+ to:to,
+ addClass:options.transitionInClass
+ })
+ .start()
+ .then(function(){
+ // Resolve with reverser function...
+ return reverseTranslate;
});
- return self
- .waitTransitionEnd(target)
- .then(function(){
- // Resolve with reverser function...
- return reverseTranslate;
- });
-
/**
* Specific reversal of the request translate animation above...
*/
function reverseTranslate (newFrom) {
- target.removeClass(options.transitionInClass)
- .addClass(options.transitionOutClass)
- .css( newFrom || from );
- return self.waitTransitionEnd(target);
+ return $animateCss(target, {
+ to: newFrom || from,
+ addClass: options.transitionOutClass,
+ removeClass: options.transitionInClass
+ }).start();
+
}
},
@@ -104,24 +101,60 @@ function AnimateDomUtils($mdUtil, $$rAF, $q, $timeout, $mdConstant) {
zoomStyle = buildZoom({
centerX: originCenterPt.x - dialogCenterPt.x,
centerY: originCenterPt.y - dialogCenterPt.y,
- scaleX: Math.min(0.5, originBnds.width / dialogRect.width),
- scaleY: Math.min(0.5, originBnds.height / dialogRect.height)
+ scaleX: Math.round(100 * Math.min(0.5, originBnds.width / dialogRect.width))/100,
+ scaleY: Math.round(100 * Math.min(0.5, originBnds.height / dialogRect.height))/100
});
}
return zoomStyle;
},
+ /**
+ * Enhance raw values to represent valid css stylings...
+ */
+ toCss : function( raw ) {
+ var css = { };
+ var lookups = 'left top right bottom width height x y min-width min-height max-width max-height';
+
+ angular.forEach(raw, function(value,key) {
+ if ( angular.isUndefined(value) ) return;
+
+ if ( lookups.indexOf(key) >= 0 ) {
+ css[key] = value + 'px';
+ } else {
+ switch (key) {
+ case 'transform':
+ convertToVendor(key, $mdConstant.CSS.TRANSFORM, value);
+ break;
+ case 'transformOrigin':
+ convertToVendor(key, $mdConstant.CSS.TRANSFORM_ORIGIN, value);
+ break;
+ }
+ }
+ });
+
+ return css;
+
+ function convertToVendor(key, vendor, value) {
+ angular.forEach(vendor.split(' '), function (key) {
+ css[key] = value;
+ });
+ }
+ },
+
/**
* Convert the translate CSS value to key/value pair(s).
*/
- toTransformCss: function (transform, addTransition) {
+ toTransformCss: function (transform, addTransition, transition) {
var css = {};
angular.forEach($mdConstant.CSS.TRANSFORM.split(' '), function (key) {
css[key] = transform;
});
- if (addTransition) css['transition'] = "all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important";
+ if (addTransition) {
+ transition = transition || "all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important";
+ css['transition'] = transition;
+ }
return css;
},
diff --git a/src/core/util/animate.spec.js b/src/core/util/animation/animate.spec.js
similarity index 100%
rename from src/core/util/animate.spec.js
rename to src/core/util/animation/animate.spec.js
diff --git a/src/core/util/animation/animateCss.js b/src/core/util/animation/animateCss.js
new file mode 100644
index 00000000000..f62cb2b21d3
--- /dev/null
+++ b/src/core/util/animation/animateCss.js
@@ -0,0 +1,402 @@
+"use strict";
+
+if (angular.version.minor >= 4) {
+ angular.module('material.animate', []);
+} else {
+(function() {
+
+ var forEach = angular.forEach;
+
+ var WEBKIT = window.ontransitionend === undefined && window.onwebkittransitionend !== undefined;
+ var TRANSITION_PROP = WEBKIT ? 'WebkitTransition' : 'transition';
+ var ANIMATION_PROP = WEBKIT ? 'WebkitAnimation' : 'animation';
+ var PREFIX = WEBKIT ? '-webkit-' : '';
+
+ var TRANSITION_EVENTS = (WEBKIT ? 'webkitTransitionEnd ' : '') + 'transitionend';
+ var ANIMATION_EVENTS = (WEBKIT ? 'webkitAnimationEnd ' : '') + 'animationend';
+
+ var $$ForceReflowFactory = ['$document', function($document) {
+ return function() {
+ return $document[0].body.clientWidth + 1;
+ }
+ }];
+
+ var $$rAFMutexFactory = ['$$rAF', function($$rAF) {
+ return function() {
+ var passed = false;
+ $$rAF(function() {
+ passed = true;
+ });
+ return function(fn) {
+ passed ? fn() : $$rAF(fn);
+ };
+ };
+ }];
+
+ var $$AnimateRunnerFactory = ['$q', '$$rAFMutex', function($q, $$rAFMutex) {
+ var INITIAL_STATE = 0;
+ var DONE_PENDING_STATE = 1;
+ var DONE_COMPLETE_STATE = 2;
+
+ function AnimateRunner(host) {
+ this.setHost(host);
+
+ this._doneCallbacks = [];
+ this._runInAnimationFrame = $$rAFMutex();
+ this._state = 0;
+ }
+
+ AnimateRunner.prototype = {
+ setHost: function(host) {
+ this.host = host || {};
+ },
+
+ done: function(fn) {
+ if (this._state === DONE_COMPLETE_STATE) {
+ fn();
+ } else {
+ this._doneCallbacks.push(fn);
+ }
+ },
+
+ progress: angular.noop,
+
+ getPromise: function() {
+ if (!this.promise) {
+ var self = this;
+ this.promise = $q(function(resolve, reject) {
+ self.done(function(status) {
+ status === false ? reject() : resolve();
+ });
+ });
+ }
+ return this.promise;
+ },
+
+ then: function(resolveHandler, rejectHandler) {
+ return this.getPromise().then(resolveHandler, rejectHandler);
+ },
+
+ 'catch': function(handler) {
+ return this.getPromise()['catch'](handler);
+ },
+
+ 'finally': function(handler) {
+ return this.getPromise()['finally'](handler);
+ },
+
+ pause: function() {
+ if (this.host.pause) {
+ this.host.pause();
+ }
+ },
+
+ resume: function() {
+ if (this.host.resume) {
+ this.host.resume();
+ }
+ },
+
+ end: function() {
+ if (this.host.end) {
+ this.host.end();
+ }
+ this._resolve(true);
+ },
+
+ cancel: function() {
+ if (this.host.cancel) {
+ this.host.cancel();
+ }
+ this._resolve(false);
+ },
+
+ complete: function(response) {
+ var self = this;
+ if (self._state === INITIAL_STATE) {
+ self._state = DONE_PENDING_STATE;
+ self._runInAnimationFrame(function() {
+ self._resolve(response);
+ });
+ }
+ },
+
+ _resolve: function(response) {
+ if (this._state !== DONE_COMPLETE_STATE) {
+ forEach(this._doneCallbacks, function(fn) {
+ fn(response);
+ });
+ this._doneCallbacks.length = 0;
+ this._state = DONE_COMPLETE_STATE;
+ }
+ }
+ };
+
+ return AnimateRunner;
+ }];
+
+ angular
+ .module('material.animate', [])
+ .factory('$$forceReflow', $$ForceReflowFactory)
+ .factory('$$AnimateRunner', $$AnimateRunnerFactory)
+ .factory('$$rAFMutex', $$rAFMutexFactory)
+ .factory('$animateCss', ['$window', '$$rAF', '$$AnimateRunner', '$$forceReflow', '$$jqLite', '$timeout',
+ function($window, $$rAF, $$AnimateRunner, $$forceReflow, $$jqLite, $timeout) {
+
+ function init(element, options) {
+
+ var temporaryStyles = [];
+ var node = getDomNode(element);
+
+ if (options.transitionStyle) {
+ temporaryStyles.push([PREFIX + 'transition', options.transitionStyle]);
+ }
+
+ if (options.keyframeStyle) {
+ temporaryStyles.push([PREFIX + 'animation', options.keyframeStyle]);
+ }
+
+ if (options.delay) {
+ temporaryStyles.push([PREFIX + 'transition-delay', options.delay + 's']);
+ }
+
+ if (options.duration) {
+ temporaryStyles.push([PREFIX + 'transition-duration', options.duration + 's']);
+ }
+
+ var hasCompleteStyles = options.keyframeStyle ||
+ (options.to && (options.duration > 0 || options.transitionStyle));
+ var hasCompleteClasses = !!options.addClass || !!options.removeClass;
+ var hasCompleteAnimation = hasCompleteStyles || hasCompleteClasses;
+
+ blockTransition(element, true);
+ applyAnimationFromStyles(element, options);
+
+ var animationClosed = false;
+ var events, eventFn;
+
+ return {
+ close: close,
+ start: function() {
+ var runner = new $$AnimateRunner();
+ waitUntilQuiet(function() {
+ blockTransition(element, false);
+ if (!hasCompleteAnimation) {
+ return close();
+ }
+
+ forEach(temporaryStyles, function(entry) {
+ var key = entry[0];
+ var value = entry[1];
+ node.style[camelCase(key)] = value;
+ });
+
+ applyClasses(element, options);
+
+ var timings = computeTimings(element);
+ if (timings.duration === 0) {
+ return close();
+ }
+
+ var moreStyles = [];
+
+ if (options.easing) {
+ if (timings.transitionDuration) {
+ moreStyles.push([PREFIX + 'transition-timing-function', options.easing]);
+ }
+ if (timings.animationDuration) {
+ moreStyles.push([PREFIX + 'animation-timing-function', options.easing]);
+ }
+ }
+
+ if (options.delay && timings.animationDelay) {
+ moreStyles.push([PREFIX + 'animation-delay', options.delay + 's']);
+ }
+
+ if (options.duration && timings.animationDuration) {
+ moreStyles.push([PREFIX + 'animation-duration', options.duration + 's']);
+ }
+
+ forEach(moreStyles, function(entry) {
+ var key = entry[0];
+ var value = entry[1];
+ node.style[camelCase(key)] = value;
+ temporaryStyles.push(entry);
+ });
+
+ var maxDelay = timings.delay;
+ var maxDelayTime = maxDelay * 1000;
+ var maxDuration = timings.duration;
+ var maxDurationTime = maxDuration * 1000;
+ var startTime = Date.now();
+
+ events = [];
+ if (timings.transitionDuration) {
+ events.push(TRANSITION_EVENTS);
+ }
+ if (timings.animationDuration) {
+ events.push(ANIMATION_EVENTS);
+ }
+ events = events.join(' ');
+ eventFn = function(event) {
+ event.stopPropagation();
+ var ev = event.originalEvent || event;
+ var timeStamp = ev.timeStamp || Date.now();
+ var elapsedTime = parseFloat(ev.elapsedTime.toFixed(3));
+ if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) {
+ close();
+ }
+ };
+ element.on(events, eventFn);
+
+ applyAnimationToStyles(element, options);
+
+ $timeout(close, maxDelayTime + maxDurationTime * 1.5, false);
+ });
+
+ return runner;
+
+ function close() {
+ if (animationClosed) return;
+ animationClosed = true;
+
+ if (events && eventFn) {
+ element.off(events, eventFn);
+ }
+ applyClasses(element, options);
+ applyAnimationStyles(element, options);
+ forEach(temporaryStyles, function(entry) {
+ node.style[camelCase(entry[0])] = '';
+ });
+ runner.complete(true);
+ return runner;
+ }
+ }
+ }
+ }
+
+ function applyClasses(element, options) {
+ if (options.addClass) {
+ $$jqLite.addClass(element, options.addClass);
+ options.addClass = null;
+ }
+ if (options.removeClass) {
+ $$jqLite.removeClass(element, options.removeClass);
+ options.removeClass = null;
+ }
+ }
+
+ function computeTimings(element) {
+ var node = getDomNode(element);
+ var cs = $window.getComputedStyle(node)
+ var tdr = parseMaxTime(cs[prop('transitionDuration')]);
+ var adr = parseMaxTime(cs[prop('animationDuration')]);
+ var tdy = parseMaxTime(cs[prop('transitionDelay')]);
+ var ady = parseMaxTime(cs[prop('animationDelay')]);
+
+ adr *= (parseInt(cs[prop('animationIterationCount')], 10) || 1);
+ var duration = Math.max(adr, tdr);
+ var delay = Math.max(ady, tdy);
+
+ return {
+ duration: duration,
+ delay: delay,
+ animationDuration: adr,
+ transitionDuration: tdr,
+ animationDelay: ady,
+ transitionDelay: tdy
+ };
+
+ function prop(key) {
+ return WEBKIT ? 'Webkit' + key.charAt(0).toUpperCase() + key.substr(1)
+ : key;
+ }
+ }
+
+ function parseMaxTime(str) {
+ var maxValue = 0;
+ var values = str.split(/\s*,\s*/);
+ forEach(values, function(value) {
+ // it's always safe to consider only second values and omit `ms` values since
+ // getComputedStyle will always handle the conversion for us
+ if (value.charAt(value.length - 1) == 's') {
+ value = value.substring(0, value.length - 1);
+ }
+ value = parseFloat(value) || 0;
+ maxValue = maxValue ? Math.max(value, maxValue) : value;
+ });
+ return maxValue;
+ }
+
+ var cancelLastRAFRequest;
+ var rafWaitQueue = [];
+ function waitUntilQuiet(callback) {
+ if (cancelLastRAFRequest) {
+ cancelLastRAFRequest(); //cancels the request
+ }
+ rafWaitQueue.push(callback);
+ cancelLastRAFRequest = $$rAF(function() {
+ cancelLastRAFRequest = null;
+
+ // DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable.
+ // PLEASE EXAMINE THE `$$forceReflow` service to understand why.
+ var pageWidth = $$forceReflow();
+
+ // we use a for loop to ensure that if the queue is changed
+ // during this looping then it will consider new requests
+ for (var i = 0; i < rafWaitQueue.length; i++) {
+ rafWaitQueue[i](pageWidth);
+ }
+ rafWaitQueue.length = 0;
+ });
+ }
+
+ function applyAnimationStyles(element, options) {
+ applyAnimationFromStyles(element, options);
+ applyAnimationToStyles(element, options);
+ }
+
+ function applyAnimationFromStyles(element, options) {
+ if (options.from) {
+ element.css(options.from);
+ options.from = null;
+ }
+ }
+
+ function applyAnimationToStyles(element, options) {
+ if (options.to) {
+ element.css(options.to);
+ options.to = null;
+ }
+ }
+
+ function getDomNode(element) {
+ for (var i = 0; i < element.length; i++) {
+ if (element[i].nodeType === 1) return element[i];
+ }
+ }
+
+ function blockTransition(element, bool) {
+ var node = getDomNode(element);
+ var key = camelCase(PREFIX + 'transition-delay');
+ node.style[key] = bool ? '-9999s' : '';
+ }
+
+ return init;
+ }]);
+
+ /**
+ * Older browsers [FF31] expect camelCase
+ * property keys.
+ * e.g.
+ * animation-duration --> animationDuration
+ */
+ function camelCase(str) {
+ return str.replace(/-[a-z]/g, function(str) {
+ return str.charAt(1).toUpperCase();
+ });
+ }
+
+})();
+
+}
diff --git a/src/core/util/animation/animateCss.spec.js b/src/core/util/animation/animateCss.spec.js
new file mode 100644
index 00000000000..a069a6b00ef
--- /dev/null
+++ b/src/core/util/animation/animateCss.spec.js
@@ -0,0 +1,457 @@
+describe('$animateCss', function() {
+
+ var jqLite = angular.element;
+ var forEach = angular.forEach;
+
+ var fromStyles, toStyles, addClassVal = 'to-add', removeClassVal = 'to-remove';
+ var element, ss, doneSpy;
+ var triggerAnimationStartFrame, moveAnimationClock;
+
+ beforeEach(module('material.animate'));
+
+ beforeEach(module(function() {
+ return function($window, $document, $$rAF, $timeout, $rootElement) {
+ ss = createMockStyleSheet($document, $window);
+ element = jqLite('
');
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ ss.addRule('.to-add', 'transition:0.5s linear all; font-size:100px;');
+ ss.addRule('.to-remove', 'transition:0.5s linear all; border:10px solid black;');
+
+ triggerAnimationStartFrame = function() {
+ $$rAF.flush();
+ };
+
+ doneSpy = jasmine.createSpy();
+ fromStyles = { backgroundColor: 'red' };
+ toStyles = { backgroundColor: 'blue' };
+
+ moveAnimationClock = function(duration, delay) {
+ var time = (delay || 0) + duration * 1.5;
+ $timeout.flush(time * 1000);
+ }
+ };
+ }));
+
+ afterEach(function() {
+ ss.destroy();
+ element.remove();
+ });
+
+
+ describe('[addClass]', function() {
+ it('should not trigger an animation if the class doesn\'t exist',
+ inject(function($animateCss) {
+
+ $animateCss(element, { addClass: 'something-fake' }).start().done(doneSpy);
+ triggerAnimationStartFrame();
+
+ assertHasClass(element, 'something-fake');
+ expect(doneSpy).toHaveBeenCalled();
+ }));
+
+ it('should not trigger an animation if the class doesn\'t contain a transition value or a keyframe value',
+ inject(function($animateCss) {
+
+ ss.addRule('.something-real', 'background-color:orange');
+
+ $animateCss(element, { addClass: 'something-real' }).start().done(doneSpy);
+ triggerAnimationStartFrame();
+
+ assertHasClass(element, 'something-real');
+ expect(doneSpy).toHaveBeenCalled();
+ }));
+
+ it('should trigger an animation if a transition is detected on the class that is being added',
+ inject(function($animateCss) {
+
+ ss.addRule('.something-shiny', 'transition:0.5s linear all; background-color: gold;');
+
+ $animateCss(element, { addClass: 'something-shiny' }).start().done(doneSpy);
+ triggerAnimationStartFrame();
+
+ expect(doneSpy).not.toHaveBeenCalled();
+ moveAnimationClock(1);
+ expect(doneSpy).toHaveBeenCalled();
+ }));
+
+ it('should trigger an animation if a keyframe is detected on the class that is being added',
+ inject(function($animateCss) {
+
+ ss.addRule('.something-spinny', 'animation: 0.5s rotate linear; animation: 0.5s rotate linear;');
+
+ $animateCss(element, { addClass: 'something-spinny' }).start().done(doneSpy);
+ triggerAnimationStartFrame();
+
+ expect(doneSpy).not.toHaveBeenCalled();
+ moveAnimationClock(1);
+ expect(doneSpy).toHaveBeenCalled();
+ }));
+
+ it('should trigger an animation if both a transition and keyframe is detected on the class that is being added and choose the max duration value',
+ inject(function($animateCss) {
+
+ ss.addRule('.something-shiny', 'transition:1.5s linear all; background-color: gold;');
+ ss.addRule('.something-spinny', 'webkit-animation: 0.5s rotate linear; animation: 0.5s rotate linear;');
+
+ $animateCss(element, { addClass: 'something-spinny something-shiny' }).start().done(doneSpy);
+ triggerAnimationStartFrame();
+
+ expect(doneSpy).not.toHaveBeenCalled();
+ moveAnimationClock(1.5);
+ expect(doneSpy).toHaveBeenCalled();
+ }));
+
+ it('should trigger an animation if a non-animatable class is added with a duration property',
+ inject(function($animateCss) {
+
+ ss.addRule('.something-boring', 'border:20px solid red;');
+
+ $animateCss(element, {
+ addClass: 'something-boring',
+ duration: 2
+ }).start().done(doneSpy);
+ triggerAnimationStartFrame();
+
+ expect(doneSpy).not.toHaveBeenCalled();
+ moveAnimationClock(2);
+ expect(doneSpy).toHaveBeenCalled();
+ }));
+ });
+
+ describe('[removeClass]', function() {
+ it('should not trigger an animation if the animatable className is removed',
+ inject(function($animateCss) {
+
+ element.addClass(removeClassVal);
+
+ $animateCss(element, {
+ removeClass: removeClassVal
+ }).start().done(doneSpy);
+
+ triggerAnimationStartFrame();
+ assertHasClass(element, removeClassVal, true);
+ }));
+
+ it('should trigger an animation if the element contains a transition already when the class is removed',
+ inject(function($animateCss) {
+
+ element.addClass(removeClassVal);
+ element.addClass('something-to-remove');
+
+ $animateCss(element, {
+ removeClass: 'something-to-remove'
+ }).start().done(doneSpy);
+
+ triggerAnimationStartFrame();
+ expect(doneSpy).not.toHaveBeenCalled();
+ moveAnimationClock(0.5);
+ expect(doneSpy).toHaveBeenCalled();
+ }));
+
+ it('should trigger an animation if the element contains a keyframe already when the class is removed',
+ inject(function($animateCss) {
+
+ ss.addRule('.something-that-spins', 'webkit-animation: 0.5s rotate linear; animation: 0.5s rotate linear;');
+
+ element.addClass('something-that-spins');
+ element.addClass('something-to-remove');
+
+ $animateCss(element, {
+ removeClass: 'something-to-remove'
+ }).start().done(doneSpy);
+
+ triggerAnimationStartFrame();
+ expect(doneSpy).not.toHaveBeenCalled();
+ moveAnimationClock(0.5);
+ expect(doneSpy).toHaveBeenCalled();
+ }));
+
+ it('should still perform an animation if an animatable class is removed, but a [duration] property is used',
+ inject(function($animateCss) {
+
+ ss.addRule('.something-that-is-hidden', 'transition:0.5s linear all; opacity:0;');
+ element.addClass('something-that-spins');
+
+ $animateCss(element, {
+ removeClass: 'something-that-is-hidden',
+ duration: 0.5
+ }).start().done(doneSpy);
+
+ triggerAnimationStartFrame();
+ expect(doneSpy).not.toHaveBeenCalled();
+ moveAnimationClock(0.5);
+ expect(doneSpy).toHaveBeenCalled();
+ }));
+ });
+
+ describe('[transitionStyle]', function() {
+ it('should not trigger an animation if the property is the only thing property',
+ inject(function($animateCss) {
+
+ $animateCss(element, {
+ transitionStyle: '3s linear all'
+ }).start().done(doneSpy);
+
+ triggerAnimationStartFrame();
+ expect(doneSpy).toHaveBeenCalled();
+ }));
+
+ it('should apply the provided porperty to the animation if there are [toStyles] used',
+ inject(function($animateCss) {
+
+ $animateCss(element, {
+ transitionStyle: '3s linear all',
+ to: toStyles
+ }).start().done(doneSpy);
+
+ triggerAnimationStartFrame();
+ expect(doneSpy).not.toHaveBeenCalled();
+ moveAnimationClock(3);
+ expect(doneSpy).toHaveBeenCalled();
+ }));
+
+ it('should apply the provided porperty to the animation if there are [addClass] used',
+ inject(function($animateCss) {
+
+ ss.addRule('.boring-class', 'border:20px solid red;');
+
+ $animateCss(element, {
+ transitionStyle: '3s linear all',
+ addClass: 'boring-class'
+ }).start().done(doneSpy);
+
+ triggerAnimationStartFrame();
+ expect(doneSpy).not.toHaveBeenCalled();
+ moveAnimationClock(3);
+ expect(doneSpy).toHaveBeenCalled();
+ }));
+
+ it('should apply the provided porperty to the animation if there are [removeClass] used',
+ inject(function($animateCss) {
+
+ ss.addRule('.boring-class', 'border:20px solid red;');
+ element.addClass('boring-class');
+
+ $animateCss(element, {
+ transitionStyle: '3s linear all',
+ removeClass: 'boring-class'
+ }).start().done(doneSpy);
+
+ triggerAnimationStartFrame();
+ expect(doneSpy).not.toHaveBeenCalled();
+ moveAnimationClock(3);
+ expect(doneSpy).toHaveBeenCalled();
+ }));
+ });
+
+ describe('[from]', function() {
+ it('should not trigger an animation to run when only a [from] property is passed in',
+ inject(function($animateCss) {
+
+ $animateCss(element, {
+ from: fromStyles
+ }).start().done(doneSpy);
+
+ triggerAnimationStartFrame();
+ expect(doneSpy).toHaveBeenCalled();
+ }));
+
+ it('should not trigger an animation to run when a [from] and a [duration] property are passed in',
+ inject(function($animateCss) {
+
+ $animateCss(element, {
+ from: fromStyles,
+ duration: 1
+ }).start().done(doneSpy);
+
+ triggerAnimationStartFrame();
+ expect(doneSpy).toHaveBeenCalled();
+ }));
+
+ it('should apply the styles as soon as the animation is called',
+ inject(function($animateCss) {
+
+ var animator = $animateCss(element, {
+ from: fromStyles
+ });
+
+ assertStyle(element, fromStyles);
+ }));
+ });
+
+ describe('[to]', function() {
+ it('should not trigger an animation to run when only a [to] property is passed in',
+ inject(function($animateCss) {
+
+ $animateCss(element, {
+ to: toStyles
+ }).start().done(doneSpy);
+
+ triggerAnimationStartFrame();
+ expect(doneSpy).toHaveBeenCalled();
+ }));
+
+ it('should trigger an animation to run when both a [to] and a [duration] property are passed in',
+ inject(function($animateCss) {
+
+ $animateCss(element, {
+ to: toStyles,
+ duration: 1
+ }).start().done(doneSpy);
+
+ triggerAnimationStartFrame();
+ expect(doneSpy).not.toHaveBeenCalled();
+ moveAnimationClock(1);
+ expect(doneSpy).toHaveBeenCalled();
+ }));
+
+ it('should apply the styles right after the next frame',
+ inject(function($animateCss) {
+
+ $animateCss(element, {
+ to: toStyles
+ }).start().done(doneSpy);
+
+ triggerAnimationStartFrame();
+ assertStyle(element, toStyles);
+ }));
+ });
+
+ describe('[from] and [to]', function() {
+ it('should not trigger an animation if [duration] is not passed in',
+ inject(function($animateCss) {
+
+ $animateCss(element, {
+ from: fromStyles,
+ to: toStyles
+ }).start().done(doneSpy);
+
+ triggerAnimationStartFrame();
+ expect(doneSpy).toHaveBeenCalled();
+ }));
+
+ it('should trigger an animation if [duration] is passed in',
+ inject(function($animateCss) {
+
+ $animateCss(element, {
+ from: fromStyles,
+ to: toStyles,
+ duration: 1
+ }).start().done(doneSpy);
+
+ triggerAnimationStartFrame();
+ expect(doneSpy).not.toHaveBeenCalled();
+ moveAnimationClock(1);
+ expect(doneSpy).toHaveBeenCalled();
+ }));
+
+ it('should trigger an animation if detected from the provided [addClass] class value',
+ inject(function($animateCss) {
+
+ $animateCss(element, {
+ from: fromStyles,
+ to: toStyles,
+ addClass: addClassVal
+ }).start().done(doneSpy);
+
+ assertStyle(element, fromStyles);
+ triggerAnimationStartFrame();
+
+ assertStyle(element, toStyles);
+ expect(doneSpy).not.toHaveBeenCalled();
+ moveAnimationClock(1);
+ expect(doneSpy).toHaveBeenCalled();
+ }));
+
+ it('should trigger an animation if detected from the provided [removeClass] class value',
+ inject(function($animateCss) {
+
+ element.addClass(removeClassVal + ' something-else-to-remove');
+
+ $animateCss(element, {
+ from: fromStyles,
+ to: toStyles,
+ removeClass: 'something-else-to-remove'
+ }).start().done(doneSpy);
+
+ assertStyle(element, fromStyles);
+ triggerAnimationStartFrame();
+
+ assertStyle(element, toStyles);
+ expect(doneSpy).not.toHaveBeenCalled();
+ moveAnimationClock(1);
+ expect(doneSpy).toHaveBeenCalled();
+ }));
+ });
+
+ describe('[duration]', function() {
+ it('should not apply a duration if it is the only property used',
+ inject(function($animateCss) {
+
+ element.addClass(removeClassVal + ' something-else-to-remove');
+
+ $animateCss(element, {
+ duration: 2
+ }).start().done(doneSpy);
+
+ triggerAnimationStartFrame();
+ expect(doneSpy).toHaveBeenCalled();
+ }));
+
+ it('should apply a duration as an inline transition-duration style',
+ inject(function($animateCss) {
+
+ element.addClass(removeClassVal + ' something-else-to-remove');
+
+ $animateCss(element, {
+ duration: 2,
+ to: toStyles
+ }).start().done(doneSpy);
+
+ triggerAnimationStartFrame();
+ assertStyle(element, 'transition-duration', '2s');
+ }));
+
+ it('should apply a duration as an inline animation-duration style if only keyframes are used',
+ inject(function($animateCss) {
+
+ element.addClass(removeClassVal + ' something-else-to-remove');
+
+ $animateCss(element, {
+ keyframeStyle: '1s rotate linear',
+ duration: 2
+ }).start().done(doneSpy);
+
+ triggerAnimationStartFrame();
+ assertStyle(element, 'animation-duration', '2s');
+ }));
+ });
+
+ function assertHasClass(element, className, not) {
+ expect(element.hasClass(className)).toBe(!not);
+ }
+
+ function assertStyle(element, prop, val, not) {
+ var node = element[0];
+ var webKit = '-webkit-';
+ if (typeof prop === 'string') {
+ var assertion = expect(node.style[camelCase(prop)] || node.style[camelCase(webKit+prop)]);
+ not ? assertion.not.toBe(val) : assertion.toBe(val);
+ } else {
+ for (var key in prop) {
+ var val = prop[key];
+ var assertion = expect(node.style[camelCase(key)] || node.style[camelCase(webKit+key)]);
+ not ? assertion.not.toBe(val) : assertion.toBe(val);
+ }
+ }
+ }
+
+ function camelCase(str) {
+ return str.replace(/-[a-z]/g, function(str) {
+ return str.charAt(1).toUpperCase();
+ });
+ }
+
+});
diff --git a/test/angular-material-mocks.js b/test/angular-material-mocks.js
index 723a320ada6..376d3a7644c 100644
--- a/test/angular-material-mocks.js
+++ b/test/angular-material-mocks.js
@@ -26,8 +26,12 @@
* The `ngMaterial-mock` module provides support
*
*/
-angular.module('ngMaterial-mock', ['ngMock', 'material.core'])
- .config(['$provide', function($provide) {
+angular.module('ngMaterial-mock', [
+ 'ngMock',
+ 'ngAnimateMock',
+ 'material.core'
+ ])
+ .config(['$provide', function($provide) {
/**
* Angular Material dynamically generates Style tags
@@ -90,4 +94,79 @@ angular.module('ngMaterial-mock', ['ngMock', 'material.core'])
}]);
+ /**
+ * Stylesheet Mocks used by `animateCss.spec.js`
+ */
+ window.createMockStyleSheet = function createMockStyleSheet(doc, wind) {
+ doc = doc ? doc[0] : window.document;
+ wind = wind || window;
+
+ var node = doc.createElement('style');
+ var head = doc.getElementsByTagName('head')[0];
+ head.appendChild(node);
+
+ var ss = doc.styleSheets[doc.styleSheets.length - 1];
+
+ return {
+ addRule: function(selector, styles) {
+ styles = addVendorPrefix(styles);
+
+ try {
+ ss.insertRule(selector + '{ ' + styles + '}', 0);
+ }
+ catch (e) {
+ try {
+ ss.addRule(selector, styles);
+ }
+ catch (e2) {}
+ }
+ },
+
+ destroy: function() {
+ head.removeChild(node);
+ }
+ };
+
+ /**
+ * Decompose styles, attached specific vendor prefixes
+ * and recompose...
+ * e.g.
+ * 'transition:0.5s linear all; font-size:100px;'
+ * becomes
+ * '-webkit-transition:0.5s linear all; transition:0.5s linear all; font-size:100px;'
+ */
+ function addVendorPrefix(styles) {
+ var cache = { };
+
+ // Decompose into cache registry
+ styles
+ .match(/([\-A-Za-z]*)\w\:\w*([A-Za-z0-9\.\-\s]*)/gi)
+ .forEach(function(style){
+ var pair = style.split(":");
+ var key = pair[0];
+
+ switch(key) {
+ case 'transition':
+ case 'transform':
+ case 'animation':
+ case 'transition-duration':
+ case 'animation-duration':
+ cache[key] = cache['-webkit-' + key] = pair[1];
+ break;
+ default:
+ cache[key] = pair[1];
+ }
+ });
+
+ // Recompose full style object (as string)
+ styles = "";
+ angular.forEach(cache, function(value, key) {
+ styles = styles + key + ":" + value + "; ";
+ });
+
+ return styles;
+ }
+
+ };
+
})(window, window.angular);
diff --git a/test/angular-material-spec.js b/test/angular-material-spec.js
index 0643a1c32eb..7b6bbba7e2e 100644
--- a/test/angular-material-spec.js
+++ b/test/angular-material-spec.js
@@ -17,24 +17,78 @@
HTMLElement.prototype.click = function() {
var ev = document.createEvent('MouseEvent');
ev.initMouseEvent(
- 'click',
- /*bubble*/true, /*cancelable*/true,
- window, null,
- 0, 0, 0, 0, /*coordinates*/
- false, false, false, false, /*modifier keys*/
- 0/*button=left*/, null
+ 'click',
+ /*bubble*/true, /*cancelable*/true,
+ window, null,
+ 0, 0, 0, 0, /*coordinates*/
+ false, false, false, false, /*modifier keys*/
+ 0/*button=left*/, null
);
this.dispatchEvent(ev);
};
}
+ var enableAnimations;
+
+ afterEach(function() {
+ enableAnimations && enableAnimations();
+ enableAnimations = null;
+ });
+
beforeEach(function() {
/**
* Before each test, require that the 'ngMaterial-mock' module is ready for injection
* NOTE: assumes that angular-material-mocks.js has been loaded.
*/
- module('ngAnimateMock','ngMaterial-mock');
+
+ module('ngAnimate');
+ module('ngMaterial-mock');
+
+ module(function() {
+ return function($mdUtil, $rootElement, $document, $animate) {
+ var DISABLE_ANIMATIONS = 'disable_animations';
+
+ // Create special animation 'stop' function used
+ // to set 0ms durations for all animations and transitions
+
+ window.disableAnimations = function disableAnimations() {
+ var body = angular.element($document[0].body);
+ var head = angular.element($document[0].getElementsByTagName('head')[0]);
+ var styleSheet = angular.element( buildStopTransitions() );
+
+ $animate.enabled(false);
+
+ head.prepend(styleSheet);
+ body.addClass(DISABLE_ANIMATIONS);
+
+ // Prepare auto-restore
+ enableAnimations = function() {
+ body.removeClass(DISABLE_ANIMATIONS);
+ styleSheet.remove();
+ };
+ };
+
+ /**
+ * Build stylesheet to set all transition and animation
+ * durations' to zero.
+ */
+ function buildStopTransitions() {
+ var style = "";
+
+ return $mdUtil.supplant(style,[ DISABLE_ANIMATIONS,
+ "transition -webkit-transition animation -webkit-animation"
+ .split(" ")
+ .map(function(key){
+ return $mdUtil.supplant("{0}: 0s none !important",[key]);
+ })
+ .join("; ")
+ ]);
+
+ }
+
+ };
+ });
/**
* Mocks angular.element#focus ONLY for the duration of a particular test.
@@ -73,10 +127,10 @@
toHaveClass: function() {
return {
compare: function(actual, expected) {
- var results = { pass : true };
+ var results = {pass: true};
var classes = expected.trim().split(/\s+/);
- for (var i=0; i