diff --git a/src/ng/animate.js b/src/ng/animate.js index 449bbc6c34e7..4901dfb15550 100644 --- a/src/ng/animate.js +++ b/src/ng/animate.js @@ -72,6 +72,7 @@ var $$CoreAnimateQueueProvider = function() { enabled: noop, on: noop, off: noop, + pin: noop, push: function(element, event, options, domOperation) { domOperation && domOperation(); @@ -272,6 +273,25 @@ var $AnimateProvider = ['$provide', function($provide) { // be interpreted as null within the sub enabled function on: $$animateQueue.on, off: $$animateQueue.off, + + /** + * @ngdoc method + * @name $animate#pin + * @kind function + * @description Associates the provided element with a host parent element to allow the element to be animated even if it exists + * outside of the DOM structure of the Angular application. By doing so, any animation triggered via `$animate` can be issued on the + * element despite being outside the realm of the application or within another application. Say for example if the application + * was bootstrapped on an element that is somewhere inside of the `` tag, but we wanted to allow for an element to be situated + * as a direct child of `document.body`, then this can be achieved by pinning the element via `$animate.pin(element)`. Keep in mind + * that calling `$animate.pin(element, parentElement)` will not actually insert into the DOM anywhere; it will just create the association. + * + * Note that this feature is only active when the `ngAnimate` module is used. + * + * @param {DOMElement} element the external element that will be pinned + * @param {DOMElement} parentElement the host parent element that will be associated with the external element + */ + pin: $$animateQueue.pin, + enabled: $$animateQueue.enabled, cancel: function(runner) { diff --git a/src/ngAnimate/.jshintrc b/src/ngAnimate/.jshintrc index c6a112ff0f22..ff0fac6abb0b 100644 --- a/src/ngAnimate/.jshintrc +++ b/src/ngAnimate/.jshintrc @@ -22,6 +22,7 @@ "ELEMENT_NODE": false, "NG_ANIMATE_CHILDREN_DATA": false, + "assertArg": false, "isPromiseLike": false, "mergeClasses": false, "mergeAnimationOptions": false, diff --git a/src/ngAnimate/animateQueue.js b/src/ngAnimate/animateQueue.js index 72c67b068ec8..1b914485edba 100644 --- a/src/ngAnimate/animateQueue.js +++ b/src/ngAnimate/animateQueue.js @@ -1,6 +1,7 @@ 'use strict'; var NG_ANIMATE_ATTR_NAME = 'data-ng-animate'; +var NG_ANIMATE_PIN_DATA = '$ngAnimatePin'; var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { var PRE_DIGEST_STATE = 1; var RUNNING_STATE = 2; @@ -167,6 +168,12 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { } }, + pin: function(element, parentElement) { + assertArg(isElement(element), 'element', 'not an element'); + assertArg(isElement(parentElement), 'parentElement', 'not an element'); + element.data(NG_ANIMATE_PIN_DATA, parentElement); + }, + push: function(element, event, options, domOperation) { options = options || {}; options.domOperation = domOperation; @@ -490,6 +497,11 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { var parentAnimationDetected = false; var animateChildren; + var parentHost = element.data(NG_ANIMATE_PIN_DATA); + if (parentHost) { + parent = parentHost; + } + while (parent && parent.length) { if (!rootElementDetected) { // angular doesn't want to attempt to animate elements outside of the application @@ -521,6 +533,18 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { // there is no need to continue traversing at this point if (parentAnimationDetected && animateChildren === false) break; + if (!rootElementDetected) { + // angular doesn't want to attempt to animate elements outside of the application + // therefore we need to ensure that the rootElement is an ancestor of the current element + rootElementDetected = isMatchingElement(parent, $rootElement); + if (!rootElementDetected) { + parentHost = parent.data(NG_ANIMATE_PIN_DATA); + if (parentHost) { + parent = parentHost; + } + } + } + if (!bodyElementDetected) { // we also need to ensure that the element is or will be apart of the body element // otherwise it is pointless to even issue an animation to be rendered diff --git a/src/ngAnimate/shared.js b/src/ngAnimate/shared.js index eb83dc87e341..bfa5a80c5916 100644 --- a/src/ngAnimate/shared.js +++ b/src/ngAnimate/shared.js @@ -22,6 +22,13 @@ var isPromiseLike = function(p) { return p && p.then ? true : false; } +function assertArg(arg, name, reason) { + if (!arg) { + throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required")); + } + return arg; +} + function mergeClasses(a,b) { if (!a && !b) return ''; if (!a) return b; diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index d162b079e58d..5a0d730cf4ea 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -1114,6 +1114,74 @@ describe("animations", function() { })); }); + describe('.pin()', function() { + var capturedAnimation; + + beforeEach(module(function($provide) { + capturedAnimation = null; + $provide.factory('$$animation', function($$AnimateRunner) { + return function() { + capturedAnimation = arguments; + return new $$AnimateRunner(); + }; + }); + })); + + it('should allow an element to pinned elsewhere and still be available in animations', + inject(function($animate, $compile, $document, $rootElement, $rootScope) { + + var body = jqLite($document[0].body); + var innerParent = jqLite('
'); + body.append(innerParent); + innerParent.append($rootElement); + + var element = jqLite('
'); + body.append(element); + + $animate.addClass(element, 'red'); + $rootScope.$digest(); + expect(capturedAnimation).toBeFalsy(); + + $animate.pin(element, $rootElement); + + $animate.addClass(element, 'blue'); + $rootScope.$digest(); + expect(capturedAnimation).toBeTruthy(); + + dealoc(element); + })); + + it('should adhere to the disabled state of the hosted parent when an element is pinned', + inject(function($animate, $compile, $document, $rootElement, $rootScope) { + + var body = jqLite($document[0].body); + var innerParent = jqLite('
'); + body.append(innerParent); + innerParent.append($rootElement); + var innerChild = jqLite('
'); + $rootElement.append(innerChild); + + var element = jqLite('
'); + body.append(element); + + $animate.pin(element, innerChild); + + $animate.enabled(innerChild, false); + + $animate.addClass(element, 'blue'); + $rootScope.$digest(); + expect(capturedAnimation).toBeFalsy(); + + $animate.enabled(innerChild, true); + + $animate.addClass(element, 'red'); + $rootScope.$digest(); + expect(capturedAnimation).toBeTruthy(); + + dealoc(element); + })); + }); + describe('callbacks', function() { var captureLog = []; var capturedAnimation = [];