diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/Angular.js | 38 | ||||
| -rw-r--r-- | src/angular-mocks.js | 12 | ||||
| -rw-r--r-- | src/jqLite.js | 22 | ||||
| -rw-r--r-- | src/service/compiler.js | 1033 | ||||
| -rw-r--r-- | src/service/formFactory.js | 72 | 
5 files changed, 813 insertions, 364 deletions
| diff --git a/src/Angular.js b/src/Angular.js index 17ede3aa..4a0589c3 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -627,6 +627,22 @@ function copy(source, destination){  }  /** + * Create a shallow copy of an object + * @param src + */ +function shallowCopy(src) { +  var dst = {}, +      key; +  for(key in src) { +    if (src.hasOwnProperty(key)) { +      dst[key] = src[key]; +    } +  } +  return dst; +} + + +/**   * @ngdoc function   * @name angular.equals   * @function @@ -750,6 +766,19 @@ function toBoolean(value) {    return value;  } +/** + * @returns {string} Returns the string representation of the element. + */ +function startingTag(element) { +  element = jqLite(element).clone(); +  try { +    // turns out IE does not let you set .html() on elements which +    // are not allowed to have children. So we just ignore it. +    element.html(''); +  } catch(e) {}; +  return jqLite('<div>').append(element).html().replace(/\<\/[\w\:\-]+\>$/, ''); +} +  ///////////////////////////////////////////////// @@ -918,6 +947,14 @@ function bootstrap(element, modules) {    return injector;  } +var SNAKE_CASE_REGEXP = /[A-Z]/g; +function snake_case(name, separator){ +  separator = separator || '_'; +  return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) { +    return (pos ? separator : '') + letter.toLowerCase(); +  }); +} +  function bindJQuery() {    // bind to jQuery if present;    jQuery = window.jQuery; @@ -951,6 +988,7 @@ function assertArg(arg, name, reason) {  }  function assertArgFn(arg, name) { +  assertArg(arg, name);    assertArg(isFunction(arg), name, 'not a function, got ' +        (typeof arg == 'object' ? arg.constructor.name || 'Object' : typeof arg));    return arg; diff --git a/src/angular-mocks.js b/src/angular-mocks.js index 0645322b..c024343e 100644 --- a/src/angular-mocks.js +++ b/src/angular-mocks.js @@ -684,19 +684,17 @@ angular.mock.dump = function(object) {   *   * <pre>     // controller -   function MyController($http) { -     var scope = this; - +   function MyController($scope, $http) {       $http.get('/auth.py').success(function(data) { -       scope.user = data; +       $scope.user = data;       });       this.saveMessage = function(message) { -       scope.status = 'Saving...'; +       $scope.status = 'Saving...';         $http.post('/add-msg.py', message).success(function(response) { -         scope.status = ''; +         $scope.status = '';         }).error(function() { -         scope.status = 'ERROR!'; +         $scope.status = 'ERROR!';         });       };     } diff --git a/src/jqLite.js b/src/jqLite.js index 9e16f8ec..e48d250b 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -100,13 +100,27 @@ function getStyle(element) {  } +var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; +var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i; +var MOZ_HACK_REGEXP = /^moz([A-Z])/;  /** - * Converts dash-separated names to camelCase. Useful for dealing with css properties. + * Converts all accepted directives format into proper directive name. + * All of these will become 'myDirective': + *   my:DiRective + *   my-directive + *   x-my-directive + *   data-my:directive + * + * Also there is special case for Moz prefix starting with upper case letter. + * @param name Name to normalize   */  function camelCase(name) { -  return name.replace(/\-(\w)/g, function(all, letter, offset){ -    return (offset == 0 && letter == 'w') ? 'w' : letter.toUpperCase(); -  }); +  return name. +    replace(PREFIX_REGEXP, ''). +    replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { +      return offset ? letter.toUpperCase() : letter; +    }). +    replace(MOZ_HACK_REGEXP, 'Moz$1');  }  ///////////////////////////////////////////// diff --git a/src/service/compiler.js b/src/service/compiler.js index adf1ffa9..6185c909 100644 --- a/src/service/compiler.js +++ b/src/service/compiler.js @@ -1,356 +1,753 @@  'use strict'; +/** + * @ngdoc function + * @name angular.module.ng.$compile + * @function + * + * @description + * Compiles a piece of HTML string or DOM into a template and produces a template function, which + * can then be used to link {@link angular.module.ng.$rootScope.Scope scope} and the template together. + * + * The compilation is a process of walking the DOM tree and trying to match DOM elements to + * {@link angular.module.ng.$compileProvider.directive directives}. For each match it + * executes corresponding template function and collects the + * instance functions into a single template function which is then returned. + * + * The template function can then be used once to produce the view or as it is the case with + * {@link angular.module.ng.$compileProvider.directive.ng:repeat repeater} many-times, in which + * case each call results in a view that is a DOM clone of the original template. + * + <doc:example module="compile"> +   <doc:source> +    <script> +      // declare a new module, and inject the $compileProvider +      angular.module('compile', [], function($compileProvider) { +        // configure new 'compile' directive by passing a directive +        // factory function. The factory function injects the '$compile' +        $compileProvider.directive('compile', function($compile) { +          // directive factory creates a link function +          return function(scope, element, attrs) { +            scope.$watch( +              function(scope) { +                 // watch the 'compile' expression for changes +                return scope.$eval(attrs.compile); +              }, +              function(scope, value) { +                // when the 'compile' expression changes +                // assign it into the current DOM +                element.html(value); -function $CompileProvider(){ -  this.$get = ['$injector', '$exceptionHandler', '$textMarkup', '$attrMarkup', '$directive', '$widget', -    function(   $injector,   $exceptionHandler,   $textMarkup,   $attrMarkup,   $directive,   $widget){ -      /** -       * Template provides directions an how to bind to a given element. -       * It contains a list of init functions which need to be called to -       * bind to a new instance of elements. It also provides a list -       * of child paths which contain child templates -       */ -      function Template() { -        this.paths = []; -        this.children = []; -        this.linkFns = []; -        this.newScope = false; +                // compile the new DOM and link it to the current +                // scope. +                // NOTE: we only compile .childNodes so that +                // we don't get into infinite loop compiling ourselves +                $compile(element.contents())(scope); +              } +            ); +          }; +        }) +      }); + +      function Ctrl() { +        this.name = 'Angular'; +        this.html = 'Hello {{name}}';        } +    </script> +    <div ng-controller="Ctrl"> +      <input ng:model="name"> <br> +      <textarea ng:model="html"></textarea> <br> +      <div compile="html"></div> +    </div> +   </doc:source> +   <doc:scenario> +     it('should auto compile', function() { +       expect(element('div[compile]').text()).toBe('Hello Angular'); +       input('html').enter('{{name}}!'); +       expect(element('div[compile]').text()).toBe('Angular!'); +     }); +   </doc:scenario> + </doc:example> -      Template.prototype = { -        link: function(element, scope) { -          var childScope = scope, -              locals = {$element: element}; -          if (this.newScope) { -            childScope = scope.$new(); -            element.data($$scope, childScope); -          } -          forEach(this.linkFns, function(fn) { -            try { -              if (isArray(fn) || fn.$inject) { -                $injector.invoke(fn, childScope, locals); -              } else { -                fn.call(childScope, element); + * + * + * @param {string|DOMElement} element Element or HTML string to compile into a template function. + * @returns {function(scope[, cloneAttachFn])} a link function which is used to bind template + * (a DOM element/tree) to a scope. Where: + * + *  * `scope` - A {@link angular.module.ng.$rootScope.Scope Scope} to bind to. + *  * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the + *               `template` and call the `cloneAttachFn` function allowing the caller to attach the + *               cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is + *               called as: <br> `cloneAttachFn(clonedElement, scope)` where: + * + *      * `clonedElement` - is a clone of the original `element` passed into the compiler. + *      * `scope` - is the current scope with which the linking function is working with. + * + * Calling the linking function returns the element of the template. It is either the original element + * passed in, or the clone of the element if the `cloneAttachFn` is provided. + * + * After linking the view is not updateh until after a call to $digest which typically is done by + * Angular automatically. + * + * If you need access to the bound view, there are two ways to do it: + * + * - If you are not asking the linking function to clone the template, create the DOM element(s) + *   before you send them to the compiler and keep this reference around. + *   <pre> + *     var element = $compile('<p>{{total}}</p>')(scope); + *   </pre> + * + * - if on the other hand, you need the element to be cloned, the view reference from the original + *   example would not point to the clone, but rather to the original template that was cloned. In + *   this case, you can access the clone via the cloneAttachFn: + *   <pre> + *     var templateHTML = angular.element('<p>{{total}}</p>'), + *         scope = ....; + * + *     var clonedElement = $compile(templateHTML)(scope, function(clonedElement, scope) { + *       //attach the clone to DOM document at the right place + *     }); + * + *     //now we have reference to the cloned DOM via `clone` + *   </pre> + * + * + * Compiler Methods For Widgets and Directives: + * + * The following methods are available for use when you write your own widgets, directives, + * and markup. + * + * + * For information on how the compiler works, see the + * {@link guide/dev_guide.compiler Angular HTML Compiler} section of the Developer Guide. + */ + + +$CompileProvider.$inject = ['$provide']; +function $CompileProvider($provide) { +  var hasDirectives = {}, +      Suffix = 'Directive', +      COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/, +      CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/, +      CONTENT_REGEXP = /\<\<content\>\>/i, +      HAS_ROOT_ELEMENT = /^\<[\s\S]*\>$/, +      SIDE_EFFECT_ATTRS = {}; + +  forEach('src,href,multiple,selected,checked,disabled,readonly,required'.split(','), function(name) { +    SIDE_EFFECT_ATTRS[name] = name; +    SIDE_EFFECT_ATTRS[directiveNormalize('ng_' + name)] = name; +  }); + + +  this.directive = function registerDirective(name, directiveFactory) { +    if (isString(name)) { +      assertArg(directiveFactory, 'directive'); +      if (!hasDirectives.hasOwnProperty(name)) { +        hasDirectives[name] = []; +        $provide.factory(name + Suffix, ['$injector', '$exceptionHandler', +          function($injector, $exceptionHandler) { +            var directives = []; +            forEach(hasDirectives[name], function(directiveFactory) { +              try { +                var directive = $injector.invoke(directiveFactory); +                if (isFunction(directive)) { +                  directive = { compile: valueFn(directive) }; +                } else if (!directive.compile && directive.link) { +                  directive.compile = valueFn(directive.link); +                } +                directive.priority = directive.priority || 0; +                directive.name = name; +                directive.restrict = directive.restrict || 'EACM'; +                directives.push(directive); +              } catch (e) { +                $exceptionHandler(e);                } -            } catch (e) { -              $exceptionHandler(e); +            }); +            return directives; +          }]); +      } +      hasDirectives[name].push(directiveFactory); +    } else { +      forEach(name, reverseParams(registerDirective)); +    } +    return this; +  }; + + +  this.$get = ['$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', +       function($injector,   $interpolate,   $exceptionHandler,   $http,   $templateCache) { + +    return function(templateElement) { +      templateElement = jqLite(templateElement); +      var linkingFn = compileNodes(templateElement, templateElement); +      return function(scope, cloneConnectFn){ +        assertArg(scope, 'scope'); +        // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart +        // and sometimes changes the structure of the DOM. +        var element = cloneConnectFn +          ? JQLitePrototype.clone.call(templateElement) // IMPORTANT!!! +          : templateElement; +        element.data('$scope', scope); +        if (cloneConnectFn) cloneConnectFn(element, scope); +        if (linkingFn) linkingFn(scope, element, element); +        return element; +      }; +    }; + +    //================================ + +    /** +     * Compile function matches each node in nodeList against the directives. Once all directives +     * for a particular node are collected their compile functions are executed. The compile +     * functions return values - the linking functions - are combined into a composite linking +     * function, which is the a linking function for the node. +     * +     * @param {NodeList} nodeList an array of nodes to compile +     * @param {DOMElement=} rootElement If the nodeList is the root of the compilation tree then the +     *        rootElement must be set the jqLite collection of the compile root. This is +     *        needed so that the jqLite collection items can be replaced with widgets. +     * @returns {?function} A composite linking function of all of the matched directives or null. +     */ +    function compileNodes(nodeList, rootElement) { +     var linkingFns = [], +         directiveLinkingFn, childLinkingFn, directives, attrs, linkingFnFound; + +     for(var i = 0, ii = nodeList.length; i < ii; i++) { +       attrs = { +         $attr: {}, +         $normalize: directiveNormalize, +         $set: attrSetter +       }; +       // we must always refer to nodeList[i] since the nodes can be replaced underneath us. +       directives = collectDirectives(nodeList[i], [], attrs); + +       directiveLinkingFn = (directives.length) +           ? applyDirectivesToNode(directives, nodeList[i], attrs, rootElement) +           : null; + +       childLinkingFn = (directiveLinkingFn && directiveLinkingFn.terminal) +           ? null +           : compileNodes(nodeList[i].childNodes); + +       linkingFns.push(directiveLinkingFn); +       linkingFns.push(childLinkingFn); +       linkingFnFound = (linkingFnFound || directiveLinkingFn || childLinkingFn); +     } + +     // return a linking function if we have found anything, null otherwise +     return linkingFnFound ? linkingFn : null; + +     function linkingFn(scope, nodeList, rootElement) { +       if (linkingFns.length != nodeList.length * 2) { +         throw Error('Template changed structure!'); +       } + +       var childLinkingFn, directiveLinkingFn, node, childScope; + +       for(var i=0, n=0, ii=linkingFns.length; i<ii; n++) { +         node = nodeList[n]; +         directiveLinkingFn = linkingFns[i++]; +         childLinkingFn = linkingFns[i++]; + +         if (directiveLinkingFn) { +           if (directiveLinkingFn.scope && !rootElement) { +             childScope = scope.$new(); +             jqLite(node).data('$scope', childScope); +           } else { +             childScope = scope; +           } +           directiveLinkingFn(childLinkingFn, childScope, node, rootElement); +         } else if (childLinkingFn) { +           childLinkingFn(scope, node.childNodes); +         } +       } +     } +   } + + +    /** +     * Looks for directives on the given node ands them to the directive collection which is sorted. +     * +     * @param node node to search +     * @param directives an array to which the directives are added to. This array is sorted before +     *        the function returns. +     * @param attrs the shared attrs object which is used to populate the normalized attributes. +     */ +    function collectDirectives(node, directives, attrs) { +      var nodeType = node.nodeType, +          attrsMap = attrs.$attr, +          match, +          className; + +      switch(nodeType) { +        case 1: /* Element */ +          // use the node name: <directive> +          addDirective(directives, directiveNormalize(nodeName_(node).toLowerCase()), 'E'); + +          // iterate over the attributes +          for (var attr, name, nName, value, nAttrs = node.attributes, +                   j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { +            attr = nAttrs[j]; +            name = attr.name; +            nName = directiveNormalize(name.toLowerCase()); +            attrsMap[nName] = name; +            attrs[nName] = value = trim((msie && name == 'href') +                ? decodeURIComponent(node.getAttribute(name, 2)) +                : attr.value); +            if (BOOLEAN_ATTR[nName]) { +              attrs[nName] = true; // presence means true              } -          }); -          var i, -              childNodes = element[0].childNodes, -              children = this.children, -              paths = this.paths, -              length = paths.length; -          for (i = 0; i < length; i++) { -            // sometimes `element` can be modified by one of the linker functions in `this.linkFns` -            // and childNodes may be added or removed -            // TODO: element structure needs to be re-evaluated if new children added -            // if the childNode still exists -            if (childNodes[paths[i]]) -              children[i].link(jqLite(childNodes[paths[i]]), childScope); -            else -              delete paths[i]; // if child no longer available, delete path +            addAttrInterpolateDirective(directives, value, nName); +            addDirective(directives, nName, 'A');            } -        }, - -        addLinkFn:function(linkingFn) { -          if (linkingFn) { -            this.linkFns.push(linkingFn); +          // use class as directive +          className = node.className; +          while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) { +            nName = directiveNormalize(match[2]); +            if (addDirective(directives, nName, 'C')) { +              attrs[nName] = trim(match[3]); +            } +            className = className.substr(match.index + match[0].length);            } -        }, +          break; +        case 3: /* Text Node */ +          addTextInterpolateDirective(directives, node.nodeValue); +          break; +        case 8: /* Comment */ +          match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); +          if (match) { +            nName = directiveNormalize(match[1]); +            if (addDirective(directives, nName, 'M')) { +              attrs[nName] = trim(match[2]); +            } +          } +          break; +      } + +      directives.sort(byPriority); +      return directives; +    } -        addChild: function(index, template) { -          if (template) { -            this.paths.push(index); -            this.children.push(template); +    /** +     * Once the directives have been collected their compile functions is executed. This method +     * is responsible for inlining widget templates as well as terminating the application +     * of the directives if the terminal directive has been reached.. +     * +     * @param {Array} directives Array of collected directives to execute their compile function. +     *        this needs to be pre-sorted by priority order. +     * @param {Node} templateNode The raw DOM node to apply the compile functions to +     * @param {Object} templateAttrs The shared attribute function +     * @param {DOMElement} rootElement If we are working on the root of the compile tree then this +     *        argument has the root jqLite array so that we can replace widgets on it. +     * @returns linkingFn +     */ +    function applyDirectivesToNode(directives, templateNode, templateAttrs, rootElement) { +      var terminalPriority = -Number.MAX_VALUE, +          preLinkingFns = [], +          postLinkingFns = [], +          newScopeDirective = null, +          templateDirective = null, +          delayedLinkingFn = null, +          element = templateAttrs.$element = jqLite(templateNode), +          directive, linkingFn; + +      // executes all directives on the current element +      for(var i = 0, ii = directives.length; i < ii; i++) { +        directive = directives[i]; + +        if (terminalPriority > directive.priority) { +          break; // prevent further processing of directives +        } + +        if (directive.scope) { +          assertNoDuplicate('new scope', newScopeDirective, directive, element); +          newScopeDirective = directive; +        } + +        if (directive.template) { +          assertNoDuplicate('template', templateDirective, directive, element); +          templateDirective = directive; + +          // include the contents of the original element into the template and replace the element +          var content = directive.template.replace(CONTENT_REGEXP, element.html()); +          templateNode = jqLite(content)[0]; +          if (directive.replace) { +            replaceWith(rootElement, element, templateNode); + +            var newTemplateAttrs = {$attr: {}}; + +            // combine directives from the original node and from the template: +            // - take the array of directives for this element +            // - split it into two parts, those that were already applied and those that weren't +            // - collect directives from the template, add them to the second group and sort them +            // - append the second group with new directives to the first group +            directives = directives.concat( +                collectDirectives( +                    templateNode, +                    directives.splice(i + 1, directives.length - (i + 1)), +                    newTemplateAttrs +                ) +            ); +            mergeTemplateAttributes(templateAttrs, newTemplateAttrs); + +            ii = directives.length; +          } else { +            element.html(content); +          } +        } + +        if (directive.templateUrl) { +          assertNoDuplicate('template', templateDirective, directive, element); +          templateDirective = directive; +          delayedLinkingFn = compileTemplateUrl(directives.splice(i, directives.length - i), +              compositeLinkFn, element, templateAttrs, rootElement, directive.replace); +          ii = directives.length; +        } else if (directive.compile) { +          try { +            linkingFn = directive.compile(element, templateAttrs); +            if (isFunction(linkingFn)) { +              postLinkingFns.push(linkingFn); +            } else if (linkingFn) { +              if (linkingFn.pre) preLinkingFns.push(linkingFn.pre); +              if (linkingFn.post) postLinkingFns.push(linkingFn.post); +            } +          } catch (e) { +            $exceptionHandler(e, startingTag(element));            } -        }, +        } -        empty: function() { -          return this.linkFns.length === 0 && this.paths.length === 0; +        if (directive.terminal) { +          compositeLinkFn.terminal = true; +          terminalPriority = Math.max(terminalPriority, directive.priority);          } -      }; -      /////////////////////////////////// -      //Compiler -      ////////////////////////////////// - -      /** -       * @ngdoc function -       * @name angular.module.ng.$compile -       * @function -       * -       * @description -       * Compiles a piece of HTML string or DOM into a template and produces a template function, which -       * can then be used to link {@link angular.module.ng.$rootScope.Scope scope} and the template together. -       * -       * The compilation is a process of walking the DOM tree and trying to match DOM elements to -       * {@link angular.markup markup}, {@link angular.attrMarkup attrMarkup}, -       * {@link angular.widget widgets}, and {@link angular.directive directives}. For each match it -       * executes corresponding markup, attrMarkup, widget or directive template function and collects the -       * instance functions into a single template function which is then returned. -       * -       * The template function can then be used once to produce the view or as it is the case with -       * {@link angular.widget.@ng:repeat repeater} many-times, in which case each call results in a view -       * that is a DOM clone of the original template. -       * -         <pre> -          angular.injector(['ng']).invoke(function($rootScope, $compile) { -            // Chose one: - -            // A: compile the entire window.document. -            var element = $compile(window.document)($rootScope); - -            // B: compile a piece of html -            var element = $compile('<div ng:click="clicked = true">click me</div>')($rootScope); - -            // C: compile a piece of html and retain reference to both the dom and scope -            var element = $compile('<div ng:click="clicked = true">click me</div>')(scope); -            // at this point template was transformed into a view -          }); -         </pre> -       * -       * -       * @param {string|DOMElement} element Element or HTML to compile into a template function. -       * @returns {function(scope[, cloneAttachFn])} a template function which is used to bind template -       * (a DOM element/tree) to a scope. Where: -       * -       *  * `scope` - A {@link angular.module.ng.$rootScope.Scope Scope} to bind to. -       *  * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the -       *               `template` and call the `cloneAttachFn` function allowing the caller to attach the -       *               cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is -       *               called as: <br/> `cloneAttachFn(clonedElement, scope)` where: -       * -       *      * `clonedElement` - is a clone of the original `element` passed into the compiler. -       *      * `scope` - is the current scope with which the linking function is working with. -       * -       * Calling the template function returns the element of the template. It is either the original element -       * passed in, or the clone of the element if the `cloneAttachFn` is provided. -       * -       * It is important to understand that the returned scope is "linked" to the view DOM, but no linking -       * (instance) functions registered by {@link angular.directive directives} or -       * {@link angular.widget widgets} found in the template have been executed yet. This means that the -       * view is likely empty and doesn't contain any values that result from evaluation on the scope. To -       * bring the view to life, the scope needs to run through a $digest phase which typically is done by -       * Angular automatically, except for the case when an application is being -       * {@link guide/dev_guide.bootstrap.manual_bootstrap} manually bootstrapped, in which case the -       * $digest phase must be invoked by calling {@link angular.module.ng.$rootScope.Scope#$apply}. -       * -       * If you need access to the bound view, there are two ways to do it: -       * -       * - If you are not asking the linking function to clone the template, create the DOM element(s) -       *   before you send them to the compiler and keep this reference around. -       *   <pre> -       *     var $injector = angular.injector(['ng']); -       *     var scope = $injector.invoke(function($rootScope, $compile){ -       *       var element = $compile('<p>{{total}}</p>')($rootScope); -       *     }); -       *   </pre> -       * -       * - if on the other hand, you need the element to be cloned, the view reference from the original -       *   example would not point to the clone, but rather to the original template that was cloned. In -       *   this case, you can access the clone via the cloneAttachFn: -       *   <pre> -       *     var original = angular.element('<p>{{total}}</p>'), -       *         scope = someParentScope.$new(), -       *         clone; -       * -       *     $compile(original)(scope, function(clonedElement, scope) { -       *       clone = clonedElement; -       *       //attach the clone to DOM document at the right place -       *     }); -       * -       *     //now we have reference to the cloned DOM via `clone` -       *   </pre> -       * -       * -       * Compiler Methods For Widgets and Directives: -       * -       * The following methods are available for use when you write your own widgets, directives, -       * and markup.  (Recall that the compile function's this is a reference to the compiler.) -       * -       *  `compile(element)` - returns linker - -       *  Invoke a new instance of the compiler to compile a DOM element and return a linker function. -       *  You can apply the linker function to the original element or a clone of the original element. -       *  The linker function returns a scope. -       * -       *  * `comment(commentText)` - returns element - Create a comment element. -       * -       *  * `element(elementName)` - returns element - Create an element by name. -       * -       *  * `text(text)` - returns element - Create a text element. -       * -       *  * `descend([set])` - returns descend state (true or false). Get or set the current descend -       *  state. If true the compiler will descend to children elements. -       * -       *  * `directives([set])` - returns directive state (true or false). Get or set the current -       *  directives processing state. The compiler will process directives only when directives set to -       *  true. -       * -       * For information on how the compiler works, see the -       * {@link guide/dev_guide.compiler Angular HTML Compiler} section of the Developer Guide. -       */ -      function Compiler(markup, attrMarkup, directives, widgets){ -        this.markup = markup; -        this.attrMarkup = attrMarkup; -        this.directives = directives; -        this.widgets = widgets;        } +      compositeLinkFn.scope = !!newScopeDirective; + +      // if we have templateUrl, then we have to delay linking +      return delayedLinkingFn || compositeLinkFn; -      Compiler.prototype = { -        compile: function(templateElement) { -          templateElement = jqLite(templateElement); -          var index = 0, -              template, -              parent = templateElement.parent(); -          if (templateElement.length > 1) { -            // https://github.com/angular/angular.js/issues/338 -            throw Error("Cannot compile multiple element roots: " + -                jqLite('<div>').append(templateElement.clone()).html()); +      //////////////////// + + +      function compositeLinkFn(childLinkingFn, scope, linkNode) { +        var attrs, element, i, ii; + +        if (templateNode === linkNode) { +          attrs = templateAttrs; +        } else { +          attrs = shallowCopy(templateAttrs); +          attrs.$element = jqLite(linkNode); +        } +        element = attrs.$element; + +        // PRELINKING +        for(i = 0, ii = preLinkingFns.length; i < ii; i++) { +          try { +            preLinkingFns[i](scope, element, attrs); +          } catch (e) { +            $exceptionHandler(e, startingTag(element));            } -          if (parent && parent[0]) { -            parent = parent[0]; -            for(var i = 0; i < parent.childNodes.length; i++) { -              if (parent.childNodes[i] == templateElement[0]) { -                index = i; -              } -            } +        } + +        // RECURSION +        childLinkingFn && childLinkingFn(scope, linkNode.childNodes); + +        // POSTLINKING +        for(i = 0, ii = postLinkingFns.length; i < ii; i++) { +          try { +            postLinkingFns[i](scope, element, attrs); +          } catch (e) { +            $exceptionHandler(e, startingTag(element));            } -          template = this.templatize(templateElement, index) || new Template(); -          return function(scope, cloneConnectFn){ -            assertArg(scope, 'scope'); -            // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart -            // and sometimes changes the structure of the DOM. -            var element = cloneConnectFn -              ? JQLitePrototype.clone.call(templateElement) // IMPORTANT!!! -              : templateElement; -            element.data($$scope, scope); -            scope.$element = element; -            (cloneConnectFn||noop)(element, scope); -            template.link(element, scope); -            return element; -          }; -        }, - -        templatize: function(element, elementIndex){ -          var self = this, -              widget, -              fn, -              directiveFns = self.directives, -              descend = true, -              directives = true, -              elementName = nodeName_(element), -              elementNamespace = elementName.indexOf(':') > 0 ? lowercase(elementName).replace(':', '-') : '', -              template, -              locals = {$element: element}, -              selfApi = { -                compile: bind(self, self.compile), -                descend: function(value){ if(isDefined(value)) descend = value; return descend;}, -                directives: function(value){ if(isDefined(value)) directives = value; return directives;}, -                scope: function(value){ if(isDefined(value)) template.newScope = template.newScope || value; return template.newScope;} -              }; -          element.addClass(elementNamespace); -          template = new Template(); -          eachAttribute(element, function(value, name){ -            if (!widget) { -              if ((widget = self.widgets('@' + name))) { -                element.addClass('ng-attr-widget'); -                if (isFunction(widget) && !widget.$inject) { -                  widget.$inject = ['$value', '$element']; -                } -                locals.$value = value; -              } -            } -          }); -          if (!widget) { -            if ((widget = self.widgets(elementName))) { -              if (elementNamespace) -                element.addClass('ng-widget'); -              if (isFunction(widget) && !widget.$inject) { -                widget.$inject = ['$element']; -              } +        } +      } +    } + + +    /** +     * looks up the directive and decorates it with exception handling and proper parameters. We +     * call this the boundDirective. +     * +     * @param {string} name name of the directive to look up. +     * @param {string} location The directive must be found in specific format. +     *   String containing any of theses characters: +     * +     *   * `E`: element name +     *   * `A': attribute +     *   * `C`: class +     *   * `M`: comment +     * @returns true if directive was added. +     */ +    function addDirective(tDirectives, name, location) { +      var match = false; +      if (hasDirectives.hasOwnProperty(name)) { +        for(var directive, directives = $injector.get(name + Suffix), +            i=0, ii = directives.length; i<ii; i++) { +          try { +            directive = directives[i]; +            if (directive.restrict.indexOf(location) != -1) { +              tDirectives.push(directive); +              match = true;              } +          } catch(e) { $exceptionHandler(e); } +        } +      } +      return match; +    } + + +    /** +     * When the element is replaced with HTML template then the new attributes +     * on the template need to be merged with the existing attributes in the DOM. +     * The desired effect is to have both of the attributes present. +     * +     * @param {object} dst destination attributes (original DOM) +     * @param {object} src source attributes (from the directive template) +     */ +    function mergeTemplateAttributes(dst, src) { +      var srcAttr = src.$attr, +          dstAttr = dst.$attr, +          element = dst.$element; +      // reapply the old attributes to the new element +      forEach(dst, function(value, key) { +        if (key.charAt(0) != '$') { +          dst.$set(key, value, srcAttr[key]); +        } +      }); +      // copy the new attributes on the old attrs object +      forEach(src, function(value, key) { +        if (key == 'class') { +          element.addClass(value); +        } else if (key == 'style') { +          element.attr('style', element.attr('style') + ';' + value); +        } else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) { +          dst[key] = value; +          dstAttr[key] = srcAttr[key]; +        } +      }); +    } + + +    function compileTemplateUrl(directives, beforeWidgetLinkFn, tElement, tAttrs, rootElement, +                                replace) { +      var linkQueue = [], +          afterWidgetLinkFn, +          afterWidgetChildrenLinkFn, +          originalWidgetNode = tElement[0], +          asyncWidgetDirective = directives.shift(), +          // The fact that we have to copy and patch the directive seems wrong! +          syncWidgetDirective = extend({}, asyncWidgetDirective, {templateUrl:null}), +          html = tElement.html(); + +      tElement.html(''); + +      $http.get(asyncWidgetDirective.templateUrl, {cache: $templateCache}). +        success(function(content) { +          content = trim(content).replace(CONTENT_REGEXP, html); +          if (replace && !content.match(HAS_ROOT_ELEMENT)) { +            throw Error('Template must have exactly one root element: ' + content);            } -          if (widget) { -            descend = false; -            directives = false; -            var parent = element.parent(); -            template.addLinkFn($injector.invoke(widget, selfApi, locals)); -            if (parent && parent[0]) { -              element = jqLite(parent[0].childNodes[elementIndex]); -            } + +          var templateNode, tempTemplateAttrs; + +          if (replace) { +            tempTemplateAttrs = {$attr: {}}; +            templateNode = jqLite(content)[0]; +            replaceWith(rootElement, tElement, templateNode); +            collectDirectives(tElement[0], directives, tempTemplateAttrs); +            mergeTemplateAttributes(tAttrs, tempTemplateAttrs); +          } else { +            templateNode = tElement[0]; +            tElement.html(content);            } -          if (descend){ -            // process markup for text nodes only -            for(var i=0, child=element[0].childNodes; -                i<child.length; i++) { -              if (isTextNode(child[i])) { -                forEach(self.markup, function(markup){ -                  if (i<child.length) { -                    var textNode = jqLite(child[i]); -                    markup.call(selfApi, textNode.text(), textNode, element); -                  } -                }); -              } + +          directives.unshift(syncWidgetDirective); +          afterWidgetLinkFn = applyDirectivesToNode(directives, tElement, tAttrs); +          afterWidgetChildrenLinkFn = compileNodes(tElement.contents()); + + +          while(linkQueue.length) { +            var linkRootElement = linkQueue.pop(), +                cLinkNode = linkQueue.pop(), +                scope = linkQueue.pop(), +                node = templateNode; + +            if (cLinkNode !== originalWidgetNode) { +              // it was cloned therefore we have to clone as well. +              node = JQLiteClone(templateNode); +              replaceWith(linkRootElement, jqLite(cLinkNode), node);              } +            afterWidgetLinkFn(function() { +              beforeWidgetLinkFn(afterWidgetChildrenLinkFn, scope, node); +            }, scope, node);            } +          linkQueue = null; +        }). +        error(function(response, code, headers, config) { +          throw Error('Failed to load template: ' + config.url); +        }); -          if (directives) { -            // Process attributes/directives -            eachAttribute(element, function(value, name){ -              forEach(self.attrMarkup, function(markup){ -                markup.call(selfApi, value, name, element); -              }); -            }); -            eachAttribute(element, function(value, name){ -              name = lowercase(name); -              fn = directiveFns[name]; -              if (fn) { -                element.addClass('ng-directive'); -                template.addLinkFn((isArray(fn) || fn.$inject) -                  ? $injector.invoke(fn, selfApi, {$value:value, $element: element}) -                  : fn.call(selfApi, value, element)); -              } +      return function(ignoreChildLinkingFn, scope, node, rootElement) { +        if (linkQueue) { +          linkQueue.push(scope); +          linkQueue.push(node); +          linkQueue.push(rootElement); +        } else { +          afterWidgetLinkFn(function() { +            beforeWidgetLinkFn(afterWidgetChildrenLinkFn, scope, node); +          }, scope, node); +        } +      }; +    } + + +    /** +     * Sorting function for bound directives. +     */ +    function byPriority(a, b) { +      return b.priority - a.priority; +    } + + +    function assertNoDuplicate(what, previousDirective, directive, element) { +      if (previousDirective) { +        throw Error('Multiple directives [' + previousDirective.name + ', ' + +          directive.name + '] asking for ' + what + ' on: ' +  startingTag(element)); +      } +    } + + +    function addTextInterpolateDirective(directives, text) { +      var interpolateFn = $interpolate(text, true); +      if (interpolateFn) { +        directives.push({ +          priority: 0, +          compile: valueFn(function(scope, node) { +            var parent = node.parent(), +                bindings = parent.data('$binding') || []; +            bindings.push(interpolateFn); +            parent.data('$binding', bindings).addClass('ng-binding'); +            scope.$watch(interpolateFn, function(scope, value) { +              node[0].nodeValue = value;              }); +          }) +        }); +      } +    } + + +    function addAttrInterpolateDirective(directives, value, name) { +      var interpolateFn = $interpolate(value, true); +      if (SIDE_EFFECT_ATTRS[name]) { +        name = SIDE_EFFECT_ATTRS[name]; +        if (BOOLEAN_ATTR[name]) { +          value = true; +        } +      } else if (!interpolateFn) { +        // we are not a side-effect attr, and we have no side-effects -> ignore +        return; +      } +      directives.push({ +        priority: 100, +        compile: function(element, attr) { +          if (interpolateFn) { +            return function(scope, element, attr) { +              scope.$watch(interpolateFn, function(scope, value){ +                attr.$set(name, value); +              }); +            }; +          } else { +            attr.$set(name, value);            } -          // Process non text child nodes -          if (descend) { -            eachNode(element, function(child, i){ -              template.addChild(i, self.templatize(child, i)); -            }); +        } +      }); +    } + + +    /** +     * This is a special jqLite.replaceWith, which can replace items which +     * have no parents, provided that the containing jqLite collection is provided. +     * +     * @param {JqLite=} rootElement The root of the compile tree. Used so that we can replace nodes +     *    in the root of the tree. +     * @param {JqLite} element The jqLite element which we are going to replace. We keep the shell, +     *    but replace its DOM node reference. +     * @param {Node} newNode The new DOM node. +     */ +    function replaceWith(rootElement, element, newNode) { +      var oldNode = element[0], +          parent = oldNode.parentNode, +          i, ii; + +      if (rootElement) { +        for(i = 0, ii = rootElement.length; i<ii; i++) { +          if (rootElement[i] == oldNode) { +            rootElement[i] = newNode;            } -          return template.empty() ? null : template;          } -      }; +      } +      if (parent) { +        parent.replaceChild(newNode, oldNode); +      } +      element[0] = newNode; +    } +  }]; -      ///////////////////////////////////////////////////////////////////// -      var compiler = new Compiler($textMarkup, $attrMarkup, $directive, $widget); -      return bind(compiler, compiler.compile); -    }]; -}; +  /** +   * Set a normalized attribute on the element in a way such that all directives +   * can share the attribute. This function properly handles boolean attributes. +   * @param {string} key Normalized key. (ie ngAttribute) +   * @param {string|boolean} value The value to set. If `null` attribute will be deleted. +   * @param {string=} attrName Optional none normalized name. Defaults to key. +   */ +  function attrSetter(key, value, attrName) { +    var booleanKey = BOOLEAN_ATTR[key.toLowerCase()]; -function eachNode(element, fn){ -  var i, chldNodes = element[0].childNodes || [], chld; -  for (i = 0; i < chldNodes.length; i++) { -    if(!isTextNode(chld = chldNodes[i])) { -      fn(jqLite(chld), i); +    if (booleanKey) { +      value = toBoolean(value); +      this.$element.prop(key, value); +      this[key] = value; +      attrName = key = booleanKey; +      value = value ? booleanKey : undefined; +    } else { +      this[key] = value; +    } + +    // translate normalized key to actual key +    if (attrName) { +      this.$attr[key] = attrName; +    } else { +      attrName = this.$attr[key]; +      if (!attrName) { +        this.$attr[key] = attrName = snake_case(key, '-'); +      }      } -  } -} -function eachAttribute(element, fn){ -  var i, attrs = element[0].attributes || [], chld, attr, name, value, attrValue = {}; -  for (i = 0; i < attrs.length; i++) { -    attr = attrs[i]; -    name = attr.name; -    value = attr.value; -    if (msie && name == 'href') { -      value = decodeURIComponent(element[0].getAttribute(name, 2)); +    if (value === null || value === undefined) { +      this.$element.removeAttr(attrName); +    } else { +      this.$element.attr(attrName, value);      } -    attrValue[name] = value;    } -  forEachSorted(attrValue, fn); +} + +var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i; +/** + * Converts all accepted directives format into proper directive name. + * All of these will become 'myDirective': + *   my:DiRective + *   my-directive + *   x-my-directive + *   data-my:directive + * + * Also there is special case for Moz prefix starting with upper case letter. + * @param name Name to normalize + */ +function directiveNormalize(name) { +  return camelCase(name.replace(PREFIX_REGEXP, ''));  } diff --git a/src/service/formFactory.js b/src/service/formFactory.js index 69c6d717..727a243c 100644 --- a/src/service/formFactory.js +++ b/src/service/formFactory.js @@ -56,41 +56,43 @@              });            } -          angular.directive('ng:contenteditable', function() { -            return ['$formFactory', '$element', function ($formFactory, element) { -              var exp = element.attr('ng:contenteditable'), -                  form = $formFactory.forElement(element), -                  widget; -              element.attr('contentEditable', true); -              widget = form.$createWidget({ -                scope: this, -                model: exp, -                controller: HTMLEditorWidget, -                controllerArgs: {$element: element}}); -              // if the element is destroyed, then we need to notify the form. -              element.bind('$destroy', function() { -                widget.$destroy(); -              }); -            }]; -          }); -        </script> -        <form name='editorForm' ng:controller="EditorCntl"> -          <div ng:contenteditable="html"></div> -          <hr/> -          HTML: <br/> -          <textarea ng:model="html" cols=80></textarea> -          <hr/> -          <pre>editorForm = {{editorForm}}</pre> -        </form> -      </doc:source> -      <doc:scenario> -        it('should enter invalid HTML', function() { -          expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-valid/); -          input('html').enter('<'); -          expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-invalid/); -        }); -      </doc:scenario> -    </doc:example> +       angular.module('formModule', [], function($compileProvider){ +         $compileProvider.directive('ngHtmlEditorModel', function ($formFactory) { +           return function(scope, element, attr) { +             var form = $formFactory.forElement(element), +                 widget; +             element.attr('contentEditable', true); +             widget = form.$createWidget({ +               scope: scope, +               model: attr.ngHtmlEditorModel, +               controller: HTMLEditorWidget, +               controllerArgs: {$element: element}}); +             // if the element is destroyed, then we need to +             // notify the form. +             element.bind('$destroy', function() { +               widget.$destroy(); +             }); +           }; +         }); +       }); +     </script> +     <form name='editorForm' ng:controller="EditorCntl"> +       <div ng:html-editor-model="htmlContent"></div> +       <hr/> +       HTML: <br/> +       <textarea ng:model="htmlContent" cols="80"></textarea> +       <hr/> +       <pre>editorForm = {{editorForm|json}}</pre> +     </form> +   </doc:source> +   <doc:scenario> +     it('should enter invalid HTML', function() { +       expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-valid/); +       input('htmlContent').enter('<'); +       expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-invalid/); +     }); +   </doc:scenario> + </doc:example>   */  /** | 
