PageRenderTime 57ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/src/ng/compile.js

https://github.com/xsurge83/angular.js
JavaScript | 1347 lines | 806 code | 155 blank | 386 comment | 214 complexity | e92822587d10e0beb85e0620098fd2f1 MD5 | raw file
Possible License(s): JSON

Large files files are truncated, but you can click here to view the full file

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

Large files files are truncated, but you can click here to view the full file