mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-20 16:30:26 +00:00
previously the compile/link fns executed in this order controlled via priority: - CompilePriorityHigh, CompilePriorityMedium, CompilePriorityLow - PreLinkPriorityHigh, PreLinkPriorityMedium, PreLinkPriorityLow - link children - PostLinkPriorityHigh, PostLinkPriorityMedium, PostLinkPriorityLow This was changed to: - CompilePriorityHigh, CompilePriorityMedium, CompilePriorityLow - PreLinkPriorityHigh, PreLinkPriorityMedium, PreLinkPriorityLow - link children - PostLinkPriorityLow, PostLinkPriorityMedium , PostLinkPriorityHigh Using this order the child transclusion directive that gets replaced onto the current element get executed correctly (see issue #3558), and more generally, the order of execution of post linking function makes more sense. The incorrect order was an oversight that has gone unnoticed for many suns and moons. (FYI: postLink functions are the default linking functions) BREAKING CHANGE: the order of postLink fn is now mirror opposite of the order in which corresponding preLinking and compile functions execute. Very few directives in practice rely on order of postLinking function (unlike on the order of compile functions), so in the rare case of this change affecting an existing directive, it might be necessary to convert it to a preLinking function or give it negative priority (look at the diff of this commit to see how an internal attribute interpolation directive was adjusted). Closes #3558
1487 lines
57 KiB
JavaScript
1487 lines
57 KiB
JavaScript
'use strict';
|
|
|
|
/* ! VARIABLE/FUNCTION NAMING CONVENTIONS THAT APPLY TO THIS FILE!
|
|
*
|
|
* DOM-related variables:
|
|
*
|
|
* - "node" - DOM Node
|
|
* - "element" - DOM Element or Node
|
|
* - "$node" or "$element" - jqLite-wrapped node or element
|
|
*
|
|
*
|
|
* Compiler related stuff:
|
|
*
|
|
* - "linkFn" - linking fn of a single directive
|
|
* - "nodeLinkFn" - function that aggregates all linking fns for a particular node
|
|
* - "childLinkFn" - function that aggregates all linking fns for child nodes of a particular node
|
|
* - "compositeLinkFn" - function that aggregates all linking fns for a compilation root (nodeList)
|
|
*/
|
|
|
|
|
|
/**
|
|
* @ngdoc function
|
|
* @name ng.$compile
|
|
* @function
|
|
*
|
|
* @description
|
|
* Compiles a piece of HTML string or DOM into a template and produces a template function, which
|
|
* can then be used to link {@link ng.$rootScope.Scope scope} and the template together.
|
|
*
|
|
* The compilation is a process of walking the DOM tree and trying to match DOM elements to
|
|
* {@link ng.$compileProvider#directive directives}. For each match it
|
|
* executes corresponding template function and collects the
|
|
* instance functions into a single template function which is then returned.
|
|
*
|
|
* The template function can then be used once to produce the view or as it is the case with
|
|
* {@link ng.directive:ngRepeat repeater} many-times, in which
|
|
* case each call results in a view that is a DOM clone of the original template.
|
|
*
|
|
<doc:example module="compile">
|
|
<doc:source>
|
|
<script>
|
|
// declare a new module, and inject the $compileProvider
|
|
angular.module('compile', [], function($compileProvider) {
|
|
// configure new 'compile' directive by passing a directive
|
|
// factory function. The factory function injects the '$compile'
|
|
$compileProvider.directive('compile', function($compile) {
|
|
// directive factory creates a link function
|
|
return function(scope, element, attrs) {
|
|
scope.$watch(
|
|
function(scope) {
|
|
// watch the 'compile' expression for changes
|
|
return scope.$eval(attrs.compile);
|
|
},
|
|
function(value) {
|
|
// when the 'compile' expression changes
|
|
// assign it into the current DOM
|
|
element.html(value);
|
|
|
|
// compile the new DOM and link it to the current
|
|
// scope.
|
|
// NOTE: we only compile .childNodes so that
|
|
// we don't get into infinite loop compiling ourselves
|
|
$compile(element.contents())(scope);
|
|
}
|
|
);
|
|
};
|
|
})
|
|
});
|
|
|
|
function Ctrl($scope) {
|
|
$scope.name = 'Angular';
|
|
$scope.html = 'Hello {{name}}';
|
|
}
|
|
</script>
|
|
<div ng-controller="Ctrl">
|
|
<input ng-model="name"> <br>
|
|
<textarea ng-model="html"></textarea> <br>
|
|
<div compile="html"></div>
|
|
</div>
|
|
</doc:source>
|
|
<doc:scenario>
|
|
it('should auto compile', function() {
|
|
expect(element('div[compile]').text()).toBe('Hello Angular');
|
|
input('html').enter('{{name}}!');
|
|
expect(element('div[compile]').text()).toBe('Angular!');
|
|
});
|
|
</doc:scenario>
|
|
</doc:example>
|
|
|
|
*
|
|
*
|
|
* @param {string|DOMElement} element Element or HTML string to compile into a template function.
|
|
* @param {function(angular.Scope[, cloneAttachFn]} transclude function available to directives.
|
|
* @param {number} maxPriority only apply directives lower then given priority (Only effects the
|
|
* root element(s), not their children)
|
|
* @returns {function(scope[, cloneAttachFn])} a link function which is used to bind template
|
|
* (a DOM element/tree) to a scope. Where:
|
|
*
|
|
* * `scope` - A {@link ng.$rootScope.Scope Scope} to bind to.
|
|
* * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the `template`
|
|
* and call the `cloneAttachFn` function allowing the caller to attach the
|
|
* cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is
|
|
* called as: <br> `cloneAttachFn(clonedElement, scope)` where:
|
|
*
|
|
* * `clonedElement` - is a clone of the original `element` passed into the compiler.
|
|
* * `scope` - is the current scope with which the linking function is working with.
|
|
*
|
|
* Calling the linking function returns the element of the template. It is either the original element
|
|
* passed in, or the clone of the element if the `cloneAttachFn` is provided.
|
|
*
|
|
* After linking the view is not updated until after a call to $digest which typically is done by
|
|
* Angular automatically.
|
|
*
|
|
* If you need access to the bound view, there are two ways to do it:
|
|
*
|
|
* - If you are not asking the linking function to clone the template, create the DOM element(s)
|
|
* before you send them to the compiler and keep this reference around.
|
|
* <pre>
|
|
* var element = $compile('<p>{{total}}</p>')(scope);
|
|
* </pre>
|
|
*
|
|
* - if on the other hand, you need the element to be cloned, the view reference from the original
|
|
* example would not point to the clone, but rather to the original template that was cloned. In
|
|
* this case, you can access the clone via the cloneAttachFn:
|
|
* <pre>
|
|
* var templateHTML = angular.element('<p>{{total}}</p>'),
|
|
* scope = ....;
|
|
*
|
|
* var clonedElement = $compile(templateHTML)(scope, function(clonedElement, scope) {
|
|
* //attach the clone to DOM document at the right place
|
|
* });
|
|
*
|
|
* //now we have reference to the cloned DOM via `clone`
|
|
* </pre>
|
|
*
|
|
*
|
|
* For information on how the compiler works, see the
|
|
* {@link guide/compiler Angular HTML Compiler} section of the Developer Guide.
|
|
*/
|
|
|
|
var $compileMinErr = minErr('$compile');
|
|
|
|
/**
|
|
* @ngdoc service
|
|
* @name ng.$compileProvider
|
|
* @function
|
|
*
|
|
* @description
|
|
*/
|
|
$CompileProvider.$inject = ['$provide'];
|
|
function $CompileProvider($provide) {
|
|
var hasDirectives = {},
|
|
Suffix = 'Directive',
|
|
COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/,
|
|
CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/,
|
|
aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/,
|
|
imgSrcSanitizationWhitelist = /^\s*(https?|ftp|file):|data:image\//;
|
|
|
|
// Ref: http://developers.whatwg.org/webappapis.html#event-handler-idl-attributes
|
|
// The assumption is that future DOM event attribute names will begin with
|
|
// 'on' and be composed of only English letters.
|
|
var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]+|formaction)$/;
|
|
|
|
/**
|
|
* @ngdoc function
|
|
* @name ng.$compileProvider#directive
|
|
* @methodOf ng.$compileProvider
|
|
* @function
|
|
*
|
|
* @description
|
|
* Register a new directive with the compiler.
|
|
*
|
|
* @param {string|Object} name Name of the directive in camel-case (i.e. <code>ngBind</code> which
|
|
* will match as <code>ng-bind</code>), or an object map of directives where the keys are the
|
|
* names and the values are the factories.
|
|
* @param {function|Array} directiveFactory An injectable directive factory function. See
|
|
* {@link guide/directive} for more info.
|
|
* @returns {ng.$compileProvider} Self for chaining.
|
|
*/
|
|
this.directive = function registerDirective(name, directiveFactory) {
|
|
if (isString(name)) {
|
|
assertArg(directiveFactory, 'directiveFactory');
|
|
if (!hasDirectives.hasOwnProperty(name)) {
|
|
hasDirectives[name] = [];
|
|
$provide.factory(name + Suffix, ['$injector', '$exceptionHandler',
|
|
function($injector, $exceptionHandler) {
|
|
var directives = [];
|
|
forEach(hasDirectives[name], function(directiveFactory) {
|
|
try {
|
|
var directive = $injector.invoke(directiveFactory);
|
|
if (isFunction(directive)) {
|
|
directive = { compile: valueFn(directive) };
|
|
} else if (!directive.compile && directive.link) {
|
|
directive.compile = valueFn(directive.link);
|
|
}
|
|
directive.priority = directive.priority || 0;
|
|
directive.name = directive.name || name;
|
|
directive.require = directive.require || (directive.controller && directive.name);
|
|
directive.restrict = directive.restrict || 'A';
|
|
directives.push(directive);
|
|
} catch (e) {
|
|
$exceptionHandler(e);
|
|
}
|
|
});
|
|
return directives;
|
|
}]);
|
|
}
|
|
hasDirectives[name].push(directiveFactory);
|
|
} else {
|
|
forEach(name, reverseParams(registerDirective));
|
|
}
|
|
return this;
|
|
};
|
|
|
|
|
|
/**
|
|
* @ngdoc function
|
|
* @name ng.$compileProvider#aHrefSanitizationWhitelist
|
|
* @methodOf ng.$compileProvider
|
|
* @function
|
|
*
|
|
* @description
|
|
* Retrieves or overrides the default regular expression that is used for whitelisting of safe
|
|
* urls during a[href] sanitization.
|
|
*
|
|
* The sanitization is a security measure aimed at prevent XSS attacks via html links.
|
|
*
|
|
* Any url about to be assigned to a[href] via data-binding is first normalized and turned into
|
|
* an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist`
|
|
* regular expression. If a match is found, the original url is written into the dom. Otherwise,
|
|
* the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
|
|
*
|
|
* @param {RegExp=} regexp New regexp to whitelist urls with.
|
|
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
|
|
* chaining otherwise.
|
|
*/
|
|
this.aHrefSanitizationWhitelist = function(regexp) {
|
|
if (isDefined(regexp)) {
|
|
aHrefSanitizationWhitelist = regexp;
|
|
return this;
|
|
}
|
|
return aHrefSanitizationWhitelist;
|
|
};
|
|
|
|
|
|
/**
|
|
* @ngdoc function
|
|
* @name ng.$compileProvider#imgSrcSanitizationWhitelist
|
|
* @methodOf ng.$compileProvider
|
|
* @function
|
|
*
|
|
* @description
|
|
* Retrieves or overrides the default regular expression that is used for whitelisting of safe
|
|
* urls during img[src] sanitization.
|
|
*
|
|
* The sanitization is a security measure aimed at prevent XSS attacks via html links.
|
|
*
|
|
* Any url about to be assigned to img[src] via data-binding is first normalized and turned into an
|
|
* absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist` regular
|
|
* expression. If a match is found, the original url is written into the dom. Otherwise, the
|
|
* absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
|
|
*
|
|
* @param {RegExp=} regexp New regexp to whitelist urls with.
|
|
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
|
|
* chaining otherwise.
|
|
*/
|
|
this.imgSrcSanitizationWhitelist = function(regexp) {
|
|
if (isDefined(regexp)) {
|
|
imgSrcSanitizationWhitelist = regexp;
|
|
return this;
|
|
}
|
|
return imgSrcSanitizationWhitelist;
|
|
};
|
|
|
|
|
|
this.$get = [
|
|
'$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse',
|
|
'$controller', '$rootScope', '$document', '$sce', '$$urlUtils', '$animate',
|
|
function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse,
|
|
$controller, $rootScope, $document, $sce, $$urlUtils, $animate) {
|
|
|
|
var Attributes = function(element, attr) {
|
|
this.$$element = element;
|
|
this.$attr = attr || {};
|
|
};
|
|
|
|
Attributes.prototype = {
|
|
$normalize: directiveNormalize,
|
|
|
|
|
|
/**
|
|
* @ngdoc function
|
|
* @name ng.$compile.directive.Attributes#$addClass
|
|
* @methodOf ng.$compile.directive.Attributes
|
|
* @function
|
|
*
|
|
* @description
|
|
* Adds the CSS class value specified by the classVal parameter to the element. If animations
|
|
* are enabled then an animation will be triggered for the class addition.
|
|
*
|
|
* @param {string} classVal The className value that will be added to the element
|
|
*/
|
|
$addClass : function(classVal) {
|
|
if(classVal && classVal.length > 0) {
|
|
$animate.addClass(this.$$element, classVal);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @ngdoc function
|
|
* @name ng.$compile.directive.Attributes#$removeClass
|
|
* @methodOf ng.$compile.directive.Attributes
|
|
* @function
|
|
*
|
|
* @description
|
|
* Removes the CSS class value specified by the classVal parameter from the element. If animations
|
|
* are enabled then an animation will be triggered for the class removal.
|
|
*
|
|
* @param {string} classVal The className value that will be removed from the element
|
|
*/
|
|
$removeClass : function(classVal) {
|
|
if(classVal && classVal.length > 0) {
|
|
$animate.removeClass(this.$$element, classVal);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Set a normalized attribute on the element in a way such that all directives
|
|
* can share the attribute. This function properly handles boolean attributes.
|
|
* @param {string} key Normalized key. (ie ngAttribute)
|
|
* @param {string|boolean} value The value to set. If `null` attribute will be deleted.
|
|
* @param {boolean=} writeAttr If false, does not write the value to DOM element attribute.
|
|
* Defaults to true.
|
|
* @param {string=} attrName Optional none normalized name. Defaults to key.
|
|
*/
|
|
$set: function(key, value, writeAttr, attrName) {
|
|
//special case for class attribute addition + removal
|
|
//so that class changes can tap into the animation
|
|
//hooks provided by the $animate service
|
|
if(key == 'class') {
|
|
value = value || '';
|
|
var current = this.$$element.attr('class') || '';
|
|
this.$removeClass(tokenDifference(current, value).join(' '));
|
|
this.$addClass(tokenDifference(value, current).join(' '));
|
|
} else {
|
|
var booleanKey = getBooleanAttrName(this.$$element[0], key),
|
|
normalizedVal,
|
|
nodeName;
|
|
|
|
if (booleanKey) {
|
|
this.$$element.prop(key, value);
|
|
attrName = booleanKey;
|
|
}
|
|
|
|
this[key] = value;
|
|
|
|
// translate normalized key to actual key
|
|
if (attrName) {
|
|
this.$attr[key] = attrName;
|
|
} else {
|
|
attrName = this.$attr[key];
|
|
if (!attrName) {
|
|
this.$attr[key] = attrName = snake_case(key, '-');
|
|
}
|
|
}
|
|
|
|
nodeName = nodeName_(this.$$element);
|
|
|
|
// sanitize a[href] and img[src] values
|
|
if ((nodeName === 'A' && key === 'href') ||
|
|
(nodeName === 'IMG' && key === 'src')) {
|
|
// NOTE: $$urlUtils.resolve() doesn't support IE < 8 so we don't sanitize for that case.
|
|
if (!msie || msie >= 8 ) {
|
|
normalizedVal = $$urlUtils.resolve(value);
|
|
if (normalizedVal !== '') {
|
|
if ((key === 'href' && !normalizedVal.match(aHrefSanitizationWhitelist)) ||
|
|
(key === 'src' && !normalizedVal.match(imgSrcSanitizationWhitelist))) {
|
|
this[key] = value = 'unsafe:' + normalizedVal;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (writeAttr !== false) {
|
|
if (value === null || value === undefined) {
|
|
this.$$element.removeAttr(attrName);
|
|
} else {
|
|
this.$$element.attr(attrName, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
// fire observers
|
|
var $$observers = this.$$observers;
|
|
$$observers && forEach($$observers[key], function(fn) {
|
|
try {
|
|
fn(value);
|
|
} catch (e) {
|
|
$exceptionHandler(e);
|
|
}
|
|
});
|
|
|
|
function tokenDifference(str1, str2) {
|
|
var values = [],
|
|
tokens1 = str1.split(/\s+/),
|
|
tokens2 = str2.split(/\s+/);
|
|
|
|
outer:
|
|
for(var i=0;i<tokens1.length;i++) {
|
|
var token = tokens1[i];
|
|
for(var j=0;j<tokens2.length;j++) {
|
|
if(token == tokens2[j]) continue outer;
|
|
}
|
|
values.push(token);
|
|
}
|
|
return values;
|
|
};
|
|
},
|
|
|
|
|
|
/**
|
|
* @ngdoc function
|
|
* @name ng.$compile.directive.Attributes#$observe
|
|
* @methodOf ng.$compile.directive.Attributes
|
|
* @function
|
|
*
|
|
* @description
|
|
* Observes an interpolated attribute.
|
|
*
|
|
* The observer function will be invoked once during the next `$digest` following
|
|
* compilation. The observer is then invoked whenever the interpolated value
|
|
* changes.
|
|
*
|
|
* @param {string} key Normalized key. (ie ngAttribute) .
|
|
* @param {function(interpolatedValue)} fn Function that will be called whenever
|
|
the interpolated value of the attribute changes.
|
|
* See the {@link guide/directive#Attributes Directives} guide for more info.
|
|
* @returns {function()} the `fn` parameter.
|
|
*/
|
|
$observe: function(key, fn) {
|
|
var attrs = this,
|
|
$$observers = (attrs.$$observers || (attrs.$$observers = {})),
|
|
listeners = ($$observers[key] || ($$observers[key] = []));
|
|
|
|
listeners.push(fn);
|
|
$rootScope.$evalAsync(function() {
|
|
if (!listeners.$$inter) {
|
|
// no one registered attribute interpolation function, so lets call it manually
|
|
fn(attrs[key]);
|
|
}
|
|
});
|
|
return fn;
|
|
}
|
|
};
|
|
|
|
var urlSanitizationNode = $document[0].createElement('a'),
|
|
startSymbol = $interpolate.startSymbol(),
|
|
endSymbol = $interpolate.endSymbol(),
|
|
denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}')
|
|
? identity
|
|
: function denormalizeTemplate(template) {
|
|
return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
|
|
},
|
|
NG_ATTR_BINDING = /^ngAttr[A-Z]/;
|
|
|
|
|
|
return compile;
|
|
|
|
//================================
|
|
|
|
function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective) {
|
|
if (!($compileNodes instanceof jqLite)) {
|
|
// jquery always rewraps, whereas we need to preserve the original selector so that we can modify it.
|
|
$compileNodes = jqLite($compileNodes);
|
|
}
|
|
// We can not compile top level text elements since text nodes can be merged and we will
|
|
// not be able to attach scope data to them, so we will wrap them in <span>
|
|
forEach($compileNodes, function(node, index){
|
|
if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) {
|
|
$compileNodes[index] = node = jqLite(node).wrap('<span></span>').parent()[0];
|
|
}
|
|
});
|
|
var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective);
|
|
return function publicLinkFn(scope, cloneConnectFn){
|
|
assertArg(scope, 'scope');
|
|
// important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart
|
|
// and sometimes changes the structure of the DOM.
|
|
var $linkNode = cloneConnectFn
|
|
? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!!
|
|
: $compileNodes;
|
|
|
|
// Attach scope only to non-text nodes.
|
|
for(var i = 0, ii = $linkNode.length; i<ii; i++) {
|
|
var node = $linkNode[i];
|
|
if (node.nodeType == 1 /* element */ || node.nodeType == 9 /* document */) {
|
|
$linkNode.eq(i).data('$scope', scope);
|
|
}
|
|
}
|
|
safeAddClass($linkNode, 'ng-scope');
|
|
if (cloneConnectFn) cloneConnectFn($linkNode, scope);
|
|
if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode);
|
|
return $linkNode;
|
|
};
|
|
}
|
|
|
|
function safeAddClass($element, className) {
|
|
try {
|
|
$element.addClass(className);
|
|
} catch(e) {
|
|
// ignore, since it means that we are trying to set class on
|
|
// SVG element, where class name is read-only.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compile function matches each node in nodeList against the directives. Once all directives
|
|
* for a particular node are collected their compile functions are executed. The compile
|
|
* functions return values - the linking functions - are combined into a composite linking
|
|
* function, which is the a linking function for the node.
|
|
*
|
|
* @param {NodeList} nodeList an array of nodes or NodeList to compile
|
|
* @param {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the
|
|
* scope argument is auto-generated to the new child of the transcluded parent scope.
|
|
* @param {DOMElement=} $rootElement If the nodeList is the root of the compilation tree then the
|
|
* rootElement must be set the jqLite collection of the compile root. This is
|
|
* needed so that the jqLite collection items can be replaced with widgets.
|
|
* @param {number=} max directive priority
|
|
* @returns {?function} A composite linking function of all of the matched directives or null.
|
|
*/
|
|
function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective) {
|
|
var linkFns = [],
|
|
nodeLinkFn, childLinkFn, directives, attrs, linkFnFound;
|
|
|
|
for(var i = 0; i < nodeList.length; i++) {
|
|
attrs = new Attributes();
|
|
|
|
// we must always refer to nodeList[i] since the nodes can be replaced underneath us.
|
|
directives = collectDirectives(nodeList[i], [], attrs, i == 0 ? maxPriority : undefined, ignoreDirective);
|
|
|
|
nodeLinkFn = (directives.length)
|
|
? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement, null, [], [])
|
|
: null;
|
|
|
|
childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || !nodeList[i].childNodes || !nodeList[i].childNodes.length)
|
|
? null
|
|
: compileNodes(nodeList[i].childNodes,
|
|
nodeLinkFn ? nodeLinkFn.transclude : transcludeFn);
|
|
|
|
linkFns.push(nodeLinkFn);
|
|
linkFns.push(childLinkFn);
|
|
linkFnFound = (linkFnFound || nodeLinkFn || childLinkFn);
|
|
}
|
|
|
|
// return a linking function if we have found anything, null otherwise
|
|
return linkFnFound ? compositeLinkFn : null;
|
|
|
|
function compositeLinkFn(scope, nodeList, $rootElement, boundTranscludeFn) {
|
|
var nodeLinkFn, childLinkFn, node, childScope, childTranscludeFn, i, ii, n;
|
|
|
|
// copy nodeList so that linking doesn't break due to live list updates.
|
|
var stableNodeList = [];
|
|
for (i = 0, ii = nodeList.length; i < ii; i++) {
|
|
stableNodeList.push(nodeList[i]);
|
|
}
|
|
|
|
for(i = 0, n = 0, ii = linkFns.length; i < ii; n++) {
|
|
node = stableNodeList[n];
|
|
nodeLinkFn = linkFns[i++];
|
|
childLinkFn = linkFns[i++];
|
|
|
|
if (nodeLinkFn) {
|
|
if (nodeLinkFn.scope) {
|
|
childScope = scope.$new(isObject(nodeLinkFn.scope));
|
|
jqLite(node).data('$scope', childScope);
|
|
} else {
|
|
childScope = scope;
|
|
}
|
|
childTranscludeFn = nodeLinkFn.transclude;
|
|
if (childTranscludeFn || (!boundTranscludeFn && transcludeFn)) {
|
|
nodeLinkFn(childLinkFn, childScope, node, $rootElement,
|
|
(function(transcludeFn) {
|
|
return function(cloneFn) {
|
|
var transcludeScope = scope.$new();
|
|
transcludeScope.$$transcluded = true;
|
|
|
|
return transcludeFn(transcludeScope, cloneFn).
|
|
on('$destroy', bind(transcludeScope, transcludeScope.$destroy));
|
|
};
|
|
})(childTranscludeFn || transcludeFn)
|
|
);
|
|
} else {
|
|
nodeLinkFn(childLinkFn, childScope, node, undefined, boundTranscludeFn);
|
|
}
|
|
} else if (childLinkFn) {
|
|
childLinkFn(scope, node.childNodes, undefined, boundTranscludeFn);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Looks for directives on the given node and adds them to the directive collection which is
|
|
* sorted.
|
|
*
|
|
* @param node Node to search.
|
|
* @param directives An array to which the directives are added to. This array is sorted before
|
|
* the function returns.
|
|
* @param attrs The shared attrs object which is used to populate the normalized attributes.
|
|
* @param {number=} maxPriority Max directive priority.
|
|
*/
|
|
function collectDirectives(node, directives, attrs, maxPriority, ignoreDirective) {
|
|
var nodeType = node.nodeType,
|
|
attrsMap = attrs.$attr,
|
|
match,
|
|
className;
|
|
|
|
switch(nodeType) {
|
|
case 1: /* Element */
|
|
// use the node name: <directive>
|
|
addDirective(directives,
|
|
directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority, ignoreDirective);
|
|
|
|
// iterate over the attributes
|
|
for (var attr, name, nName, ngAttrName, value, nAttrs = node.attributes,
|
|
j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) {
|
|
var attrStartName = false;
|
|
var attrEndName = false;
|
|
|
|
attr = nAttrs[j];
|
|
if (!msie || msie >= 8 || attr.specified) {
|
|
name = attr.name;
|
|
// support ngAttr attribute binding
|
|
ngAttrName = directiveNormalize(name);
|
|
if (NG_ATTR_BINDING.test(ngAttrName)) {
|
|
name = snake_case(ngAttrName.substr(6), '-');
|
|
}
|
|
|
|
var directiveNName = ngAttrName.replace(/(Start|End)$/, '');
|
|
if (ngAttrName === directiveNName + 'Start') {
|
|
attrStartName = name;
|
|
attrEndName = name.substr(0, name.length - 5) + 'end';
|
|
name = name.substr(0, name.length - 6);
|
|
}
|
|
|
|
nName = directiveNormalize(name.toLowerCase());
|
|
attrsMap[nName] = name;
|
|
attrs[nName] = value = trim((msie && name == 'href')
|
|
? decodeURIComponent(node.getAttribute(name, 2))
|
|
: attr.value);
|
|
if (getBooleanAttrName(node, nName)) {
|
|
attrs[nName] = true; // presence means true
|
|
}
|
|
addAttrInterpolateDirective(node, directives, value, nName);
|
|
addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, attrEndName);
|
|
}
|
|
}
|
|
|
|
// use class as directive
|
|
className = node.className;
|
|
if (isString(className) && className !== '') {
|
|
while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) {
|
|
nName = directiveNormalize(match[2]);
|
|
if (addDirective(directives, nName, 'C', maxPriority, ignoreDirective)) {
|
|
attrs[nName] = trim(match[3]);
|
|
}
|
|
className = className.substr(match.index + match[0].length);
|
|
}
|
|
}
|
|
break;
|
|
case 3: /* Text Node */
|
|
addTextInterpolateDirective(directives, node.nodeValue);
|
|
break;
|
|
case 8: /* Comment */
|
|
try {
|
|
match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue);
|
|
if (match) {
|
|
nName = directiveNormalize(match[1]);
|
|
if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) {
|
|
attrs[nName] = trim(match[2]);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// turns out that under some circumstances IE9 throws errors when one attempts to read comment's node value.
|
|
// Just ignore it and continue. (Can't seem to reproduce in test case.)
|
|
}
|
|
break;
|
|
}
|
|
|
|
directives.sort(byPriority);
|
|
return directives;
|
|
}
|
|
|
|
/**
|
|
* Given a node with an directive-start it collects all of the siblings until it find directive-end.
|
|
* @param node
|
|
* @param attrStart
|
|
* @param attrEnd
|
|
* @returns {*}
|
|
*/
|
|
function groupScan(node, attrStart, attrEnd) {
|
|
var nodes = [];
|
|
var depth = 0;
|
|
if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) {
|
|
var startNode = node;
|
|
do {
|
|
if (!node) {
|
|
throw $compileMinErr('uterdir', "Unterminated attribute, found '{0}' but no matching '{1}' found.", attrStart, attrEnd);
|
|
}
|
|
if (node.nodeType == 1 /** Element **/) {
|
|
if (node.hasAttribute(attrStart)) depth++;
|
|
if (node.hasAttribute(attrEnd)) depth--;
|
|
}
|
|
nodes.push(node);
|
|
node = node.nextSibling;
|
|
} while (depth > 0);
|
|
} else {
|
|
nodes.push(node);
|
|
}
|
|
|
|
return jqLite(nodes);
|
|
}
|
|
|
|
/**
|
|
* Wrapper for linking function which converts normal linking function into a grouped
|
|
* linking function.
|
|
* @param linkFn
|
|
* @param attrStart
|
|
* @param attrEnd
|
|
* @returns {Function}
|
|
*/
|
|
function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) {
|
|
return function(scope, element, attrs, controllers) {
|
|
element = groupScan(element[0], attrStart, attrEnd);
|
|
return linkFn(scope, element, attrs, controllers);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Once the directives have been collected, their compile functions are executed. This method
|
|
* is responsible for inlining directive templates as well as terminating the application
|
|
* of the directives if the terminal directive has been reached.
|
|
*
|
|
* @param {Array} directives Array of collected directives to execute their compile function.
|
|
* this needs to be pre-sorted by priority order.
|
|
* @param {Node} compileNode The raw DOM node to apply the compile functions to
|
|
* @param {Object} templateAttrs The shared attribute function
|
|
* @param {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the
|
|
* scope argument is auto-generated to the new child of the transcluded parent scope.
|
|
* @param {JQLite} jqCollection If we are working on the root of the compile tree then this
|
|
* argument has the root jqLite array so that we can replace nodes on it.
|
|
* @param {Object=} ignoreDirective An optional directive that will be ignored when compiling
|
|
* the transclusion.
|
|
* @param {Array.<Function>} preLinkFns
|
|
* @param {Array.<Function>} postLinkFns
|
|
* @returns linkFn
|
|
*/
|
|
function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, jqCollection,
|
|
originalReplaceDirective, preLinkFns, postLinkFns) {
|
|
var terminalPriority = -Number.MAX_VALUE,
|
|
newScopeDirective = null,
|
|
newIsolateScopeDirective = null,
|
|
templateDirective = null,
|
|
$compileNode = templateAttrs.$$element = jqLite(compileNode),
|
|
directive,
|
|
directiveName,
|
|
$template,
|
|
transcludeDirective,
|
|
replaceDirective = originalReplaceDirective,
|
|
childTranscludeFn = transcludeFn,
|
|
controllerDirectives,
|
|
linkFn,
|
|
directiveValue;
|
|
|
|
// executes all directives on the current element
|
|
for(var i = 0, ii = directives.length; i < ii; i++) {
|
|
directive = directives[i];
|
|
var attrStart = directive.$$start;
|
|
var attrEnd = directive.$$end;
|
|
|
|
// collect multiblock sections
|
|
if (attrStart) {
|
|
$compileNode = groupScan(compileNode, attrStart, attrEnd)
|
|
}
|
|
$template = undefined;
|
|
|
|
if (terminalPriority > directive.priority) {
|
|
break; // prevent further processing of directives
|
|
}
|
|
|
|
if (directiveValue = directive.scope) {
|
|
newScopeDirective = newScopeDirective || directive;
|
|
|
|
// skip the check for directives with async templates, we'll check the derived sync directive when
|
|
// the template arrives
|
|
if (!directive.templateUrl) {
|
|
assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive, $compileNode);
|
|
if (isObject(directiveValue)) {
|
|
safeAddClass($compileNode, 'ng-isolate-scope');
|
|
newIsolateScopeDirective = directive;
|
|
}
|
|
safeAddClass($compileNode, 'ng-scope');
|
|
}
|
|
}
|
|
|
|
directiveName = directive.name;
|
|
|
|
if (!directive.templateUrl && directive.controller) {
|
|
directiveValue = directive.controller;
|
|
controllerDirectives = controllerDirectives || {};
|
|
assertNoDuplicate("'" + directiveName + "' controller",
|
|
controllerDirectives[directiveName], directive, $compileNode);
|
|
controllerDirectives[directiveName] = directive;
|
|
}
|
|
|
|
if (directiveValue = directive.transclude) {
|
|
assertNoDuplicate('transclusion', transcludeDirective, directive, $compileNode);
|
|
transcludeDirective = directive;
|
|
|
|
if (directiveValue == 'element') {
|
|
terminalPriority = directive.priority;
|
|
$template = groupScan(compileNode, attrStart, attrEnd)
|
|
$compileNode = templateAttrs.$$element =
|
|
jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' '));
|
|
compileNode = $compileNode[0];
|
|
replaceWith(jqCollection, jqLite(sliceArgs($template)), compileNode);
|
|
|
|
childTranscludeFn = compile($template, transcludeFn, terminalPriority,
|
|
replaceDirective && replaceDirective.name);
|
|
} else {
|
|
$template = jqLite(JQLiteClone(compileNode)).contents();
|
|
$compileNode.html(''); // clear contents
|
|
childTranscludeFn = compile($template, transcludeFn);
|
|
}
|
|
}
|
|
|
|
if (directive.template) {
|
|
assertNoDuplicate('template', templateDirective, directive, $compileNode);
|
|
templateDirective = directive;
|
|
|
|
directiveValue = (isFunction(directive.template))
|
|
? directive.template($compileNode, templateAttrs)
|
|
: directive.template;
|
|
|
|
directiveValue = denormalizeTemplate(directiveValue);
|
|
|
|
if (directive.replace) {
|
|
replaceDirective = directive;
|
|
$template = jqLite('<div>' +
|
|
trim(directiveValue) +
|
|
'</div>').contents();
|
|
compileNode = $template[0];
|
|
|
|
if ($template.length != 1 || compileNode.nodeType !== 1) {
|
|
throw $compileMinErr('tplrt', "Template for directive '{0}' must have exactly one root element. {1}", directiveName, '');
|
|
}
|
|
|
|
replaceWith(jqCollection, $compileNode, compileNode);
|
|
|
|
var newTemplateAttrs = {$attr: {}};
|
|
|
|
// combine directives from the original node and from the template:
|
|
// - take the array of directives for this element
|
|
// - split it into two parts, those that were already applied and those that weren't
|
|
// - collect directives from the template, add them to the second group and sort them
|
|
// - append the second group with new directives to the first group
|
|
directives = directives.concat(
|
|
collectDirectives(
|
|
compileNode,
|
|
directives.splice(i + 1, directives.length - (i + 1)),
|
|
newTemplateAttrs
|
|
)
|
|
);
|
|
mergeTemplateAttributes(templateAttrs, newTemplateAttrs);
|
|
|
|
ii = directives.length;
|
|
} else {
|
|
$compileNode.html(directiveValue);
|
|
}
|
|
}
|
|
|
|
if (directive.templateUrl) {
|
|
assertNoDuplicate('template', templateDirective, directive, $compileNode);
|
|
templateDirective = directive;
|
|
|
|
if (directive.replace) {
|
|
replaceDirective = directive;
|
|
}
|
|
|
|
nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode,
|
|
templateAttrs, jqCollection, childTranscludeFn, preLinkFns, postLinkFns);
|
|
ii = directives.length;
|
|
} else if (directive.compile) {
|
|
try {
|
|
linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn);
|
|
if (isFunction(linkFn)) {
|
|
addLinkFns(null, linkFn, attrStart, attrEnd);
|
|
} else if (linkFn) {
|
|
addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd);
|
|
}
|
|
} catch (e) {
|
|
$exceptionHandler(e, startingTag($compileNode));
|
|
}
|
|
}
|
|
|
|
if (directive.terminal) {
|
|
nodeLinkFn.terminal = true;
|
|
terminalPriority = Math.max(terminalPriority, directive.priority);
|
|
}
|
|
|
|
}
|
|
|
|
nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope;
|
|
nodeLinkFn.transclude = transcludeDirective && childTranscludeFn;
|
|
|
|
// might be normal or delayed nodeLinkFn depending on if templateUrl is present
|
|
return nodeLinkFn;
|
|
|
|
////////////////////
|
|
|
|
function addLinkFns(pre, post, attrStart, attrEnd) {
|
|
if (pre) {
|
|
if (attrStart) pre = groupElementsLinkFnWrapper(pre, attrStart, attrEnd);
|
|
pre.require = directive.require;
|
|
preLinkFns.push(pre);
|
|
}
|
|
if (post) {
|
|
if (attrStart) post = groupElementsLinkFnWrapper(post, attrStart, attrEnd);
|
|
post.require = directive.require;
|
|
postLinkFns.push(post);
|
|
}
|
|
}
|
|
|
|
|
|
function getControllers(require, $element) {
|
|
var value, retrievalMethod = 'data', optional = false;
|
|
if (isString(require)) {
|
|
while((value = require.charAt(0)) == '^' || value == '?') {
|
|
require = require.substr(1);
|
|
if (value == '^') {
|
|
retrievalMethod = 'inheritedData';
|
|
}
|
|
optional = optional || value == '?';
|
|
}
|
|
|
|
value = $element[retrievalMethod]('$' + require + 'Controller');
|
|
|
|
if ($element[0].nodeType == 8 && $element[0].$$controller) { // Transclusion comment node
|
|
value = value || $element[0].$$controller;
|
|
$element[0].$$controller = null;
|
|
}
|
|
|
|
if (!value && !optional) {
|
|
throw $compileMinErr('ctreq', "Controller '{0}', required by directive '{1}', can't be found!", require, directiveName);
|
|
}
|
|
return value;
|
|
} else if (isArray(require)) {
|
|
value = [];
|
|
forEach(require, function(require) {
|
|
value.push(getControllers(require, $element));
|
|
});
|
|
}
|
|
return value;
|
|
}
|
|
|
|
|
|
function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) {
|
|
var attrs, $element, i, ii, linkFn, controller;
|
|
|
|
if (compileNode === linkNode) {
|
|
attrs = templateAttrs;
|
|
} else {
|
|
attrs = shallowCopy(templateAttrs, new Attributes(jqLite(linkNode), templateAttrs.$attr));
|
|
}
|
|
$element = attrs.$$element;
|
|
|
|
if (newIsolateScopeDirective) {
|
|
var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;
|
|
|
|
var parentScope = scope.$parent || scope;
|
|
|
|
forEach(newIsolateScopeDirective.scope, function(definition, scopeName) {
|
|
var match = definition.match(LOCAL_REGEXP) || [],
|
|
attrName = match[3] || scopeName,
|
|
optional = (match[2] == '?'),
|
|
mode = match[1], // @, =, or &
|
|
lastValue,
|
|
parentGet, parentSet;
|
|
|
|
scope.$$isolateBindings[scopeName] = mode + attrName;
|
|
|
|
switch (mode) {
|
|
|
|
case '@': {
|
|
attrs.$observe(attrName, function(value) {
|
|
scope[scopeName] = value;
|
|
});
|
|
attrs.$$observers[attrName].$$scope = parentScope;
|
|
if( attrs[attrName] ) {
|
|
// If the attribute has been provided then we trigger an interpolation to ensure the value is there for use in the link fn
|
|
scope[scopeName] = $interpolate(attrs[attrName])(parentScope);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case '=': {
|
|
if (optional && !attrs[attrName]) {
|
|
return;
|
|
}
|
|
parentGet = $parse(attrs[attrName]);
|
|
parentSet = parentGet.assign || function() {
|
|
// reset the change, or we will throw this exception on every $digest
|
|
lastValue = scope[scopeName] = parentGet(parentScope);
|
|
throw $compileMinErr('nonassign', "Expression '{0}' used with directive '{1}' is non-assignable!",
|
|
attrs[attrName], newIsolateScopeDirective.name);
|
|
};
|
|
lastValue = scope[scopeName] = parentGet(parentScope);
|
|
scope.$watch(function parentValueWatch() {
|
|
var parentValue = parentGet(parentScope);
|
|
|
|
if (parentValue !== scope[scopeName]) {
|
|
// we are out of sync and need to copy
|
|
if (parentValue !== lastValue) {
|
|
// parent changed and it has precedence
|
|
lastValue = scope[scopeName] = parentValue;
|
|
} else {
|
|
// if the parent can be assigned then do so
|
|
parentSet(parentScope, parentValue = lastValue = scope[scopeName]);
|
|
}
|
|
}
|
|
return parentValue;
|
|
});
|
|
break;
|
|
}
|
|
|
|
case '&': {
|
|
parentGet = $parse(attrs[attrName]);
|
|
scope[scopeName] = function(locals) {
|
|
return parentGet(parentScope, locals);
|
|
};
|
|
break;
|
|
}
|
|
|
|
default: {
|
|
throw $compileMinErr('iscp', "Invalid isolate scope definition for directive '{0}'. Definition: {... {1}: '{2}' ...}",
|
|
newIsolateScopeDirective.name, scopeName, definition);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (controllerDirectives) {
|
|
forEach(controllerDirectives, function(directive) {
|
|
var locals = {
|
|
$scope: scope,
|
|
$element: $element,
|
|
$attrs: attrs,
|
|
$transclude: boundTranscludeFn
|
|
}, controllerInstance;
|
|
|
|
controller = directive.controller;
|
|
if (controller == '@') {
|
|
controller = attrs[directive.name];
|
|
}
|
|
|
|
controllerInstance = $controller(controller, locals);
|
|
|
|
// Directives with element transclusion and a controller need to attach controller
|
|
// to the comment node created by the compiler, but jQuery .data doesn't support
|
|
// attaching data to comment nodes so instead we set it directly on the element and
|
|
// remove it after we read it later.
|
|
if ($element[0].nodeType == 8) { // Transclusion comment node
|
|
$element[0].$$controller = controllerInstance;
|
|
} else {
|
|
$element.data('$' + directive.name + 'Controller', controllerInstance);
|
|
}
|
|
if (directive.controllerAs) {
|
|
locals.$scope[directive.controllerAs] = controllerInstance;
|
|
}
|
|
});
|
|
}
|
|
|
|
// PRELINKING
|
|
for(i = 0, ii = preLinkFns.length; i < ii; i++) {
|
|
try {
|
|
linkFn = preLinkFns[i];
|
|
linkFn(scope, $element, attrs,
|
|
linkFn.require && getControllers(linkFn.require, $element));
|
|
} catch (e) {
|
|
$exceptionHandler(e, startingTag($element));
|
|
}
|
|
}
|
|
|
|
// RECURSION
|
|
childLinkFn && childLinkFn(scope, linkNode.childNodes, undefined, boundTranscludeFn);
|
|
|
|
// POSTLINKING
|
|
for(i = postLinkFns.length - 1; i >= 0; i--) {
|
|
try {
|
|
linkFn = postLinkFns[i];
|
|
linkFn(scope, $element, attrs,
|
|
linkFn.require && getControllers(linkFn.require, $element));
|
|
} catch (e) {
|
|
$exceptionHandler(e, startingTag($element));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* looks up the directive and decorates it with exception handling and proper parameters. We
|
|
* call this the boundDirective.
|
|
*
|
|
* @param {string} name name of the directive to look up.
|
|
* @param {string} location The directive must be found in specific format.
|
|
* String containing any of theses characters:
|
|
*
|
|
* * `E`: element name
|
|
* * `A': attribute
|
|
* * `C`: class
|
|
* * `M`: comment
|
|
* @returns true if directive was added.
|
|
*/
|
|
function addDirective(tDirectives, name, location, maxPriority, ignoreDirective, startAttrName, endAttrName) {
|
|
if (name === ignoreDirective) return null;
|
|
var match = null;
|
|
if (hasDirectives.hasOwnProperty(name)) {
|
|
for(var directive, directives = $injector.get(name + Suffix),
|
|
i = 0, ii = directives.length; i<ii; i++) {
|
|
try {
|
|
directive = directives[i];
|
|
if ( (maxPriority === undefined || maxPriority > directive.priority) &&
|
|
directive.restrict.indexOf(location) != -1) {
|
|
if (startAttrName) {
|
|
directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName});
|
|
}
|
|
tDirectives.push(directive);
|
|
match = directive;
|
|
}
|
|
} catch(e) { $exceptionHandler(e); }
|
|
}
|
|
}
|
|
return match;
|
|
}
|
|
|
|
|
|
/**
|
|
* When the element is replaced with HTML template then the new attributes
|
|
* on the template need to be merged with the existing attributes in the DOM.
|
|
* The desired effect is to have both of the attributes present.
|
|
*
|
|
* @param {object} dst destination attributes (original DOM)
|
|
* @param {object} src source attributes (from the directive template)
|
|
*/
|
|
function mergeTemplateAttributes(dst, src) {
|
|
var srcAttr = src.$attr,
|
|
dstAttr = dst.$attr,
|
|
$element = dst.$$element;
|
|
|
|
// reapply the old attributes to the new element
|
|
forEach(dst, function(value, key) {
|
|
if (key.charAt(0) != '$') {
|
|
if (src[key]) {
|
|
value += (key === 'style' ? ';' : ' ') + src[key];
|
|
}
|
|
dst.$set(key, value, true, srcAttr[key]);
|
|
}
|
|
});
|
|
|
|
// copy the new attributes on the old attrs object
|
|
forEach(src, function(value, key) {
|
|
if (key == 'class') {
|
|
safeAddClass($element, value);
|
|
dst['class'] = (dst['class'] ? dst['class'] + ' ' : '') + value;
|
|
} else if (key == 'style') {
|
|
$element.attr('style', $element.attr('style') + ';' + value);
|
|
} else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) {
|
|
dst[key] = value;
|
|
dstAttr[key] = srcAttr[key];
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
function compileTemplateUrl(directives, $compileNode, tAttrs,
|
|
$rootElement, childTranscludeFn, preLinkFns, postLinkFns) {
|
|
var linkQueue = [],
|
|
afterTemplateNodeLinkFn,
|
|
afterTemplateChildLinkFn,
|
|
beforeTemplateCompileNode = $compileNode[0],
|
|
origAsyncDirective = directives.shift(),
|
|
// The fact that we have to copy and patch the directive seems wrong!
|
|
derivedSyncDirective = extend({}, origAsyncDirective, {
|
|
templateUrl: null, transclude: null, replace: null
|
|
}),
|
|
templateUrl = (isFunction(origAsyncDirective.templateUrl))
|
|
? origAsyncDirective.templateUrl($compileNode, tAttrs)
|
|
: origAsyncDirective.templateUrl;
|
|
|
|
$compileNode.html('');
|
|
|
|
$http.get($sce.getTrustedResourceUrl(templateUrl), {cache: $templateCache}).
|
|
success(function(content) {
|
|
var compileNode, tempTemplateAttrs, $template;
|
|
|
|
content = denormalizeTemplate(content);
|
|
|
|
if (origAsyncDirective.replace) {
|
|
$template = jqLite('<div>' + trim(content) + '</div>').contents();
|
|
compileNode = $template[0];
|
|
|
|
if ($template.length != 1 || compileNode.nodeType !== 1) {
|
|
throw $compileMinErr('tplrt', "Template for directive '{0}' must have exactly one root element. {1}",
|
|
origAsyncDirective.name, templateUrl);
|
|
}
|
|
|
|
tempTemplateAttrs = {$attr: {}};
|
|
replaceWith($rootElement, $compileNode, compileNode);
|
|
collectDirectives(compileNode, directives, tempTemplateAttrs);
|
|
mergeTemplateAttributes(tAttrs, tempTemplateAttrs);
|
|
} else {
|
|
compileNode = beforeTemplateCompileNode;
|
|
$compileNode.html(content);
|
|
}
|
|
|
|
directives.unshift(derivedSyncDirective);
|
|
|
|
afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs,
|
|
childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns);
|
|
forEach($rootElement, function(node, i) {
|
|
if (node == compileNode) {
|
|
$rootElement[i] = $compileNode[0];
|
|
}
|
|
});
|
|
afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes, childTranscludeFn);
|
|
|
|
|
|
while(linkQueue.length) {
|
|
var scope = linkQueue.shift(),
|
|
beforeTemplateLinkNode = linkQueue.shift(),
|
|
linkRootElement = linkQueue.shift(),
|
|
controller = linkQueue.shift(),
|
|
linkNode = $compileNode[0];
|
|
|
|
if (beforeTemplateLinkNode !== beforeTemplateCompileNode) {
|
|
// it was cloned therefore we have to clone as well.
|
|
linkNode = JQLiteClone(compileNode);
|
|
replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode);
|
|
}
|
|
|
|
afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, controller);
|
|
}
|
|
linkQueue = null;
|
|
}).
|
|
error(function(response, code, headers, config) {
|
|
throw $compileMinErr('tpload', 'Failed to load template: {0}', config.url);
|
|
});
|
|
|
|
return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, controller) {
|
|
if (linkQueue) {
|
|
linkQueue.push(scope);
|
|
linkQueue.push(node);
|
|
linkQueue.push(rootElement);
|
|
linkQueue.push(controller);
|
|
} else {
|
|
afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, controller);
|
|
}
|
|
};
|
|
}
|
|
|
|
|
|
/**
|
|
* Sorting function for bound directives.
|
|
*/
|
|
function byPriority(a, b) {
|
|
return b.priority - a.priority;
|
|
}
|
|
|
|
|
|
function assertNoDuplicate(what, previousDirective, directive, element) {
|
|
if (previousDirective) {
|
|
throw $compileMinErr('multidir', 'Multiple directives [{0}, {1}] asking for {2} on: {3}',
|
|
previousDirective.name, directive.name, what, startingTag(element));
|
|
}
|
|
}
|
|
|
|
|
|
function addTextInterpolateDirective(directives, text) {
|
|
var interpolateFn = $interpolate(text, true);
|
|
if (interpolateFn) {
|
|
directives.push({
|
|
priority: 0,
|
|
compile: valueFn(function textInterpolateLinkFn(scope, node) {
|
|
var parent = node.parent(),
|
|
bindings = parent.data('$binding') || [];
|
|
bindings.push(interpolateFn);
|
|
safeAddClass(parent.data('$binding', bindings), 'ng-binding');
|
|
scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {
|
|
node[0].nodeValue = value;
|
|
});
|
|
})
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
function getTrustedContext(node, attrNormalizedName) {
|
|
// maction[xlink:href] can source SVG. It's not limited to <maction>.
|
|
if (attrNormalizedName == "xlinkHref" ||
|
|
(nodeName_(node) != "IMG" && (attrNormalizedName == "src" ||
|
|
attrNormalizedName == "ngSrc"))) {
|
|
return $sce.RESOURCE_URL;
|
|
}
|
|
}
|
|
|
|
|
|
function addAttrInterpolateDirective(node, directives, value, name) {
|
|
var interpolateFn = $interpolate(value, true);
|
|
|
|
// no interpolation found -> ignore
|
|
if (!interpolateFn) return;
|
|
|
|
|
|
if (name === "multiple" && nodeName_(node) === "SELECT") {
|
|
throw $compileMinErr("selmulti", "Binding to the 'multiple' attribute is not supported. Element: {0}",
|
|
startingTag(node));
|
|
}
|
|
|
|
directives.push({
|
|
priority: -100,
|
|
compile: valueFn(function attrInterpolateLinkFn(scope, element, attr) {
|
|
var $$observers = (attr.$$observers || (attr.$$observers = {}));
|
|
|
|
if (EVENT_HANDLER_ATTR_REGEXP.test(name)) {
|
|
throw $compileMinErr('nodomevents',
|
|
"Interpolations for HTML DOM event attributes are disallowed. Please use the ng- " +
|
|
"versions (such as ng-click instead of onclick) instead.");
|
|
}
|
|
|
|
// we need to interpolate again, in case the attribute value has been updated
|
|
// (e.g. by another directive's compile function)
|
|
interpolateFn = $interpolate(attr[name], true, getTrustedContext(node, name));
|
|
|
|
// if attribute was updated so that there is no interpolation going on we don't want to
|
|
// register any observers
|
|
if (!interpolateFn) return;
|
|
|
|
// TODO(i): this should likely be attr.$set(name, iterpolateFn(scope) so that we reset the actual attr value
|
|
attr[name] = interpolateFn(scope);
|
|
($$observers[name] || ($$observers[name] = [])).$$inter = true;
|
|
(attr.$$observers && attr.$$observers[name].$$scope || scope).
|
|
$watch(interpolateFn, function interpolateFnWatchAction(value) {
|
|
attr.$set(name, value);
|
|
});
|
|
})
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* This is a special jqLite.replaceWith, which can replace items which
|
|
* have no parents, provided that the containing jqLite collection is provided.
|
|
*
|
|
* @param {JqLite=} $rootElement The root of the compile tree. Used so that we can replace nodes
|
|
* in the root of the tree.
|
|
* @param {JqLite} elementsToRemove The jqLite element which we are going to replace. We keep the shell,
|
|
* but replace its DOM node reference.
|
|
* @param {Node} newNode The new DOM node.
|
|
*/
|
|
function replaceWith($rootElement, elementsToRemove, newNode) {
|
|
var firstElementToRemove = elementsToRemove[0],
|
|
removeCount = elementsToRemove.length,
|
|
parent = firstElementToRemove.parentNode,
|
|
i, ii;
|
|
|
|
if ($rootElement) {
|
|
for(i = 0, ii = $rootElement.length; i < ii; i++) {
|
|
if ($rootElement[i] == firstElementToRemove) {
|
|
$rootElement[i++] = newNode;
|
|
for (var j = i, j2 = j + removeCount - 1,
|
|
jj = $rootElement.length;
|
|
j < jj; j++, j2++) {
|
|
if (j2 < jj) {
|
|
$rootElement[j] = $rootElement[j2];
|
|
} else {
|
|
delete $rootElement[j];
|
|
}
|
|
}
|
|
$rootElement.length -= removeCount - 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (parent) {
|
|
parent.replaceChild(newNode, firstElementToRemove);
|
|
}
|
|
var fragment = document.createDocumentFragment();
|
|
fragment.appendChild(firstElementToRemove);
|
|
newNode[jqLite.expando] = firstElementToRemove[jqLite.expando];
|
|
for (var k = 1, kk = elementsToRemove.length; k < kk; k++) {
|
|
var element = elementsToRemove[k];
|
|
jqLite(element).remove(); // must do this way to clean up expando
|
|
fragment.appendChild(element);
|
|
delete elementsToRemove[k];
|
|
}
|
|
|
|
elementsToRemove[0] = newNode;
|
|
elementsToRemove.length = 1
|
|
}
|
|
}];
|
|
}
|
|
|
|
var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i;
|
|
/**
|
|
* Converts all accepted directives format into proper directive name.
|
|
* All of these will become 'myDirective':
|
|
* my:Directive
|
|
* my-directive
|
|
* x-my-directive
|
|
* data-my:directive
|
|
*
|
|
* Also there is special case for Moz prefix starting with upper case letter.
|
|
* @param name Name to normalize
|
|
*/
|
|
function directiveNormalize(name) {
|
|
return camelCase(name.replace(PREFIX_REGEXP, ''));
|
|
}
|
|
|
|
/**
|
|
* @ngdoc object
|
|
* @name ng.$compile.directive.Attributes
|
|
* @description
|
|
*
|
|
* A shared object between directive compile / linking functions which contains normalized DOM element
|
|
* attributes. The the values reflect current binding state `{{ }}`. The normalization is needed
|
|
* since all of these are treated as equivalent in Angular:
|
|
*
|
|
* <span ng:bind="a" ng-bind="a" data-ng-bind="a" x-ng-bind="a">
|
|
*/
|
|
|
|
/**
|
|
* @ngdoc property
|
|
* @name ng.$compile.directive.Attributes#$attr
|
|
* @propertyOf ng.$compile.directive.Attributes
|
|
* @returns {object} A map of DOM element attribute names to the normalized name. This is
|
|
* needed to do reverse lookup from normalized name back to actual name.
|
|
*/
|
|
|
|
|
|
/**
|
|
* @ngdoc function
|
|
* @name ng.$compile.directive.Attributes#$set
|
|
* @methodOf ng.$compile.directive.Attributes
|
|
* @function
|
|
*
|
|
* @description
|
|
* Set DOM element attribute value.
|
|
*
|
|
*
|
|
* @param {string} name Normalized element attribute name of the property to modify. The name is
|
|
* revers translated using the {@link ng.$compile.directive.Attributes#$attr $attr}
|
|
* property to the original name.
|
|
* @param {string} value Value to set the attribute to. The value can be an interpolated string.
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
* Closure compiler type information
|
|
*/
|
|
|
|
function nodesetLinkingFn(
|
|
/* angular.Scope */ scope,
|
|
/* NodeList */ nodeList,
|
|
/* Element */ rootElement,
|
|
/* function(Function) */ boundTranscludeFn
|
|
){}
|
|
|
|
function directiveLinkingFn(
|
|
/* nodesetLinkingFn */ nodesetLinkingFn,
|
|
/* angular.Scope */ scope,
|
|
/* Node */ node,
|
|
/* Element */ rootElement,
|
|
/* function(Function) */ boundTranscludeFn
|
|
){}
|