PageRenderTime 64ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/src/ng/compile.js

https://github.com/9kopb/angular.js
JavaScript | 1230 lines | 716 code | 149 blank | 365 comment | 178 complexity | 9eea6b29ec915e96f6c937be379cf631 MD5 | raw file
Possible License(s): JSON, Apache-2.0
  1. 'use strict';
  2. /* ! VARIABLE/FUNCTION NAMING CONVENTIONS THAT APPLY TO THIS FILE!
  3. *
  4. * DOM-related variables:
  5. *
  6. * - "node" - DOM Node
  7. * - "element" - DOM Element or Node
  8. * - "$node" or "$element" - jqLite-wrapped node or element
  9. *
  10. *
  11. * Compiler related stuff:
  12. *
  13. * - "linkFn" - linking fn of a single directive
  14. * - "nodeLinkFn" - function that aggregates all linking fns for a particular node
  15. * - "childLinkFn" - function that aggregates all linking fns for child nodes of a particular node
  16. * - "compositeLinkFn" - function that aggregates all linking fns for a compilation root (nodeList)
  17. */
  18. var NON_ASSIGNABLE_MODEL_EXPRESSION = 'Non-assignable model expression: ';
  19. /**
  20. * @ngdoc function
  21. * @name ng.$compile
  22. * @function
  23. *
  24. * @description
  25. * Compiles a piece of HTML string or DOM into a template and produces a template function, which
  26. * can then be used to link {@link ng.$rootScope.Scope scope} and the template together.
  27. *
  28. * The compilation is a process of walking the DOM tree and trying to match DOM elements to
  29. * {@link ng.$compileProvider#directive directives}. For each match it
  30. * executes corresponding template function and collects the
  31. * instance functions into a single template function which is then returned.
  32. *
  33. * The template function can then be used once to produce the view or as it is the case with
  34. * {@link ng.directive:ngRepeat repeater} many-times, in which
  35. * case each call results in a view that is a DOM clone of the original template.
  36. *
  37. <doc:example module="compile">
  38. <doc:source>
  39. <script>
  40. // declare a new module, and inject the $compileProvider
  41. angular.module('compile', [], function($compileProvider) {
  42. // configure new 'compile' directive by passing a directive
  43. // factory function. The factory function injects the '$compile'
  44. $compileProvider.directive('compile', function($compile) {
  45. // directive factory creates a link function
  46. return function(scope, element, attrs) {
  47. scope.$watch(
  48. function(scope) {
  49. // watch the 'compile' expression for changes
  50. return scope.$eval(attrs.compile);
  51. },
  52. function(value) {
  53. // when the 'compile' expression changes
  54. // assign it into the current DOM
  55. element.html(value);
  56. // compile the new DOM and link it to the current
  57. // scope.
  58. // NOTE: we only compile .childNodes so that
  59. // we don't get into infinite loop compiling ourselves
  60. $compile(element.contents())(scope);
  61. }
  62. );
  63. };
  64. })
  65. });
  66. function Ctrl($scope) {
  67. $scope.name = 'Angular';
  68. $scope.html = 'Hello {{name}}';
  69. }
  70. </script>
  71. <div ng-controller="Ctrl">
  72. <input ng-model="name"> <br>
  73. <textarea ng-model="html"></textarea> <br>
  74. <div compile="html"></div>
  75. </div>
  76. </doc:source>
  77. <doc:scenario>
  78. it('should auto compile', function() {
  79. expect(element('div[compile]').text()).toBe('Hello Angular');
  80. input('html').enter('{{name}}!');
  81. expect(element('div[compile]').text()).toBe('Angular!');
  82. });
  83. </doc:scenario>
  84. </doc:example>
  85. *
  86. *
  87. * @param {string|DOMElement} element Element or HTML string to compile into a template function.
  88. * @param {function(angular.Scope[, cloneAttachFn]} transclude function available to directives.
  89. * @param {number} maxPriority only apply directives lower then given priority (Only effects the
  90. * root element(s), not their children)
  91. * @returns {function(scope[, cloneAttachFn])} a link function which is used to bind template
  92. * (a DOM element/tree) to a scope. Where:
  93. *
  94. * * `scope` - A {@link ng.$rootScope.Scope Scope} to bind to.
  95. * * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the
  96. * `template` and call the `cloneAttachFn` function allowing the caller to attach the
  97. * cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is
  98. * called as: <br> `cloneAttachFn(clonedElement, scope)` where:
  99. *
  100. * * `clonedElement` - is a clone of the original `element` passed into the compiler.
  101. * * `scope` - is the current scope with which the linking function is working with.
  102. *
  103. * Calling the linking function returns the element of the template. It is either the original element
  104. * passed in, or the clone of the element if the `cloneAttachFn` is provided.
  105. *
  106. * After linking the view is not updated until after a call to $digest which typically is done by
  107. * Angular automatically.
  108. *
  109. * If you need access to the bound view, there are two ways to do it:
  110. *
  111. * - If you are not asking the linking function to clone the template, create the DOM element(s)
  112. * before you send them to the compiler and keep this reference around.
  113. * <pre>
  114. * var element = $compile('<p>{{total}}</p>')(scope);
  115. * </pre>
  116. *
  117. * - if on the other hand, you need the element to be cloned, the view reference from the original
  118. * example would not point to the clone, but rather to the original template that was cloned. In
  119. * this case, you can access the clone via the cloneAttachFn:
  120. * <pre>
  121. * var templateHTML = angular.element('<p>{{total}}</p>'),
  122. * scope = ....;
  123. *
  124. * var clonedElement = $compile(templateHTML)(scope, function(clonedElement, scope) {
  125. * //attach the clone to DOM document at the right place
  126. * });
  127. *
  128. * //now we have reference to the cloned DOM via `clone`
  129. * </pre>
  130. *
  131. *
  132. * For information on how the compiler works, see the
  133. * {@link guide/compiler Angular HTML Compiler} section of the Developer Guide.
  134. */
  135. /**
  136. * @ngdoc service
  137. * @name ng.$compileProvider
  138. * @function
  139. *
  140. * @description
  141. */
  142. $CompileProvider.$inject = ['$provide'];
  143. function $CompileProvider($provide) {
  144. var hasDirectives = {},
  145. Suffix = 'Directive',
  146. COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/,
  147. CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/,
  148. MULTI_ROOT_TEMPLATE_ERROR = 'Template must have exactly one root element. was: ',
  149. urlSanitizationWhitelist = /^\s*(https?|ftp|mailto|file):/;
  150. /**
  151. * @ngdoc function
  152. * @name ng.$compileProvider#directive
  153. * @methodOf ng.$compileProvider
  154. * @function
  155. *
  156. * @description
  157. * Register a new directives with the compiler.
  158. *
  159. * @param {string} name Name of the directive in camel-case. (ie <code>ngBind</code> which will match as
  160. * <code>ng-bind</code>).
  161. * @param {function} directiveFactory An injectable directive factory function. See {@link guide/directive} for more
  162. * info.
  163. * @returns {ng.$compileProvider} Self for chaining.
  164. */
  165. this.directive = function registerDirective(name, directiveFactory) {
  166. if (isString(name)) {
  167. assertArg(directiveFactory, 'directive');
  168. if (!hasDirectives.hasOwnProperty(name)) {
  169. hasDirectives[name] = [];
  170. $provide.factory(name + Suffix, ['$injector', '$exceptionHandler',
  171. function($injector, $exceptionHandler) {
  172. var directives = [];
  173. forEach(hasDirectives[name], function(directiveFactory) {
  174. try {
  175. var directive = $injector.invoke(directiveFactory);
  176. if (isFunction(directive)) {
  177. directive = { compile: valueFn(directive) };
  178. } else if (!directive.compile && directive.link) {
  179. directive.compile = valueFn(directive.link);
  180. }
  181. directive.priority = directive.priority || 0;
  182. directive.name = directive.name || name;
  183. directive.require = directive.require || (directive.controller && directive.name);
  184. directive.restrict = directive.restrict || 'A';
  185. directives.push(directive);
  186. } catch (e) {
  187. $exceptionHandler(e);
  188. }
  189. });
  190. return directives;
  191. }]);
  192. }
  193. hasDirectives[name].push(directiveFactory);
  194. } else {
  195. forEach(name, reverseParams(registerDirective));
  196. }
  197. return this;
  198. };
  199. /**
  200. * @ngdoc function
  201. * @name ng.$compileProvider#urlSanitizationWhitelist
  202. * @methodOf ng.$compileProvider
  203. * @function
  204. *
  205. * @description
  206. * Retrieves or overrides the default regular expression that is used for whitelisting of safe
  207. * urls during a[href] sanitization.
  208. *
  209. * The sanitization is a security measure aimed at prevent XSS attacks via html links.
  210. *
  211. * Any url about to be assigned to a[href] via data-binding is first normalized and turned into an
  212. * absolute url. Afterwards the url is matched against the `urlSanitizationWhitelist` regular
  213. * expression. If a match is found the original url is written into the dom. Otherwise the
  214. * absolute url is prefixed with `'unsafe:'` string and only then it is written into the DOM.
  215. *
  216. * @param {RegExp=} regexp New regexp to whitelist urls with.
  217. * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
  218. * chaining otherwise.
  219. */
  220. this.urlSanitizationWhitelist = function(regexp) {
  221. if (isDefined(regexp)) {
  222. urlSanitizationWhitelist = regexp;
  223. return this;
  224. }
  225. return urlSanitizationWhitelist;
  226. };
  227. this.$get = [
  228. '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse',
  229. '$controller', '$rootScope', '$document',
  230. function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse,
  231. $controller, $rootScope, $document) {
  232. var Attributes = function(element, attr) {
  233. this.$$element = element;
  234. this.$attr = attr || {};
  235. };
  236. Attributes.prototype = {
  237. $normalize: directiveNormalize,
  238. /**
  239. * Set a normalized attribute on the element in a way such that all directives
  240. * can share the attribute. This function properly handles boolean attributes.
  241. * @param {string} key Normalized key. (ie ngAttribute)
  242. * @param {string|boolean} value The value to set. If `null` attribute will be deleted.
  243. * @param {boolean=} writeAttr If false, does not write the value to DOM element attribute.
  244. * Defaults to true.
  245. * @param {string=} attrName Optional none normalized name. Defaults to key.
  246. */
  247. $set: function(key, value, writeAttr, attrName) {
  248. var booleanKey = getBooleanAttrName(this.$$element[0], key),
  249. $$observers = this.$$observers,
  250. normalizedVal;
  251. if (booleanKey) {
  252. this.$$element.prop(key, value);
  253. attrName = booleanKey;
  254. }
  255. this[key] = value;
  256. // translate normalized key to actual key
  257. if (attrName) {
  258. this.$attr[key] = attrName;
  259. } else {
  260. attrName = this.$attr[key];
  261. if (!attrName) {
  262. this.$attr[key] = attrName = snake_case(key, '-');
  263. }
  264. }
  265. // sanitize a[href] values
  266. if (nodeName_(this.$$element[0]) === 'A' && key === 'href') {
  267. urlSanitizationNode.setAttribute('href', value);
  268. // href property always returns normalized absolute url, so we can match against that
  269. normalizedVal = urlSanitizationNode.href;
  270. if (!normalizedVal.match(urlSanitizationWhitelist)) {
  271. this[key] = value = 'unsafe:' + normalizedVal;
  272. }
  273. }
  274. if (writeAttr !== false) {
  275. if (value === null || value === undefined) {
  276. this.$$element.removeAttr(attrName);
  277. } else {
  278. this.$$element.attr(attrName, value);
  279. }
  280. }
  281. // fire observers
  282. $$observers && forEach($$observers[key], function(fn) {
  283. try {
  284. fn(value);
  285. } catch (e) {
  286. $exceptionHandler(e);
  287. }
  288. });
  289. },
  290. /**
  291. * Observe an interpolated attribute.
  292. * The observer will never be called, if given attribute is not interpolated.
  293. *
  294. * @param {string} key Normalized key. (ie ngAttribute) .
  295. * @param {function(*)} fn Function that will be called whenever the attribute value changes.
  296. * @returns {function(*)} the `fn` Function passed in.
  297. */
  298. $observe: function(key, fn) {
  299. var attrs = this,
  300. $$observers = (attrs.$$observers || (attrs.$$observers = {})),
  301. listeners = ($$observers[key] || ($$observers[key] = []));
  302. listeners.push(fn);
  303. $rootScope.$evalAsync(function() {
  304. if (!listeners.$$inter) {
  305. // no one registered attribute interpolation function, so lets call it manually
  306. fn(attrs[key]);
  307. }
  308. });
  309. return fn;
  310. }
  311. };
  312. var urlSanitizationNode = $document[0].createElement('a'),
  313. startSymbol = $interpolate.startSymbol(),
  314. endSymbol = $interpolate.endSymbol(),
  315. denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}')
  316. ? identity
  317. : function denormalizeTemplate(template) {
  318. return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
  319. },
  320. NG_ATTR_BINDING = /^ngAttr[A-Z]/;
  321. return compile;
  322. //================================
  323. function compile($compileNodes, transcludeFn, maxPriority) {
  324. if (!($compileNodes instanceof jqLite)) {
  325. // jquery always rewraps, whereas we need to preserve the original selector so that we can modify it.
  326. $compileNodes = jqLite($compileNodes);
  327. }
  328. // We can not compile top level text elements since text nodes can be merged and we will
  329. // not be able to attach scope data to them, so we will wrap them in <span>
  330. forEach($compileNodes, function(node, index){
  331. if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) {
  332. $compileNodes[index] = jqLite(node).wrap('<span></span>').parent()[0];
  333. }
  334. });
  335. var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority);
  336. return function publicLinkFn(scope, cloneConnectFn){
  337. assertArg(scope, 'scope');
  338. // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart
  339. // and sometimes changes the structure of the DOM.
  340. var $linkNode = cloneConnectFn
  341. ? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!!
  342. : $compileNodes;
  343. // Attach scope only to non-text nodes.
  344. for(var i = 0, ii = $linkNode.length; i<ii; i++) {
  345. var node = $linkNode[i];
  346. if (node.nodeType == 1 /* element */ || node.nodeType == 9 /* document */) {
  347. $linkNode.eq(i).data('$scope', scope);
  348. }
  349. }
  350. safeAddClass($linkNode, 'ng-scope');
  351. if (cloneConnectFn) cloneConnectFn($linkNode, scope);
  352. if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode);
  353. return $linkNode;
  354. };
  355. }
  356. function wrongMode(localName, mode) {
  357. throw Error("Unsupported '" + mode + "' for '" + localName + "'.");
  358. }
  359. function safeAddClass($element, className) {
  360. try {
  361. $element.addClass(className);
  362. } catch(e) {
  363. // ignore, since it means that we are trying to set class on
  364. // SVG element, where class name is read-only.
  365. }
  366. }
  367. /**
  368. * Compile function matches each node in nodeList against the directives. Once all directives
  369. * for a particular node are collected their compile functions are executed. The compile
  370. * functions return values - the linking functions - are combined into a composite linking
  371. * function, which is the a linking function for the node.
  372. *
  373. * @param {NodeList} nodeList an array of nodes or NodeList to compile
  374. * @param {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the
  375. * scope argument is auto-generated to the new child of the transcluded parent scope.
  376. * @param {DOMElement=} $rootElement If the nodeList is the root of the compilation tree then the
  377. * rootElement must be set the jqLite collection of the compile root. This is
  378. * needed so that the jqLite collection items can be replaced with widgets.
  379. * @param {number=} max directive priority
  380. * @returns {?function} A composite linking function of all of the matched directives or null.
  381. */
  382. function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority) {
  383. var linkFns = [],
  384. nodeLinkFn, childLinkFn, directives, attrs, linkFnFound;
  385. for(var i = 0; i < nodeList.length; i++) {
  386. attrs = new Attributes();
  387. // we must always refer to nodeList[i] since the nodes can be replaced underneath us.
  388. directives = collectDirectives(nodeList[i], [], attrs, maxPriority);
  389. nodeLinkFn = (directives.length)
  390. ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement)
  391. : null;
  392. childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || !nodeList[i].childNodes || !nodeList[i].childNodes.length)
  393. ? null
  394. : compileNodes(nodeList[i].childNodes,
  395. nodeLinkFn ? nodeLinkFn.transclude : transcludeFn);
  396. linkFns.push(nodeLinkFn);
  397. linkFns.push(childLinkFn);
  398. linkFnFound = (linkFnFound || nodeLinkFn || childLinkFn);
  399. }
  400. // return a linking function if we have found anything, null otherwise
  401. return linkFnFound ? compositeLinkFn : null;
  402. function compositeLinkFn(scope, nodeList, $rootElement, boundTranscludeFn) {
  403. var nodeLinkFn, childLinkFn, node, childScope, childTranscludeFn, i, ii, n;
  404. // copy nodeList so that linking doesn't break due to live list updates.
  405. var stableNodeList = [];
  406. for (i = 0, ii = nodeList.length; i < ii; i++) {
  407. stableNodeList.push(nodeList[i]);
  408. }
  409. for(i = 0, n = 0, ii = linkFns.length; i < ii; n++) {
  410. node = stableNodeList[n];
  411. nodeLinkFn = linkFns[i++];
  412. childLinkFn = linkFns[i++];
  413. if (nodeLinkFn) {
  414. if (nodeLinkFn.scope) {
  415. childScope = scope.$new(isObject(nodeLinkFn.scope));
  416. jqLite(node).data('$scope', childScope);
  417. } else {
  418. childScope = scope;
  419. }
  420. childTranscludeFn = nodeLinkFn.transclude;
  421. if (childTranscludeFn || (!boundTranscludeFn && transcludeFn)) {
  422. nodeLinkFn(childLinkFn, childScope, node, $rootElement,
  423. (function(transcludeFn) {
  424. return function(cloneFn) {
  425. var transcludeScope = scope.$new();
  426. transcludeScope.$$transcluded = true;
  427. return transcludeFn(transcludeScope, cloneFn).
  428. bind('$destroy', bind(transcludeScope, transcludeScope.$destroy));
  429. };
  430. })(childTranscludeFn || transcludeFn)
  431. );
  432. } else {
  433. nodeLinkFn(childLinkFn, childScope, node, undefined, boundTranscludeFn);
  434. }
  435. } else if (childLinkFn) {
  436. childLinkFn(scope, node.childNodes, undefined, boundTranscludeFn);
  437. }
  438. }
  439. }
  440. }
  441. /**
  442. * Looks for directives on the given node and adds them to the directive collection which is
  443. * sorted.
  444. *
  445. * @param node Node to search.
  446. * @param directives An array to which the directives are added to. This array is sorted before
  447. * the function returns.
  448. * @param attrs The shared attrs object which is used to populate the normalized attributes.
  449. * @param {number=} maxPriority Max directive priority.
  450. */
  451. function collectDirectives(node, directives, attrs, maxPriority) {
  452. var nodeType = node.nodeType,
  453. attrsMap = attrs.$attr,
  454. match,
  455. className;
  456. switch(nodeType) {
  457. case 1: /* Element */
  458. // use the node name: <directive>
  459. addDirective(directives,
  460. directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority);
  461. // iterate over the attributes
  462. for (var attr, name, nName, ngAttrName, value, nAttrs = node.attributes,
  463. j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) {
  464. attr = nAttrs[j];
  465. if (attr.specified) {
  466. name = attr.name;
  467. // support ngAttr attribute binding
  468. ngAttrName = directiveNormalize(name);
  469. if (NG_ATTR_BINDING.test(ngAttrName)) {
  470. name = ngAttrName.substr(6).toLowerCase();
  471. }
  472. nName = directiveNormalize(name.toLowerCase());
  473. attrsMap[nName] = name;
  474. attrs[nName] = value = trim((msie && name == 'href')
  475. ? decodeURIComponent(node.getAttribute(name, 2))
  476. : attr.value);
  477. if (getBooleanAttrName(node, nName)) {
  478. attrs[nName] = true; // presence means true
  479. }
  480. addAttrInterpolateDirective(node, directives, value, nName);
  481. addDirective(directives, nName, 'A', maxPriority);
  482. }
  483. }
  484. // use class as directive
  485. className = node.className;
  486. if (isString(className) && className !== '') {
  487. while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) {
  488. nName = directiveNormalize(match[2]);
  489. if (addDirective(directives, nName, 'C', maxPriority)) {
  490. attrs[nName] = trim(match[3]);
  491. }
  492. className = className.substr(match.index + match[0].length);
  493. }
  494. }
  495. break;
  496. case 3: /* Text Node */
  497. addTextInterpolateDirective(directives, node.nodeValue);
  498. break;
  499. case 8: /* Comment */
  500. try {
  501. match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue);
  502. if (match) {
  503. nName = directiveNormalize(match[1]);
  504. if (addDirective(directives, nName, 'M', maxPriority)) {
  505. attrs[nName] = trim(match[2]);
  506. }
  507. }
  508. } catch (e) {
  509. // turns out that under some circumstances IE9 throws errors when one attempts to read comment's node value.
  510. // Just ignore it and continue. (Can't seem to reproduce in test case.)
  511. }
  512. break;
  513. }
  514. directives.sort(byPriority);
  515. return directives;
  516. }
  517. /**
  518. * Once the directives have been collected, their compile functions are executed. This method
  519. * is responsible for inlining directive templates as well as terminating the application
  520. * of the directives if the terminal directive has been reached.
  521. *
  522. * @param {Array} directives Array of collected directives to execute their compile function.
  523. * this needs to be pre-sorted by priority order.
  524. * @param {Node} compileNode The raw DOM node to apply the compile functions to
  525. * @param {Object} templateAttrs The shared attribute function
  526. * @param {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the
  527. * scope argument is auto-generated to the new child of the transcluded parent scope.
  528. * @param {JQLite} jqCollection If we are working on the root of the compile tree then this
  529. * argument has the root jqLite array so that we can replace nodes on it.
  530. * @returns linkFn
  531. */
  532. function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, jqCollection) {
  533. var terminalPriority = -Number.MAX_VALUE,
  534. preLinkFns = [],
  535. postLinkFns = [],
  536. newScopeDirective = null,
  537. newIsolateScopeDirective = null,
  538. templateDirective = null,
  539. $compileNode = templateAttrs.$$element = jqLite(compileNode),
  540. directive,
  541. directiveName,
  542. $template,
  543. transcludeDirective,
  544. childTranscludeFn = transcludeFn,
  545. controllerDirectives,
  546. linkFn,
  547. directiveValue;
  548. // executes all directives on the current element
  549. for(var i = 0, ii = directives.length; i < ii; i++) {
  550. directive = directives[i];
  551. $template = undefined;
  552. if (terminalPriority > directive.priority) {
  553. break; // prevent further processing of directives
  554. }
  555. if (directiveValue = directive.scope) {
  556. assertNoDuplicate('isolated scope', newIsolateScopeDirective, directive, $compileNode);
  557. if (isObject(directiveValue)) {
  558. safeAddClass($compileNode, 'ng-isolate-scope');
  559. newIsolateScopeDirective = directive;
  560. }
  561. safeAddClass($compileNode, 'ng-scope');
  562. newScopeDirective = newScopeDirective || directive;
  563. }
  564. directiveName = directive.name;
  565. if (directiveValue = directive.controller) {
  566. controllerDirectives = controllerDirectives || {};
  567. assertNoDuplicate("'" + directiveName + "' controller",
  568. controllerDirectives[directiveName], directive, $compileNode);
  569. controllerDirectives[directiveName] = directive;
  570. }
  571. if (directiveValue = directive.transclude) {
  572. assertNoDuplicate('transclusion', transcludeDirective, directive, $compileNode);
  573. transcludeDirective = directive;
  574. terminalPriority = directive.priority;
  575. if (directiveValue == 'element') {
  576. $template = jqLite(compileNode);
  577. $compileNode = templateAttrs.$$element =
  578. jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' '));
  579. compileNode = $compileNode[0];
  580. replaceWith(jqCollection, jqLite($template[0]), compileNode);
  581. childTranscludeFn = compile($template, transcludeFn, terminalPriority);
  582. } else {
  583. $template = jqLite(JQLiteClone(compileNode)).contents();
  584. $compileNode.html(''); // clear contents
  585. childTranscludeFn = compile($template, transcludeFn);
  586. }
  587. }
  588. if (directive.template) {
  589. assertNoDuplicate('template', templateDirective, directive, $compileNode);
  590. templateDirective = directive;
  591. directiveValue = (isFunction(directive.template))
  592. ? directive.template($compileNode, templateAttrs)
  593. : directive.template;
  594. directiveValue = denormalizeTemplate(directiveValue);
  595. if (directive.replace) {
  596. $template = jqLite('<div>' +
  597. trim(directiveValue) +
  598. '</div>').contents();
  599. compileNode = $template[0];
  600. if ($template.length != 1 || compileNode.nodeType !== 1) {
  601. throw new Error(MULTI_ROOT_TEMPLATE_ERROR + directiveValue);
  602. }
  603. replaceWith(jqCollection, $compileNode, compileNode);
  604. var newTemplateAttrs = {$attr: {}};
  605. // combine directives from the original node and from the template:
  606. // - take the array of directives for this element
  607. // - split it into two parts, those that were already applied and those that weren't
  608. // - collect directives from the template, add them to the second group and sort them
  609. // - append the second group with new directives to the first group
  610. directives = directives.concat(
  611. collectDirectives(
  612. compileNode,
  613. directives.splice(i + 1, directives.length - (i + 1)),
  614. newTemplateAttrs
  615. )
  616. );
  617. mergeTemplateAttributes(templateAttrs, newTemplateAttrs);
  618. ii = directives.length;
  619. } else {
  620. $compileNode.html(directiveValue);
  621. }
  622. }
  623. if (directive.templateUrl) {
  624. assertNoDuplicate('template', templateDirective, directive, $compileNode);
  625. templateDirective = directive;
  626. nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i),
  627. nodeLinkFn, $compileNode, templateAttrs, jqCollection, directive.replace,
  628. childTranscludeFn);
  629. ii = directives.length;
  630. } else if (directive.compile) {
  631. try {
  632. linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn);
  633. if (isFunction(linkFn)) {
  634. addLinkFns(null, linkFn);
  635. } else if (linkFn) {
  636. addLinkFns(linkFn.pre, linkFn.post);
  637. }
  638. } catch (e) {
  639. $exceptionHandler(e, startingTag($compileNode));
  640. }
  641. }
  642. if (directive.terminal) {
  643. nodeLinkFn.terminal = true;
  644. terminalPriority = Math.max(terminalPriority, directive.priority);
  645. }
  646. }
  647. nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope;
  648. nodeLinkFn.transclude = transcludeDirective && childTranscludeFn;
  649. // might be normal or delayed nodeLinkFn depending on if templateUrl is present
  650. return nodeLinkFn;
  651. ////////////////////
  652. function addLinkFns(pre, post) {
  653. if (pre) {
  654. pre.require = directive.require;
  655. preLinkFns.push(pre);
  656. }
  657. if (post) {
  658. post.require = directive.require;
  659. postLinkFns.push(post);
  660. }
  661. }
  662. function getControllers(require, $element) {
  663. var value, retrievalMethod = 'data', optional = false;
  664. if (isString(require)) {
  665. while((value = require.charAt(0)) == '^' || value == '?') {
  666. require = require.substr(1);
  667. if (value == '^') {
  668. retrievalMethod = 'inheritedData';
  669. }
  670. optional = optional || value == '?';
  671. }
  672. value = $element[retrievalMethod]('$' + require + 'Controller');
  673. if (!value && !optional) {
  674. throw Error("No controller: " + require);
  675. }
  676. return value;
  677. } else if (isArray(require)) {
  678. value = [];
  679. forEach(require, function(require) {
  680. value.push(getControllers(require, $element));
  681. });
  682. }
  683. return value;
  684. }
  685. function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) {
  686. var attrs, $element, i, ii, linkFn, controller;
  687. if (compileNode === linkNode) {
  688. attrs = templateAttrs;
  689. } else {
  690. attrs = shallowCopy(templateAttrs, new Attributes(jqLite(linkNode), templateAttrs.$attr));
  691. }
  692. $element = attrs.$$element;
  693. if (newIsolateScopeDirective) {
  694. var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;
  695. var parentScope = scope.$parent || scope;
  696. forEach(newIsolateScopeDirective.scope, function(definiton, scopeName) {
  697. var match = definiton.match(LOCAL_REGEXP) || [],
  698. attrName = match[3] || scopeName,
  699. optional = (match[2] == '?'),
  700. mode = match[1], // @, =, or &
  701. lastValue,
  702. parentGet, parentSet;
  703. scope.$$isolateBindings[scopeName] = mode + attrName;
  704. switch (mode) {
  705. case '@': {
  706. attrs.$observe(attrName, function(value) {
  707. scope[scopeName] = value;
  708. });
  709. attrs.$$observers[attrName].$$scope = parentScope;
  710. if( attrs[attrName] ) {
  711. // If the attribute has been provided then we trigger an interpolation to ensure the value is there for use in the link fn
  712. scope[scopeName] = $interpolate(attrs[attrName])(parentScope);
  713. }
  714. break;
  715. }
  716. case '=': {
  717. if (optional && !attrs[attrName]) {
  718. return;
  719. }
  720. parentGet = $parse(attrs[attrName]);
  721. parentSet = parentGet.assign || function() {
  722. // reset the change, or we will throw this exception on every $digest
  723. lastValue = scope[scopeName] = parentGet(parentScope);
  724. throw Error(NON_ASSIGNABLE_MODEL_EXPRESSION + attrs[attrName] +
  725. ' (directive: ' + newIsolateScopeDirective.name + ')');
  726. };
  727. lastValue = scope[scopeName] = parentGet(parentScope);
  728. scope.$watch(function parentValueWatch() {
  729. var parentValue = parentGet(parentScope);
  730. if (parentValue !== scope[scopeName]) {
  731. // we are out of sync and need to copy
  732. if (parentValue !== lastValue) {
  733. // parent changed and it has precedence
  734. lastValue = scope[scopeName] = parentValue;
  735. } else {
  736. // if the parent can be assigned then do so
  737. parentSet(parentScope, parentValue = lastValue = scope[scopeName]);
  738. }
  739. }
  740. return parentValue;
  741. });
  742. break;
  743. }
  744. case '&': {
  745. parentGet = $parse(attrs[attrName]);
  746. scope[scopeName] = function(locals) {
  747. return parentGet(parentScope, locals);
  748. };
  749. break;
  750. }
  751. default: {
  752. throw Error('Invalid isolate scope definition for directive ' +
  753. newIsolateScopeDirective.name + ': ' + definiton);
  754. }
  755. }
  756. });
  757. }
  758. if (controllerDirectives) {
  759. forEach(controllerDirectives, function(directive) {
  760. var locals = {
  761. $scope: scope,
  762. $element: $element,
  763. $attrs: attrs,
  764. $transclude: boundTranscludeFn
  765. };
  766. controller = directive.controller;
  767. if (controller == '@') {
  768. controller = attrs[directive.name];
  769. }
  770. $element.data(
  771. '$' + directive.name + 'Controller',
  772. $controller(controller, locals));
  773. });
  774. }
  775. // PRELINKING
  776. for(i = 0, ii = preLinkFns.length; i < ii; i++) {
  777. try {
  778. linkFn = preLinkFns[i];
  779. linkFn(scope, $element, attrs,
  780. linkFn.require && getControllers(linkFn.require, $element));
  781. } catch (e) {
  782. $exceptionHandler(e, startingTag($element));
  783. }
  784. }
  785. // RECURSION
  786. childLinkFn && childLinkFn(scope, linkNode.childNodes, undefined, boundTranscludeFn);
  787. // POSTLINKING
  788. for(i = 0, ii = postLinkFns.length; i < ii; i++) {
  789. try {
  790. linkFn = postLinkFns[i];
  791. linkFn(scope, $element, attrs,
  792. linkFn.require && getControllers(linkFn.require, $element));
  793. } catch (e) {
  794. $exceptionHandler(e, startingTag($element));
  795. }
  796. }
  797. }
  798. }
  799. /**
  800. * looks up the directive and decorates it with exception handling and proper parameters. We
  801. * call this the boundDirective.
  802. *
  803. * @param {string} name name of the directive to look up.
  804. * @param {string} location The directive must be found in specific format.
  805. * String containing any of theses characters:
  806. *
  807. * * `E`: element name
  808. * * `A': attribute
  809. * * `C`: class
  810. * * `M`: comment
  811. * @returns true if directive was added.
  812. */
  813. function addDirective(tDirectives, name, location, maxPriority) {
  814. var match = false;
  815. if (hasDirectives.hasOwnProperty(name)) {
  816. for(var directive, directives = $injector.get(name + Suffix),
  817. i = 0, ii = directives.length; i<ii; i++) {
  818. try {
  819. directive = directives[i];
  820. if ( (maxPriority === undefined || maxPriority > directive.priority) &&
  821. directive.restrict.indexOf(location) != -1) {
  822. tDirectives.push(directive);
  823. match = true;
  824. }
  825. } catch(e) { $exceptionHandler(e); }
  826. }
  827. }
  828. return match;
  829. }
  830. /**
  831. * When the element is replaced with HTML template then the new attributes
  832. * on the template need to be merged with the existing attributes in the DOM.
  833. * The desired effect is to have both of the attributes present.
  834. *
  835. * @param {object} dst destination attributes (original DOM)
  836. * @param {object} src source attributes (from the directive template)
  837. */
  838. function mergeTemplateAttributes(dst, src) {
  839. var srcAttr = src.$attr,
  840. dstAttr = dst.$attr,
  841. $element = dst.$$element;
  842. // reapply the old attributes to the new element
  843. forEach(dst, function(value, key) {
  844. if (key.charAt(0) != '$') {
  845. if (src[key]) {
  846. value += (key === 'style' ? ';' : ' ') + src[key];
  847. }
  848. dst.$set(key, value, true, srcAttr[key]);
  849. }
  850. });
  851. // copy the new attributes on the old attrs object
  852. forEach(src, function(value, key) {
  853. if (key == 'class') {
  854. safeAddClass($element, value);
  855. dst['class'] = (dst['class'] ? dst['class'] + ' ' : '') + value;
  856. } else if (key == 'style') {
  857. $element.attr('style', $element.attr('style') + ';' + value);
  858. } else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) {
  859. dst[key] = value;
  860. dstAttr[key] = srcAttr[key];
  861. }
  862. });
  863. }
  864. function compileTemplateUrl(directives, beforeTemplateNodeLinkFn, $compileNode, tAttrs,
  865. $rootElement, replace, childTranscludeFn) {
  866. var linkQueue = [],
  867. afterTemplateNodeLinkFn,
  868. afterTemplateChildLinkFn,
  869. beforeTemplateCompileNode = $compileNode[0],
  870. origAsyncDirective = directives.shift(),
  871. // The fact that we have to copy and patch the directive seems wrong!
  872. derivedSyncDirective = extend({}, origAsyncDirective, {
  873. controller: null, templateUrl: null, transclude: null, scope: null
  874. }),
  875. templateUrl = (isFunction(origAsyncDirective.templateUrl))
  876. ? origAsyncDirective.templateUrl($compileNode, tAttrs)
  877. : origAsyncDirective.templateUrl;
  878. $compileNode.html('');
  879. $http.get(templateUrl, {cache: $templateCache}).
  880. success(function(content) {
  881. var compileNode, tempTemplateAttrs, $template;
  882. content = denormalizeTemplate(content);
  883. if (replace) {
  884. $template = jqLite('<div>' + trim(content) + '</div>').contents();
  885. compileNode = $template[0];
  886. if ($template.length != 1 || compileNode.nodeType !== 1) {
  887. throw new Error(MULTI_ROOT_TEMPLATE_ERROR + content);
  888. }
  889. tempTemplateAttrs = {$attr: {}};
  890. replaceWith($rootElement, $compileNode, compileNode);
  891. collectDirectives(compileNode, directives, tempTemplateAttrs);
  892. mergeTemplateAttributes(tAttrs, tempTemplateAttrs);
  893. } else {
  894. compileNode = beforeTemplateCompileNode;
  895. $compileNode.html(content);
  896. }
  897. directives.unshift(derivedSyncDirective);
  898. afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs, childTranscludeFn);
  899. afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes, childTranscludeFn);
  900. while(linkQueue.length) {
  901. var scope = linkQueue.shift(),
  902. beforeTemplateLinkNode = linkQueue.shift(),
  903. linkRootElement = linkQueue.shift(),
  904. controller = linkQueue.shift(),
  905. linkNode = compileNode;
  906. if (beforeTemplateLinkNode !== beforeTemplateCompileNode) {
  907. // it was cloned therefore we have to clone as well.
  908. linkNode = JQLiteClone(compileNode);
  909. replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode);
  910. }
  911. afterTemplateNodeLinkFn(function() {
  912. beforeTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, controller);
  913. }, scope, linkNode, $rootElement, controller);
  914. }
  915. linkQueue = null;
  916. }).
  917. error(function(response, code, headers, config) {
  918. throw Error('Failed to load template: ' + config.url);
  919. });
  920. return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, controller) {
  921. if (linkQueue) {
  922. linkQueue.push(scope);
  923. linkQueue.push(node);
  924. linkQueue.push(rootElement);
  925. linkQueue.push(controller);
  926. } else {
  927. afterTemplateNodeLinkFn(function() {
  928. beforeTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, controller);
  929. }, scope, node, rootElement, controller);
  930. }
  931. };
  932. }
  933. /**
  934. * Sorting function for bound directives.
  935. */
  936. function byPriority(a, b) {
  937. return b.priority - a.priority;
  938. }
  939. function assertNoDuplicate(what, previousDirective, directive, element) {
  940. if (previousDirective) {
  941. throw Error('Multiple directives [' + previousDirective.name + ', ' +
  942. directive.name + '] asking for ' + what + ' on: ' + startingTag(element));
  943. }
  944. }
  945. function addTextInterpolateDirective(directives, text) {
  946. var interpolateFn = $interpolate(text, true);
  947. if (interpolateFn) {
  948. directives.push({
  949. priority: 0,
  950. compile: valueFn(function textInterpolateLinkFn(scope, node) {
  951. var parent = node.parent(),
  952. bindings = parent.data('$binding') || [];
  953. bindings.push(interpolateFn);
  954. safeAddClass(parent.data('$binding', bindings), 'ng-binding');
  955. scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {
  956. node[0].nodeValue = value;
  957. });
  958. })
  959. });
  960. }
  961. }
  962. function addAttrInterpolateDirective(node, directives, value, name) {
  963. var interpolateFn = $interpolate(value, true);
  964. // no interpolation found -> ignore
  965. if (!interpolateFn) return;
  966. directives.push({
  967. priority: 100,
  968. compile: valueFn(function attrInterpolateLinkFn(scope, element, attr) {
  969. var $$observers = (attr.$$observers || (attr.$$observers = {}));
  970. // we need to interpolate again, in case the attribute value has been updated
  971. // (e.g. by another directive's compile function)
  972. interpolateFn = $interpolate(attr[name], true);
  973. // if attribute was updated so that there is no interpolation going on we don't want to
  974. // register any observers
  975. if (!interpolateFn) return;
  976. attr[name] = interpolateFn(scope);
  977. ($$observers[name] || ($$observers[name] = [])).$$inter = true;
  978. (attr.$$observers && attr.$$observers[name].$$scope || scope).
  979. $watch(interpolateFn, function interpolateFnWatchAction(value) {
  980. attr.$set(name, value);
  981. });
  982. })
  983. });
  984. }
  985. /**
  986. * This is a special jqLite.replaceWith, which can replace items which
  987. * have no parents, provided that the containing jqLite collection is provided.
  988. *
  989. * @param {JqLite=} $rootElement The root of the compile tree. Used so that we can replace nodes
  990. * in the root of the tree.
  991. * @param {JqLite} $element The jqLite element which we are going to replace. We keep the shell,
  992. * but replace its DOM node reference.
  993. * @param {Node} newNode The new DOM node.
  994. */
  995. function replaceWith($rootElement, $element, newNode) {
  996. var oldNode = $element[0],
  997. parent = oldNode.parentNode,
  998. i, ii;
  999. if ($rootElement) {
  1000. for(i = 0, ii = $rootElement.length; i < ii; i++) {
  1001. if ($rootElement[i] == oldNode) {
  1002. $rootElement[i] = newNode;
  1003. break;
  1004. }
  1005. }
  1006. }
  1007. if (parent) {
  1008. parent.replaceChild(newNode, oldNode);
  1009. }
  1010. newNode[jqLite.expando] = oldNode[jqLite.expando];
  1011. $element[0] = newNode;
  1012. }
  1013. }];
  1014. }
  1015. var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i;
  1016. /**
  1017. * Converts all accepted directives format into proper directive name.
  1018. * All of these will become 'myDirective':
  1019. * my:DiRective
  1020. * my-directive
  1021. * x-my-directive
  1022. * data-my:directive
  1023. *
  1024. * Also there is special case for Moz prefix starting with upper case letter.
  1025. * @param name Name to normalize
  1026. */
  1027. function directiveNormalize(name) {
  1028. return camelCase(name.replace(PREFIX_REGEXP, ''));
  1029. }
  1030. /**
  1031. * @ngdoc object
  1032. * @name ng.$compile.directive.Attributes
  1033. * @description
  1034. *
  1035. * A shared object between directive compile / linking functions which contains normalized DOM element
  1036. * attributes. The the values reflect current binding state `{{ }}`. The normalization is needed
  1037. * since all of these are treated as equivalent in Angular:
  1038. *
  1039. * <span ng:bind="a" ng-bind="a" data-ng-bind="a" x-ng-bind="a">
  1040. */
  1041. /**
  1042. * @ngdoc property
  1043. * @name ng.$compile.directive.Attributes#$attr
  1044. * @propertyOf ng.$compile.directive.Attributes
  1045. * @returns {object} A map of DOM element attribute names to the normalized name. This is
  1046. * needed to do reverse lookup from normalized name back to actual name.
  1047. */
  1048. /**
  1049. * @ngdoc function
  1050. * @name ng.$compile.directive.Attributes#$set
  1051. * @methodOf ng.$compile.directive.Attributes
  1052. * @function
  1053. *
  1054. * @description
  1055. * Set DOM element attribute value.
  1056. *
  1057. *
  1058. * @param {string} name Normalized element attribute name of the property to modify. The name is
  1059. * revers translated using the {@link ng.$compile.directive.Attributes#$attr $attr}
  1060. * property to the original name.
  1061. * @param {string} value Value to set the attribute to. The value can be an interpolated string.
  1062. */
  1063. /**
  1064. * Closure compiler type information
  1065. */
  1066. function nodesetLinkingFn(
  1067. /* angular.Scope */ scope,
  1068. /* NodeList */ nodeList,
  1069. /* Element */ rootElement,
  1070. /* function(Function) */ boundTranscludeFn
  1071. ){}
  1072. function directiveLinkingFn(
  1073. /* nodesetLinkingFn */ nodesetLinkingFn,
  1074. /* angular.Scope */ scope,
  1075. /* Node */ node,
  1076. /* Element */ rootElement,
  1077. /* function(Function) */ boundTranscludeFn
  1078. ){}