PageRenderTime 54ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/src/ng/compile.js

https://github.com/frimfram/angular.js
JavaScript | 1214 lines | 706 code | 146 blank | 362 comment | 177 complexity | 5c6fcec34d68973b7c7e14482696fd16 MD5 | raw file
Possible License(s): Apache-2.0, JSON
  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 factroy 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. return compile;
  321. //================================
  322. function compile($compileNodes, transcludeFn, maxPriority) {
  323. if (!($compileNodes instanceof jqLite)) {
  324. // jquery always rewraps, whereas we need to preserve the original selector so that we can modify it.
  325. $compileNodes = jqLite($compileNodes);
  326. }
  327. // We can not compile top level text elements since text nodes can be merged and we will
  328. // not be able to attach scope data to them, so we will wrap them in <span>
  329. forEach($compileNodes, function(node, index){
  330. if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) {
  331. $compileNodes[index] = jqLite(node).wrap('<span></span>').parent()[0];
  332. }
  333. });
  334. var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority);
  335. return function publicLinkFn(scope, cloneConnectFn){
  336. assertArg(scope, 'scope');
  337. // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart
  338. // and sometimes changes the structure of the DOM.
  339. var $linkNode = cloneConnectFn
  340. ? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!!
  341. : $compileNodes;
  342. // Attach scope only to non-text nodes.
  343. for(var i = 0, ii = $linkNode.length; i<ii; i++) {
  344. var node = $linkNode[i];
  345. if (node.nodeType == 1 /* element */ || node.nodeType == 9 /* document */) {
  346. $linkNode.eq(i).data('$scope', scope);
  347. }
  348. }
  349. safeAddClass($linkNode, 'ng-scope');
  350. if (cloneConnectFn) cloneConnectFn($linkNode, scope);
  351. if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode);
  352. return $linkNode;
  353. };
  354. }
  355. function wrongMode(localName, mode) {
  356. throw Error("Unsupported '" + mode + "' for '" + localName + "'.");
  357. }
  358. function safeAddClass($element, className) {
  359. try {
  360. $element.addClass(className);
  361. } catch(e) {
  362. // ignore, since it means that we are trying to set class on
  363. // SVG element, where class name is read-only.
  364. }
  365. }
  366. /**
  367. * Compile function matches each node in nodeList against the directives. Once all directives
  368. * for a particular node are collected their compile functions are executed. The compile
  369. * functions return values - the linking functions - are combined into a composite linking
  370. * function, which is the a linking function for the node.
  371. *
  372. * @param {NodeList} nodeList an array of nodes or NodeList to compile
  373. * @param {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the
  374. * scope argument is auto-generated to the new child of the transcluded parent scope.
  375. * @param {DOMElement=} $rootElement If the nodeList is the root of the compilation tree then the
  376. * rootElement must be set the jqLite collection of the compile root. This is
  377. * needed so that the jqLite collection items can be replaced with widgets.
  378. * @param {number=} max directive priority
  379. * @returns {?function} A composite linking function of all of the matched directives or null.
  380. */
  381. function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority) {
  382. var linkFns = [],
  383. nodeLinkFn, childLinkFn, directives, attrs, linkFnFound;
  384. for(var i = 0; i < nodeList.length; i++) {
  385. attrs = new Attributes();
  386. // we must always refer to nodeList[i] since the nodes can be replaced underneath us.
  387. directives = collectDirectives(nodeList[i], [], attrs, maxPriority);
  388. nodeLinkFn = (directives.length)
  389. ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement)
  390. : null;
  391. childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || !nodeList[i].childNodes || !nodeList[i].childNodes.length)
  392. ? null
  393. : compileNodes(nodeList[i].childNodes,
  394. nodeLinkFn ? nodeLinkFn.transclude : transcludeFn);
  395. linkFns.push(nodeLinkFn);
  396. linkFns.push(childLinkFn);
  397. linkFnFound = (linkFnFound || nodeLinkFn || childLinkFn);
  398. }
  399. // return a linking function if we have found anything, null otherwise
  400. return linkFnFound ? compositeLinkFn : null;
  401. function compositeLinkFn(scope, nodeList, $rootElement, boundTranscludeFn) {
  402. var nodeLinkFn, childLinkFn, node, childScope, childTranscludeFn, i, ii, n;
  403. // copy nodeList so that linking doesn't break due to live list updates.
  404. var stableNodeList = [];
  405. for (i = 0, ii = nodeList.length; i < ii; i++) {
  406. stableNodeList.push(nodeList[i]);
  407. }
  408. for(i = 0, n = 0, ii = linkFns.length; i < ii; n++) {
  409. node = stableNodeList[n];
  410. nodeLinkFn = linkFns[i++];
  411. childLinkFn = linkFns[i++];
  412. if (nodeLinkFn) {
  413. if (nodeLinkFn.scope) {
  414. childScope = scope.$new(isObject(nodeLinkFn.scope));
  415. jqLite(node).data('$scope', childScope);
  416. } else {
  417. childScope = scope;
  418. }
  419. childTranscludeFn = nodeLinkFn.transclude;
  420. if (childTranscludeFn || (!boundTranscludeFn && transcludeFn)) {
  421. nodeLinkFn(childLinkFn, childScope, node, $rootElement,
  422. (function(transcludeFn) {
  423. return function(cloneFn) {
  424. var transcludeScope = scope.$new();
  425. transcludeScope.$$transcluded = true;
  426. return transcludeFn(transcludeScope, cloneFn).
  427. bind('$destroy', bind(transcludeScope, transcludeScope.$destroy));
  428. };
  429. })(childTranscludeFn || transcludeFn)
  430. );
  431. } else {
  432. nodeLinkFn(childLinkFn, childScope, node, undefined, boundTranscludeFn);
  433. }
  434. } else if (childLinkFn) {
  435. childLinkFn(scope, node.childNodes, undefined, boundTranscludeFn);
  436. }
  437. }
  438. }
  439. }
  440. /**
  441. * Looks for directives on the given node and adds them to the directive collection which is
  442. * sorted.
  443. *
  444. * @param node Node to search.
  445. * @param directives An array to which the directives are added to. This array is sorted before
  446. * the function returns.
  447. * @param attrs The shared attrs object which is used to populate the normalized attributes.
  448. * @param {number=} maxPriority Max directive priority.
  449. */
  450. function collectDirectives(node, directives, attrs, maxPriority) {
  451. var nodeType = node.nodeType,
  452. attrsMap = attrs.$attr,
  453. match,
  454. className;
  455. switch(nodeType) {
  456. case 1: /* Element */
  457. // use the node name: <directive>
  458. addDirective(directives,
  459. directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority);
  460. // iterate over the attributes
  461. for (var attr, name, nName, value, nAttrs = node.attributes,
  462. j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) {
  463. attr = nAttrs[j];
  464. if (attr.specified) {
  465. name = attr.name;
  466. nName = directiveNormalize(name.toLowerCase());
  467. attrsMap[nName] = name;
  468. attrs[nName] = value = trim((msie && name == 'href')
  469. ? decodeURIComponent(node.getAttribute(name, 2))
  470. : attr.value);
  471. if (getBooleanAttrName(node, nName)) {
  472. attrs[nName] = true; // presence means true
  473. }
  474. addAttrInterpolateDirective(node, directives, value, nName);
  475. addDirective(directives, nName, 'A', maxPriority);
  476. }
  477. }
  478. // use class as directive
  479. className = node.className;
  480. if (isString(className) && className !== '') {
  481. while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) {
  482. nName = directiveNormalize(match[2]);
  483. if (addDirective(directives, nName, 'C', maxPriority)) {
  484. attrs[nName] = trim(match[3]);
  485. }
  486. className = className.substr(match.index + match[0].length);
  487. }
  488. }
  489. break;
  490. case 3: /* Text Node */
  491. addTextInterpolateDirective(directives, node.nodeValue);
  492. break;
  493. case 8: /* Comment */
  494. try {
  495. match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue);
  496. if (match) {
  497. nName = directiveNormalize(match[1]);
  498. if (addDirective(directives, nName, 'M', maxPriority)) {
  499. attrs[nName] = trim(match[2]);
  500. }
  501. }
  502. } catch (e) {
  503. // turns out that under some circumstances IE9 throws errors when one attempts to read comment's node value.
  504. // Just ignore it and continue. (Can't seem to reproduce in test case.)
  505. }
  506. break;
  507. }
  508. directives.sort(byPriority);
  509. return directives;
  510. }
  511. /**
  512. * Once the directives have been collected their compile functions is executed. This method
  513. * is responsible for inlining directive templates as well as terminating the application
  514. * of the directives if the terminal directive has been reached..
  515. *
  516. * @param {Array} directives Array of collected directives to execute their compile function.
  517. * this needs to be pre-sorted by priority order.
  518. * @param {Node} compileNode The raw DOM node to apply the compile functions to
  519. * @param {Object} templateAttrs The shared attribute function
  520. * @param {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the
  521. * scope argument is auto-generated to the new child of the transcluded parent scope.
  522. * @param {DOMElement} $rootElement If we are working on the root of the compile tree then this
  523. * argument has the root jqLite array so that we can replace widgets on it.
  524. * @returns linkFn
  525. */
  526. function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, $rootElement) {
  527. var terminalPriority = -Number.MAX_VALUE,
  528. preLinkFns = [],
  529. postLinkFns = [],
  530. newScopeDirective = null,
  531. newIsolateScopeDirective = null,
  532. templateDirective = null,
  533. $compileNode = templateAttrs.$$element = jqLite(compileNode),
  534. directive,
  535. directiveName,
  536. $template,
  537. transcludeDirective,
  538. childTranscludeFn = transcludeFn,
  539. controllerDirectives,
  540. linkFn,
  541. directiveValue;
  542. // executes all directives on the current element
  543. for(var i = 0, ii = directives.length; i < ii; i++) {
  544. directive = directives[i];
  545. $template = undefined;
  546. if (terminalPriority > directive.priority) {
  547. break; // prevent further processing of directives
  548. }
  549. if (directiveValue = directive.scope) {
  550. assertNoDuplicate('isolated scope', newIsolateScopeDirective, directive, $compileNode);
  551. if (isObject(directiveValue)) {
  552. safeAddClass($compileNode, 'ng-isolate-scope');
  553. newIsolateScopeDirective = directive;
  554. }
  555. safeAddClass($compileNode, 'ng-scope');
  556. newScopeDirective = newScopeDirective || directive;
  557. }
  558. directiveName = directive.name;
  559. if (directiveValue = directive.controller) {
  560. controllerDirectives = controllerDirectives || {};
  561. assertNoDuplicate("'" + directiveName + "' controller",
  562. controllerDirectives[directiveName], directive, $compileNode);
  563. controllerDirectives[directiveName] = directive;
  564. }
  565. if (directiveValue = directive.transclude) {
  566. assertNoDuplicate('transclusion', transcludeDirective, directive, $compileNode);
  567. transcludeDirective = directive;
  568. terminalPriority = directive.priority;
  569. if (directiveValue == 'element') {
  570. $template = jqLite(compileNode);
  571. $compileNode = templateAttrs.$$element =
  572. jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' '));
  573. compileNode = $compileNode[0];
  574. replaceWith($rootElement, jqLite($template[0]), compileNode);
  575. childTranscludeFn = compile($template, transcludeFn, terminalPriority);
  576. } else {
  577. $template = jqLite(JQLiteClone(compileNode)).contents();
  578. $compileNode.html(''); // clear contents
  579. childTranscludeFn = compile($template, transcludeFn);
  580. }
  581. }
  582. if ((directiveValue = directive.template)) {
  583. assertNoDuplicate('template', templateDirective, directive, $compileNode);
  584. templateDirective = directive;
  585. directiveValue = denormalizeTemplate(directiveValue);
  586. if (directive.replace) {
  587. $template = jqLite('<div>' +
  588. trim(directiveValue) +
  589. '</div>').contents();
  590. compileNode = $template[0];
  591. if ($template.length != 1 || compileNode.nodeType !== 1) {
  592. throw new Error(MULTI_ROOT_TEMPLATE_ERROR + directiveValue);
  593. }
  594. replaceWith($rootElement, $compileNode, compileNode);
  595. var newTemplateAttrs = {$attr: {}};
  596. // combine directives from the original node and from the template:
  597. // - take the array of directives for this element
  598. // - split it into two parts, those that were already applied and those that weren't
  599. // - collect directives from the template, add them to the second group and sort them
  600. // - append the second group with new directives to the first group
  601. directives = directives.concat(
  602. collectDirectives(
  603. compileNode,
  604. directives.splice(i + 1, directives.length - (i + 1)),
  605. newTemplateAttrs
  606. )
  607. );
  608. mergeTemplateAttributes(templateAttrs, newTemplateAttrs);
  609. ii = directives.length;
  610. } else {
  611. $compileNode.html(directiveValue);
  612. }
  613. }
  614. if (directive.templateUrl) {
  615. assertNoDuplicate('template', templateDirective, directive, $compileNode);
  616. templateDirective = directive;
  617. nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i),
  618. nodeLinkFn, $compileNode, templateAttrs, $rootElement, directive.replace,
  619. childTranscludeFn);
  620. ii = directives.length;
  621. } else if (directive.compile) {
  622. try {
  623. linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn);
  624. if (isFunction(linkFn)) {
  625. addLinkFns(null, linkFn);
  626. } else if (linkFn) {
  627. addLinkFns(linkFn.pre, linkFn.post);
  628. }
  629. } catch (e) {
  630. $exceptionHandler(e, startingTag($compileNode));
  631. }
  632. }
  633. if (directive.terminal) {
  634. nodeLinkFn.terminal = true;
  635. terminalPriority = Math.max(terminalPriority, directive.priority);
  636. }
  637. }
  638. nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope;
  639. nodeLinkFn.transclude = transcludeDirective && childTranscludeFn;
  640. // might be normal or delayed nodeLinkFn depending on if templateUrl is present
  641. return nodeLinkFn;
  642. ////////////////////
  643. function addLinkFns(pre, post) {
  644. if (pre) {
  645. pre.require = directive.require;
  646. preLinkFns.push(pre);
  647. }
  648. if (post) {
  649. post.require = directive.require;
  650. postLinkFns.push(post);
  651. }
  652. }
  653. function getControllers(require, $element) {
  654. var value, retrievalMethod = 'data', optional = false;
  655. if (isString(require)) {
  656. while((value = require.charAt(0)) == '^' || value == '?') {
  657. require = require.substr(1);
  658. if (value == '^') {
  659. retrievalMethod = 'inheritedData';
  660. }
  661. optional = optional || value == '?';
  662. }
  663. value = $element[retrievalMethod]('$' + require + 'Controller');
  664. if (!value && !optional) {
  665. throw Error("No controller: " + require);
  666. }
  667. return value;
  668. } else if (isArray(require)) {
  669. value = [];
  670. forEach(require, function(require) {
  671. value.push(getControllers(require, $element));
  672. });
  673. }
  674. return value;
  675. }
  676. function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) {
  677. var attrs, $element, i, ii, linkFn, controller;
  678. if (compileNode === linkNode) {
  679. attrs = templateAttrs;
  680. } else {
  681. attrs = shallowCopy(templateAttrs, new Attributes(jqLite(linkNode), templateAttrs.$attr));
  682. }
  683. $element = attrs.$$element;
  684. if (newIsolateScopeDirective) {
  685. var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;
  686. var parentScope = scope.$parent || scope;
  687. forEach(newIsolateScopeDirective.scope, function(definiton, scopeName) {
  688. var match = definiton.match(LOCAL_REGEXP) || [],
  689. attrName = match[3] || scopeName,
  690. optional = (match[2] == '?'),
  691. mode = match[1], // @, =, or &
  692. lastValue,
  693. parentGet, parentSet;
  694. scope.$$isolateBindings[scopeName] = mode + attrName;
  695. switch (mode) {
  696. case '@': {
  697. attrs.$observe(attrName, function(value) {
  698. scope[scopeName] = value;
  699. });
  700. attrs.$$observers[attrName].$$scope = parentScope;
  701. if( attrs[attrName] ) {
  702. // If the attribute has been provided then we trigger an interpolation to ensure the value is there for use in the link fn
  703. scope[scopeName] = $interpolate(attrs[attrName])(parentScope);
  704. }
  705. break;
  706. }
  707. case '=': {
  708. if (optional && !attrs[attrName]) {
  709. return;
  710. }
  711. parentGet = $parse(attrs[attrName]);
  712. parentSet = parentGet.assign || function() {
  713. // reset the change, or we will throw this exception on every $digest
  714. lastValue = scope[scopeName] = parentGet(parentScope);
  715. throw Error(NON_ASSIGNABLE_MODEL_EXPRESSION + attrs[attrName] +
  716. ' (directive: ' + newIsolateScopeDirective.name + ')');
  717. };
  718. lastValue = scope[scopeName] = parentGet(parentScope);
  719. scope.$watch(function parentValueWatch() {
  720. var parentValue = parentGet(parentScope);
  721. if (parentValue !== scope[scopeName]) {
  722. // we are out of sync and need to copy
  723. if (parentValue !== lastValue) {
  724. // parent changed and it has precedence
  725. lastValue = scope[scopeName] = parentValue;
  726. } else {
  727. // if the parent can be assigned then do so
  728. parentSet(parentScope, parentValue = lastValue = scope[scopeName]);
  729. }
  730. }
  731. return parentValue;
  732. });
  733. break;
  734. }
  735. case '&': {
  736. parentGet = $parse(attrs[attrName]);
  737. scope[scopeName] = function(locals) {
  738. return parentGet(parentScope, locals);
  739. }
  740. break;
  741. }
  742. default: {
  743. throw Error('Invalid isolate scope definition for directive ' +
  744. newIsolateScopeDirective.name + ': ' + definiton);
  745. }
  746. }
  747. });
  748. }
  749. if (controllerDirectives) {
  750. forEach(controllerDirectives, function(directive) {
  751. var locals = {
  752. $scope: scope,
  753. $element: $element,
  754. $attrs: attrs,
  755. $transclude: boundTranscludeFn
  756. };
  757. controller = directive.controller;
  758. if (controller == '@') {
  759. controller = attrs[directive.name];
  760. }
  761. $element.data(
  762. '$' + directive.name + 'Controller',
  763. $controller(controller, locals));
  764. });
  765. }
  766. // PRELINKING
  767. for(i = 0, ii = preLinkFns.length; i < ii; i++) {
  768. try {
  769. linkFn = preLinkFns[i];
  770. linkFn(scope, $element, attrs,
  771. linkFn.require && getControllers(linkFn.require, $element));
  772. } catch (e) {
  773. $exceptionHandler(e, startingTag($element));
  774. }
  775. }
  776. // RECURSION
  777. childLinkFn && childLinkFn(scope, linkNode.childNodes, undefined, boundTranscludeFn);
  778. // POSTLINKING
  779. for(i = 0, ii = postLinkFns.length; i < ii; i++) {
  780. try {
  781. linkFn = postLinkFns[i];
  782. linkFn(scope, $element, attrs,
  783. linkFn.require && getControllers(linkFn.require, $element));
  784. } catch (e) {
  785. $exceptionHandler(e, startingTag($element));
  786. }
  787. }
  788. }
  789. }
  790. /**
  791. * looks up the directive and decorates it with exception handling and proper parameters. We
  792. * call this the boundDirective.
  793. *
  794. * @param {string} name name of the directive to look up.
  795. * @param {string} location The directive must be found in specific format.
  796. * String containing any of theses characters:
  797. *
  798. * * `E`: element name
  799. * * `A': attribute
  800. * * `C`: class
  801. * * `M`: comment
  802. * @returns true if directive was added.
  803. */
  804. function addDirective(tDirectives, name, location, maxPriority) {
  805. var match = false;
  806. if (hasDirectives.hasOwnProperty(name)) {
  807. for(var directive, directives = $injector.get(name + Suffix),
  808. i = 0, ii = directives.length; i<ii; i++) {
  809. try {
  810. directive = directives[i];
  811. if ( (maxPriority === undefined || maxPriority > directive.priority) &&
  812. directive.restrict.indexOf(location) != -1) {
  813. tDirectives.push(directive);
  814. match = true;
  815. }
  816. } catch(e) { $exceptionHandler(e); }
  817. }
  818. }
  819. return match;
  820. }
  821. /**
  822. * When the element is replaced with HTML template then the new attributes
  823. * on the template need to be merged with the existing attributes in the DOM.
  824. * The desired effect is to have both of the attributes present.
  825. *
  826. * @param {object} dst destination attributes (original DOM)
  827. * @param {object} src source attributes (from the directive template)
  828. */
  829. function mergeTemplateAttributes(dst, src) {
  830. var srcAttr = src.$attr,
  831. dstAttr = dst.$attr,
  832. $element = dst.$$element;
  833. // reapply the old attributes to the new element
  834. forEach(dst, function(value, key) {
  835. if (key.charAt(0) != '$') {
  836. if (src[key]) {
  837. value += (key === 'style' ? ';' : ' ') + src[key];
  838. }
  839. dst.$set(key, value, true, srcAttr[key]);
  840. }
  841. });
  842. // copy the new attributes on the old attrs object
  843. forEach(src, function(value, key) {
  844. if (key == 'class') {
  845. safeAddClass($element, value);
  846. dst['class'] = (dst['class'] ? dst['class'] + ' ' : '') + value;
  847. } else if (key == 'style') {
  848. $element.attr('style', $element.attr('style') + ';' + value);
  849. } else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) {
  850. dst[key] = value;
  851. dstAttr[key] = srcAttr[key];
  852. }
  853. });
  854. }
  855. function compileTemplateUrl(directives, beforeTemplateNodeLinkFn, $compileNode, tAttrs,
  856. $rootElement, replace, childTranscludeFn) {
  857. var linkQueue = [],
  858. afterTemplateNodeLinkFn,
  859. afterTemplateChildLinkFn,
  860. beforeTemplateCompileNode = $compileNode[0],
  861. origAsyncDirective = directives.shift(),
  862. // The fact that we have to copy and patch the directive seems wrong!
  863. derivedSyncDirective = extend({}, origAsyncDirective, {
  864. controller: null, templateUrl: null, transclude: null, scope: null
  865. });
  866. $compileNode.html('');
  867. $http.get(origAsyncDirective.templateUrl, {cache: $templateCache}).
  868. success(function(content) {
  869. var compileNode, tempTemplateAttrs, $template;
  870. content = denormalizeTemplate(content);
  871. if (replace) {
  872. $template = jqLite('<div>' + trim(content) + '</div>').contents();
  873. compileNode = $template[0];
  874. if ($template.length != 1 || compileNode.nodeType !== 1) {
  875. throw new Error(MULTI_ROOT_TEMPLATE_ERROR + content);
  876. }
  877. tempTemplateAttrs = {$attr: {}};
  878. replaceWith($rootElement, $compileNode, compileNode);
  879. collectDirectives(compileNode, directives, tempTemplateAttrs);
  880. mergeTemplateAttributes(tAttrs, tempTemplateAttrs);
  881. } else {
  882. compileNode = beforeTemplateCompileNode;
  883. $compileNode.html(content);
  884. }
  885. directives.unshift(derivedSyncDirective);
  886. afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs, childTranscludeFn);
  887. afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes, childTranscludeFn);
  888. while(linkQueue.length) {
  889. var controller = linkQueue.pop(),
  890. linkRootElement = linkQueue.pop(),
  891. beforeTemplateLinkNode = linkQueue.pop(),
  892. scope = linkQueue.pop(),
  893. linkNode = compileNode;
  894. if (beforeTemplateLinkNode !== beforeTemplateCompileNode) {
  895. // it was cloned therefore we have to clone as well.
  896. linkNode = JQLiteClone(compileNode);
  897. replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode);
  898. }
  899. afterTemplateNodeLinkFn(function() {
  900. beforeTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, controller);
  901. }, scope, linkNode, $rootElement, controller);
  902. }
  903. linkQueue = null;
  904. }).
  905. error(function(response, code, headers, config) {
  906. throw Error('Failed to load template: ' + config.url);
  907. });
  908. return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, controller) {
  909. if (linkQueue) {
  910. linkQueue.push(scope);
  911. linkQueue.push(node);
  912. linkQueue.push(rootElement);
  913. linkQueue.push(controller);
  914. } else {
  915. afterTemplateNodeLinkFn(function() {
  916. beforeTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, controller);
  917. }, scope, node, rootElement, controller);
  918. }
  919. };
  920. }
  921. /**
  922. * Sorting function for bound directives.
  923. */
  924. function byPriority(a, b) {
  925. return b.priority - a.priority;
  926. }
  927. function assertNoDuplicate(what, previousDirective, directive, element) {
  928. if (previousDirective) {
  929. throw Error('Multiple directives [' + previousDirective.name + ', ' +
  930. directive.name + '] asking for ' + what + ' on: ' + startingTag(element));
  931. }
  932. }
  933. function addTextInterpolateDirective(directives, text) {
  934. var interpolateFn = $interpolate(text, true);
  935. if (interpolateFn) {
  936. directives.push({
  937. priority: 0,
  938. compile: valueFn(function textInterpolateLinkFn(scope, node) {
  939. var parent = node.parent(),
  940. bindings = parent.data('$binding') || [];
  941. bindings.push(interpolateFn);
  942. safeAddClass(parent.data('$binding', bindings), 'ng-binding');
  943. scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {
  944. node[0].nodeValue = value;
  945. });
  946. })
  947. });
  948. }
  949. }
  950. function addAttrInterpolateDirective(node, directives, value, name) {
  951. var interpolateFn = $interpolate(value, true);
  952. // no interpolation found -> ignore
  953. if (!interpolateFn) return;
  954. directives.push({
  955. priority: 100,
  956. compile: valueFn(function attrInterpolateLinkFn(scope, element, attr) {
  957. var $$observers = (attr.$$observers || (attr.$$observers = {}));
  958. if (name === 'class') {
  959. // we need to interpolate classes again, in the case the element was replaced
  960. // and therefore the two class attrs got merged - we want to interpolate the result
  961. interpolateFn = $interpolate(attr[name], true);
  962. }
  963. attr[name] = interpolateFn(scope);
  964. ($$observers[name] || ($$observers[name] = [])).$$inter = true;
  965. (attr.$$observers && attr.$$observers[name].$$scope || scope).
  966. $watch(interpolateFn, function interpolateFnWatchAction(value) {
  967. attr.$set(name, value);
  968. });
  969. })
  970. });
  971. }
  972. /**
  973. * This is a special jqLite.replaceWith, which can replace items which
  974. * have no parents, provided that the containing jqLite collection is provided.
  975. *
  976. * @param {JqLite=} $rootElement The root of the compile tree. Used so that we can replace nodes
  977. * in the root of the tree.
  978. * @param {JqLite} $element The jqLite element which we are going to replace. We keep the shell,
  979. * but replace its DOM node reference.
  980. * @param {Node} newNode The new DOM node.
  981. */
  982. function replaceWith($rootElement, $element, newNode) {
  983. var oldNode = $element[0],
  984. parent = oldNode.parentNode,
  985. i, ii;
  986. if ($rootElement) {
  987. for(i = 0, ii = $rootElement.length; i < ii; i++) {
  988. if ($rootElement[i] == oldNode) {
  989. $rootElement[i] = newNode;
  990. break;
  991. }
  992. }
  993. }
  994. if (parent) {
  995. parent.replaceChild(newNode, oldNode);
  996. }
  997. newNode[jqLite.expando] = oldNode[jqLite.expando];
  998. $element[0] = newNode;
  999. }
  1000. }];
  1001. }
  1002. var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i;
  1003. /**
  1004. * Converts all accepted directives format into proper directive name.
  1005. * All of these will become 'myDirective':
  1006. * my:DiRective
  1007. * my-directive
  1008. * x-my-directive
  1009. * data-my:directive
  1010. *
  1011. * Also there is special case for Moz prefix starting with upper case letter.
  1012. * @param name Name to normalize
  1013. */
  1014. function directiveNormalize(name) {
  1015. return camelCase(name.replace(PREFIX_REGEXP, ''));
  1016. }
  1017. /**
  1018. * @ngdoc object
  1019. * @name ng.$compile.directive.Attributes
  1020. * @description
  1021. *
  1022. * A shared object between directive compile / linking functions which contains normalized DOM element
  1023. * attributes. The the values reflect current binding state `{{ }}`. The normalization is needed
  1024. * since all of these are treated as equivalent in Angular:
  1025. *
  1026. * <span ng:bind="a" ng-bind="a" data-ng-bind="a" x-ng-bind="a">
  1027. */
  1028. /**
  1029. * @ngdoc property
  1030. * @name ng.$compile.directive.Attributes#$attr
  1031. * @propertyOf ng.$compile.directive.Attributes
  1032. * @returns {object} A map of DOM element attribute names to the normalized name. This is
  1033. * needed to do reverse lookup from normalized name back to actual name.
  1034. */
  1035. /**
  1036. * @ngdoc function
  1037. * @name ng.$compile.directive.Attributes#$set
  1038. * @methodOf ng.$compile.directive.Attributes
  1039. * @function
  1040. *
  1041. * @description
  1042. * Set DOM element attribute value.
  1043. *
  1044. *
  1045. * @param {string} name Normalized element attribute name of the property to modify. The name is
  1046. * revers translated using the {@link ng.$compile.directive.Attributes#$attr $attr}
  1047. * property to the original name.
  1048. * @param {string} value Value to set the attribute to.
  1049. */
  1050. /**
  1051. * Closure compiler type information
  1052. */
  1053. function nodesetLinkingFn(
  1054. /* angular.Scope */ scope,
  1055. /* NodeList */ nodeList,
  1056. /* Element */ rootElement,
  1057. /* function(Function) */ boundTranscludeFn
  1058. ){}
  1059. function directiveLinkingFn(
  1060. /* nodesetLinkingFn */ nodesetLinkingFn,
  1061. /* angular.Scope */ scope,
  1062. /* Node */ node,
  1063. /* Element */ rootElement,
  1064. /* function(Function) */ boundTranscludeFn
  1065. ){}