diff options
| author | Misko Hevery | 2012-03-23 14:03:24 -0700 | 
|---|---|---|
| committer | Misko Hevery | 2012-03-28 11:16:35 -0700 | 
| commit | 2430f52bb97fa9d682e5f028c977c5bf94c5ec38 (patch) | |
| tree | e7529b741d70199f36d52090b430510bad07f233 /src/ng | |
| parent | 944098a4e0f753f06b40c73ca3e79991cec6c2e2 (diff) | |
| download | angular.js-2430f52bb97fa9d682e5f028c977c5bf94c5ec38.tar.bz2 | |
chore(module): move files around in preparation for more modules
Diffstat (limited to 'src/ng')
53 files changed, 11864 insertions, 0 deletions
| diff --git a/src/ng/anchorScroll.js b/src/ng/anchorScroll.js new file mode 100644 index 00000000..19a09498 --- /dev/null +++ b/src/ng/anchorScroll.js @@ -0,0 +1,66 @@ +/** + * @ngdoc function + * @name angular.module.ng.$anchorScroll + * @requires $window + * @requires $location + * @requires $rootScope + * + * @description + * When called, it checks current value of `$location.hash()` and scroll to related element, + * according to rules specified in + * {@link http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document Html5 spec}. + * + * It also watches the `$location.hash()` and scroll whenever it changes to match any anchor. + * This can be disabled by calling `$anchorScrollProvider.disableAutoScrolling()`. + */ +function $AnchorScrollProvider() { + +  var autoScrollingEnabled = true; + +  this.disableAutoScrolling = function() { +    autoScrollingEnabled = false; +  }; + +  this.$get = ['$window', '$location', '$rootScope', function($window, $location, $rootScope) { +    var document = $window.document; + +    // helper function to get first anchor from a NodeList +    // can't use filter.filter, as it accepts only instances of Array +    // and IE can't convert NodeList to an array using [].slice +    // TODO(vojta): use filter if we change it to accept lists as well +    function getFirstAnchor(list) { +      var result = null; +      forEach(list, function(element) { +        if (!result && lowercase(element.nodeName) === 'a') result = element; +      }); +      return result; +    } + +    function scroll() { +      var hash = $location.hash(), elm; + +      // empty hash, scroll to the top of the page +      if (!hash) $window.scrollTo(0, 0); + +      // element with given id +      else if ((elm = document.getElementById(hash))) elm.scrollIntoView(); + +      // first anchor with given name :-D +      else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) elm.scrollIntoView(); + +      // no element and hash == 'top', scroll to the top of the page +      else if (hash === 'top') $window.scrollTo(0, 0); +    } + +    // does not scroll when user clicks on anchor link that is currently on +    // (no url change, no $locaiton.hash() change), browser native does scroll +    if (autoScrollingEnabled) { +      $rootScope.$watch(function() {return $location.hash();}, function() { +        $rootScope.$evalAsync(scroll); +      }); +    } + +    return scroll; +  }]; +} + diff --git a/src/ng/browser.js b/src/ng/browser.js new file mode 100644 index 00000000..97e9cf3e --- /dev/null +++ b/src/ng/browser.js @@ -0,0 +1,413 @@ +'use strict'; + +/** + * @ngdoc object + * @name angular.module.ng.$browser + * @requires $log + * @description + * This object has two goals: + * + * - hide all the global state in the browser caused by the window object + * - abstract away all the browser specific features and inconsistencies + * + * For tests we provide {@link angular.module.ngMock.$browser mock implementation} of the `$browser` + * service, which can be used for convenient testing of the application without the interaction with + * the real browser apis. + */ +/** + * @param {object} window The global window object. + * @param {object} document jQuery wrapped document. + * @param {object} body jQuery wrapped document.body. + * @param {function()} XHR XMLHttpRequest constructor. + * @param {object} $log console.log or an object with the same interface. + * @param {object} $sniffer $sniffer service + */ +function Browser(window, document, body, $log, $sniffer) { +  var self = this, +      rawDocument = document[0], +      location = window.location, +      history = window.history, +      setTimeout = window.setTimeout, +      clearTimeout = window.clearTimeout, +      pendingDeferIds = {}; + +  self.isMock = false; + +  var outstandingRequestCount = 0; +  var outstandingRequestCallbacks = []; + +  // TODO(vojta): remove this temporary api +  self.$$completeOutstandingRequest = completeOutstandingRequest; +  self.$$incOutstandingRequestCount = function() { outstandingRequestCount++; }; + +  /** +   * Executes the `fn` function(supports currying) and decrements the `outstandingRequestCallbacks` +   * counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed. +   */ +  function completeOutstandingRequest(fn) { +    try { +      fn.apply(null, sliceArgs(arguments, 1)); +    } finally { +      outstandingRequestCount--; +      if (outstandingRequestCount === 0) { +        while(outstandingRequestCallbacks.length) { +          try { +            outstandingRequestCallbacks.pop()(); +          } catch (e) { +            $log.error(e); +          } +        } +      } +    } +  } + +  /** +   * @private +   * Note: this method is used only by scenario runner +   * TODO(vojta): prefix this method with $$ ? +   * @param {function()} callback Function that will be called when no outstanding request +   */ +  self.notifyWhenNoOutstandingRequests = function(callback) { +    // force browser to execute all pollFns - this is needed so that cookies and other pollers fire +    // at some deterministic time in respect to the test runner's actions. Leaving things up to the +    // regular poller would result in flaky tests. +    forEach(pollFns, function(pollFn){ pollFn(); }); + +    if (outstandingRequestCount === 0) { +      callback(); +    } else { +      outstandingRequestCallbacks.push(callback); +    } +  }; + +  ////////////////////////////////////////////////////////////// +  // Poll Watcher API +  ////////////////////////////////////////////////////////////// +  var pollFns = [], +      pollTimeout; + +  /** +   * @ngdoc method +   * @name angular.module.ng.$browser#addPollFn +   * @methodOf angular.module.ng.$browser +   * +   * @param {function()} fn Poll function to add +   * +   * @description +   * Adds a function to the list of functions that poller periodically executes, +   * and starts polling if not started yet. +   * +   * @returns {function()} the added function +   */ +  self.addPollFn = function(fn) { +    if (isUndefined(pollTimeout)) startPoller(100, setTimeout); +    pollFns.push(fn); +    return fn; +  }; + +  /** +   * @param {number} interval How often should browser call poll functions (ms) +   * @param {function()} setTimeout Reference to a real or fake `setTimeout` function. +   * +   * @description +   * Configures the poller to run in the specified intervals, using the specified +   * setTimeout fn and kicks it off. +   */ +  function startPoller(interval, setTimeout) { +    (function check() { +      forEach(pollFns, function(pollFn){ pollFn(); }); +      pollTimeout = setTimeout(check, interval); +    })(); +  } + +  ////////////////////////////////////////////////////////////// +  // URL API +  ////////////////////////////////////////////////////////////// + +  var lastBrowserUrl = location.href; + +  /** +   * @ngdoc method +   * @name angular.module.ng.$browser#url +   * @methodOf angular.module.ng.$browser +   * +   * @description +   * GETTER: +   * Without any argument, this method just returns current value of location.href. +   * +   * SETTER: +   * With at least one argument, this method sets url to new value. +   * If html5 history api supported, pushState/replaceState is used, otherwise +   * location.href/location.replace is used. +   * Returns its own instance to allow chaining +   * +   * NOTE: this api is intended for use only by the $location service. Please use the +   * {@link angular.module.ng.$location $location service} to change url. +   * +   * @param {string} url New url (when used as setter) +   * @param {boolean=} replace Should new url replace current history record ? +   */ +  self.url = function(url, replace) { +    // setter +    if (url) { +      lastBrowserUrl = url; +      if ($sniffer.history) { +        if (replace) history.replaceState(null, '', url); +        else history.pushState(null, '', url); +      } else { +        if (replace) location.replace(url); +        else location.href = url; +      } +      return self; +    // getter +    } else { +      return location.href; +    } +  }; + +  var urlChangeListeners = [], +      urlChangeInit = false; + +  function fireUrlChange() { +    if (lastBrowserUrl == self.url()) return; + +    lastBrowserUrl = self.url(); +    forEach(urlChangeListeners, function(listener) { +      listener(self.url()); +    }); +  } + +  /** +   * @ngdoc method +   * @name angular.module.ng.$browser#onUrlChange +   * @methodOf angular.module.ng.$browser +   * @TODO(vojta): refactor to use node's syntax for events +   * +   * @description +   * Register callback function that will be called, when url changes. +   * +   * It's only called when the url is changed by outside of angular: +   * - user types different url into address bar +   * - user clicks on history (forward/back) button +   * - user clicks on a link +   * +   * It's not called when url is changed by $browser.url() method +   * +   * The listener gets called with new url as parameter. +   * +   * NOTE: this api is intended for use only by the $location service. Please use the +   * {@link angular.module.ng.$location $location service} to monitor url changes in angular apps. +   * +   * @param {function(string)} listener Listener function to be called when url changes. +   * @return {function(string)} Returns the registered listener fn - handy if the fn is anonymous. +   */ +  self.onUrlChange = function(callback) { +    if (!urlChangeInit) { +      // We listen on both (hashchange/popstate) when available, as some browsers (e.g. Opera) +      // don't fire popstate when user change the address bar and don't fire hashchange when url +      // changed by push/replaceState + +      // html5 history api - popstate event +      if ($sniffer.history) jqLite(window).bind('popstate', fireUrlChange); +      // hashchange event +      if ($sniffer.hashchange) jqLite(window).bind('hashchange', fireUrlChange); +      // polling +      else self.addPollFn(fireUrlChange); + +      urlChangeInit = true; +    } + +    urlChangeListeners.push(callback); +    return callback; +  }; + +  ////////////////////////////////////////////////////////////// +  // Cookies API +  ////////////////////////////////////////////////////////////// +  var lastCookies = {}; +  var lastCookieString = ''; + +  /** +   * @ngdoc method +   * @name angular.module.ng.$browser#cookies +   * @methodOf angular.module.ng.$browser +   * +   * @param {string=} name Cookie name +   * @param {string=} value Cokkie value +   * +   * @description +   * The cookies method provides a 'private' low level access to browser cookies. +   * It is not meant to be used directly, use the $cookie service instead. +   * +   * The return values vary depending on the arguments that the method was called with as follows: +   * <ul> +   *   <li>cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify it</li> +   *   <li>cookies(name, value) -> set name to value, if value is undefined delete the cookie</li> +   *   <li>cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that way)</li> +   * </ul> +   * +   * @returns {Object} Hash of all cookies (if called without any parameter) +   */ +  self.cookies = function(name, value) { +    var cookieLength, cookieArray, cookie, i, keyValue, index; + +    if (name) { +      if (value === undefined) { +        rawDocument.cookie = escape(name) + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT"; +      } else { +        if (isString(value)) { +          rawDocument.cookie = escape(name) + '=' + escape(value); + +          cookieLength = name.length + value.length + 1; +          if (cookieLength > 4096) { +            $log.warn("Cookie '"+ name +"' possibly not set or overflowed because it was too large ("+ +              cookieLength + " > 4096 bytes)!"); +          } +          if (lastCookies.length > 20) { +            $log.warn("Cookie '"+ name +"' possibly not set or overflowed because too many cookies " + +              "were already set (" + lastCookies.length + " > 20 )"); +          } +        } +      } +    } else { +      if (rawDocument.cookie !== lastCookieString) { +        lastCookieString = rawDocument.cookie; +        cookieArray = lastCookieString.split("; "); +        lastCookies = {}; + +        for (i = 0; i < cookieArray.length; i++) { +          cookie = cookieArray[i]; +          index = cookie.indexOf('='); +          if (index > 0) { //ignore nameless cookies +            lastCookies[unescape(cookie.substring(0, index))] = unescape(cookie.substring(index + 1)); +          } +        } +      } +      return lastCookies; +    } +  }; + + +  /** +   * @ngdoc method +   * @name angular.module.ng.$browser#defer +   * @methodOf angular.module.ng.$browser +   * @param {function()} fn A function, who's execution should be defered. +   * @param {number=} [delay=0] of milliseconds to defer the function execution. +   * @returns {*} DeferId that can be used to cancel the task via `$browser.defer.cancel()`. +   * +   * @description +   * Executes a fn asynchroniously via `setTimeout(fn, delay)`. +   * +   * Unlike when calling `setTimeout` directly, in test this function is mocked and instead of using +   * `setTimeout` in tests, the fns are queued in an array, which can be programmatically flushed +   * via `$browser.defer.flush()`. +   * +   */ +  self.defer = function(fn, delay) { +    var timeoutId; +    outstandingRequestCount++; +    timeoutId = setTimeout(function() { +      delete pendingDeferIds[timeoutId]; +      completeOutstandingRequest(fn); +    }, delay || 0); +    pendingDeferIds[timeoutId] = true; +    return timeoutId; +  }; + + +  /** +   * THIS DOC IS NOT VISIBLE because ngdocs can't process docs for foo#method.method +   * +   * @name angular.module.ng.$browser#defer.cancel +   * @methodOf angular.module.ng.$browser.defer +   * +   * @description +   * Cancels a defered task identified with `deferId`. +   * +   * @param {*} deferId Token returned by the `$browser.defer` function. +   * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfuly canceled. +   */ +  self.defer.cancel = function(deferId) { +    if (pendingDeferIds[deferId]) { +      delete pendingDeferIds[deferId]; +      clearTimeout(deferId); +      completeOutstandingRequest(noop); +      return true; +    } +    return false; +  }; + + +  ////////////////////////////////////////////////////////////// +  // Misc API +  ////////////////////////////////////////////////////////////// + +  /** +   * @ngdoc method +   * @name angular.module.ng.$browser#addCss +   * @methodOf angular.module.ng.$browser +   * +   * @param {string} url Url to css file +   * @description +   * Adds a stylesheet tag to the head. +   */ +  self.addCss = function(url) { +    var link = jqLite(rawDocument.createElement('link')); +    link.attr('rel', 'stylesheet'); +    link.attr('type', 'text/css'); +    link.attr('href', url); +    body.append(link); +  }; + + +  /** +   * @ngdoc method +   * @name angular.module.ng.$browser#addJs +   * @methodOf angular.module.ng.$browser +   * +   * @param {string} url Url to js file +   * +   * @description +   * Adds a script tag to the head. +   */ +  self.addJs = function(url, done) { +    // we can't use jQuery/jqLite here because jQuery does crazy shit with script elements, e.g.: +    // - fetches local scripts via XHR and evals them +    // - adds and immediately removes script elements from the document +    var script = rawDocument.createElement('script'); + +    script.type = 'text/javascript'; +    script.src = url; + +    if (msie) { +      script.onreadystatechange = function() { +        /loaded|complete/.test(script.readyState) && done && done(); +      }; +    } else { +      if (done) script.onload = script.onerror = done; +    } + +    body[0].appendChild(script); + +    return script; +  }; + +  /** +   * Returns current <base href> +   * (always relative - without domain) +   * +   * @returns {string=} +   */ +  self.baseHref = function() { +    var href = document.find('base').attr('href'); +    return href ? href.replace(/^https?\:\/\/[^\/]*/, '') : href; +  }; +} + +function $BrowserProvider(){ +  this.$get = ['$window', '$log', '$sniffer', '$document', +      function( $window,   $log,   $sniffer,   $document){ +        return new Browser($window, $document, $document.find('body'), $log, $sniffer); +      }]; +} diff --git a/src/ng/cacheFactory.js b/src/ng/cacheFactory.js new file mode 100644 index 00000000..82c939cc --- /dev/null +++ b/src/ng/cacheFactory.js @@ -0,0 +1,159 @@ +/** + * @ngdoc object + * @name angular.module.ng.$cacheFactory + * + * @description + * Factory that constructs cache objects. + * + * + * @param {string} cacheId Name or id of the newly created cache. + * @param {object=} options Options object that specifies the cache behavior. Properties: + * + *   - `{number=}` `capacity` — turns the cache into LRU cache. + * + * @returns {object} Newly created cache object with the following set of methods: + * + * - `{object}` `info()` — Returns id, size, and options of cache. + * - `{void}` `put({string} key, {*} value)` — Puts a new key-value pair into the cache. + * - `{{*}} `get({string} key) — Returns cached value for `key` or undefined for cache miss. + * - `{void}` `remove({string} key) — Removes a key-value pair from the cache. + * - `{void}` `removeAll() — Removes all cached values. + * - `{void}` `destroy() — Removes references to this cache from $cacheFactory. + * + */ +function $CacheFactoryProvider() { + +  this.$get = function() { +    var caches = {}; + +    function cacheFactory(cacheId, options) { +      if (cacheId in caches) { +        throw Error('cacheId ' + cacheId + ' taken'); +      } + +      var size = 0, +          stats = extend({}, options, {id: cacheId}), +          data = {}, +          capacity = (options && options.capacity) || Number.MAX_VALUE, +          lruHash = {}, +          freshEnd = null, +          staleEnd = null; + +      return caches[cacheId] = { + +        put: function(key, value) { +          var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); + +          refresh(lruEntry); + +          if (isUndefined(value)) return; +          if (!(key in data)) size++; +          data[key] = value; + +          if (size > capacity) { +            this.remove(staleEnd.key); +          } +        }, + + +        get: function(key) { +          var lruEntry = lruHash[key]; + +          if (!lruEntry) return; + +          refresh(lruEntry); + +          return data[key]; +        }, + + +        remove: function(key) { +          var lruEntry = lruHash[key]; + +          if (lruEntry == freshEnd) freshEnd = lruEntry.p; +          if (lruEntry == staleEnd) staleEnd = lruEntry.n; +          link(lruEntry.n,lruEntry.p); + +          delete lruHash[key]; +          delete data[key]; +          size--; +        }, + + +        removeAll: function() { +          data = {}; +          size = 0; +          lruHash = {}; +          freshEnd = staleEnd = null; +        }, + + +        destroy: function() { +          data = null; +          stats = null; +          lruHash = null; +          delete caches[cacheId]; +        }, + + +        info: function() { +          return extend({}, stats, {size: size}); +        } +      }; + + +      /** +       * makes the `entry` the freshEnd of the LRU linked list +       */ +      function refresh(entry) { +        if (entry != freshEnd) { +          if (!staleEnd) { +            staleEnd = entry; +          } else if (staleEnd == entry) { +            staleEnd = entry.n; +          } + +          link(entry.n, entry.p); +          link(entry, freshEnd); +          freshEnd = entry; +          freshEnd.n = null; +        } +      } + + +      /** +       * bidirectionally links two entries of the LRU linked list +       */ +      function link(nextEntry, prevEntry) { +        if (nextEntry != prevEntry) { +          if (nextEntry) nextEntry.p = prevEntry; //p stands for previous, 'prev' didn't minify +          if (prevEntry) prevEntry.n = nextEntry; //n stands for next, 'next' didn't minify +        } +      } +    } + + +    cacheFactory.info = function() { +      var info = {}; +      forEach(caches, function(cache, cacheId) { +        info[cacheId] = cache.info(); +      }); +      return info; +    }; + + +    cacheFactory.get = function(cacheId) { +      return caches[cacheId]; +    }; + + +    return cacheFactory; +  }; +} + +function $TemplateCacheProvider() { +  this.$get = ['$cacheFactory', function($cacheFactory) { +    return $cacheFactory('templates'); +  }]; +} + diff --git a/src/ng/compiler.js b/src/ng/compiler.js new file mode 100644 index 00000000..a22c5d66 --- /dev/null +++ b/src/ng/compiler.js @@ -0,0 +1,1014 @@ +'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(value) { +                // when the 'compile' expression changes +                // assign it into the current DOM +                element.html(value); + +                // compile the new DOM and link it to the current +                // scope. +                // NOTE: we only compile .childNodes so that +                // we don't get into infinite loop compiling ourselves +                $compile(element.contents())(scope); +              } +            ); +          }; +        }) +      }); + +      function Ctrl($scope) { +        $scope.name = 'Angular'; +        $scope.html = 'Hello {{name}}'; +      } +    </script> +    <div ng-controller="Ctrl"> +      <input ng-model="name"> <br> +      <textarea ng-model="html"></textarea> <br> +      <div compile="html"></div> +    </div> +   </doc:source> +   <doc:scenario> +     it('should auto compile', function() { +       expect(element('div[compile]').text()).toBe('Hello Angular'); +       input('html').enter('{{name}}!'); +       expect(element('div[compile]').text()).toBe('Angular!'); +     }); +   </doc:scenario> + </doc:example> + + * + * + * @param {string|DOMElement} element Element or HTML string to compile into a template function. + * @param {function(angular.Scope[, cloneAttachFn]} transclude function available to directives. + * @param {number} maxPriority only apply directives lower then given priority (Only effects the + *                 root element(s), not their children) + * @returns {function(scope[, cloneAttachFn])} a link function which is used to bind template + * (a DOM element/tree) to a scope. Where: + * + *  * `scope` - A {@link 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> + * + * + * 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]*\>$/; + + +  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 = directive.name || name; +                directive.require = directive.require || (directive.controller && directive.name); +                directive.restrict = directive.restrict || 'A'; +                directives.push(directive); +              } catch (e) { +                $exceptionHandler(e); +              } +            }); +            return directives; +          }]); +      } +      hasDirectives[name].push(directiveFactory); +    } else { +      forEach(name, reverseParams(registerDirective)); +    } +    return this; +  }; + + +  this.$get = [ +            '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse', +            '$controller', +    function($injector,   $interpolate,   $exceptionHandler,   $http,   $templateCache,   $parse, +             $controller) { + +    var LOCAL_MODE = { +      attribute: function(localName, mode, parentScope, scope, attr) { +        scope[localName] = attr[localName]; +      }, + +      evaluate: function(localName, mode, parentScope, scope, attr) { +        scope[localName] = parentScope.$eval(attr[localName]); +      }, + +      bind: function(localName, mode, parentScope, scope, attr) { +        var getter = $interpolate(attr[localName]); +        scope.$watch( +          function() { return getter(parentScope); }, +          function(v) { scope[localName] = v; } +        ); +      }, + +      accessor: function(localName, mode, parentScope, scope, attr) { +        var getter = noop, +            setter = noop, +            exp = attr[localName]; + +        if (exp) { +          getter = $parse(exp); +          setter = getter.assign || function() { +            throw Error("Expression '" + exp + "' not assignable."); +          }; +        } + +        scope[localName] = function(value) { +          return arguments.length ? setter(parentScope, value) : getter(parentScope); +        }; +      }, + +      expression: function(localName, mode, parentScope, scope, attr) { +        scope[localName] = function(locals) { +          $parse(attr[localName])(parentScope, locals); +        }; +      } +    }; + +    return compile; + +    //================================ + +    function compile(templateElement, transcludeFn, maxPriority) { +      if (!(templateElement instanceof jqLite)) { +        // jquery always rewraps, where as we need to preserve the original selector so that we can modify it. +        templateElement = jqLite(templateElement); +      } +      // We can not compile top level text elements since text nodes can be merged and we will +      // not be able to attach scope data to them, so we will wrap them in <span> +      forEach(templateElement, function(node, index){ +        if (node.nodeType == 3 /* text node */) { +          templateElement[index] = jqLite(node).wrap('<span>').parent()[0]; +        } +      }); +      var linkingFn = compileNodes(templateElement, transcludeFn, templateElement, maxPriority); +      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; +        safeAddClass(element.data('$scope', scope), 'ng-scope'); +        if (cloneConnectFn) cloneConnectFn(element, scope); +        if (linkingFn) linkingFn(scope, element, element); +        return element; +      }; +    } + +    function wrongMode(localName, mode) { +      throw Error("Unsupported '" + mode + "' for '" + localName + "'."); +    } + +    function safeAddClass(element, className) { +      try { +        element.addClass(className); +      } catch(e) { +        // ignore, since it means that we are trying to set class on +        // SVG element, where class name is read-only. +      } +    } + +    /** +     * Compile function matches each node in nodeList against the directives. Once all directives +     * for a particular node are collected their compile functions are executed. The compile +     * functions return values - the linking functions - are combined into a composite linking +     * function, which is the a linking function for the node. +     * +     * @param {NodeList} nodeList an array of nodes to compile +     * @param {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the +     *        scope argument is auto-generated to the new child of the transcluded parent scope. +     * @param {DOMElement=} rootElement If the nodeList is the root of the compilation tree then the +     *        rootElement must be set the jqLite collection of the compile root. This is +     *        needed so that the jqLite collection items can be replaced with widgets. +     * @param {number=} max directive priority +     * @returns {?function} A composite linking function of all of the matched directives or null. +     */ +    function compileNodes(nodeList, transcludeFn, rootElement, maxPriority) { +     var linkingFns = [], +         directiveLinkingFn, childLinkingFn, directives, attrs, linkingFnFound; + +     for(var i = 0, ii = nodeList.length; i < ii; i++) { +       attrs = { +         $attr: {}, +         $normalize: directiveNormalize, +         $set: attrSetter, +         $observe: interpolatedAttrObserve, +         $observers: {} +       }; +       // we must always refer to nodeList[i] since the nodes can be replaced underneath us. +       directives = collectDirectives(nodeList[i], [], attrs, maxPriority); + +       directiveLinkingFn = (directives.length) +           ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, rootElement) +           : null; + +       childLinkingFn = (directiveLinkingFn && directiveLinkingFn.terminal) +           ? null +           : compileNodes(nodeList[i].childNodes, +                directiveLinkingFn ? directiveLinkingFn.transclude : transcludeFn); + +       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; + +     /* nodesetLinkingFn */ function linkingFn(scope, nodeList, rootElement, boundTranscludeFn) { +       if (linkingFns.length != nodeList.length * 2) { +         throw Error('Template changed structure!'); +       } + +       var childLinkingFn, directiveLinkingFn, node, childScope, childTransclusionFn; + +       for(var i=0, n=0, ii=linkingFns.length; i<ii; n++) { +         node = nodeList[n]; +         directiveLinkingFn = /* directiveLinkingFn */ linkingFns[i++]; +         childLinkingFn = /* nodesetLinkingFn */ linkingFns[i++]; + +         if (directiveLinkingFn) { +           if (directiveLinkingFn.scope) { +             childScope = scope.$new(isObject(directiveLinkingFn.scope)); +             jqLite(node).data('$scope', childScope); +           } else { +             childScope = scope; +           } +           childTransclusionFn = directiveLinkingFn.transclude; +           if (childTransclusionFn || (!boundTranscludeFn && transcludeFn)) { +             directiveLinkingFn(childLinkingFn, childScope, node, rootElement, +                 (function(transcludeFn) { +                   return function(cloneFn) { +                     var transcludeScope = scope.$new(); + +                     return transcludeFn(transcludeScope, cloneFn). +                         bind('$destroy', bind(transcludeScope, transcludeScope.$destroy)); +                    }; +                  })(childTransclusionFn || transcludeFn) +             ); +           } else { +             directiveLinkingFn(childLinkingFn, childScope, node, undefined, boundTranscludeFn); +           } +         } else if (childLinkingFn) { +           childLinkingFn(scope, node.childNodes, undefined, boundTranscludeFn); +         } +       } +     } +   } + + +    /** +     * 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. +     * @param {number=} max directive priority +     */ +    function collectDirectives(node, directives, attrs, maxPriority) { +      var nodeType = node.nodeType, +          attrsMap = attrs.$attr, +          match, +          className; + +      switch(nodeType) { +        case 1: /* Element */ +          // use the node name: <directive> +          addDirective(directives, +              directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority); + +          // 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]; +            if (attr.specified) { +              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 (isBooleanAttr(node, nName)) { +                attrs[nName] = true; // presence means true +              } +              addAttrInterpolateDirective(node, directives, value, nName) +              addDirective(directives, nName, 'A', maxPriority); +            } +          } + +          // use class as directive +          className = node.className; +          if (isString(className)) { +            while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) { +              nName = directiveNormalize(match[2]); +              if (addDirective(directives, nName, 'C', maxPriority)) { +                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', maxPriority)) { +              attrs[nName] = trim(match[2]); +            } +          } +          break; +      } + +      directives.sort(byPriority); +      return directives; +    } + + +    /** +     * Once the directives have been collected their compile functions is executed. This method +     * is responsible for inlining directive templates as well as terminating the application +     * of the directives if the terminal directive has been reached.. +     * +     * @param {Array} directives Array of collected directives to execute their compile function. +     *        this needs to be pre-sorted by priority order. +     * @param {Node} templateNode The raw DOM node to apply the compile functions to +     * @param {Object} templateAttrs The shared attribute function +     * @param {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the +     *        scope argument is auto-generated to the new child of the transcluded parent scope. +     * @param {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, transcludeFn, rootElement) { +      var terminalPriority = -Number.MAX_VALUE, +          preLinkingFns = [], +          postLinkingFns = [], +          newScopeDirective = null, +          newIsolatedScopeDirective = null, +          templateDirective = null, +          delayedLinkingFn = null, +          element = templateAttrs.$element = jqLite(templateNode), +          directive, +          directiveName, +          template, +          transcludeDirective, +          childTranscludeFn = transcludeFn, +          controllerDirectives, +          linkingFn, +          directiveValue; + +      // executes all directives on the current element +      for(var i = 0, ii = directives.length; i < ii; i++) { +        directive = directives[i]; +        template = undefined; + +        if (terminalPriority > directive.priority) { +          break; // prevent further processing of directives +        } + +        if (directiveValue = directive.scope) { +          assertNoDuplicate('isolated scope', newIsolatedScopeDirective, directive, element); +          if (isObject(directiveValue)) { +            safeAddClass(element, 'ng-isolate-scope'); +            newIsolatedScopeDirective = directive; +          } +          safeAddClass(element, 'ng-scope'); +          newScopeDirective = newScopeDirective || directive; +        } + +        directiveName = directive.name; + +        if (directiveValue = directive.controller) { +          controllerDirectives = controllerDirectives || {}; +          assertNoDuplicate("'" + directiveName + "' controller", +              controllerDirectives[directiveName], directive, element); +          controllerDirectives[directiveName] = directive; +        } + +        if (directiveValue = directive.transclude) { +          assertNoDuplicate('transclusion', transcludeDirective, directive, element); +          transcludeDirective = directive; +          terminalPriority = directive.priority; +          if (directiveValue == 'element') { +            template = jqLite(templateNode); +            templateNode = (element = templateAttrs.$element = jqLite( +                '<!-- ' + directiveName + ': ' + templateAttrs[directiveName]  + ' -->'))[0]; +            replaceWith(rootElement, jqLite(template[0]), templateNode); +            childTranscludeFn = compile(template, transcludeFn, terminalPriority); +          } else { +            template = jqLite(JQLiteClone(templateNode)); +            element.html(''); // clear contents +            childTranscludeFn = compile(template.contents(), transcludeFn); +          } +        } + +        if (directiveValue = 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 = directiveValue.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), +              /* directiveLinkingFn */ compositeLinkFn, element, templateAttrs, rootElement, +              directive.replace, childTranscludeFn); +          ii = directives.length; +        } else if (directive.compile) { +          try { +            linkingFn = directive.compile(element, templateAttrs, childTranscludeFn); +            if (isFunction(linkingFn)) { +              addLinkingFns(null, linkingFn); +            } else if (linkingFn) { +              addLinkingFns(linkingFn.pre, linkingFn.post); +            } +          } catch (e) { +            $exceptionHandler(e, startingTag(element)); +          } +        } + +        if (directive.terminal) { +          compositeLinkFn.terminal = true; +          terminalPriority = Math.max(terminalPriority, directive.priority); +        } + +      } + +      linkingFn = delayedLinkingFn || compositeLinkFn; +      linkingFn.scope = newScopeDirective && newScopeDirective.scope; +      linkingFn.transclude = transcludeDirective && childTranscludeFn; + +      // if we have templateUrl, then we have to delay linking +      return linkingFn; + +      //////////////////// + +      function addLinkingFns(pre, post) { +        if (pre) { +          pre.require = directive.require; +          preLinkingFns.push(pre); +        } +        if (post) { +          post.require = directive.require; +          postLinkingFns.push(post); +        } +      } + + +      function getControllers(require, element) { +        var value, retrievalMethod = 'data', optional = false; +        if (isString(require)) { +          while((value = require.charAt(0)) == '^' || value == '?') { +            require = require.substr(1); +            if (value == '^') { +              retrievalMethod = 'inheritedData'; +            } +            optional = optional || value == '?'; +          } +          value = element[retrievalMethod]('$' + require + 'Controller'); +          if (!value && !optional) { +            throw Error("No controller: " + require); +          } +          return value; +        } else if (isArray(require)) { +          value = []; +          forEach(require, function(require) { +            value.push(getControllers(require, element)); +          }); +        } +        return value; +      } + + +      /* directiveLinkingFn */ +      function compositeLinkFn(/* nodesetLinkingFn */ childLinkingFn, +                               scope, linkNode, rootElement, boundTranscludeFn) { +        var attrs, element, i, ii, linkingFn, controller; + +        if (templateNode === linkNode) { +          attrs = templateAttrs; +        } else { +          attrs = shallowCopy(templateAttrs); +          attrs.$element = jqLite(linkNode); +        } +        element = attrs.$element; + +        if (newScopeDirective && isObject(newScopeDirective.scope)) { +          forEach(newScopeDirective.scope, function(mode, name) { +            (LOCAL_MODE[mode] || wrongMode)(name, mode, +                scope.$parent || scope, scope, attrs); +          }); +        } + +        if (controllerDirectives) { +          forEach(controllerDirectives, function(directive) { +            var locals = { +              $scope: scope, +              $element: element, +              $attrs: attrs, +              $transclude: boundTranscludeFn +            }; + + +            forEach(directive.inject || {}, function(mode, name) { +              (LOCAL_MODE[mode] || wrongMode)(name, mode, +                  newScopeDirective ? scope.$parent || scope : scope, locals, attrs); +            }); + +            controller = directive.controller; +            if (controller == '@') { +              controller = attrs[directive.name]; +            } + +            element.data( +                '$' + directive.name + 'Controller', +                $controller(controller, locals)); +          }); +        } + +        // PRELINKING +        for(i = 0, ii = preLinkingFns.length; i < ii; i++) { +          try { +            linkingFn = preLinkingFns[i]; +            linkingFn(scope, element, attrs, +                linkingFn.require && getControllers(linkingFn.require, element)); +          } catch (e) { +            $exceptionHandler(e, startingTag(element)); +          } +        } + +        // RECURSION +        childLinkingFn && childLinkingFn(scope, linkNode.childNodes, undefined, boundTranscludeFn); + +        // POSTLINKING +        for(i = 0, ii = postLinkingFns.length; i < ii; i++) { +          try { +            linkingFn = postLinkingFns[i]; +            linkingFn(scope, element, attrs, +                linkingFn.require && getControllers(linkingFn.require, element)); +          } catch (e) { +            $exceptionHandler(e, startingTag(element)); +          } +        } +      } +    } + + +    /** +     * looks up the directive and decorates it with exception handling and proper parameters. We +     * call this the boundDirective. +     * +     * @param {string} name name of the directive to look up. +     * @param {string} location The directive must be found in specific format. +     *   String containing any of theses characters: +     * +     *   * `E`: element name +     *   * `A': attribute +     *   * `C`: class +     *   * `M`: comment +     * @returns true if directive was added. +     */ +    function addDirective(tDirectives, name, location, maxPriority) { +      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 ( (maxPriority === undefined || maxPriority > directive.priority) && +                 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) != '$') { +          if (src[key]) { +            value += (key === 'style' ? ';' : ' ') + src[key]; +          } +          dst.$set(key, value, true, srcAttr[key]); +        } +      }); +      // copy the new attributes on the old attrs object +      forEach(src, function(value, key) { +        if (key == 'class') { +          safeAddClass(element, value); +        } 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, /* directiveLinkingFn */ beforeWidgetLinkFn, +                                tElement, tAttrs, rootElement, replace, transcludeFn) { +      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, transclude: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); +          } + +          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); +          } + +          directives.unshift(syncWidgetDirective); +          afterWidgetLinkFn = /* directiveLinkingFn */ applyDirectivesToNode(directives, tElement, tAttrs, transcludeFn); +          afterWidgetChildrenLinkFn = /* nodesetLinkingFn */ compileNodes(tElement.contents(), transcludeFn); + + +          while(linkQueue.length) { +            var controller = linkQueue.pop(), +                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, rootElement, controller); +            }, scope, node, rootElement, controller); +          } +          linkQueue = null; +        }). +        error(function(response, code, headers, config) { +          throw Error('Failed to load template: ' + config.url); +        }); + +      return /* directiveLinkingFn */ function(ignoreChildLinkingFn, scope, node, rootElement, +                                               controller) { +        if (linkQueue) { +          linkQueue.push(scope); +          linkQueue.push(node); +          linkQueue.push(rootElement); +          linkQueue.push(controller); +        } else { +          afterWidgetLinkFn(function() { +            beforeWidgetLinkFn(afterWidgetChildrenLinkFn, scope, node, rootElement, controller); +          }, scope, node, rootElement, controller); +        } +      }; +    } + + +    /** +     * Sorting function for bound directives. +     */ +    function byPriority(a, b) { +      return b.priority - a.priority; +    } + + +    function assertNoDuplicate(what, previousDirective, directive, element) { +      if (previousDirective) { +        throw 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); +            safeAddClass(parent.data('$binding', bindings), 'ng-binding'); +            scope.$watch(interpolateFn, function(value) { +              node[0].nodeValue = value; +            }); +          }) +        }); +      } +    } + + +    function addAttrInterpolateDirective(node, directives, value, name) { +      var interpolateFn = $interpolate(value, true); + + +      // no interpolation found -> ignore +      if (!interpolateFn) return; + +      directives.push({ +        priority: 100, +        compile: valueFn(function(scope, element, attr) { +          if (name === 'class') { +            // we need to interpolate classes again, in the case the element was replaced +            // and therefore the two class attrs got merged - we want to interpolate the result +            interpolateFn = $interpolate(attr[name], true); +          } + +          // we define observers array only for interpolated attrs +          // and ignore observers for non interpolated attrs to save some memory +          attr.$observers[name] = []; +          attr[name] = undefined; +          scope.$watch(interpolateFn, function(value) { +            attr.$set(name, value); +          }); +        }) +      }); +    } + + +    /** +     * This is a special jqLite.replaceWith, which can replace items which +     * have no parents, provided that the containing jqLite collection is provided. +     * +     * @param {JqLite=} rootElement The root of the compile tree. Used so that we can replace nodes +     *    in the root of the tree. +     * @param {JqLite} 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; +          } +        } +      } +      if (parent) { +        parent.replaceChild(newNode, oldNode); +      } +      element[0] = newNode; +    } + + +    /** +     * Set a normalized attribute on the element in a way such that all directives +     * can share the attribute. This function properly handles boolean attributes. +     * @param {string} key Normalized key. (ie ngAttribute) +     * @param {string|boolean} value The value to set. If `null` attribute will be deleted. +     * @param {boolean=} writeAttr If false, does not write the value to DOM element attribute. +     *     Defaults to true. +     * @param {string=} attrName Optional none normalized name. Defaults to key. +     */ +    function attrSetter(key, value, writeAttr, attrName) { +      var booleanKey = isBooleanAttr(this.$element[0], key.toLowerCase()); + +      if (booleanKey) { +        this.$element.prop(key, value); +        attrName = booleanKey; +      } + +      this[key] = value; + +      // translate normalized key to actual key +      if (attrName) { +        this.$attr[key] = attrName; +      } else { +        attrName = this.$attr[key]; +        if (!attrName) { +          this.$attr[key] = attrName = snake_case(key, '-'); +        } +      } + +      if (writeAttr !== false) { +        if (value === null || value === undefined) { +          this.$element.removeAttr(attrName); +        } else { +          this.$element.attr(attrName, value); +        } +      } + + +      // fire observers +      forEach(this.$observers[key], function(fn) { +        try { +          fn(value); +        } catch (e) { +          $exceptionHandler(e); +        } +      }); +    } + + +    /** +     * Observe an interpolated attribute. +     * The observer will never be called, if given attribute is not interpolated. +     * +     * @param {string} key Normalized key. (ie ngAttribute) . +     * @param {function(*)} fn Function that will be called whenever the attribute value changes. +     */ +    function interpolatedAttrObserve(key, fn) { +      // keep only observers for interpolated attrs +      if (this.$observers[key]) { +        this.$observers[key].push(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, '')); +} + + + +/** + * Closure compiler type information + */ + +function nodesetLinkingFn( +  /* angular.Scope */ scope, +  /* NodeList */ nodeList, +  /* Element */ rootElement, +  /* function(Function) */ boundTranscludeFn +){} + +function directiveLinkingFn( +  /* nodesetLinkingFn */ nodesetLinkingFn, +  /* angular.Scope */ scope, +  /* Node */ node, +  /* Element */ rootElement, +  /* function(Function) */ boundTranscludeFn +){} diff --git a/src/ng/controller.js b/src/ng/controller.js new file mode 100644 index 00000000..fa90f8cd --- /dev/null +++ b/src/ng/controller.js @@ -0,0 +1,68 @@ +'use strict'; + +/** + * @ngdoc object + * @name angular.module.ng.$controllerProvider + * @description + * The {@link angular.module.ng.$controller $controller service} is used by Angular to create new + * controllers. + * + * This provider allows controller registration via the + * {@link angular.module.ng.$controllerProvider#register register} method. + */ +function $ControllerProvider() { +  var controllers = {}; + + +  /** +   * @ngdoc function +   * @name angular.module.ng.$controllerProvider#register +   * @methodOf angular.module.ng.$controllerProvider +   * @param {string} name Controller name +   * @param {Function|Array} constructor Controller constructor fn (optionally decorated with DI +   *    annotations in the array notation). +   */ +  this.register = function(name, constructor) { +    controllers[name] = constructor; +  }; + + +  this.$get = ['$injector', '$window', function($injector, $window) { + +    /** +     * @ngdoc function +     * @name angular.module.ng.$controller +     * @requires $injector +     * +     * @param {Function|string} constructor If called with a function then it's considered to be the +     *    controller constructor function. Otherwise it's considered to be a string which is used +     *    to retrieve the controller constructor using the following steps: +     * +     *    * check if a controller with given name is registered via `$controllerProvider` +     *    * check if evaluating the string on the current scope returns a constructor +     *    * check `window[constructor]` on the global `window` object +     * +     * @param {Object} locals Injection locals for Controller. +     * @return {Object} Instance of given controller. +     * +     * @description +     * `$controller` service is responsible for instantiating controllers. +     * +     * It's just simple call to {@link angular.module.AUTO.$injector $injector}, but extracted into +     * a service, so that one can override this service with {@link https://gist.github.com/1649788 +     * BC version}. +     */ +    return function(constructor, locals) { +      if(isString(constructor)) { +        var name = constructor; +        constructor = controllers.hasOwnProperty(name) +            ? controllers[name] +            : getter(locals.$scope, name, true) || getter($window, name, true); + +        assertArgFn(constructor, name, true); +      } + +      return $injector.instantiate(constructor, locals); +    }; +  }]; +} diff --git a/src/ng/cookieStore.js b/src/ng/cookieStore.js new file mode 100644 index 00000000..e6b7cd21 --- /dev/null +++ b/src/ng/cookieStore.js @@ -0,0 +1,64 @@ +'use strict'; + +/** + * @ngdoc object + * @name angular.module.ng.$cookieStore + * @requires $cookies + * + * @description + * Provides a key-value (string-object) storage, that is backed by session cookies. + * Objects put or retrieved from this storage are automatically serialized or + * deserialized by angular's toJson/fromJson. + * @example + */ +function $CookieStoreProvider(){ +  this.$get = ['$cookies', function($cookies) { + +    return { +      /** +       * @ngdoc method +       * @name angular.module.ng.$cookieStore#get +       * @methodOf angular.module.ng.$cookieStore +       * +       * @description +       * Returns the value of given cookie key +       * +       * @param {string} key Id to use for lookup. +       * @returns {Object} Deserialized cookie value. +       */ +      get: function(key) { +        return fromJson($cookies[key]); +      }, + +      /** +       * @ngdoc method +       * @name angular.module.ng.$cookieStore#put +       * @methodOf angular.module.ng.$cookieStore +       * +       * @description +       * Sets a value for given cookie key +       * +       * @param {string} key Id for the `value`. +       * @param {Object} value Value to be stored. +       */ +      put: function(key, value) { +        $cookies[key] = toJson(value); +      }, + +      /** +       * @ngdoc method +       * @name angular.module.ng.$cookieStore#remove +       * @methodOf angular.module.ng.$cookieStore +       * +       * @description +       * Remove given cookie +       * +       * @param {string} key Id of the key-value pair to delete. +       */ +      remove: function(key) { +        delete $cookies[key]; +      } +    }; + +  }]; +} diff --git a/src/ng/cookies.js b/src/ng/cookies.js new file mode 100644 index 00000000..cd953eb1 --- /dev/null +++ b/src/ng/cookies.js @@ -0,0 +1,94 @@ +'use strict'; + +/** + * @ngdoc object + * @name angular.module.ng.$cookies + * @requires $browser + * + * @description + * Provides read/write access to browser's cookies. + * + * Only a simple Object is exposed and by adding or removing properties to/from + * this object, new cookies are created/deleted at the end of current $eval. + * + * @example + */ +function $CookiesProvider() { +  this.$get = ['$rootScope', '$browser', function ($rootScope, $browser) { +    var cookies = {}, +        lastCookies = {}, +        lastBrowserCookies, +        runEval = false; + +    //creates a poller fn that copies all cookies from the $browser to service & inits the service +    $browser.addPollFn(function() { +      var currentCookies = $browser.cookies(); +      if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl +        lastBrowserCookies = currentCookies; +        copy(currentCookies, lastCookies); +        copy(currentCookies, cookies); +        if (runEval) $rootScope.$apply(); +      } +    })(); + +    runEval = true; + +    //at the end of each eval, push cookies +    //TODO: this should happen before the "delayed" watches fire, because if some cookies are not +    //      strings or browser refuses to store some cookies, we update the model in the push fn. +    $rootScope.$watch(push); + +    return cookies; + + +    /** +     * Pushes all the cookies from the service to the browser and verifies if all cookies were stored. +     */ +    function push() { +      var name, +          value, +          browserCookies, +          updated; + +      //delete any cookies deleted in $cookies +      for (name in lastCookies) { +        if (isUndefined(cookies[name])) { +          $browser.cookies(name, undefined); +        } +      } + +      //update all cookies updated in $cookies +      for(name in cookies) { +        value = cookies[name]; +        if (!isString(value)) { +          if (isDefined(lastCookies[name])) { +            cookies[name] = lastCookies[name]; +          } else { +            delete cookies[name]; +          } +        } else if (value !== lastCookies[name]) { +          $browser.cookies(name, value); +          updated = true; +        } +      } + +      //verify what was actually stored +      if (updated){ +        updated = false; +        browserCookies = $browser.cookies(); + +        for (name in cookies) { +          if (cookies[name] !== browserCookies[name]) { +            //delete or reset all cookies that the browser dropped from $cookies +            if (isUndefined(browserCookies[name])) { +              delete cookies[name]; +            } else { +              cookies[name] = browserCookies[name]; +            } +            updated = true; +          } +        } +      } +    } +  }]; +} diff --git a/src/ng/defer.js b/src/ng/defer.js new file mode 100644 index 00000000..f2a893bc --- /dev/null +++ b/src/ng/defer.js @@ -0,0 +1,45 @@ +'use strict'; + +/** + * @ngdoc function + * @name angular.module.ng.$defer + * @requires $browser + * + * @description + * Delegates to {@link angular.module.ng.$browser#defer $browser.defer}, but wraps the `fn` function + * into a try/catch block and delegates any exceptions to + * {@link angular.module.ng.$exceptionHandler $exceptionHandler} service. + * + * In tests you can use `$browser.defer.flush()` to flush the queue of deferred functions. + * + * @param {function()} fn A function, who's execution should be deferred. + * @param {number=} [delay=0] of milliseconds to defer the function execution. + * @returns {*} DeferId that can be used to cancel the task via `$defer.cancel()`. + */ + +/** + * @ngdoc function + * @name angular.module.ng.$defer#cancel + * @methodOf angular.module.ng.$defer + * + * @description + * Cancels a defered task identified with `deferId`. + * + * @param {*} deferId Token returned by the `$defer` function. + * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfuly canceled. + */ +function $DeferProvider(){ +  this.$get = ['$rootScope', '$browser', function($rootScope, $browser) { +    function defer(fn, delay) { +      return $browser.defer(function() { +        $rootScope.$apply(fn); +      }, delay); +    } + +    defer.cancel = function(deferId) { +      return $browser.defer.cancel(deferId); +    }; + +    return defer; +  }]; +} diff --git a/src/ng/directive/a.js b/src/ng/directive/a.js new file mode 100644 index 00000000..d96af784 --- /dev/null +++ b/src/ng/directive/a.js @@ -0,0 +1,29 @@ +'use strict'; + +/* + * Modifies the default behavior of html A tag, so that the default action is prevented when href + * attribute is empty. + * + * The reasoning for this change is to allow easy creation of action links with ng-click without + * changing the location or causing page reloads, e.g.: + * <a href="" ng-click="model.$save()">Save</a> + */ +var htmlAnchorDirective = valueFn({ +  restrict: 'E', +  compile: function(element, attr) { +    // turn <a href ng-click="..">link</a> into a link in IE +    // but only if it doesn't have name attribute, in which case it's an anchor +    if (!attr.href) { +      attr.$set('href', ''); +    } + +    return function(scope, element) { +      element.bind('click', function(event){ +        // if we have no href url, then don't navigate anywhere. +        if (!element.attr('href')) { +          event.preventDefault(); +        } +      }); +    } +  } +}); diff --git a/src/ng/directive/booleanAttrDirs.js b/src/ng/directive/booleanAttrDirs.js new file mode 100644 index 00000000..7da52db0 --- /dev/null +++ b/src/ng/directive/booleanAttrDirs.js @@ -0,0 +1,314 @@ +'use strict'; + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-href + * @restrict A + * + * @description + * Using <angular/> markup like {{hash}} in an href attribute makes + * the page open to a wrong URL, if the user clicks that link before + * angular has a chance to replace the {{hash}} with actual URL, the + * link will be broken and will most likely return a 404 error. + * The `ng-href` solves this problem by placing the `href` in the + * `ng-` namespace. + * + * The buggy way to write it: + * <pre> + * <a href="http://www.gravatar.com/avatar/{{hash}}"/> + * </pre> + * + * The correct way to write it: + * <pre> + * <a ng-href="http://www.gravatar.com/avatar/{{hash}}"/> + * </pre> + * + * @element A + * @param {template} ng-href any string which can contain `{{}}` markup. + * + * @example + * This example uses `link` variable inside `href` attribute: +    <doc:example> +      <doc:source> +        <input ng-model="value" /><br /> +        <a id="link-1" href ng-click="value = 1">link 1</a> (link, don't reload)<br /> +        <a id="link-2" href="" ng-click="value = 2">link 2</a> (link, don't reload)<br /> +        <a id="link-3" ng-href="/{{'123'}}" ng-ext-link>link 3</a> (link, reload!)<br /> +        <a id="link-4" href="" name="xx" ng-click="value = 4">anchor</a> (link, don't reload)<br /> +        <a id="link-5" name="xxx" ng-click="value = 5">anchor</a> (no link)<br /> +        <a id="link-6" ng-href="/{{value}}" ng-ext-link>link</a> (link, change hash) +      </doc:source> +      <doc:scenario> +        it('should execute ng-click but not reload when href without value', function() { +          element('#link-1').click(); +          expect(input('value').val()).toEqual('1'); +          expect(element('#link-1').attr('href')).toBe(""); +        }); + +        it('should execute ng-click but not reload when href empty string', function() { +          element('#link-2').click(); +          expect(input('value').val()).toEqual('2'); +          expect(element('#link-2').attr('href')).toBe(""); +        }); + +        it('should execute ng-click and change url when ng-href specified', function() { +          expect(element('#link-3').attr('href')).toBe("/123"); + +          element('#link-3').click(); +          expect(browser().window().path()).toEqual('/123'); +        }); + +        it('should execute ng-click but not reload when href empty string and name specified', function() { +          element('#link-4').click(); +          expect(input('value').val()).toEqual('4'); +          expect(element('#link-4').attr('href')).toBe(""); +        }); + +        it('should execute ng-click but not reload when no href but name specified', function() { +          element('#link-5').click(); +          expect(input('value').val()).toEqual('5'); +          expect(element('#link-5').attr('href')).toBe(""); +        }); + +        it('should only change url when only ng-href', function() { +          input('value').enter('6'); +          expect(element('#link-6').attr('href')).toBe("/6"); + +          element('#link-6').click(); +          expect(browser().window().path()).toEqual('/6'); +        }); +      </doc:scenario> +    </doc:example> + */ + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-src + * @restrict A + * + * @description + * Using <angular/> markup like `{{hash}}` in a `src` attribute doesn't + * work right: The browser will fetch from the URL with the literal + * text `{{hash}}` until <angular/> replaces the expression inside + * `{{hash}}`. The `ng-src` attribute solves this problem by placing + *  the `src` attribute in the `ng-` namespace. + * + * The buggy way to write it: + * <pre> + * <img src="http://www.gravatar.com/avatar/{{hash}}"/> + * </pre> + * + * The correct way to write it: + * <pre> + * <img ng-src="http://www.gravatar.com/avatar/{{hash}}"/> + * </pre> + * + * @element IMG + * @param {template} ng-src any string which can contain `{{}}` markup. + */ + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-disabled + * @restrict A + * + * @description + * + * The following markup will make the button enabled on Chrome/Firefox but not on IE8 and older IEs: + * <pre> + * <div ng-init="scope = { isDisabled: false }"> + *  <button disabled="{{scope.isDisabled}}">Disabled</button> + * </div> + * </pre> + * + * The HTML specs do not require browsers to preserve the special attributes such as disabled. + * (The presence of them means true and absence means false) + * This prevents the angular compiler from correctly retrieving the binding expression. + * To solve this problem, we introduce ng-disabled. + * + * @example +    <doc:example> +      <doc:source> +        Click me to toggle: <input type="checkbox" ng-model="checked"><br/> +        <button ng-model="button" ng-disabled="checked">Button</button> +      </doc:source> +      <doc:scenario> +        it('should toggle button', function() { +          expect(element('.doc-example-live :button').prop('disabled')).toBeFalsy(); +          input('checked').check(); +          expect(element('.doc-example-live :button').prop('disabled')).toBeTruthy(); +        }); +      </doc:scenario> +    </doc:example> + * + * @element INPUT + * @param {string} expression Angular expression that will be evaluated. + */ + + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-checked + * @restrict A + * + * @description + * The HTML specs do not require browsers to preserve the special attributes such as checked. + * (The presence of them means true and absence means false) + * This prevents the angular compiler from correctly retrieving the binding expression. + * To solve this problem, we introduce ng-checked. + * @example +    <doc:example> +      <doc:source> +        Check me to check both: <input type="checkbox" ng-model="master"><br/> +        <input id="checkSlave" type="checkbox" ng-checked="master"> +      </doc:source> +      <doc:scenario> +        it('should check both checkBoxes', function() { +          expect(element('.doc-example-live #checkSlave').prop('checked')).toBeFalsy(); +          input('master').check(); +          expect(element('.doc-example-live #checkSlave').prop('checked')).toBeTruthy(); +        }); +      </doc:scenario> +    </doc:example> + * + * @element INPUT + * @param {string} expression Angular expression that will be evaluated. + */ + + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-multiple + * @restrict A + * + * @description + * The HTML specs do not require browsers to preserve the special attributes such as multiple. + * (The presence of them means true and absence means false) + * This prevents the angular compiler from correctly retrieving the binding expression. + * To solve this problem, we introduce ng-multiple. + * + * @example +     <doc:example> +       <doc:source> +         Check me check multiple: <input type="checkbox" ng-model="checked"><br/> +         <select id="select" ng-multiple="checked"> +           <option>Misko</option> +           <option>Igor</option> +           <option>Vojta</option> +           <option>Di</option> +         </select> +       </doc:source> +       <doc:scenario> +         it('should toggle multiple', function() { +           expect(element('.doc-example-live #select').prop('multiple')).toBeFalsy(); +           input('checked').check(); +           expect(element('.doc-example-live #select').prop('multiple')).toBeTruthy(); +         }); +       </doc:scenario> +     </doc:example> + * + * @element SELECT + * @param {string} expression Angular expression that will be evaluated. + */ + + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-readonly + * @restrict A + * + * @description + * The HTML specs do not require browsers to preserve the special attributes such as readonly. + * (The presence of them means true and absence means false) + * This prevents the angular compiler from correctly retrieving the binding expression. + * To solve this problem, we introduce ng-readonly. + * @example +    <doc:example> +      <doc:source> +        Check me to make text readonly: <input type="checkbox" ng-model="checked"><br/> +        <input type="text" ng-readonly="checked" value="I'm Angular"/> +      </doc:source> +      <doc:scenario> +        it('should toggle readonly attr', function() { +          expect(element('.doc-example-live :text').prop('readonly')).toBeFalsy(); +          input('checked').check(); +          expect(element('.doc-example-live :text').prop('readonly')).toBeTruthy(); +        }); +      </doc:scenario> +    </doc:example> + * + * @element INPUT + * @param {string} expression Angular expression that will be evaluated. + */ + + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-selected + * @restrict A + * + * @description + * The HTML specs do not require browsers to preserve the special attributes such as selected. + * (The presence of them means true and absence means false) + * This prevents the angular compiler from correctly retrieving the binding expression. + * To solve this problem, we introduce ng-selected. + * @example +    <doc:example> +      <doc:source> +        Check me to select: <input type="checkbox" ng-model="selected"><br/> +        <select> +          <option>Hello!</option> +          <option id="greet" ng-selected="selected">Greetings!</option> +        </select> +      </doc:source> +      <doc:scenario> +        it('should select Greetings!', function() { +          expect(element('.doc-example-live #greet').prop('selected')).toBeFalsy(); +          input('selected').check(); +          expect(element('.doc-example-live #greet').prop('selected')).toBeTruthy(); +        }); +      </doc:scenario> +    </doc:example> + * + * @element OPTION + * @param {string} expression Angular expression that will be evaluated. + */ + + +var ngAttributeAliasDirectives = {}; + + +// boolean attrs are evaluated +forEach(BOOLEAN_ATTR, function(propName, attrName) { +  var normalized = directiveNormalize('ng-' + attrName); +  ngAttributeAliasDirectives[normalized] = function() { +    return { +      compile: function(tpl, attr) { +        attr.$observers[attrName] = []; +        return function(scope, element, attr) { +          scope.$watch(attr[normalized], function(value) { +            attr.$set(attrName, value); +          }); +        }; +      } +    }; +  }; +}); + + +// ng-src, ng-href are interpolated +forEach(['src', 'href'], function(attrName) { +  var normalized = directiveNormalize('ng-' + attrName); +  ngAttributeAliasDirectives[normalized] = function() { +    return { +      compile: function(tpl, attr) { +        attr.$observers[attrName] = []; +        return function(scope, element, attr) { +          attr.$observe(normalized, function(value) { +            attr.$set(attrName, value); +          }); +        }; +      } +    }; +  }; +}); diff --git a/src/ng/directive/directives.js b/src/ng/directive/directives.js new file mode 100644 index 00000000..123645f9 --- /dev/null +++ b/src/ng/directive/directives.js @@ -0,0 +1,11 @@ +'use strict'; + +function ngDirective(directive) { +  if (isFunction(directive)) { +    directive = { +      link: directive +    } +  } +  directive.restrict = directive.restrict || 'AC'; +  return valueFn(directive); +}; diff --git a/src/ng/directive/form.js b/src/ng/directive/form.js new file mode 100644 index 00000000..b6d3f4be --- /dev/null +++ b/src/ng/directive/form.js @@ -0,0 +1,267 @@ +'use strict'; + + +var nullFormCtrl = { +  $addControl: noop, +  $removeControl: noop, +  $setValidity: noop, +  $setDirty: noop +} + +/** + * @ngdoc object + * @name angular.module.ng.$compileProvider.directive.form.FormController + * + * @property {boolean} $pristine True if user has not interacted with the form yet. + * @property {boolean} $dirty True if user has already interacted with the form. + * @property {boolean} $valid True if all of the containg forms and controls are valid. + * @property {boolean} $invalid True if at least one containing control or form is invalid. + * + * @property {Object} $error Is an object hash, containing references to all invalid controls or + *  forms, where: + * + *  - keys are validation tokens (error names) — such as `REQUIRED`, `URL` or `EMAIL`), + *  - values are arrays of controls or forms that are invalid with given error. + * + * @description + * `FormController` keeps track of all its controls and nested forms as well as state of them, + * such as being valid/invalid or dirty/pristine. + * + * Each {@link angular.module.ng.$compileProvider.directive.form form} directive creates an instance + * of `FormController`. + * + */ +FormController.$inject = ['$element', '$attrs']; +function FormController(element, attrs) { +  var form = this, +      parentForm = element.parent().controller('form') || nullFormCtrl, +      invalidCount = 0, // used to easily determine if we are valid +      errors = form.$error = {}; + +  // init state +  form.$name = attrs.name; +  form.$dirty = false; +  form.$pristine = true; +  form.$valid = true; +  form.$invalid = false; + +  parentForm.$addControl(form); + +  // Setup initial state of the control +  element.addClass(PRISTINE_CLASS); +  toggleValidCss(true); + +  // convenience method for easy toggling of classes +  function toggleValidCss(isValid, validationErrorKey) { +    validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; +    element. +      removeClass((isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey). +      addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); +  } + +  form.$addControl = function(control) { +    if (control.$name && !form.hasOwnProperty(control.$name)) { +      form[control.$name] = control; +    } +  }; + +  form.$removeControl = function(control) { +    if (control.$name && form[control.$name] === control) { +      delete form[control.$name]; +    } +    forEach(errors, cleanupControlErrors, control); +  }; + +  form.$setValidity = function(validationToken, isValid, control) { +    if (isValid) { +      cleanupControlErrors(errors[validationToken], validationToken, control); + +      if (!invalidCount) { +        toggleValidCss(isValid); +        form.$valid = true; +        form.$invalid = false; +      } +    } else { +      if (!invalidCount) { +        toggleValidCss(isValid); +      } +      addControlError(validationToken, control); + +      form.$valid = false; +      form.$invalid = true; +    } +  }; + +  form.$setDirty = function() { +    element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS); +    form.$dirty = true; +    form.$pristine = false; +  }; + +  function cleanupControlErrors(queue, validationToken, control) { +    if (queue) { +      control = control || this; // so that we can be used in forEach; +      arrayRemove(queue, control); +      if (!queue.length) { +        invalidCount--; +        errors[validationToken] = false; +        toggleValidCss(true, validationToken); +        parentForm.$setValidity(validationToken, true, form); +      } +    } +  } + +  function addControlError(validationToken, control) { +    var queue = errors[validationToken]; +    if (queue) { +      if (includes(queue, control)) return; +    } else { +      errors[validationToken] = queue = []; +      invalidCount++; +      toggleValidCss(false, validationToken); +      parentForm.$setValidity(validationToken, false, form); +    } +    queue.push(control); +  } +} + + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-form + * @restrict EAC + * + * @description + * Nestable alias of {@link angular.module.ng.$compileProvider.directive.form `form`} directive. HTML + * does not allow nesting of form elements. It is useful to nest forms, for example if the validity of a + * sub-group of controls needs to be determined. + * + * @param {string=} ng-form|name Name of the form. If specified, the form controller will be published into + *                       related scope, under this name. + * + */ + + /** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.form + * @restrict E + * + * @description + * Directive that instantiates + * {@link angular.module.ng.$compileProvider.directive.form.FormController FormController}. + * + * If `name` attribute is specified, the form controller is published onto the current scope under + * this name. + * + * # Alias: {@link angular.module.ng.$compileProvider.directive.ng-form `ng-form`} + * + * In angular forms can be nested. This means that the outer form is valid when all of the child + * forms are valid as well. However browsers do not allow nesting of `<form>` elements, for this + * reason angular provides {@link angular.module.ng.$compileProvider.directive.ng-form `ng-form`} alias + * which behaves identical to `<form>` but allows form nesting. + * + * + * # CSS classes + *  - `ng-valid` Is set if the form is valid. + *  - `ng-invalid` Is set if the form is invalid. + *  - `ng-pristine` Is set if the form is pristine. + *  - `ng-dirty` Is set if the form is dirty. + * + * + * # Submitting a form and preventing default action + * + * Since the role of forms in client-side Angular applications is different than in classical + * roundtrip apps, it is desirable for the browser not to translate the form submission into a full + * page reload that sends the data to the server. Instead some javascript logic should be triggered + * to handle the form submission in application specific way. + * + * For this reason, Angular prevents the default action (form submission to the server) unless the + * `<form>` element has an `action` attribute specified. + * + * You can use one of the following two ways to specify what javascript method should be called when + * a form is submitted: + * + * - ng-submit on the form element (add link to ng-submit) + * - ng-click on the first button or input field of type submit (input[type=submit]) + * + * To prevent double execution of the handler, use only one of ng-submit or ng-click. This is + * because of the following form submission rules coming from the html spec: + * + * - If a form has only one input field then hitting enter in this field triggers form submit + * (`ng-submit`) + * - if a form has has 2+ input fields and no buttons or input[type=submit] then hitting enter + * doesn't trigger submit + * - if a form has one or more input fields and one or more buttons or input[type=submit] then + * hitting enter in any of the input fields will trigger the click handler on the *first* button or + * input[type=submit] (`ng-click`) *and* a submit handler on the enclosing form (`ng-submit`) + * + * @param {string=} name Name of the form. If specified, the form controller will be published into + *                       related scope, under this name. + * + * @example +    <doc:example> +      <doc:source> +       <script> +         function Ctrl($scope) { +           $scope.userType = 'guest'; +         } +       </script> +       <form name="myForm" ng-controller="Ctrl"> +         userType: <input name="input" ng-model="userType" required> +         <span class="error" ng-show="myForm.input.$error.REQUIRED">Required!</span><br> +         <tt>userType = {{userType}}</tt><br> +         <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br> +         <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br> +         <tt>myForm.$valid = {{myForm.$valid}}</tt><br> +         <tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br> +        </form> +      </doc:source> +      <doc:scenario> +        it('should initialize to model', function() { +         expect(binding('userType')).toEqual('guest'); +         expect(binding('myForm.input.$valid')).toEqual('true'); +        }); + +        it('should be invalid if empty', function() { +         input('userType').enter(''); +         expect(binding('userType')).toEqual(''); +         expect(binding('myForm.input.$valid')).toEqual('false'); +        }); +      </doc:scenario> +    </doc:example> + */ +var formDirectiveDir = { +  name: 'form', +  restrict: 'E', +  controller: FormController, +  compile: function() { +    return { +      pre: function(scope, formElement, attr, controller) { +        if (!attr.action) { +          formElement.bind('submit', function(event) { +            event.preventDefault(); +          }); +        } + +        var parentFormCtrl = formElement.parent().controller('form'), +            alias = attr.name || attr.ngForm; + +        if (alias) { +          scope[alias] = controller; +        } +        if (parentFormCtrl) { +          formElement.bind('$destroy', function() { +            parentFormCtrl.$removeControl(controller); +            if (alias) { +              scope[alias] = undefined; +            } +            extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards +          }); +        } +      } +    }; +  } +}; + +var formDirective = valueFn(formDirectiveDir); +var ngFormDirective = valueFn(extend(copy(formDirectiveDir), {restrict: 'EAC'})); diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js new file mode 100644 index 00000000..348c9f25 --- /dev/null +++ b/src/ng/directive/input.js @@ -0,0 +1,1194 @@ +'use strict'; + +var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/; +var EMAIL_REGEXP = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/; +var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/; + +var inputType = { + +  /** +   * @ngdoc inputType +   * @name angular.module.ng.$compileProvider.directive.input.text +   * +   * @description +   * Standard HTML text input with angular data binding. +   * +   * @param {string} ng-model Assignable angular expression to data-bind to. +   * @param {string=} name Property name of the form under which the control is published. +   * @param {string=} required Sets `required` validation error key if the value is not entered. +   * @param {number=} ng-minlength Sets `minlength` validation error key if the value is shorter than +   *    minlength. +   * @param {number=} ng-maxlength Sets `maxlength` validation error key if the value is longer than +   *    maxlength. +   * @param {string=} ng-pattern Sets `pattern` validation error key if the value does not match the +   *    RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for +   *    patterns defined as scope expressions. +   * @param {string=} ng-change Angular expression to be executed when input changes due to user +   *    interaction with the input element. +   * +   * @example +      <doc:example> +        <doc:source> +         <script> +           function Ctrl($scope) { +             $scope.text = 'guest'; +             $scope.word = /^\w*$/; +           } +         </script> +         <form name="myForm" ng-controller="Ctrl"> +           Single word: <input type="text" name="input" ng-model="text" +                               ng-pattern="word" required> +           <span class="error" ng-show="myForm.input.$error.required"> +             Required!</span> +           <span class="error" ng-show="myForm.input.$error.pattern"> +             Single word only!</span> + +           <tt>text = {{text}}</tt><br/> +           <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> +           <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> +           <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> +           <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> +          </form> +        </doc:source> +        <doc:scenario> +          it('should initialize to model', function() { +            expect(binding('text')).toEqual('guest'); +            expect(binding('myForm.input.$valid')).toEqual('true'); +          }); + +          it('should be invalid if empty', function() { +            input('text').enter(''); +            expect(binding('text')).toEqual(''); +            expect(binding('myForm.input.$valid')).toEqual('false'); +          }); + +          it('should be invalid if multi word', function() { +            input('text').enter('hello world'); +            expect(binding('myForm.input.$valid')).toEqual('false'); +          }); +        </doc:scenario> +      </doc:example> +   */ +  'text': textInputType, + + +  /** +   * @ngdoc inputType +   * @name angular.module.ng.$compileProvider.directive.input.number +   * +   * @description +   * Text input with number validation and transformation. Sets the `number` validation +   * error if not a valid number. +   * +   * @param {string} ng-model Assignable angular expression to data-bind to. +   * @param {string=} name Property name of the form under which the control is published. +   * @param {string=} min Sets the `min` validation error key if the value entered is less then `min`. +   * @param {string=} max Sets the `max` validation error key if the value entered is greater then `min`. +   * @param {string=} required Sets `required` validation error key if the value is not entered. +   * @param {number=} ng-minlength Sets `minlength` validation error key if the value is shorter than +   *    minlength. +   * @param {number=} ng-maxlength Sets `maxlength` validation error key if the value is longer than +   *    maxlength. +   * @param {string=} ng-pattern Sets `pattern` validation error key if the value does not match the +   *    RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for +   *    patterns defined as scope expressions. +   * @param {string=} ng-change Angular expression to be executed when input changes due to user +   *    interaction with the input element. +   * +   * @example +      <doc:example> +        <doc:source> +         <script> +           function Ctrl($scope) { +             $scope.value = 12; +           } +         </script> +         <form name="myForm" ng-controller="Ctrl"> +           Number: <input type="number" name="input" ng-model="value" +                          min="0" max="99" required> +           <span class="error" ng-show="myForm.list.$error.required"> +             Required!</span> +           <span class="error" ng-show="myForm.list.$error.number"> +             Not valid number!</span> +           <tt>value = {{value}}</tt><br/> +           <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> +           <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> +           <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> +           <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> +          </form> +        </doc:source> +        <doc:scenario> +          it('should initialize to model', function() { +           expect(binding('value')).toEqual('12'); +           expect(binding('myForm.input.$valid')).toEqual('true'); +          }); + +          it('should be invalid if empty', function() { +           input('value').enter(''); +           expect(binding('value')).toEqual(''); +           expect(binding('myForm.input.$valid')).toEqual('false'); +          }); + +          it('should be invalid if over max', function() { +           input('value').enter('123'); +           expect(binding('value')).toEqual(''); +           expect(binding('myForm.input.$valid')).toEqual('false'); +          }); +        </doc:scenario> +      </doc:example> +   */ +  'number': numberInputType, + + +  /** +   * @ngdoc inputType +   * @name angular.module.ng.$compileProvider.directive.input.url +   * +   * @description +   * Text input with URL validation. Sets the `url` validation error key if the content is not a +   * valid URL. +   * +   * @param {string} ng-model Assignable angular expression to data-bind to. +   * @param {string=} name Property name of the form under which the control is published. +   * @param {string=} required Sets `required` validation error key if the value is not entered. +   * @param {number=} ng-minlength Sets `minlength` validation error key if the value is shorter than +   *    minlength. +   * @param {number=} ng-maxlength Sets `maxlength` validation error key if the value is longer than +   *    maxlength. +   * @param {string=} ng-pattern Sets `pattern` validation error key if the value does not match the +   *    RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for +   *    patterns defined as scope expressions. +   * @param {string=} ng-change Angular expression to be executed when input changes due to user +   *    interaction with the input element. +   * +   * @example +      <doc:example> +        <doc:source> +         <script> +           function Ctrl($scope) { +             $scope.text = 'http://google.com'; +           } +         </script> +         <form name="myForm" ng-controller="Ctrl"> +           URL: <input type="url" name="input" ng-model="text" required> +           <span class="error" ng-show="myForm.input.$error.required"> +             Required!</span> +           <span class="error" ng-show="myForm.input.$error.url"> +             Not valid url!</span> +           <tt>text = {{text}}</tt><br/> +           <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> +           <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> +           <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> +           <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> +           <tt>myForm.$error.url = {{!!myForm.$error.url}}</tt><br/> +          </form> +        </doc:source> +        <doc:scenario> +          it('should initialize to model', function() { +            expect(binding('text')).toEqual('http://google.com'); +            expect(binding('myForm.input.$valid')).toEqual('true'); +          }); + +          it('should be invalid if empty', function() { +            input('text').enter(''); +            expect(binding('text')).toEqual(''); +            expect(binding('myForm.input.$valid')).toEqual('false'); +          }); + +          it('should be invalid if not url', function() { +            input('text').enter('xxx'); +            expect(binding('myForm.input.$valid')).toEqual('false'); +          }); +        </doc:scenario> +      </doc:example> +   */ +  'url': urlInputType, + + +  /** +   * @ngdoc inputType +   * @name angular.module.ng.$compileProvider.directive.input.email +   * +   * @description +   * Text input with email validation. Sets the `email` validation error key if not a valid email +   * address. +   * +   * @param {string} ng-model Assignable angular expression to data-bind to. +   * @param {string=} name Property name of the form under which the control is published. +   * @param {string=} required Sets `required` validation error key if the value is not entered. +   * @param {number=} ng-minlength Sets `minlength` validation error key if the value is shorter than +   *    minlength. +   * @param {number=} ng-maxlength Sets `maxlength` validation error key if the value is longer than +   *    maxlength. +   * @param {string=} ng-pattern Sets `pattern` validation error key if the value does not match the +   *    RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for +   *    patterns defined as scope expressions. +   * +   * @example +      <doc:example> +        <doc:source> +         <script> +           function Ctrl($scope) { +             $scope.text = 'me@example.com'; +           } +         </script> +           <form name="myForm" ng-controller="Ctrl"> +             Email: <input type="email" name="input" ng-model="text" required> +             <span class="error" ng-show="myForm.input.$error.required"> +               Required!</span> +             <span class="error" ng-show="myForm.input.$error.email"> +               Not valid email!</span> +             <tt>text = {{text}}</tt><br/> +             <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> +             <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> +             <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> +             <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> +             <tt>myForm.$error.email = {{!!myForm.$error.email}}</tt><br/> +           </form> +        </doc:source> +        <doc:scenario> +          it('should initialize to model', function() { +            expect(binding('text')).toEqual('me@example.com'); +            expect(binding('myForm.input.$valid')).toEqual('true'); +          }); + +          it('should be invalid if empty', function() { +            input('text').enter(''); +            expect(binding('text')).toEqual(''); +            expect(binding('myForm.input.$valid')).toEqual('false'); +          }); + +          it('should be invalid if not email', function() { +            input('text').enter('xxx'); +            expect(binding('myForm.input.$valid')).toEqual('false'); +          }); +        </doc:scenario> +      </doc:example> +   */ +  'email': emailInputType, + + +  /** +   * @ngdoc inputType +   * @name angular.module.ng.$compileProvider.directive.input.radio +   * +   * @description +   * HTML radio button. +   * +   * @param {string} ng-model Assignable angular expression to data-bind to. +   * @param {string} value The value to which the expression should be set when selected. +   * @param {string=} name Property name of the form under which the control is published. +   * @param {string=} ng-change Angular expression to be executed when input changes due to user +   *    interaction with the input element. +   * +   * @example +      <doc:example> +        <doc:source> +         <script> +           function Ctrl($scope) { +             $scope.color = 'blue'; +           } +         </script> +         <form name="myForm" ng-controller="Ctrl"> +           <input type="radio" ng-model="color" value="red">  Red <br/> +           <input type="radio" ng-model="color" value="green"> Green <br/> +           <input type="radio" ng-model="color" value="blue"> Blue <br/> +           <tt>color = {{color}}</tt><br/> +          </form> +        </doc:source> +        <doc:scenario> +          it('should change state', function() { +            expect(binding('color')).toEqual('blue'); + +            input('color').select('red'); +            expect(binding('color')).toEqual('red'); +          }); +        </doc:scenario> +      </doc:example> +   */ +  'radio': radioInputType, + + +  /** +   * @ngdoc inputType +   * @name angular.module.ng.$compileProvider.directive.input.checkbox +   * +   * @description +   * HTML checkbox. +   * +   * @param {string} ng-model Assignable angular expression to data-bind to. +   * @param {string=} name Property name of the form under which the control is published. +   * @param {string=} ng-true-value The value to which the expression should be set when selected. +   * @param {string=} ng-false-value The value to which the expression should be set when not selected. +   * @param {string=} ng-change Angular expression to be executed when input changes due to user +   *    interaction with the input element. +   * +   * @example +      <doc:example> +        <doc:source> +         <script> +           function Ctrl($scope) { +             $scope.value1 = true; +             $scope.value2 = 'YES' +           } +         </script> +         <form name="myForm" ng-controller="Ctrl"> +           Value1: <input type="checkbox" ng-model="value1"> <br/> +           Value2: <input type="checkbox" ng-model="value2" +                          ng-true-value="YES" ng-false-value="NO"> <br/> +           <tt>value1 = {{value1}}</tt><br/> +           <tt>value2 = {{value2}}</tt><br/> +          </form> +        </doc:source> +        <doc:scenario> +          it('should change state', function() { +            expect(binding('value1')).toEqual('true'); +            expect(binding('value2')).toEqual('YES'); + +            input('value1').check(); +            input('value2').check(); +            expect(binding('value1')).toEqual('false'); +            expect(binding('value2')).toEqual('NO'); +          }); +        </doc:scenario> +      </doc:example> +   */ +  'checkbox': checkboxInputType, + +  'hidden': noop, +  'button': noop, +  'submit': noop, +  'reset': noop +}; + + +function isEmpty(value) { +  return isUndefined(value) || value === '' || value === null || value !== value; +} + + +function textInputType(scope, element, attr, ctrl) { +  element.bind('blur', function() { +    scope.$apply(function() { +      ctrl.$setViewValue(trim(element.val())); +    }); +  }); + +  ctrl.$render = function() { +    element.val(isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue); +  }; + +  // pattern validator +  var pattern = attr.ngPattern, +      patternValidator; + +  var validate = function(regexp, value) { +    if (isEmpty(value) || regexp.test(value)) { +      ctrl.$setValidity('pattern', true); +      return value; +    } else { +      ctrl.$setValidity('pattern', false); +      return undefined; +    } +  }; + +  if (pattern) { +    if (pattern.match(/^\/(.*)\/$/)) { +      pattern = new RegExp(pattern.substr(1, pattern.length - 2)); +      patternValidator = function(value) { +        return validate(pattern, value) +      }; +    } else { +      patternValidator = function(value) { +        var patternObj = scope.$eval(pattern); + +        if (!patternObj || !patternObj.test) { +          throw new Error('Expected ' + pattern + ' to be a RegExp but was ' + patternObj); +        } +        return validate(patternObj, value); +      }; +    } + +    ctrl.$formatters.push(patternValidator); +    ctrl.$parsers.push(patternValidator); +  } + +  // min length validator +  if (attr.ngMinlength) { +    var minlength = int(attr.ngMinlength); +    var minLengthValidator = function(value) { +      if (!isEmpty(value) && value.length < minlength) { +        ctrl.$setValidity('minlength', false); +        return undefined; +      } else { +        ctrl.$setValidity('minlength', true); +        return value; +      } +    }; + +    ctrl.$parsers.push(minLengthValidator); +    ctrl.$formatters.push(minLengthValidator); +  } + +  // max length validator +  if (attr.ngMaxlength) { +    var maxlength = int(attr.ngMaxlength); +    var maxLengthValidator = function(value) { +      if (!isEmpty(value) && value.length > maxlength) { +        ctrl.$setValidity('maxlength', false); +        return undefined; +      } else { +        ctrl.$setValidity('maxlength', true); +        return value; +      } +    }; + +    ctrl.$parsers.push(maxLengthValidator); +    ctrl.$formatters.push(maxLengthValidator); +  } +}; + +function numberInputType(scope, element, attr, ctrl) { +  textInputType(scope, element, attr, ctrl); + +  ctrl.$parsers.push(function(value) { +    var empty = isEmpty(value); +    if (empty || NUMBER_REGEXP.test(value)) { +      ctrl.$setValidity('number', true); +      return value === '' ? null : (empty ? value : parseFloat(value)); +    } else { +      ctrl.$setValidity('number', false); +      return undefined; +    } +  }); + +  ctrl.$formatters.push(function(value) { +    return isEmpty(value) ? '' : '' + value; +  }); + +  if (attr.min) { +    var min = parseFloat(attr.min); +    var minValidator = function(value) { +      if (!isEmpty(value) && value < min) { +        ctrl.$setValidity('min', false); +        return undefined; +      } else { +        ctrl.$setValidity('min', true); +        return value; +      } +    }; + +    ctrl.$parsers.push(minValidator); +    ctrl.$formatters.push(minValidator); +  } + +  if (attr.max) { +    var max = parseFloat(attr.max); +    var maxValidator = function(value) { +      if (!isEmpty(value) && value > max) { +        ctrl.$setValidity('max', false); +        return undefined; +      } else { +        ctrl.$setValidity('max', true); +        return value; +      } +    }; + +    ctrl.$parsers.push(maxValidator); +    ctrl.$formatters.push(maxValidator); +  } + +  ctrl.$formatters.push(function(value) { + +    if (isEmpty(value) || isNumber(value)) { +      ctrl.$setValidity('number', true); +      return value; +    } else { +      ctrl.$setValidity('number', false); +      return undefined; +    } +  }); +} + +function urlInputType(scope, element, attr, ctrl) { +  textInputType(scope, element, attr, ctrl); + +  var urlValidator = function(value) { +    if (isEmpty(value) || URL_REGEXP.test(value)) { +      ctrl.$setValidity('url', true); +      return value; +    } else { +      ctrl.$setValidity('url', false); +      return undefined; +    } +  }; + +  ctrl.$formatters.push(urlValidator); +  ctrl.$parsers.push(urlValidator); +} + +function emailInputType(scope, element, attr, ctrl) { +  textInputType(scope, element, attr, ctrl); + +  var emailValidator = function(value) { +    if (isEmpty(value) || EMAIL_REGEXP.test(value)) { +      ctrl.$setValidity('email', true); +      return value; +    } else { +      ctrl.$setValidity('email', false); +      return undefined; +    } +  }; + +  ctrl.$formatters.push(emailValidator); +  ctrl.$parsers.push(emailValidator); +} + +function radioInputType(scope, element, attr, ctrl) { +  // correct the name +  element.attr('name', attr.id + '@' + attr.name); + +  element.bind('click', function() { +    if (element[0].checked) { +      scope.$apply(function() { +        ctrl.$setViewValue(attr.value); +      }); +    }; +  }); + +  ctrl.$render = function() { +    var value = attr.value; +    element[0].checked = (value == ctrl.$viewValue); +  }; + +  attr.$observe('value', ctrl.$render); +} + +function checkboxInputType(scope, element, attr, ctrl) { +  var trueValue = attr.ngTrueValue, +      falseValue = attr.ngFalseValue; + +  if (!isString(trueValue)) trueValue = true; +  if (!isString(falseValue)) falseValue = false; + +  element.bind('click', function() { +    scope.$apply(function() { +      ctrl.$setViewValue(element[0].checked); +    }); +  }); + +  ctrl.$render = function() { +    element[0].checked = ctrl.$viewValue; +  }; + +  ctrl.$formatters.push(function(value) { +    return value === trueValue; +  }); + +  ctrl.$parsers.push(function(value) { +    return value ? trueValue : falseValue; +  }); +} + + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.textarea + * + * @description + * HTML textarea element control with angular data-binding. The data-binding and validation + * properties of this element are exactly the same as those of the + * {@link angular.module.ng.$compileProvider.directive.input input element}. + * + * @param {string} ng-model Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {number=} ng-minlength Sets `minlength` validation error key if the value is shorter than + *    minlength. + * @param {number=} ng-maxlength Sets `maxlength` validation error key if the value is longer than + *    maxlength. + * @param {string=} ng-pattern Sets `pattern` validation error key if the value does not match the + *    RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for + *    patterns defined as scope expressions. + * @param {string=} ng-change Angular expression to be executed when input changes due to user + *    interaction with the input element. + */ + + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.input + * @restrict E + * + * @description + * HTML input element control with angular data-binding. Input control follows HTML5 input types + * and polyfills the HTML5 validation behavior for older browsers. + * + * @param {string} ng-model Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {number=} ng-minlength Sets `minlength` validation error key if the value is shorter than + *    minlength. + * @param {number=} ng-maxlength Sets `maxlength` validation error key if the value is longer than + *    maxlength. + * @param {string=} ng-pattern Sets `pattern` validation error key if the value does not match the + *    RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for + *    patterns defined as scope expressions. + * @param {string=} ng-change Angular expression to be executed when input changes due to user + *    interaction with the input element. + * + * @example +    <doc:example> +      <doc:source> +       <script> +         function Ctrl($scope) { +           $scope.user = {name: 'guest', last: 'visitor'}; +         } +       </script> +       <div ng-controller="Ctrl"> +         <form name="myForm"> +           User name: <input type="text" name="userName" ng-model="user.name" required> +           <span class="error" ng-show="myForm.userName.$error.required"> +             Required!</span><br> +           Last name: <input type="text" name="lastName" ng-model="user.last" +             ng-minlength="3" ng-maxlength="10"> +           <span class="error" ng-show="myForm.lastName.$error.minlength"> +             Too short!</span> +           <span class="error" ng-show="myForm.lastName.$error.maxlength"> +             Too long!</span><br> +         </form> +         <hr> +         <tt>user = {{user}}</tt><br/> +         <tt>myForm.userName.$valid = {{myForm.userName.$valid}}</tt><br> +         <tt>myForm.userName.$error = {{myForm.userName.$error}}</tt><br> +         <tt>myForm.lastName.$valid = {{myForm.lastName.$valid}}</tt><br> +         <tt>myForm.userName.$error = {{myForm.lastName.$error}}</tt><br> +         <tt>myForm.$valid = {{myForm.$valid}}</tt><br> +         <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br> +         <tt>myForm.$error.minlength = {{!!myForm.$error.minlength}}</tt><br> +         <tt>myForm.$error.maxlength = {{!!myForm.$error.maxlength}}</tt><br> +       </div> +      </doc:source> +      <doc:scenario> +        it('should initialize to model', function() { +          expect(binding('user')).toEqual('{"last":"visitor","name":"guest"}'); +          expect(binding('myForm.userName.$valid')).toEqual('true'); +          expect(binding('myForm.$valid')).toEqual('true'); +        }); + +        it('should be invalid if empty when required', function() { +          input('user.name').enter(''); +          expect(binding('user')).toEqual('{"last":"visitor"}'); +          expect(binding('myForm.userName.$valid')).toEqual('false'); +          expect(binding('myForm.$valid')).toEqual('false'); +        }); + +        it('should be valid if empty when min length is set', function() { +          input('user.last').enter(''); +          expect(binding('user')).toEqual('{"last":"","name":"guest"}'); +          expect(binding('myForm.lastName.$valid')).toEqual('true'); +          expect(binding('myForm.$valid')).toEqual('true'); +        }); + +        it('should be invalid if less than required min length', function() { +          input('user.last').enter('xx'); +          expect(binding('user')).toEqual('{"name":"guest"}'); +          expect(binding('myForm.lastName.$valid')).toEqual('false'); +          expect(binding('myForm.lastName.$error')).toMatch(/minlength/); +          expect(binding('myForm.$valid')).toEqual('false'); +        }); + +        it('should be invalid if longer than max length', function() { +          input('user.last').enter('some ridiculously long name'); +          expect(binding('user')) +            .toEqual('{"name":"guest"}'); +          expect(binding('myForm.lastName.$valid')).toEqual('false'); +          expect(binding('myForm.lastName.$error')).toMatch(/maxlength/); +          expect(binding('myForm.$valid')).toEqual('false'); +        }); +      </doc:scenario> +    </doc:example> + */ +var inputDirective = [function() { +  return { +    restrict: 'E', +    require: '?ngModel', +    link: function(scope, element, attr, ctrl) { +      if (ctrl) { +        (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl); +      } +    } +  }; +}]; + +var VALID_CLASS = 'ng-valid', +    INVALID_CLASS = 'ng-invalid', +    PRISTINE_CLASS = 'ng-pristine', +    DIRTY_CLASS = 'ng-dirty'; + +/** + * @ngdoc object + * @name angular.module.ng.$compileProvider.directive.ng-model.NgModelController + * + * @property {string} $viewValue Actual string value in the view. + * @property {*} $modelValue The value in the model, that the control is bound to. + * @property {Array.<Function>} $parsers Whenever the control reads value from the DOM, it executes + *     all of these functions to sanitize / convert the value as well as validate. + * + * @property {Array.<Function>} $formatters Whenever the model value changes, it executes all of + *     these functions to convert the value as well as validate. + * + * @property {Object} $error An bject hash with all errors as keys. + * + * @property {boolean} $pristine True if user has not interacted with the control yet. + * @property {boolean} $dirty True if user has already interacted with the control. + * @property {boolean} $valid True if there is no error. + * @property {boolean} $invalid True if at least one error on the control. + * + * @description + * + */ +var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$element', +    function($scope, $exceptionHandler, $attr, ngModel, $element) { +  this.$viewValue = Number.NaN; +  this.$modelValue = Number.NaN; +  this.$parsers = []; +  this.$formatters = []; +  this.$viewChangeListeners = []; +  this.$pristine = true; +  this.$dirty = false; +  this.$valid = true; +  this.$invalid = false; +  this.$render = noop; +  this.$name = $attr.name; + +  var parentForm = $element.inheritedData('$formController') || nullFormCtrl, +      invalidCount = 0, // used to easily determine if we are valid +      $error = this.$error = {}; // keep invalid keys here + + +  // Setup initial state of the control +  $element.addClass(PRISTINE_CLASS); +  toggleValidCss(true); + +  // convenience method for easy toggling of classes +  function toggleValidCss(isValid, validationErrorKey) { +    validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; +    $element. +      removeClass((isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey). +      addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); +  } + +  /** +   * @ngdoc function +   * @name angular.module.ng.$compileProvider.directive.ng-model.NgModelController#$setValidity +   * @methodOf angular.module.ng.$compileProvider.directive.ng-model.NgModelController +   * +   * @description +   * Change the validity state, and notifies the form when the control changes validity. (i.e. it +   * does not notify form if given validator is already marked as invalid). +   * +   * This method should be called by validators - i.e. the parser or formatter functions. +   * +   * @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign +   *        to `$error[validationErrorKey]=isValid` so that it is available for data-binding. +   *        The `validationErrorKey` should be in camelCase and will get converted into dash-case +   *        for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` +   *        class and can be bound to as  `{{someForm.someControl.$error.myError}}` . +   * @param {boolean} isValid Whether the current state is valid (true) or invalid (false). +   */ +  this.$setValidity = function(validationErrorKey, isValid) { +    if ($error[validationErrorKey] === !isValid) return; + +    if (isValid) { +      if ($error[validationErrorKey]) invalidCount--; +      if (!invalidCount) { +        toggleValidCss(true); +        this.$valid = true; +        this.$invalid = false; +      } +    } else { +      toggleValidCss(false) +      this.$invalid = true; +      this.$valid = false; +      invalidCount++; +    } + +    $error[validationErrorKey] = !isValid; +    toggleValidCss(isValid, validationErrorKey); + +    parentForm.$setValidity(validationErrorKey, isValid, this); +  }; + + +  /** +   * @ngdoc function +   * @name angular.module.ng.$compileProvider.directive.ng-model.NgModelController#$setViewValue +   * @methodOf angular.module.ng.$compileProvider.directive.ng-model.NgModelController +   * +   * @description +   * Read a value from view. +   * +   * This method should be called from within a DOM event handler. +   * For example {@link angular.module.ng.$compileProvider.directive.input input} or +   * {@link angular.module.ng.$compileProvider.directive.select select} directives call it. +   * +   * It internally calls all `formatters` and if resulted value is valid, updates the model and +   * calls all registered change listeners. +   * +   * @param {string} value Value from the view. +   */ +  this.$setViewValue = function(value) { +    this.$viewValue = value; + +    // change to dirty +    if (this.$pristine) { +      this.$dirty = true; +      this.$pristine = false; +      $element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS); +      parentForm.$setDirty(); +    } + +    forEach(this.$parsers, function(fn) { +      value = fn(value); +    }); + +    if (this.$modelValue !== value) { +      this.$modelValue = value; +      ngModel(value); +      forEach(this.$viewChangeListeners, function(listener) { +        try { +          listener(); +        } catch(e) { +          $exceptionHandler(e); +        } +      }) +    } +  }; + +  // model -> value +  var ctrl = this; +  $scope.$watch(function() { +    return ngModel(); +  }, function(value) { + +    // ignore change from view +    if (ctrl.$modelValue === value) return; + +    var formatters = ctrl.$formatters, +        idx = formatters.length; + +    ctrl.$modelValue = value; +    while(idx--) { +      value = formatters[idx](value); +    } + +    if (ctrl.$viewValue !== value) { +      ctrl.$viewValue = value; +      ctrl.$render(); +    } +  }); +}]; + + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-model + * + * @element input + * + * @description + * Is directive that tells Angular to do two-way data binding. It works together with `input`, + * `select`, `textarea`. You can easily write your own directives to use `ng-model` as well. + * + * `ng-model` is responsible for: + * + * - binding the view into the model, which other directives such as `input`, `textarea` or `select` + *   require, + * - providing validation behavior (i.e. required, number, email, url), + * - keeping state of the control (valid/invalid, dirty/pristine, validation errors), + * - setting related css class onto the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`), + * - register the control with parent {@link angular.module.ng.$compileProvider.directive.form form}. + * + * For basic examples, how to use `ng-model`, see: + * + *  - {@link angular.module.ng.$compileProvider.directive.input input} + *    - {@link angular.module.ng.$compileProvider.directive.input.text text} + *    - {@link angular.module.ng.$compileProvider.directive.input.checkbox checkbox} + *    - {@link angular.module.ng.$compileProvider.directive.input.radio radio} + *    - {@link angular.module.ng.$compileProvider.directive.input.number number} + *    - {@link angular.module.ng.$compileProvider.directive.input.email email} + *    - {@link angular.module.ng.$compileProvider.directive.input.url url} + *  - {@link angular.module.ng.$compileProvider.directive.select select} + *  - {@link angular.module.ng.$compileProvider.directive.textarea textarea} + * + */ +var ngModelDirective = [function() { +  return { +    inject: { +      ngModel: 'accessor' +    }, +    require: ['ngModel', '^?form'], +    controller: NgModelController, +    link: function(scope, element, attr, ctrls) { +      // notify others, especially parent forms + +      var modelCtrl = ctrls[0], +          formCtrl = ctrls[1] || nullFormCtrl; + +      formCtrl.$addControl(modelCtrl); + +      element.bind('$destroy', function() { +        formCtrl.$removeControl(modelCtrl); +      }); +    } +  }; +}]; + + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-change + * @restrict E + * + * @description + * Evaluate given expression when user changes the input. + * The expression is not evaluated when the value change is coming from the model. + * + * Note, this directive requires `ng-model` to be present. + * + * @element input + * + * @example + * <doc:example> + *   <doc:source> + *     <script> + *       function Controller($scope) { + *         $scope.counter = 0; + *         $scope.change = function() { + *           $scope.counter++; + *         }; + *       } + *     </script> + *     <div ng-controller="Controller"> + *       <input type="checkbox" ng-model="confirmed" ng-change="change()" id="ng-change-example1" /> + *       <input type="checkbox" ng-model="confirmed" id="ng-change-example2" /> + *       <label for="ng-change-example2">Confirmed</label><br /> + *       debug = {{confirmed}}<br /> + *       counter = {{counter}} + *     </div> + *   </doc:source> + *   <doc:scenario> + *     it('should evaluate the expression if changing from view', function() { + *       expect(binding('counter')).toEqual('0'); + *       element('#ng-change-example1').click(); + *       expect(binding('counter')).toEqual('1'); + *       expect(binding('confirmed')).toEqual('true'); + *     }); + * + *     it('should not evaluate the expression if changing from model', function() { + *       element('#ng-change-example2').click(); + *       expect(binding('counter')).toEqual('0'); + *       expect(binding('confirmed')).toEqual('true'); + *     }); + *   </doc:scenario> + * </doc:example> + */ +var ngChangeDirective = valueFn({ +  require: 'ngModel', +  link: function(scope, element, attr, ctrl) { +    ctrl.$viewChangeListeners.push(function() { +      scope.$eval(attr.ngChange); +    }); +  } +}); + + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-model-instant + * + * @element input + * + * @description + * By default, Angular udpates the model only on `blur` event - when the input looses focus. + * If you want to update after every key stroke, use `ng-model-instant`. + * + * @example + * <doc:example> + *   <doc:source> + *     First name: <input type="text" ng-model="firstName" /><br /> + *     Last name: <input type="text" ng-model="lastName" ng-model-instant /><br /> + * + *     First name ({{firstName}}) is only updated on `blur` event, but the last name ({{lastName}}) + *     is updated immediately, because of using `ng-model-instant`. + *   </doc:source> + *   <doc:scenario> + *     it('should update first name on blur', function() { + *       input('firstName').enter('santa', 'blur'); + *       expect(binding('firstName')).toEqual('santa'); + *     }); + * + *     it('should update last name immediately', function() { + *       input('lastName').enter('santa', 'keydown'); + *       expect(binding('lastName')).toEqual('santa'); + *     }); + *   </doc:scenario> + * </doc:example> + */ +var ngModelInstantDirective = ['$browser', function($browser) { +  return { +    require: 'ngModel', +    link: function(scope, element, attr, ctrl) { +      var handler = function() { +        scope.$apply(function() { +          ctrl.$setViewValue(trim(element.val())); +        }); +      }; + +      var timeout; +      element.bind('keydown', function(event) { +        var key = event.keyCode; + +        //    command            modifiers                   arrows +        if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return; + +        if (!timeout) { +          timeout = $browser.defer(function() { +            handler(); +            timeout = null; +          }); +        } +      }); + +      element.bind('change input', handler); +    } +  }; +}]; + + +var requiredDirective = [function() { +  return { +    require: '?ngModel', +    link: function(scope, elm, attr, ctrl) { +      if (!ctrl) return; + +      var validator = function(value) { +        if (attr.required && (isEmpty(value) || value === false)) { +          ctrl.$setValidity('required', false); +          return; +        } else { +          ctrl.$setValidity('required', true); +          return value; +        } +      }; + +      ctrl.$formatters.push(validator); +      ctrl.$parsers.unshift(validator); + +      attr.$observe('required', function() { +        validator(ctrl.$viewValue); +      }); +    } +  }; +}]; + + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-list + * + * @description + * Text input that converts between comma-seperated string into an array of strings. + * + * @element input + * @param {string=} ng-list optional delimiter that should be used to split the value. If + *   specified in form `/something/` then the value will be converted into a regular expression. + * + * @example +    <doc:example> +      <doc:source> +       <script> +         function Ctrl($scope) { +           $scope.names = ['igor', 'misko', 'vojta']; +         } +       </script> +       <form name="myForm" ng-controller="Ctrl"> +         List: <input name="namesInput" ng-model="names" ng-list required> +         <span class="error" ng-show="myForm.list.$error.required"> +           Required!</span> +         <tt>names = {{names}}</tt><br/> +         <tt>myForm.namesInput.$valid = {{myForm.namesInput.$valid}}</tt><br/> +         <tt>myForm.namesInput.$error = {{myForm.namesInput.$error}}</tt><br/> +         <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> +         <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> +        </form> +      </doc:source> +      <doc:scenario> +        it('should initialize to model', function() { +          expect(binding('names')).toEqual('["igor","misko","vojta"]'); +          expect(binding('myForm.namesInput.$valid')).toEqual('true'); +        }); + +        it('should be invalid if empty', function() { +          input('names').enter(''); +          expect(binding('names')).toEqual('[]'); +          expect(binding('myForm.namesInput.$valid')).toEqual('false'); +        }); +      </doc:scenario> +    </doc:example> + */ +var ngListDirective = function() { +  return { +    require: 'ngModel', +    link: function(scope, element, attr, ctrl) { +      var match = /\/(.*)\//.exec(attr.ngList), +          separator = match && new RegExp(match[1]) || attr.ngList || ','; + +      var parse = function(viewValue) { +        var list = []; + +        if (viewValue) { +          forEach(viewValue.split(separator), function(value) { +            if (value) list.push(trim(value)); +          }); +        } + +        return list; +      }; + +      ctrl.$parsers.push(parse); +      ctrl.$formatters.push(function(value) { +        if (isArray(value) && !equals(parse(ctrl.$viewValue), value)) { +          return value.join(', '); +        } + +        return undefined; +      }); +    } +  }; +}; + + +var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; + +var ngValueDirective = [function() { +  return { +    priority: 100, +    compile: function(tpl, attr) { +      if (CONSTANT_VALUE_REGEXP.test(attr.ngValue)) { +        return function(scope) { +          attr.$set('value', scope.$eval(attr.ngValue)); +        }; +      } else { +        attr.$observers.value = []; + +        return function(scope) { +          scope.$watch(attr.ngValue, function(value) { +            attr.$set('value', value, false); +          }); +        }; +      } +    } +  }; +}]; diff --git a/src/ng/directive/ngBind.js b/src/ng/directive/ngBind.js new file mode 100644 index 00000000..32be2f4b --- /dev/null +++ b/src/ng/directive/ngBind.js @@ -0,0 +1,155 @@ +'use strict'; + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-bind + * + * @description + * The `ng-bind` attribute tells Angular to replace the text content of the specified HTML element + * with the value of a given expression, and to update the text content when the value of that + * expression changes. + * + * Typically, you don't use `ng-bind` directly, but instead you use the double curly markup like + * `{{ expression }}` and let the Angular compiler transform it to + * `<span ng-bind="expression"></span>` when the template is compiled. + * + * @element ANY + * @param {expression} ng-bind {@link guide/dev_guide.expressions Expression} to evaluate. + * + * @example + * Enter a name in the Live Preview text box; the greeting below the text box changes instantly. +   <doc:example> +     <doc:source> +       <script> +         function Ctrl($scope) { +           $scope.name = 'Whirled'; +         } +       </script> +       <div ng-controller="Ctrl"> +         Enter name: <input type="text" ng-model="name" ng-model-instant><br> +         Hello <span ng-bind="name"></span>! +       </div> +     </doc:source> +     <doc:scenario> +       it('should check ng-bind', function() { +         expect(using('.doc-example-live').binding('name')).toBe('Whirled'); +         using('.doc-example-live').input('name').enter('world'); +         expect(using('.doc-example-live').binding('name')).toBe('world'); +       }); +     </doc:scenario> +   </doc:example> + */ +var ngBindDirective = ngDirective(function(scope, element, attr) { +  element.addClass('ng-binding').data('$binding', attr.ngBind); +  scope.$watch(attr.ngBind, function(value) { +    element.text(value == undefined ? '' : value); +  }); +}); + + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-bind-html-unsafe + * + * @description + * Creates a binding that will innerHTML the result of evaluating the `expression` into the current + * element. *The innerHTML-ed content will not be sanitized!* You should use this directive only if + * {@link angular.module.ng.$compileProvider.directive.ng-bind-html ng-bind-html} directive is too + * restrictive and when you absolutely trust the source of the content you are binding to. + * + * See {@link angular.module.ng.$sanitize $sanitize} docs for examples. + * + * @element ANY + * @param {expression} ng-bind-html-unsafe {@link guide/dev_guide.expressions Expression} to evaluate. + */ +var ngBindHtmlUnsafeDirective = ngDirective(function(scope, element, attr) { +  element.addClass('ng-binding').data('$binding', attr.ngBindHtmlUnsafe); +  scope.$watch(attr.ngBindHtmlUnsafe, function(value) { +    element.html(value == undefined ? '' : value); +  }); +}); + + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-bind-html + * + * @description + * Creates a binding that will sanitize the result of evaluating the `expression` with the + * {@link angular.module.ng.$sanitize $sanitize} service and innerHTML the result into the current + * element. + * + * See {@link angular.module.ng.$sanitize $sanitize} docs for examples. + * + * @element ANY + * @param {expression} ng-bind-html {@link guide/dev_guide.expressions Expression} to evaluate. + */ +var ngBindHtmlDirective = ['$sanitize', function($sanitize) { +  return function(scope, element, attr) { +    element.addClass('ng-binding').data('$binding', attr.ngBindHtml); +    scope.$watch(attr.ngBindHtml, function(value) { +      if (value = $sanitize(value)) { +        element.html(value); +      } +    }); +  } +}]; + + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-bind-template + * + * @description + * The `ng-bind-template` attribute specifies that the element + * text should be replaced with the template in ng-bind-template. + * Unlike ng-bind the ng-bind-template can contain multiple `{{` `}}` + * expressions. (This is required since some HTML elements + * can not have SPAN elements such as TITLE, or OPTION to name a few.) + * + * @element ANY + * @param {string} ng-bind-template template of form + *   <tt>{{</tt> <tt>expression</tt> <tt>}}</tt> to eval. + * + * @example + * Try it here: enter text in text box and watch the greeting change. +   <doc:example> +     <doc:source> +       <script> +         function Ctrl($scope) { +           $scope.salutation = 'Hello'; +           $scope.name = 'World'; +         } +       </script> +       <div ng-controller="Ctrl"> +        Salutation: <input type="text" ng-model="salutation" ng-model-instant><br> +        Name: <input type="text" ng-model="name" ng-model-instant><br> +        <pre ng-bind-template="{{salutation}} {{name}}!"></pre> +       </div> +     </doc:source> +     <doc:scenario> +       it('should check ng-bind', function() { +         expect(using('.doc-example-live').binding('salutation')). +           toBe('Hello'); +         expect(using('.doc-example-live').binding('name')). +           toBe('World'); +         using('.doc-example-live').input('salutation').enter('Greetings'); +         using('.doc-example-live').input('name').enter('user'); +         expect(using('.doc-example-live').binding('salutation')). +           toBe('Greetings'); +         expect(using('.doc-example-live').binding('name')). +           toBe('user'); +       }); +     </doc:scenario> +   </doc:example> + */ +var ngBindTemplateDirective = ['$interpolate', function($interpolate) { +  return function(scope, element, attr) { +    // TODO: move this to scenario runner +    var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate)); +    element.addClass('ng-binding').data('$binding', interpolateFn); +    attr.$observe('ngBindTemplate', function(value) { +      element.text(value); +    }); +  } +}]; diff --git a/src/ng/directive/ngClass.js b/src/ng/directive/ngClass.js new file mode 100644 index 00000000..21b75dd0 --- /dev/null +++ b/src/ng/directive/ngClass.js @@ -0,0 +1,143 @@ +'use strict'; + +function classDirective(name, selector) { +  name = 'ngClass' + name; +  return ngDirective(function(scope, element, attr) { +    scope.$watch(attr[name], function(newVal, oldVal) { +      if (selector === true || scope.$index % 2 === selector) { +        if (oldVal && (newVal !== oldVal)) { +           if (isObject(oldVal) && !isArray(oldVal)) +             oldVal = map(oldVal, function(v, k) { if (v) return k }); +           element.removeClass(isArray(oldVal) ? oldVal.join(' ') : oldVal); +         } +         if (isObject(newVal) && !isArray(newVal)) +            newVal = map(newVal, function(v, k) { if (v) return k }); +         if (newVal) element.addClass(isArray(newVal) ? newVal.join(' ') : newVal);      } +    }, true); +  }); +} + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-class + * + * @description + * The `ng-class` allows you to set CSS class on HTML element dynamically by databinding an + * expression that represents all classes to be added. + * + * The directive won't add duplicate classes if a particular class was already set. + * + * When the expression changes, the previously added classes are removed and only then the classes + * new classes are added. + * + * @element ANY + * @param {expression} ng-class {@link guide/dev_guide.expressions Expression} to eval. The result + *   of the evaluation can be a string representing space delimited class + *   names, an array, or a map of class names to boolean values. + * + * @example +   <doc:example> +     <doc:source> +      <input type="button" value="set" ng-click="myVar='ng-invalid'"> +      <input type="button" value="clear" ng-click="myVar=''"> +      <br> +      <span ng-class="myVar">Sample Text     </span> +     </doc:source> +     <doc:scenario> +       it('should check ng-class', function() { +         expect(element('.doc-example-live span').prop('className')).not(). +           toMatch(/ng-invalid/); + +         using('.doc-example-live').element(':button:first').click(); + +         expect(element('.doc-example-live span').prop('className')). +           toMatch(/ng-invalid/); + +         using('.doc-example-live').element(':button:last').click(); + +         expect(element('.doc-example-live span').prop('className')).not(). +           toMatch(/ng-invalid/); +       }); +     </doc:scenario> +   </doc:example> + */ +var ngClassDirective = classDirective('', true); + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-class-odd + * + * @description + * The `ng-class-odd` and `ng-class-even` works exactly as + * {@link angular.module.ng.$compileProvider.directive.ng-class ng-class}, except it works in conjunction with `ng-repeat` and + * takes affect only on odd (even) rows. + * + * This directive can be applied only within a scope of an + * {@link angular.module.ng.$compileProvider.directive.ng-repeat ng-repeat}. + * + * @element ANY + * @param {expression} ng-class-odd {@link guide/dev_guide.expressions Expression} to eval. The result + *   of the evaluation can be a string representing space delimited class names or an array. + * + * @example +   <doc:example> +     <doc:source> +        <ol ng-init="names=['John', 'Mary', 'Cate', 'Suz']"> +          <li ng-repeat="name in names"> +           <span ng-class-odd="'ng-format-negative'" +                 ng-class-even="'ng-invalid'"> +             {{name}}       +           </span> +          </li> +        </ol> +     </doc:source> +     <doc:scenario> +       it('should check ng-class-odd and ng-class-even', function() { +         expect(element('.doc-example-live li:first span').prop('className')). +           toMatch(/ng-format-negative/); +         expect(element('.doc-example-live li:last span').prop('className')). +           toMatch(/ng-invalid/); +       }); +     </doc:scenario> +   </doc:example> + */ +var ngClassOddDirective = classDirective('Odd', 0); + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-class-even + * + * @description + * The `ng-class-odd` and `ng-class-even` works exactly as + * {@link angular.module.ng.$compileProvider.directive.ng-class ng-class}, except it works in + * conjunction with `ng-repeat` and takes affect only on odd (even) rows. + * + * This directive can be applied only within a scope of an + * {@link angular.module.ng.$compileProvider.directive.ng-repeat ng-repeat}. + * + * @element ANY + * @param {expression} ng-class-even {@link guide/dev_guide.expressions Expression} to eval. The + *   result of the evaluation can be a string representing space delimited class names or an array. + * + * @example +   <doc:example> +     <doc:source> +        <ol ng-init="names=['John', 'Mary', 'Cate', 'Suz']"> +          <li ng-repeat="name in names"> +           <span ng-class-odd="'odd'" ng-class-even="'even'"> +             {{name}}       +           </span> +          </li> +        </ol> +     </doc:source> +     <doc:scenario> +       it('should check ng-class-odd and ng-class-even', function() { +         expect(element('.doc-example-live li:first span').prop('className')). +           toMatch(/odd/); +         expect(element('.doc-example-live li:last span').prop('className')). +           toMatch(/even/); +       }); +     </doc:scenario> +   </doc:example> + */ +var ngClassEvenDirective = classDirective('Even', 1); diff --git a/src/ng/directive/ngCloak.js b/src/ng/directive/ngCloak.js new file mode 100644 index 00000000..7422e15a --- /dev/null +++ b/src/ng/directive/ngCloak.js @@ -0,0 +1,61 @@ +'use strict'; + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-cloak + * + * @description + * The `ng-cloak` directive is used to prevent the Angular html template from being briefly + * displayed by the browser in its raw (uncompiled) form while your application is loading. Use this + * directive to avoid the undesirable flicker effect caused by the html template display. + * + * The directive can be applied to the `<body>` element, but typically a fine-grained application is + * prefered in order to benefit from progressive rendering of the browser view. + * + * `ng-cloak` works in cooperation with a css rule that is embedded within `angular.js` and + *  `angular.min.js` files. Following is the css rule: + * + * <pre> + * [ng\:cloak], .ng-cloak { + *   display: none; + * } + * </pre> + * + * When this css rule is loaded by the browser, all html elements (including their children) that + * are tagged with the `ng-cloak` directive are hidden. When Angular comes across this directive + * during the compilation of the template it deletes the `ng-cloak` element attribute, which + * makes the compiled element visible. + * + * For the best result, `angular.js` script must be loaded in the head section of the html file; + * alternatively, the css rule (above) must be included in the external stylesheet of the + * application. + * + * Legacy browsers, like IE7, do not provide attribute selector support (added in CSS 2.1) so they + * cannot match the `[ng\:cloak]` selector. To work around this limitation, you must add the css + * class `ng-cloak` in addition to `ng-cloak` directive as shown in the example below. + * + * @element ANY + * + * @example +   <doc:example> +     <doc:source> +        <div id="template1" ng-cloak>{{ 'hello' }}</div> +        <div id="template2" ng-cloak class="ng-cloak">{{ 'hello IE7' }}</div> +     </doc:source> +     <doc:scenario> +       it('should remove the template directive and css class', function() { +         expect(element('.doc-example-live #template1').attr('ng-cloak')). +           not().toBeDefined(); +         expect(element('.doc-example-live #template2').attr('ng-cloak')). +           not().toBeDefined(); +       }); +     </doc:scenario> +   </doc:example> + * + */ +var ngCloakDirective = ngDirective({ +  compile: function(element, attr) { +    attr.$set('ngCloak', undefined); +    element.removeClass('ng-cloak'); +  } +}); diff --git a/src/ng/directive/ngController.js b/src/ng/directive/ngController.js new file mode 100644 index 00000000..8e2c6f3b --- /dev/null +++ b/src/ng/directive/ngController.js @@ -0,0 +1,103 @@ +'use strict'; + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-controller + * + * @description + * The `ng-controller` directive assigns behavior to a scope. This is a key aspect of how angular + * supports the principles behind the Model-View-Controller design pattern. + * + * MVC components in angular: + * + * * Model — The Model is data in scope properties; scopes are attached to the DOM. + * * View — The template (HTML with data bindings) is rendered into the View. + * * Controller — The `ng-controller` directive specifies a Controller class; the class has + *   methods that typically express the business logic behind the application. + * + * Note that an alternative way to define controllers is via the `{@link angular.module.ng.$route}` + * service. + * + * @element ANY + * @scope + * @param {expression} ng-controller Name of a globally accessible constructor function or an + *     {@link guide/dev_guide.expressions expression} that on the current scope evaluates to a + *     constructor function. + * + * @example + * Here is a simple form for editing user contact information. Adding, removing, clearing, and + * greeting are methods declared on the controller (see source tab). These methods can + * easily be called from the angular markup. Notice that the scope becomes the `this` for the + * controller's instance. This allows for easy access to the view data from the controller. Also + * notice that any changes to the data are automatically reflected in the View without the need + * for a manual update. +   <doc:example> +     <doc:source> +      <script type="text/javascript"> +        function SettingsController($scope) { +          $scope.name = "John Smith"; +          $scope.contacts = [ +            {type:'phone', value:'408 555 1212'}, +            {type:'email', value:'john.smith@example.org'} ]; + +          $scope.greet = function() { +           alert(this.name); +          }; + +          $scope.addContact = function() { +           this.contacts.push({type:'email', value:'yourname@example.org'}); +          }; + +          $scope.removeContact = function(contactToRemove) { +           var index = this.contacts.indexOf(contactToRemove); +           this.contacts.splice(index, 1); +          }; + +          $scope.clearContact = function(contact) { +           contact.type = 'phone'; +           contact.value = ''; +          }; +        } +      </script> +      <div ng-controller="SettingsController"> +        Name: <input type="text" ng-model="name"/> +        [ <a href="" ng-click="greet()">greet</a> ]<br/> +        Contact: +        <ul> +          <li ng-repeat="contact in contacts"> +            <select ng-model="contact.type"> +               <option>phone</option> +               <option>email</option> +            </select> +            <input type="text" ng-model="contact.value"/> +            [ <a href="" ng-click="clearContact(contact)">clear</a> +            | <a href="" ng-click="removeContact(contact)">X</a> ] +          </li> +          <li>[ <a href="" ng-click="addContact()">add</a> ]</li> +       </ul> +      </div> +     </doc:source> +     <doc:scenario> +       it('should check controller', function() { +         expect(element('.doc-example-live div>:input').val()).toBe('John Smith'); +         expect(element('.doc-example-live li:nth-child(1) input').val()) +           .toBe('408 555 1212'); +         expect(element('.doc-example-live li:nth-child(2) input').val()) +           .toBe('john.smith@example.org'); + +         element('.doc-example-live li:first a:contains("clear")').click(); +         expect(element('.doc-example-live li:first input').val()).toBe(''); + +         element('.doc-example-live li:last a:contains("add")').click(); +         expect(element('.doc-example-live li:nth-child(3) input').val()) +           .toBe('yourname@example.org'); +       }); +     </doc:scenario> +   </doc:example> + */ +var ngControllerDirective = [function() { +  return { +    scope: true, +    controller: '@' +  }; +}]; diff --git a/src/ng/directive/ngEventDirs.js b/src/ng/directive/ngEventDirs.js new file mode 100644 index 00000000..74028fee --- /dev/null +++ b/src/ng/directive/ngEventDirs.js @@ -0,0 +1,222 @@ +'use strict'; + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-click + * + * @description + * The ng-click allows you to specify custom behavior when + * element is clicked. + * + * @element ANY + * @param {expression} ng-click {@link guide/dev_guide.expressions Expression} to evaluate upon + * click. (Event object is available as `$event`) + * + * @example +   <doc:example> +     <doc:source> +      <button ng-click="count = count + 1" ng-init="count=0"> +        Increment +      </button> +      count: {{count}} +     </doc:source> +     <doc:scenario> +       it('should check ng-click', function() { +         expect(binding('count')).toBe('0'); +         element('.doc-example-live :button').click(); +         expect(binding('count')).toBe('1'); +       }); +     </doc:scenario> +   </doc:example> + */ +/* + * A directive that allows creation of custom onclick handlers that are defined as angular + * expressions and are compiled and executed within the current scope. + * + * Events that are handled via these handler are always configured not to propagate further. + */ +var ngEventDirectives = {}; +forEach( +  'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave'.split(' '), +  function(name) { +    var directiveName = directiveNormalize('ng-' + name); +    ngEventDirectives[directiveName] = ['$parse', function($parse) { +      return function(scope, element, attr) { +        var fn = $parse(attr[directiveName]); +        element.bind(lowercase(name), function(event) { +          scope.$apply(function() { +            fn(scope, {$event:event}); +          }); +        }); +      }; +    }]; +  } +); + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-dblclick + * + * @description + * The ng-dblclick allows you to specify custom behavior on dblclick event. + * + * @element ANY + * @param {expression} ng-dblclick {@link guide/dev_guide.expressions Expression} to evaluate upon + * dblclick. (Event object is available as `$event`) + * + * @example + * See {@link angular.module.ng.$compileProvider.directive.ng-click ng-click} + */ + + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-mousedown + * + * @description + * The ng-mousedown allows you to specify custom behavior on mousedown event. + * + * @element ANY + * @param {expression} ng-mousedown {@link guide/dev_guide.expressions Expression} to evaluate upon + * mousedown. (Event object is available as `$event`) + * + * @example + * See {@link angular.module.ng.$compileProvider.directive.ng-click ng-click} + */ + + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-mouseup + * + * @description + * Specify custom behavior on mouseup event. + * + * @element ANY + * @param {expression} ng-mouseup {@link guide/dev_guide.expressions Expression} to evaluate upon + * mouseup. (Event object is available as `$event`) + * + * @example + * See {@link angular.module.ng.$compileProvider.directive.ng-click ng-click} + */ + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-mouseover + * + * @description + * Specify custom behavior on mouseover event. + * + * @element ANY + * @param {expression} ng-mouseover {@link guide/dev_guide.expressions Expression} to evaluate upon + * mouseover. (Event object is available as `$event`) + * + * @example + * See {@link angular.module.ng.$compileProvider.directive.ng-click ng-click} + */ + + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-mouseenter + * + * @description + * Specify custom behavior on mouseenter event. + * + * @element ANY + * @param {expression} ng-mouseenter {@link guide/dev_guide.expressions Expression} to evaluate upon + * mouseenter. (Event object is available as `$event`) + * + * @example + * See {@link angular.module.ng.$compileProvider.directive.ng-click ng-click} + */ + + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-mouseleave + * + * @description + * Specify custom behavior on mouseleave event. + * + * @element ANY + * @param {expression} ng-mouseleave {@link guide/dev_guide.expressions Expression} to evaluate upon + * mouseleave. (Event object is available as `$event`) + * + * @example + * See {@link angular.module.ng.$compileProvider.directive.ng-click ng-click} + */ + + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-mousemove + * + * @description + * Specify custom behavior on mousemove event. + * + * @element ANY + * @param {expression} ng-mousemove {@link guide/dev_guide.expressions Expression} to evaluate upon + * mousemove. (Event object is available as `$event`) + * + * @example + * See {@link angular.module.ng.$compileProvider.directive.ng-click ng-click} + */ + + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-submit + * + * @description + * Enables binding angular expressions to onsubmit events. + * + * Additionally it prevents the default action (which for form means sending the request to the + * server and reloading the current page). + * + * @element form + * @param {expression} ng-submit {@link guide/dev_guide.expressions Expression} to eval. + * + * @example +   <doc:example> +     <doc:source> +      <script> +        function Ctrl($scope) { +          $scope.list = []; +          $scope.text = 'hello'; +          $scope.submit = function() { +            if (this.text) { +              this.list.push(this.text); +              this.text = ''; +            } +          }; +        } +      </script> +      <form ng-submit="submit()" ng-controller="Ctrl"> +        Enter text and hit enter: +        <input type="text" ng-model="text" name="text" /> +        <input type="submit" id="submit" value="Submit" /> +        <pre>list={{list}}</pre> +      </form> +     </doc:source> +     <doc:scenario> +       it('should check ng-submit', function() { +         expect(binding('list')).toBe('[]'); +         element('.doc-example-live #submit').click(); +         expect(binding('list')).toBe('["hello"]'); +         expect(input('text').val()).toBe(''); +       }); +       it('should ignore empty strings', function() { +         expect(binding('list')).toBe('[]'); +         element('.doc-example-live #submit').click(); +         element('.doc-example-live #submit').click(); +         expect(binding('list')).toBe('["hello"]'); +       }); +     </doc:scenario> +   </doc:example> + */ +var ngSubmitDirective = ngDirective(function(scope, element, attrs) { +  element.bind('submit', function() { +    scope.$apply(attrs.ngSubmit); +  }); +}); diff --git a/src/ng/directive/ngInclude.js b/src/ng/directive/ngInclude.js new file mode 100644 index 00000000..90fd0b40 --- /dev/null +++ b/src/ng/directive/ngInclude.js @@ -0,0 +1,131 @@ +'use strict'; + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-include + * @restrict EA + * + * @description + * Fetches, compiles and includes an external HTML fragment. + * + * Keep in mind that Same Origin Policy applies to included resources + * (e.g. ng-include won't work for file:// access). + * + * @scope + * + * @param {string} ng-include|src angular expression evaluating to URL. If the source is a string constant, + *                 make sure you wrap it in quotes, e.g. `src="'myPartialTemplate.html'"`. + * @param {Scope=} [scope=new_child_scope] optional expression which evaluates to an + *                 instance of angular.module.ng.$rootScope.Scope to set the HTML fragment to. + * @param {string=} onload Expression to evaluate when a new partial is loaded. + * + * @param {string=} autoscroll Whether `ng-include` should call {@link angular.module.ng.$anchorScroll + *                  $anchorScroll} to scroll the viewport after the content is loaded. + * + *                  - If the attribute is not set, disable scrolling. + *                  - If the attribute is set without value, enable scrolling. + *                  - Otherwise enable scrolling only if the expression evaluates to truthy value. + * + * @example +    <doc:example> +      <doc:source jsfiddle="false"> +       <script> +         function Ctrl($scope) { +           $scope.templates = +             [ { name: 'template1.html', url: 'examples/ng-include/template1.html'} +             , { name: 'template2.html', url: 'examples/ng-include/template2.html'} ]; +           $scope.template = $scope.templates[0]; +         } +       </script> +       <div ng-controller="Ctrl"> +         <select ng-model="template" ng-options="t.name for t in templates"> +          <option value="">(blank)</option> +         </select> +         url of the template: <tt><a href="{{template.url}}">{{template.url}}</a></tt> +         <hr/> +         <div ng-include src="template.url"></div> +       </div> +      </doc:source> +      <doc:scenario> +        it('should load template1.html', function() { +         expect(element('.doc-example-live [ng-include]').text()). +           toBe('Content of template1.html\n'); +        }); +        it('should load template2.html', function() { +         select('template').option('1'); +         expect(element('.doc-example-live [ng-include]').text()). +           toBe('Content of template2.html\n'); +        }); +        it('should change to blank', function() { +         select('template').option(''); +         expect(element('.doc-example-live [ng-include]').text()).toEqual(''); +        }); +      </doc:scenario> +    </doc:example> + */ + + +/** + * @ngdoc event + * @name angular.module.ng.$compileProvider.directive.ng-include#$includeContentLoaded + * @eventOf angular.module.ng.$compileProvider.directive.ng-include + * @eventType emit on the current ng-include scope + * @description + * Emitted every time the ng-include content is reloaded. + */ +var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile', +                  function($http,   $templateCache,   $anchorScroll,   $compile) { +  return { +    restrict: 'EA', +    compile: function(element, attr) { +      var srcExp = attr.ngInclude  || attr.src, +          scopeExp = attr.scope || '', +          onloadExp = attr.onload || '', +          autoScrollExp = attr.autoscroll; + +      return function(scope, element, attr) { +        var changeCounter = 0, +            childScope; + +        function incrementChange() { changeCounter++;} +        scope.$watch(srcExp, incrementChange); +        scope.$watch(function() { +          var includeScope = scope.$eval(scopeExp); +          if (includeScope) return includeScope.$id; +        }, incrementChange); +        scope.$watch(function() {return changeCounter;}, function(newChangeCounter) { +           var src = scope.$eval(srcExp), +               useScope = scope.$eval(scopeExp); + +          function clearContent() { +            // if this callback is still desired +            if (newChangeCounter === changeCounter) { +              if (childScope) childScope.$destroy(); +              childScope = null; +              element.html(''); +            } +          } + +           if (src) { +             $http.get(src, {cache: $templateCache}).success(function(response) { +               // if this callback is still desired +               if (newChangeCounter === changeCounter) { +                 element.html(response); +                 if (childScope) childScope.$destroy(); +                 childScope = useScope ? useScope : scope.$new(); +                 $compile(element.contents())(childScope); +                 if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) { +                   $anchorScroll(); +                 } +                 scope.$emit('$includeContentLoaded'); +                 scope.$eval(onloadExp); +               } +             }).error(clearContent); +           } else { +             clearContent(); +           } +        }); +      }; +    } +  } +}]; diff --git a/src/ng/directive/ngInit.js b/src/ng/directive/ngInit.js new file mode 100644 index 00000000..cbd1b3ed --- /dev/null +++ b/src/ng/directive/ngInit.js @@ -0,0 +1,37 @@ +'use strict'; + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-init + * + * @description + * The `ng-init` attribute specifies initialization tasks to be executed + *  before the template enters execution mode during bootstrap. + * + * @element ANY + * @param {expression} ng-init {@link guide/dev_guide.expressions Expression} to eval. + * + * @example +   <doc:example> +     <doc:source> +    <div ng-init="greeting='Hello'; person='World'"> +      {{greeting}} {{person}}! +    </div> +     </doc:source> +     <doc:scenario> +       it('should check greeting', function() { +         expect(binding('greeting')).toBe('Hello'); +         expect(binding('person')).toBe('World'); +       }); +     </doc:scenario> +   </doc:example> + */ +var ngInitDirective = ngDirective({ +  compile: function() { +    return { +      pre: function(scope, element, attrs) { +        scope.$eval(attrs.ngInit); +      } +    } +  } +}); diff --git a/src/ng/directive/ngNonBindable.js b/src/ng/directive/ngNonBindable.js new file mode 100644 index 00000000..2e9faa5a --- /dev/null +++ b/src/ng/directive/ngNonBindable.js @@ -0,0 +1,33 @@ +'use strict'; + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-non-bindable + * @priority 1000 + * + * @description + * Sometimes it is necessary to write code which looks like bindings but which should be left alone + * by angular. Use `ng-non-bindable` to make angular ignore a chunk of HTML. + * + * @element ANY + * + * @example + * In this example there are two location where a simple binding (`{{}}`) is present, but the one + * wrapped in `ng-non-bindable` is left alone. + * + * @example +    <doc:example> +      <doc:source> +        <div>Normal: {{1 + 2}}</div> +        <div ng-non-bindable>Ignored: {{1 + 2}}</div> +      </doc:source> +      <doc:scenario> +       it('should check ng-non-bindable', function() { +         expect(using('.doc-example-live').binding('1 + 2')).toBe('3'); +         expect(using('.doc-example-live').element('div:last').text()). +           toMatch(/1 \+ 2/); +       }); +      </doc:scenario> +    </doc:example> + */ +var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); diff --git a/src/ng/directive/ngPluralize.js b/src/ng/directive/ngPluralize.js new file mode 100644 index 00000000..a8cc40a6 --- /dev/null +++ b/src/ng/directive/ngPluralize.js @@ -0,0 +1,204 @@ +'use strict'; + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-pluralize + * @restrict EA + * + * @description + * # Overview + * ng-pluralize is a directive that displays messages according to en-US localization rules. + * These rules are bundled with angular.js and the rules can be overridden + * (see {@link guide/dev_guide.i18n Angular i18n} dev guide). You configure ng-pluralize by + * specifying the mappings between + * {@link http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html + * plural categories} and the strings to be displayed. + * + * # Plural categories and explicit number rules + * There are two + * {@link http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html + * plural categories} in Angular's default en-US locale: "one" and "other". + * + * While a pural category may match many numbers (for example, in en-US locale, "other" can match + * any number that is not 1), an explicit number rule can only match one number. For example, the + * explicit number rule for "3" matches the number 3. You will see the use of plural categories + * and explicit number rules throughout later parts of this documentation. + * + * # Configuring ng-pluralize + * You configure ng-pluralize by providing 2 attributes: `count` and `when`. + * You can also provide an optional attribute, `offset`. + * + * The value of the `count` attribute can be either a string or an {@link guide/dev_guide.expressions + * Angular expression}; these are evaluated on the current scope for its binded value. + * + * The `when` attribute specifies the mappings between plural categories and the actual + * string to be displayed. The value of the attribute should be a JSON object so that Angular + * can interpret it correctly. + * + * The following example shows how to configure ng-pluralize: + * + * <pre> + * <ng-pluralize count="personCount" +                 when="{'0': 'Nobody is viewing.', + *                      'one': '1 person is viewing.', + *                      'other': '{} people are viewing.'}"> + * </ng-pluralize> + *</pre> + * + * In the example, `"0: Nobody is viewing."` is an explicit number rule. If you did not + * specify this rule, 0 would be matched to the "other" category and "0 people are viewing" + * would be shown instead of "Nobody is viewing". You can specify an explicit number rule for + * other numbers, for example 12, so that instead of showing "12 people are viewing", you can + * show "a dozen people are viewing". + * + * You can use a set of closed braces(`{}`) as a placeholder for the number that you want substituted + * into pluralized strings. In the previous example, Angular will replace `{}` with + * <span ng-non-bindable>`{{personCount}}`</span>. The closed braces `{}` is a placeholder + * for <span ng-non-bindable>{{numberExpression}}</span>. + * + * # Configuring ng-pluralize with offset + * The `offset` attribute allows further customization of pluralized text, which can result in + * a better user experience. For example, instead of the message "4 people are viewing this document", + * you might display "John, Kate and 2 others are viewing this document". + * The offset attribute allows you to offset a number by any desired value. + * Let's take a look at an example: + * + * <pre> + * <ng-pluralize count="personCount" offset=2 + *               when="{'0': 'Nobody is viewing.', + *                      '1': '{{person1}} is viewing.', + *                      '2': '{{person1}} and {{person2}} are viewing.', + *                      'one': '{{person1}}, {{person2}} and one other person are viewing.', + *                      'other': '{{person1}}, {{person2}} and {} other people are viewing.'}"> + * </ng-pluralize> + * </pre> + * + * Notice that we are still using two plural categories(one, other), but we added + * three explicit number rules 0, 1 and 2. + * When one person, perhaps John, views the document, "John is viewing" will be shown. + * When three people view the document, no explicit number rule is found, so + * an offset of 2 is taken off 3, and Angular uses 1 to decide the plural category. + * In this case, plural category 'one' is matched and "John, Marry and one other person are viewing" + * is shown. + * + * Note that when you specify offsets, you must provide explicit number rules for + * numbers from 0 up to and including the offset. If you use an offset of 3, for example, + * you must provide explicit number rules for 0, 1, 2 and 3. You must also provide plural strings for + * plural categories "one" and "other". + * + * @param {string|expression} count The variable to be bounded to. + * @param {string} when The mapping between plural category to its correspoding strings. + * @param {number=} offset Offset to deduct from the total number. + * + * @example +    <doc:example> +      <doc:source> +        <script> +          function Ctrl($scope) { +            $scope.person1 = 'Igor'; +            $scope.person2 = 'Misko'; +            $scope.personCount = 1; +          } +        </script> +        <div ng-controller="Ctrl"> +          Person 1:<input type="text" ng-model="person1" value="Igor" /><br/> +          Person 2:<input type="text" ng-model="person2" value="Misko" /><br/> +          Number of People:<input type="text" ng-model="personCount" value="1" /><br/> + +          <!--- Example with simple pluralization rules for en locale ---> +          Without Offset: +          <ng-pluralize count="personCount" +                        when="{'0': 'Nobody is viewing.', +                               'one': '1 person is viewing.', +                               'other': '{} people are viewing.'}"> +          </ng-pluralize><br> + +          <!--- Example with offset ---> +          With Offset(2): +          <ng-pluralize count="personCount" offset=2 +                        when="{'0': 'Nobody is viewing.', +                               '1': '{{person1}} is viewing.', +                               '2': '{{person1}} and {{person2}} are viewing.', +                               'one': '{{person1}}, {{person2}} and one other person are viewing.', +                               'other': '{{person1}}, {{person2}} and {} other people are viewing.'}"> +          </ng-pluralize> +        </div> +      </doc:source> +      <doc:scenario> +        it('should show correct pluralized string', function() { +          expect(element('.doc-example-live ng-pluralize:first').text()). +                                             toBe('1 person is viewing.'); +          expect(element('.doc-example-live ng-pluralize:last').text()). +                                                toBe('Igor is viewing.'); + +          using('.doc-example-live').input('personCount').enter('0'); +          expect(element('.doc-example-live ng-pluralize:first').text()). +                                               toBe('Nobody is viewing.'); +          expect(element('.doc-example-live ng-pluralize:last').text()). +                                              toBe('Nobody is viewing.'); + +          using('.doc-example-live').input('personCount').enter('2'); +          expect(element('.doc-example-live ng-pluralize:first').text()). +                                            toBe('2 people are viewing.'); +          expect(element('.doc-example-live ng-pluralize:last').text()). +                              toBe('Igor and Misko are viewing.'); + +          using('.doc-example-live').input('personCount').enter('3'); +          expect(element('.doc-example-live ng-pluralize:first').text()). +                                            toBe('3 people are viewing.'); +          expect(element('.doc-example-live ng-pluralize:last').text()). +                              toBe('Igor, Misko and one other person are viewing.'); + +          using('.doc-example-live').input('personCount').enter('4'); +          expect(element('.doc-example-live ng-pluralize:first').text()). +                                            toBe('4 people are viewing.'); +          expect(element('.doc-example-live ng-pluralize:last').text()). +                              toBe('Igor, Misko and 2 other people are viewing.'); +        }); + +        it('should show data-binded names', function() { +          using('.doc-example-live').input('personCount').enter('4'); +          expect(element('.doc-example-live ng-pluralize:last').text()). +              toBe('Igor, Misko and 2 other people are viewing.'); + +          using('.doc-example-live').input('person1').enter('Di'); +          using('.doc-example-live').input('person2').enter('Vojta'); +          expect(element('.doc-example-live ng-pluralize:last').text()). +              toBe('Di, Vojta and 2 other people are viewing.'); +        }); +      </doc:scenario> +    </doc:example> + */ +var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interpolate) { +  var BRACE = /{}/g; +  return { +    restrict: 'EA', +    link: function(scope, element, attr) { +      var numberExp = attr.count, +          whenExp = element.attr(attr.$attr.when), // this is because we have {{}} in attrs +          offset = attr.offset || 0, +          whens = scope.$eval(whenExp), +          whensExpFns = {}; + +      forEach(whens, function(expression, key) { +        whensExpFns[key] = +          $interpolate(expression.replace(BRACE, '{{' + numberExp + '-' + offset + '}}')); +      }); + +      scope.$watch(function() { +        var value = parseFloat(scope.$eval(numberExp)); + +        if (!isNaN(value)) { +          //if explicit number rule such as 1, 2, 3... is defined, just use it. Otherwise, +          //check it against pluralization rules in $locale service +          if (!whens[value]) value = $locale.pluralCat(value - offset); +           return whensExpFns[value](scope, element, true); +        } else { +          return ''; +        } +      }, function(newVal) { +        element.text(newVal); +      }); +    } +  }; +}]; diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js new file mode 100644 index 00000000..82f8b9c7 --- /dev/null +++ b/src/ng/directive/ngRepeat.js @@ -0,0 +1,181 @@ +'use strict'; + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-repeat + * + * @description + * The `ng-repeat` directive instantiates a template once per item from a collection. Each template + * instance gets its own scope, where the given loop variable is set to the current collection item, + * and `$index` is set to the item index or key. + * + * Special properties are exposed on the local scope of each template instance, including: + * + *   * `$index` – `{number}` – iterator offset of the repeated element (0..length-1) + *   * `$position` – `{string}` – position of the repeated element in the iterator. One of: + *        * `'first'`, + *        * `'middle'` + *        * `'last'` + * + * + * @element ANY + * @scope + * @priority 1000 + * @param {repeat_expression} ng-repeat The expression indicating how to enumerate a collection. Two + *   formats are currently supported: + * + *   * `variable in expression` – where variable is the user defined loop variable and `expression` + *     is a scope expression giving the collection to enumerate. + * + *     For example: `track in cd.tracks`. + * + *   * `(key, value) in expression` – where `key` and `value` can be any user defined identifiers, + *     and `expression` is the scope expression giving the collection to enumerate. + * + *     For example: `(name, age) in {'adam':10, 'amalie':12}`. + * + * @example + * This example initializes the scope to a list of names and + * then uses `ng-repeat` to display every person: +    <doc:example> +      <doc:source> +        <div ng-init="friends = [{name:'John', age:25}, {name:'Mary', age:28}]"> +          I have {{friends.length}} friends. They are: +          <ul> +            <li ng-repeat="friend in friends"> +              [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old. +            </li> +          </ul> +        </div> +      </doc:source> +      <doc:scenario> +         it('should check ng-repeat', function() { +           var r = using('.doc-example-live').repeater('ul li'); +           expect(r.count()).toBe(2); +           expect(r.row(0)).toEqual(["1","John","25"]); +           expect(r.row(1)).toEqual(["2","Mary","28"]); +         }); +      </doc:scenario> +    </doc:example> + */ +var ngRepeatDirective = ngDirective({ +  transclude: 'element', +  priority: 1000, +  terminal: true, +  compile: function(element, attr, linker) { +    return function(scope, iterStartElement, attr){ +      var expression = attr.ngRepeat; +      var match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/), +        lhs, rhs, valueIdent, keyIdent; +      if (! match) { +        throw Error("Expected ng-repeat in form of '_item_ in _collection_' but got '" + +          expression + "'."); +      } +      lhs = match[1]; +      rhs = match[2]; +      match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/); +      if (!match) { +        throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" + +            lhs + "'."); +      } +      valueIdent = match[3] || match[1]; +      keyIdent = match[2]; + +      // Store a list of elements from previous run. This is a hash where key is the item from the +      // iterator, and the value is an array of objects with following properties. +      //   - scope: bound scope +      //   - element: previous element. +      //   - index: position +      // We need an array of these objects since the same object can be returned from the iterator. +      // We expect this to be a rare case. +      var lastOrder = new HashQueueMap(); +      scope.$watch(function(scope){ +        var index, length, +            collection = scope.$eval(rhs), +            collectionLength = size(collection, true), +            childScope, +            // Same as lastOrder but it has the current state. It will become the +            // lastOrder on the next iteration. +            nextOrder = new HashQueueMap(), +            key, value, // key/value of iteration +            array, last,       // last object information {scope, element, index} +            cursor = iterStartElement;     // current position of the node + +        if (!isArray(collection)) { +          // if object, extract keys, sort them and use to determine order of iteration over obj props +          array = []; +          for(key in collection) { +            if (collection.hasOwnProperty(key) && key.charAt(0) != '$') { +              array.push(key); +            } +          } +          array.sort(); +        } else { +          array = collection || []; +        } + +        // we are not using forEach for perf reasons (trying to avoid #call) +        for (index = 0, length = array.length; index < length; index++) { +          key = (collection === array) ? index : array[index]; +          value = collection[key]; +          last = lastOrder.shift(value); +          if (last) { +            // if we have already seen this object, then we need to reuse the +            // associated scope/element +            childScope = last.scope; +            nextOrder.push(value, last); + +            if (index === last.index) { +              // do nothing +              cursor = last.element; +            } else { +              // existing item which got moved +              last.index = index; +              // This may be a noop, if the element is next, but I don't know of a good way to +              // figure this out,  since it would require extra DOM access, so let's just hope that +              // the browsers realizes that it is noop, and treats it as such. +              cursor.after(last.element); +              cursor = last.element; +            } +          } else { +            // new item which we don't know about +            childScope = scope.$new(); +          } + +          childScope[valueIdent] = value; +          if (keyIdent) childScope[keyIdent] = key; +          childScope.$index = index; +          childScope.$position = index === 0 ? +              'first' : +              (index == collectionLength - 1 ? 'last' : 'middle'); + +          if (!last) { +            linker(childScope, function(clone){ +              cursor.after(clone); +              last = { +                  scope: childScope, +                  element: (cursor = clone), +                  index: index +                }; +              nextOrder.push(value, last); +            }); +          } +        } + +        //shrink children +        for (key in lastOrder) { +          if (lastOrder.hasOwnProperty(key)) { +            array = lastOrder[key]; +            while(array.length) { +              value = array.pop(); +              value.element.remove(); +              value.scope.$destroy(); +            } +          } +        } + +        lastOrder = nextOrder; +      }); +    }; +  } +}); diff --git a/src/ng/directive/ngShowHide.js b/src/ng/directive/ngShowHide.js new file mode 100644 index 00000000..40a8a68e --- /dev/null +++ b/src/ng/directive/ngShowHide.js @@ -0,0 +1,80 @@ +'use strict'; + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-show + * + * @description + * The `ng-show` and `ng-hide` directives show or hide a portion of the DOM tree (HTML) + * conditionally. + * + * @element ANY + * @param {expression} ng-show If the {@link guide/dev_guide.expressions expression} is truthy + *     then the element is shown or hidden respectively. + * + * @example +   <doc:example> +     <doc:source> +        Click me: <input type="checkbox" ng-model="checked"><br/> +        Show: <span ng-show="checked">I show up when your checkbox is checked.</span> <br/> +        Hide: <span ng-hide="checked">I hide when your checkbox is checked.</span> +     </doc:source> +     <doc:scenario> +       it('should check ng-show / ng-hide', function() { +         expect(element('.doc-example-live span:first:hidden').count()).toEqual(1); +         expect(element('.doc-example-live span:last:visible').count()).toEqual(1); + +         input('checked').check(); + +         expect(element('.doc-example-live span:first:visible').count()).toEqual(1); +         expect(element('.doc-example-live span:last:hidden').count()).toEqual(1); +       }); +     </doc:scenario> +   </doc:example> + */ +//TODO(misko): refactor to remove element from the DOM +var ngShowDirective = ngDirective(function(scope, element, attr){ +  scope.$watch(attr.ngShow, function(value){ +    element.css('display', toBoolean(value) ? '' : 'none'); +  }); +}); + + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-hide + * + * @description + * The `ng-hide` and `ng-show` directives hide or show a portion + * of the HTML conditionally. + * + * @element ANY + * @param {expression} ng-hide If the {@link guide/dev_guide.expressions expression} truthy then + *     the element is shown or hidden respectively. + * + * @example +   <doc:example> +     <doc:source> +        Click me: <input type="checkbox" ng-model="checked"><br/> +        Show: <span ng-show="checked">I show up when you checkbox is checked?</span> <br/> +        Hide: <span ng-hide="checked">I hide when you checkbox is checked?</span> +     </doc:source> +     <doc:scenario> +       it('should check ng-show / ng-hide', function() { +         expect(element('.doc-example-live span:first:hidden').count()).toEqual(1); +         expect(element('.doc-example-live span:last:visible').count()).toEqual(1); + +         input('checked').check(); + +         expect(element('.doc-example-live span:first:visible').count()).toEqual(1); +         expect(element('.doc-example-live span:last:hidden').count()).toEqual(1); +       }); +     </doc:scenario> +   </doc:example> + */ +//TODO(misko): refactor to remove element from the DOM +var ngHideDirective = ngDirective(function(scope, element, attr){ +  scope.$watch(attr.ngHide, function(value){ +    element.css('display', toBoolean(value) ? 'none' : ''); +  }); +}); diff --git a/src/ng/directive/ngStyle.js b/src/ng/directive/ngStyle.js new file mode 100644 index 00000000..8a4e7458 --- /dev/null +++ b/src/ng/directive/ngStyle.js @@ -0,0 +1,42 @@ +'use strict'; + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-style + * + * @description + * The ng-style allows you to set CSS style on an HTML element conditionally. + * + * @element ANY + * @param {expression} ng-style {@link guide/dev_guide.expressions Expression} which evals to an + *      object whose keys are CSS style names and values are corresponding values for those CSS + *      keys. + * + * @example +   <doc:example> +     <doc:source> +        <input type="button" value="set" ng-click="myStyle={color:'red'}"> +        <input type="button" value="clear" ng-click="myStyle={}"> +        <br/> +        <span ng-style="myStyle">Sample Text</span> +        <pre>myStyle={{myStyle}}</pre> +     </doc:source> +     <doc:scenario> +       it('should check ng-style', function() { +         expect(element('.doc-example-live span').css('color')).toBe('rgb(0, 0, 0)'); +         element('.doc-example-live :button[value=set]').click(); +         expect(element('.doc-example-live span').css('color')).toBe('rgb(255, 0, 0)'); +         element('.doc-example-live :button[value=clear]').click(); +         expect(element('.doc-example-live span').css('color')).toBe('rgb(0, 0, 0)'); +       }); +     </doc:scenario> +   </doc:example> + */ +var ngStyleDirective = ngDirective(function(scope, element, attr) { +  scope.$watch(attr.ngStyle, function(newStyles, oldStyles) { +    if (oldStyles && (newStyles !== oldStyles)) { +      forEach(oldStyles, function(val, style) { element.css(style, '');}); +    } +    if (newStyles) element.css(newStyles); +  }, true); +}); diff --git a/src/ng/directive/ngSwitch.js b/src/ng/directive/ngSwitch.js new file mode 100644 index 00000000..16b0c4d4 --- /dev/null +++ b/src/ng/directive/ngSwitch.js @@ -0,0 +1,112 @@ +'use strict'; + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-switch + * @restrict EA + * + * @description + * Conditionally change the DOM structure. + * + * @usageContent + * <any ng-switch-when="matchValue1">...</any> + *   <any ng-switch-when="matchValue2">...</any> + *   ... + *   <any ng-switch-default>...</any> + * + * @scope + * @param {*} ng-switch|on expression to match against <tt>ng-switch-when</tt>. + * @paramDescription + * On child elments add: + * + * * `ng-switch-when`: the case statement to match against. If match then this + *   case will be displayed. + * * `ng-switch-default`: the default case when no other casses match. + * + * @example +    <doc:example> +      <doc:source> +        <script> +          function Ctrl($scope) { +            $scope.items = ['settings', 'home', 'other']; +            $scope.selection = $scope.items[0]; +          } +        </script> +        <div ng-controller="Ctrl"> +          <select ng-model="selection" ng-options="item for item in items"> +          </select> +          <tt>selection={{selection}}</tt> +          <hr/> +          <div ng-switch on="selection" > +            <div ng-switch-when="settings">Settings Div</div> +            <span ng-switch-when="home">Home Span</span> +            <span ng-switch-default>default</span> +          </div> +        </div> +      </doc:source> +      <doc:scenario> +        it('should start in settings', function() { +         expect(element('.doc-example-live [ng-switch]').text()).toMatch(/Settings Div/); +        }); +        it('should change to home', function() { +         select('selection').option('home'); +         expect(element('.doc-example-live [ng-switch]').text()).toMatch(/Home Span/); +        }); +        it('should select deafault', function() { +         select('selection').option('other'); +         expect(element('.doc-example-live [ng-switch]').text()).toMatch(/default/); +        }); +      </doc:scenario> +    </doc:example> + */ +var NG_SWITCH = 'ng-switch'; +var ngSwitchDirective = valueFn({ +  restrict: 'EA', +  compile: function(element, attr) { +    var watchExpr = attr.ngSwitch || attr.on, +        cases = {}; + +    element.data(NG_SWITCH, cases); +    return function(scope, element){ +      var selectedTransclude, +          selectedElement, +          selectedScope; + +      scope.$watch(watchExpr, function(value) { +        if (selectedElement) { +          selectedScope.$destroy(); +          selectedElement.remove(); +          selectedElement = selectedScope = null; +        } +        if ((selectedTransclude = cases['!' + value] || cases['?'])) { +          scope.$eval(attr.change); +          selectedScope = scope.$new(); +          selectedTransclude(selectedScope, function(caseElement) { +            selectedElement = caseElement; +            element.append(caseElement); +          }); +        } +      }); +    }; +  } +}); + +var ngSwitchWhenDirective = ngDirective({ +  transclude: 'element', +  priority: 500, +  compile: function(element, attrs, transclude) { +    var cases = element.inheritedData(NG_SWITCH); +    assertArg(cases); +    cases['!' + attrs.ngSwitchWhen] = transclude; +  } +}); + +var ngSwitchDefaultDirective = ngDirective({ +  transclude: 'element', +  priority: 500, +  compile: function(element, attrs, transclude) { +    var cases = element.inheritedData(NG_SWITCH); +    assertArg(cases); +    cases['?'] = transclude; +  } +}); diff --git a/src/ng/directive/ngTransclude.js b/src/ng/directive/ngTransclude.js new file mode 100644 index 00000000..ab4011f0 --- /dev/null +++ b/src/ng/directive/ngTransclude.js @@ -0,0 +1,58 @@ +'use strict'; + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-transclude + * + * @description + * Insert the transcluded DOM here. + * + * @element ANY + * + * @example +   <doc:example module="transclude"> +     <doc:source> +       <script> +         function Ctrl($scope) { +           $scope.title = 'Lorem Ipsum'; +           $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...'; +         } + +         angular.module('transclude', []) +          .directive('pane', function(){ +             return { +               restrict: 'E', +               transclude: true, +               scope: 'isolate', +               locals: { title:'bind' }, +               template: '<div style="border: 1px solid black;">' + +                           '<div style="background-color: gray">{{title}}</div>' + +                           '<div ng-transclude></div>' + +                         '</div>' +             }; +         }); +       </script> +       <div ng-controller="Ctrl"> +         <input ng-model="title"><br> +         <textarea ng-model="text"></textarea> <br/> +         <pane title="{{title}}">{{text}}</pane> +       </div> +     </doc:source> +     <doc:scenario> +        it('should have transcluded', function() { +          input('title').enter('TITLE'); +          input('text').enter('TEXT'); +          expect(binding('title')).toEqual('TITLE'); +          expect(binding('text')).toEqual('TEXT'); +        }); +     </doc:scenario> +   </doc:example> + * + */ +var ngTranscludeDirective = ngDirective({ +  controller: ['$transclude', '$element', function($transclude, $element) { +    $transclude(function(clone) { +      $element.append(clone); +    }); +  }] +}); diff --git a/src/ng/directive/ngView.js b/src/ng/directive/ngView.js new file mode 100644 index 00000000..95b1546d --- /dev/null +++ b/src/ng/directive/ngView.js @@ -0,0 +1,170 @@ +'use strict'; + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng-view + * @restrict ECA + * + * @description + * # Overview + * `ng-view` is a directive that complements the {@link angular.module.ng.$route $route} service by + * including the rendered template of the current route into the main layout (`index.html`) file. + * Every time the current route changes, the included view changes with it according to the + * configuration of the `$route` service. + * + * @scope + * @example +    <doc:example module="ngView"> +      <doc:source> +        <script type="text/ng-template" id="examples/book.html"> +          controller: {{name}}<br /> +          Book Id: {{params.bookId}}<br /> +        </script> + +        <script type="text/ng-template" id="examples/chapter.html"> +          controller: {{name}}<br /> +          Book Id: {{params.bookId}}<br /> +          Chapter Id: {{params.chapterId}} +        </script> + +        <script> +          angular.module('ngView', [], function($routeProvider, $locationProvider) { +            $routeProvider.when('/Book/:bookId', { +              template: 'examples/book.html', +              controller: BookCntl +            }); +            $routeProvider.when('/Book/:bookId/ch/:chapterId', { +              template: 'examples/chapter.html', +              controller: ChapterCntl +            }); + +            // configure html5 to get links working on jsfiddle +            $locationProvider.html5Mode(true); +          }); + +          function MainCntl($scope, $route, $routeParams, $location) { +            $scope.$route = $route; +            $scope.$location = $location; +            $scope.$routeParams = $routeParams; +          } + +          function BookCntl($scope, $routeParams) { +            $scope.name = "BookCntl"; +            $scope.params = $routeParams; +          } + +          function ChapterCntl($scope, $routeParams) { +            $scope.name = "ChapterCntl"; +            $scope.params = $routeParams; +          } +        </script> + +        <div ng-controller="MainCntl"> +          Choose: +          <a href="/Book/Moby">Moby</a> | +          <a href="/Book/Moby/ch/1">Moby: Ch1</a> | +          <a href="/Book/Gatsby">Gatsby</a> | +          <a href="/Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a> | +          <a href="/Book/Scarlet">Scarlet Letter</a><br/> + +          <div ng-view></div> +          <hr /> + +          <pre>$location.path() = {{$location.path()}}</pre> +          <pre>$route.current.template = {{$route.current.template}}</pre> +          <pre>$route.current.params = {{$route.current.params}}</pre> +          <pre>$route.current.scope.name = {{$route.current.scope.name}}</pre> +          <pre>$routeParams = {{$routeParams}}</pre> +        </div> +      </doc:source> +      <doc:scenario> +        it('should load and compile correct template', function() { +          element('a:contains("Moby: Ch1")').click(); +          var content = element('.doc-example-live [ng-view]').text(); +          expect(content).toMatch(/controller\: ChapterCntl/); +          expect(content).toMatch(/Book Id\: Moby/); +          expect(content).toMatch(/Chapter Id\: 1/); + +          element('a:contains("Scarlet")').click(); +          content = element('.doc-example-live [ng-view]').text(); +          expect(content).toMatch(/controller\: BookCntl/); +          expect(content).toMatch(/Book Id\: Scarlet/); +        }); +      </doc:scenario> +    </doc:example> + */ + + +/** + * @ngdoc event + * @name angular.module.ng.$compileProvider.directive.ng-view#$viewContentLoaded + * @eventOf angular.module.ng.$compileProvider.directive.ng-view + * @eventType emit on the current ng-view scope + * @description + * Emitted every time the ng-view content is reloaded. + */ +var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$compile', +                       '$controller', +               function($http,   $templateCache,   $route,   $anchorScroll,   $compile, +                        $controller) { +  return { +    restrict: 'ECA', +    terminal: true, +    link: function(scope, element, attr) { +      var changeCounter = 0, +          lastScope, +          onloadExp = attr.onload || ''; + +      scope.$on('$afterRouteChange', function(event, next, previous) { +        changeCounter++; +      }); + +      scope.$watch(function() {return changeCounter;}, function(newChangeCounter) { +        var template = $route.current && $route.current.template; + +        function destroyLastScope() { +          if (lastScope) { +            lastScope.$destroy(); +            lastScope = null; +          } +        } + +        function clearContent() { +          // ignore callback if another route change occured since +          if (newChangeCounter == changeCounter) { +            element.html(''); +          } +          destroyLastScope(); +        } + +        if (template) { +          $http.get(template, {cache: $templateCache}).success(function(response) { +            // ignore callback if another route change occured since +            if (newChangeCounter == changeCounter) { +              element.html(response); +              destroyLastScope(); + +              var link = $compile(element.contents()), +                  current = $route.current; + +              lastScope = current.scope = scope.$new(); +              if (current.controller) { +                element.contents(). +                  data('$ngControllerController', $controller(current.controller, {$scope: lastScope})); +              } + +              link(lastScope); +              lastScope.$emit('$viewContentLoaded'); +              lastScope.$eval(onloadExp); + +              // $anchorScroll might listen on event... +              $anchorScroll(); +            } +          }).error(clearContent); +        } else { +          clearContent(); +        } +      }); +    } +  }; +}]; diff --git a/src/ng/directive/script.js b/src/ng/directive/script.js new file mode 100644 index 00000000..4090ae24 --- /dev/null +++ b/src/ng/directive/script.js @@ -0,0 +1,43 @@ +'use strict'; + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.script + * + * @description + * Load content of a script tag, with type `text/ng-template`, into `$templateCache`, so that the + * template can be used by `ng-include`, `ng-view` or directive templates. + * + * @restrict E + * @param {'text/ng-template'} type must be set to `'text/ng-template'` + * + * @example +  <doc:example> +    <doc:source> +      <script type="text/ng-template" id="/tpl.html"> +        Content of the template. +      </script> + +      <a ng-click="currentTpl='/tpl.html'" id="tpl-link">Load inlined template</a> +      <div id="tpl-content" ng-include src="currentTpl"></div> +    </doc:source> +    <doc:scenario> +      it('should load template defined inside script tag', function() { +        element('#tpl-link').click(); +        expect(element('#tpl-content').text()).toMatch(/Content of the template/); +      }); +    </doc:scenario> +  </doc:example> + */ +var scriptDirective = ['$templateCache', function($templateCache) { +  return { +    restrict: 'E', +    terminal: true, +    compile: function(element, attr) { +      if (attr.type == 'text/ng-template') { +        var templateUrl = attr.id; +        $templateCache.put(templateUrl, element.text()); +      } +    } +  }; +}]; diff --git a/src/ng/directive/select.js b/src/ng/directive/select.js new file mode 100644 index 00000000..b70339fc --- /dev/null +++ b/src/ng/directive/select.js @@ -0,0 +1,449 @@ +'use strict'; + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.select + * @restrict E + * + * @description + * HTML `SELECT` element with angular data-binding. + * + * # `ng-options` + * + * Optionally `ng-options` attribute can be used to dynamically generate a list of `<option>` + * elements for a `<select>` element using an array or an object obtained by evaluating the + * `ng-options` expression. + *˝˝ + * When an item in the select menu is select, the value of array element or object property + * represented by the selected option will be bound to the model identified by the `ng-model` attribute + * of the parent select element. + * + * Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can + * be nested into the `<select>` element. This element will then represent `null` or "not selected" + * option. See example below for demonstration. + * + * Note: `ng-options` provides iterator facility for `<option>` element which must be used instead + * of {@link angular.module.ng.$compileProvider.directive.ng-repeat ng-repeat}. `ng-repeat` is not suitable for use with + * `<option>` element because of the following reasons: + * + *   * value attribute of the option element that we need to bind to requires a string, but the + *     source of data for the iteration might be in a form of array containing objects instead of + *     strings + *   * {@link angular.module.ng.$compileProvider.directive.ng-repeat ng-repeat} unrolls after the select binds causing + *     incorect rendering on most browsers. + *   * binding to a value not in list confuses most browsers. + * + * @param {string} name assignable expression to data-bind to. + * @param {string=} required The control is considered valid only if value is entered. + * @param {comprehension_expression=} ng-options in one of the following forms: + * + *   * for array data sources: + *     * `label` **`for`** `value` **`in`** `array` + *     * `select` **`as`** `label` **`for`** `value` **`in`** `array` + *     * `label`  **`group by`** `group` **`for`** `value` **`in`** `array` + *     * `select` **`as`** `label` **`group by`** `group` **`for`** `value` **`in`** `array` + *   * for object data sources: + *     * `label` **`for (`**`key` **`,`** `value`**`) in`** `object` + *     * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object` + *     * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object` + *     * `select` **`as`** `label` **`group by`** `group` + *         **`for` `(`**`key`**`,`** `value`**`) in`** `object` + * + * Where: + * + *   * `array` / `object`: an expression which evaluates to an array / object to iterate over. + *   * `value`: local variable which will refer to each item in the `array` or each property value + *      of `object` during iteration. + *   * `key`: local variable which will refer to a property name in `object` during iteration. + *   * `label`: The result of this expression will be the label for `<option>` element. The + *     `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`). + *   * `select`: The result of this expression will be bound to the model of the parent `<select>` + *      element. If not specified, `select` expression will default to `value`. + *   * `group`: The result of this expression will be used to group options using the `<optgroup>` + *      DOM element. + * + * @example +    <doc:example> +      <doc:source> +        <script> +        function MyCntrl($scope) { +          $scope.colors = [ +            {name:'black', shade:'dark'}, +            {name:'white', shade:'light'}, +            {name:'red', shade:'dark'}, +            {name:'blue', shade:'dark'}, +            {name:'yellow', shade:'light'} +          ]; +          $scope.color = $scope.colors[2]; // red +        } +        </script> +        <div ng-controller="MyCntrl"> +          <ul> +            <li ng-repeat="color in colors"> +              Name: <input ng-model="color.name"> +              [<a href ng-click="colors.$remove(color)">X</a>] +            </li> +            <li> +              [<a href ng-click="colors.push({})">add</a>] +            </li> +          </ul> +          <hr/> +          Color (null not allowed): +          <select ng-model="color" ng-options="c.name for c in colors"></select><br> + +          Color (null allowed): +          <div  class="nullable"> +            <select ng-model="color" ng-options="c.name for c in colors"> +              <option value="">-- chose color --</option> +            </select> +          </div><br/> + +          Color grouped by shade: +          <select ng-model="color" ng-options="c.name group by c.shade for c in colors"> +          </select><br/> + + +          Select <a href ng-click="color={name:'not in list'}">bogus</a>.<br> +          <hr/> +          Currently selected: {{ {selected_color:color}  }} +          <div style="border:solid 1px black; height:20px" +               ng-style="{'background-color':color.name}"> +          </div> +        </div> +      </doc:source> +      <doc:scenario> +         it('should check ng-options', function() { +           expect(binding('{selected_color:color}')).toMatch('red'); +           select('color').option('0'); +           expect(binding('{selected_color:color}')).toMatch('black'); +           using('.nullable').select('color').option(''); +           expect(binding('{selected_color:color}')).toMatch('null'); +         }); +      </doc:scenario> +    </doc:example> + */ + +var ngOptionsDirective = valueFn({ terminal: true }); +var selectDirective = ['$compile', '$parse', function($compile,   $parse) { +                         //00001111100000000000222200000000000000000000003333000000000000044444444444444444000000000555555555555555550000000666666666666666660000000000000007777 +  var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w\d]*)|(?:\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*)$/; + +  return { +    restrict: 'E', +    require: '?ngModel', +    link: function(scope, element, attr, ctrl) { +      if (!ctrl) return; + +      var multiple = attr.multiple, +          optionsExp = attr.ngOptions; + +      // required validator +      if (multiple && (attr.required || attr.ngRequired)) { +        var requiredValidator = function(value) { +          ctrl.$setValidity('required', !attr.required || (value && value.length)); +          return value; +        }; + +        ctrl.$parsers.push(requiredValidator); +        ctrl.$formatters.unshift(requiredValidator); + +        attr.$observe('required', function() { +          requiredValidator(ctrl.$viewValue); +        }); +      } + +      if (optionsExp) Options(scope, element, ctrl); +      else if (multiple) Multiple(scope, element, ctrl); +      else Single(scope, element, ctrl); + + +      //////////////////////////// + + + +      function Single(scope, selectElement, ctrl) { +        ctrl.$render = function() { +          selectElement.val(ctrl.$viewValue); +        }; + +        selectElement.bind('change', function() { +          scope.$apply(function() { +            ctrl.$setViewValue(selectElement.val()); +          }); +        }); +      } + +      function Multiple(scope, selectElement, ctrl) { +        var lastView; +        ctrl.$render = function() { +          var items = new HashMap(ctrl.$viewValue); +          forEach(selectElement.children(), function(option) { +            option.selected = isDefined(items.get(option.value)); +          }); +        }; + +        // we have to do it on each watch since ng-model watches reference, but +        // we need to work of an array, so we need to see if anything was inserted/removed +        scope.$watch(function() { +          if (!equals(lastView, ctrl.$viewValue)) { +            lastView = copy(ctrl.$viewValue); +            ctrl.$render(); +          } +        }); + +        selectElement.bind('change', function() { +          scope.$apply(function() { +            var array = []; +            forEach(selectElement.children(), function(option) { +              if (option.selected) { +                array.push(option.value); +              } +            }); +            ctrl.$setViewValue(array); +          }); +        }); +      } + +      function Options(scope, selectElement, ctrl) { +        var match; + +        if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) { +          throw Error( +            "Expected ng-options in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" + +            " but got '" + optionsExp + "'."); +        } + +        var displayFn = $parse(match[2] || match[1]), +            valueName = match[4] || match[6], +            keyName = match[5], +            groupByFn = $parse(match[3] || ''), +            valueFn = $parse(match[2] ? match[1] : valueName), +            valuesFn = $parse(match[7]), +            // we can't just jqLite('<option>') since jqLite is not smart enough +            // to create it in <select> and IE barfs otherwise. +            optionTemplate = jqLite(document.createElement('option')), +            optGroupTemplate = jqLite(document.createElement('optgroup')), +            nullOption = false, // if false then user will not be able to select it +            // This is an array of array of existing option groups in DOM. We try to reuse these if possible +            // optionGroupsCache[0] is the options with no option group +            // optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element +            optionGroupsCache = [[{element: selectElement, label:''}]]; + +        // find existing special options +        forEach(selectElement.children(), function(option) { +          if (option.value == '') { +            // developer declared null option, so user should be able to select it +            nullOption = jqLite(option).remove(); +            // compile the element since there might be bindings in it +            $compile(nullOption)(scope); +          } +        }); +        selectElement.html(''); // clear contents + +        selectElement.bind('change', function() { +          scope.$apply(function() { +            var optionGroup, +                collection = valuesFn(scope) || [], +                locals = {}, +                key, value, optionElement, index, groupIndex, length, groupLength; + +            if (multiple) { +              value = []; +              for (groupIndex = 0, groupLength = optionGroupsCache.length; +              groupIndex < groupLength; +              groupIndex++) { +                // list of options for that group. (first item has the parent) +                optionGroup = optionGroupsCache[groupIndex]; + +                for(index = 1, length = optionGroup.length; index < length; index++) { +                  if ((optionElement = optionGroup[index].element)[0].selected) { +                    key = optionElement.val(); +                    if (keyName) locals[keyName] = key; +                    locals[valueName] = collection[key]; +                    value.push(valueFn(scope, locals)); +                  } +                } +              } +            } else { +              key = selectElement.val(); +              if (key == '?') { +                value = undefined; +              } else if (key == ''){ +                value = null; +              } else { +                locals[valueName] = collection[key]; +                if (keyName) locals[keyName] = key; +                value = valueFn(scope, locals); +              } +            } +            ctrl.$setViewValue(value); +          }); +        }); + +        ctrl.$render = render; + +        // TODO(vojta): can't we optimize this ? +        scope.$watch(render); + +        function render() { +          var optionGroups = {'':[]}, // Temporary location for the option groups before we render them +              optionGroupNames = [''], +              optionGroupName, +              optionGroup, +              option, +              existingParent, existingOptions, existingOption, +              modelValue = ctrl.$modelValue, +              values = valuesFn(scope) || [], +              keys = keyName ? sortedKeys(values) : values, +              groupLength, length, +              groupIndex, index, +              locals = {}, +              selected, +              selectedSet = false, // nothing is selected yet +              lastElement, +              element; + +          if (multiple) { +            selectedSet = new HashMap(modelValue); +          } else if (modelValue === null || nullOption) { +            // if we are not multiselect, and we are null then we have to add the nullOption +            optionGroups[''].push({selected:modelValue === null, id:'', label:''}); +            selectedSet = true; +          } + +          // We now build up the list of options we need (we merge later) +          for (index = 0; length = keys.length, index < length; index++) { +               locals[valueName] = values[keyName ? locals[keyName]=keys[index]:index]; +               optionGroupName = groupByFn(scope, locals) || ''; +            if (!(optionGroup = optionGroups[optionGroupName])) { +              optionGroup = optionGroups[optionGroupName] = []; +              optionGroupNames.push(optionGroupName); +            } +            if (multiple) { +              selected = selectedSet.remove(valueFn(scope, locals)) != undefined; +            } else { +              selected = modelValue === valueFn(scope, locals); +              selectedSet = selectedSet || selected; // see if at least one item is selected +            } +            optionGroup.push({ +              id: keyName ? keys[index] : index,   // either the index into array or key from object +              label: displayFn(scope, locals) || '', // what will be seen by the user +              selected: selected                   // determine if we should be selected +            }); +          } +          if (!multiple && !selectedSet) { +            // nothing was selected, we have to insert the undefined item +            optionGroups[''].unshift({id:'?', label:'', selected:true}); +          } + +          // Now we need to update the list of DOM nodes to match the optionGroups we computed above +          for (groupIndex = 0, groupLength = optionGroupNames.length; +               groupIndex < groupLength; +               groupIndex++) { +            // current option group name or '' if no group +            optionGroupName = optionGroupNames[groupIndex]; + +            // list of options for that group. (first item has the parent) +            optionGroup = optionGroups[optionGroupName]; + +            if (optionGroupsCache.length <= groupIndex) { +              // we need to grow the optionGroups +              existingParent = { +                element: optGroupTemplate.clone().attr('label', optionGroupName), +                label: optionGroup.label +              }; +              existingOptions = [existingParent]; +              optionGroupsCache.push(existingOptions); +              selectElement.append(existingParent.element); +            } else { +              existingOptions = optionGroupsCache[groupIndex]; +              existingParent = existingOptions[0];  // either SELECT (no group) or OPTGROUP element + +              // update the OPTGROUP label if not the same. +              if (existingParent.label != optionGroupName) { +                existingParent.element.attr('label', existingParent.label = optionGroupName); +              } +            } + +            lastElement = null;  // start at the begining +            for(index = 0, length = optionGroup.length; index < length; index++) { +              option = optionGroup[index]; +              if ((existingOption = existingOptions[index+1])) { +                // reuse elements +                lastElement = existingOption.element; +                if (existingOption.label !== option.label) { +                  lastElement.text(existingOption.label = option.label); +                } +                if (existingOption.id !== option.id) { +                  lastElement.val(existingOption.id = option.id); +                } +                if (existingOption.element.selected !== option.selected) { +                  lastElement.prop('selected', (existingOption.selected = option.selected)); +                } +              } else { +                // grow elements + +                // if it's a null option +                if (option.id === '' && nullOption) { +                  // put back the pre-compiled element +                  element = nullOption; +                } else { +                  // jQuery(v1.4.2) Bug: We should be able to chain the method calls, but +                  // in this version of jQuery on some browser the .text() returns a string +                  // rather then the element. +                  (element = optionTemplate.clone()) +                      .val(option.id) +                      .attr('selected', option.selected) +                      .text(option.label); +                } + +                existingOptions.push(existingOption = { +                    element: element, +                    label: option.label, +                    id: option.id, +                    selected: option.selected +                }); +                if (lastElement) { +                  lastElement.after(element); +                } else { +                  existingParent.element.append(element); +                } +                lastElement = element; +              } +            } +            // remove any excessive OPTIONs in a group +            index++; // increment since the existingOptions[0] is parent element not OPTION +            while(existingOptions.length > index) { +              existingOptions.pop().element.remove(); +            } +          } +          // remove any excessive OPTGROUPs from select +          while(optionGroupsCache.length > groupIndex) { +            optionGroupsCache.pop()[0].element.remove(); +          } +        }; +      } +    } +  } +}]; + +var optionDirective = ['$interpolate', function($interpolate) { +  return { +    restrict: 'E', +    priority: 100, +    compile: function(element, attr) { +      if (isUndefined(attr.value)) { +        var interpolateFn = $interpolate(element.text(), true); +        if (interpolateFn) { +          return function (scope, element, attr) { +            scope.$watch(interpolateFn, function(value) { +              attr.$set('value', value); +            }); +          } +        } else { +          attr.$set('value', element.text()); +        } +      } +    } +  } +}]; diff --git a/src/ng/directive/style.js b/src/ng/directive/style.js new file mode 100644 index 00000000..68ea1465 --- /dev/null +++ b/src/ng/directive/style.js @@ -0,0 +1,6 @@ +'use strict'; + +var styleDirective = valueFn({ +  restrict: 'E', +  terminal: true +}); diff --git a/src/ng/document.js b/src/ng/document.js new file mode 100644 index 00000000..53b59b39 --- /dev/null +++ b/src/ng/document.js @@ -0,0 +1,16 @@ +'use strict'; + +/** + * @ngdoc object + * @name angular.module.ng.$document + * @requires $window + * + * @description + * A {@link angular.element jQuery (lite)}-wrapped reference to the browser's `window.document` + * element. + */ +function $DocumentProvider(){ +  this.$get = ['$window', function(window){ +    return jqLite(window.document); +  }]; +} diff --git a/src/ng/exceptionHandler.js b/src/ng/exceptionHandler.js new file mode 100644 index 00000000..26ea5845 --- /dev/null +++ b/src/ng/exceptionHandler.js @@ -0,0 +1,26 @@ +'use strict'; + +/** + * @ngdoc function + * @name angular.module.ng.$exceptionHandler + * @requires $log + * + * @description + * Any uncaught exception in angular expressions is delegated to this service. + * The default implementation simply delegates to `$log.error` which logs it into + * the browser console. + * + * In unit tests, if `angular-mocks.js` is loaded, this service is overridden by + * {@link angular.module.ngMock.$exceptionHandler mock $exceptionHandler} + * + * @param {Error} exception Exception associated with the error. + * @param {string=} cause optional information about the context in which + *       the error was thrown. + */ +function $ExceptionHandlerProvider() { +  this.$get = ['$log', function($log){ +    return function(exception, cause) { +      $log.error.apply($log, arguments); +    }; +  }]; +} diff --git a/src/ng/filter.js b/src/ng/filter.js new file mode 100644 index 00000000..4ed3f620 --- /dev/null +++ b/src/ng/filter.js @@ -0,0 +1,104 @@ +'use strict'; + +/** + * @ngdoc object + * @name angular.module.ng.$filterProvider + * @description + * + * Filters are just functions which transform input to an output. However filters need to be Dependency Injected. To + * achieve this a filter definition consists of a factory function which is annotated with dependencies and is + * responsible for creating a the filter function. + * + * <pre> + *   // Filter registration + *   function MyModule($provide, $filterProvider) { + *     // create a service to demonstrate injection (not always needed) + *     $provide.value('greet', function(name){ + *       return 'Hello ' + name + '!': + *     }); + * + *     // register a filter factory which uses the + *     // greet service to demonstrate DI. + *     $filterProvider.register('greet', function(greet){ + *       // return the filter function which uses the greet service + *       // to generate salutation + *       return function(text) { + *         // filters need to be forgiving so check input validity + *         return text && greet(text) || text; + *       }; + *     }; + *   } + * </pre> + * + * The filter function is registered with the `$injector` under the filter name suffixe with `Filter`. + * <pre> + *   it('should be the same instance', inject( + *     function($filterProvider) { + *       $filterProvider.register('reverse', function(){ + *         return ...; + *       }); + *     }, + *     function($filter, reverseFilter) { + *       expect($filter('reverse')).toBe(reverseFilter); + *     }); + * </pre> + * + * + * For more information about how angular filters work, and how to create your own filters, see + * {@link guide/dev_guide.templates.filters Understanding Angular Filters} in the angular Developer + * Guide. + */ +/** + * @ngdoc method + * @name angular.module.ng.$filterProvider#register + * @methodOf angular.module.ng.$filterProvider + * @description + * Register filter factory function. + * + * @param {String} name Name of the filter. + * @param {function} fn The filter factory function which is injectable. + */ + + +/** + * @ngdoc function + * @name angular.module.ng.$filter + * @function + * @description + * Filters are used for formatting data displayed to the user. + * + * The general syntax in templates is as follows: + * + *         {{ expression | [ filter_name ] }} + * + * @param {String} name Name of the filter function to retrieve + * @return {Function} the filter function + */ +$FilterProvider.$inject = ['$provide']; +function $FilterProvider($provide) { +  var suffix = 'Filter'; + +  function register(name, factory) { +    return $provide.factory(name + suffix, factory); +  } +  this.register = register; + +  this.$get = ['$injector', function($injector) { +    return function(name) { +      return $injector.get(name + suffix); +    } +  }]; + +  //////////////////////////////////////// + +  register('currency', currencyFilter); +  register('date', dateFilter); +  register('filter', filterFilter); +  register('json', jsonFilter); +  register('limitTo', limitToFilter); +  register('linky', linkyFilter); +  register('lowercase', lowercaseFilter); +  register('number', numberFilter); +  register('orderBy', orderByFilter); +  register('uppercase', uppercaseFilter); +} diff --git a/src/ng/filter/filter.js b/src/ng/filter/filter.js new file mode 100644 index 00000000..008897a1 --- /dev/null +++ b/src/ng/filter/filter.js @@ -0,0 +1,164 @@ +'use strict'; + +/** + * @ngdoc filter + * @name angular.module.ng.$filter.filter + * @function + * + * @description + * Selects a subset of items from `array` and returns it as a new array. + * + * Note: This function is used to augment the `Array` type in Angular expressions. See + * {@link angular.module.ng.$filter} for more information about Angular arrays. + * + * @param {Array} array The source array. + * @param {string|Object|function()} expression The predicate to be used for selecting items from + *   `array`. + * + *   Can be one of: + * + *   - `string`: Predicate that results in a substring match using the value of `expression` + *     string. All strings or objects with string properties in `array` that contain this string + *     will be returned. The predicate can be negated by prefixing the string with `!`. + * + *   - `Object`: A pattern object can be used to filter specific properties on objects contained + *     by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items + *     which have property `name` containing "M" and property `phone` containing "1". A special + *     property name `$` can be used (as in `{$:"text"}`) to accept a match against any + *     property of the object. That's equivalent to the simple substring match with a `string` + *     as described above. + * + *   - `function`: A predicate function can be used to write arbitrary filters. The function is + *     called for each element of `array`. The final result is an array of those elements that + *     the predicate returned true for. + * + * @example +   <doc:example> +     <doc:source> +       <div ng-init="friends = [{name:'John', phone:'555-1276'}, +                                {name:'Mary', phone:'800-BIG-MARY'}, +                                {name:'Mike', phone:'555-4321'}, +                                {name:'Adam', phone:'555-5678'}, +                                {name:'Julie', phone:'555-8765'}]"></div> + +       Search: <input ng-model="searchText" ng-model-instant> +       <table id="searchTextResults"> +         <tr><th>Name</th><th>Phone</th><tr> +         <tr ng-repeat="friend in friends | filter:searchText"> +           <td>{{friend.name}}</td> +           <td>{{friend.phone}}</td> +         <tr> +       </table> +       <hr> +       Any: <input ng-model="search.$" ng-model-instant> <br> +       Name only <input ng-model="search.name" ng-model-instant><br> +       Phone only <input ng-model="search.phone" ng-model-instant><br> +       <table id="searchObjResults"> +         <tr><th>Name</th><th>Phone</th><tr> +         <tr ng-repeat="friend in friends | filter:search"> +           <td>{{friend.name}}</td> +           <td>{{friend.phone}}</td> +         <tr> +       </table> +     </doc:source> +     <doc:scenario> +       it('should search across all fields when filtering with a string', function() { +         input('searchText').enter('m'); +         expect(repeater('#searchTextResults tr', 'friend in friends').column('friend.name')). +           toEqual(['Mary', 'Mike', 'Adam']); + +         input('searchText').enter('76'); +         expect(repeater('#searchTextResults tr', 'friend in friends').column('friend.name')). +           toEqual(['John', 'Julie']); +       }); + +       it('should search in specific fields when filtering with a predicate object', function() { +         input('search.$').enter('i'); +         expect(repeater('#searchObjResults tr', 'friend in friends').column('friend.name')). +           toEqual(['Mary', 'Mike', 'Julie']); +       }); +     </doc:scenario> +   </doc:example> + */ +function filterFilter() { +  return function(array, expression) { +    if (!(array instanceof Array)) return array; +    var predicates = []; +    predicates.check = function(value) { +      for (var j = 0; j < predicates.length; j++) { +        if(!predicates[j](value)) { +          return false; +        } +      } +      return true; +    }; +    var search = function(obj, text){ +      if (text.charAt(0) === '!') { +        return !search(obj, text.substr(1)); +      } +      switch (typeof obj) { +        case "boolean": +        case "number": +        case "string": +          return ('' + obj).toLowerCase().indexOf(text) > -1; +        case "object": +          for ( var objKey in obj) { +            if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) { +              return true; +            } +          } +          return false; +        case "array": +          for ( var i = 0; i < obj.length; i++) { +            if (search(obj[i], text)) { +              return true; +            } +          } +          return false; +        default: +          return false; +      } +    }; +    switch (typeof expression) { +      case "boolean": +      case "number": +      case "string": +        expression = {$:expression}; +      case "object": +        for (var key in expression) { +          if (key == '$') { +            (function() { +              var text = (''+expression[key]).toLowerCase(); +              if (!text) return; +              predicates.push(function(value) { +                return search(value, text); +              }); +            })(); +          } else { +            (function() { +              var path = key; +              var text = (''+expression[key]).toLowerCase(); +              if (!text) return; +              predicates.push(function(value) { +                return search(getter(value, path), text); +              }); +            })(); +          } +        } +        break; +      case 'function': +        predicates.push(expression); +        break; +      default: +        return array; +    } +    var filtered = []; +    for ( var j = 0; j < array.length; j++) { +      var value = array[j]; +      if (predicates.check(value)) { +        filtered.push(value); +      } +    } +    return filtered; +  } +} diff --git a/src/ng/filter/filters.js b/src/ng/filter/filters.js new file mode 100644 index 00000000..078c54fc --- /dev/null +++ b/src/ng/filter/filters.js @@ -0,0 +1,527 @@ +'use strict'; + +/** + * @ngdoc filter + * @name angular.module.ng.$filter.currency + * @function + * + * @description + * Formats a number as a currency (ie $1,234.56). When no currency symbol is provided, default + * symbol for current locale is used. + * + * @param {number} amount Input to filter. + * @param {string=} symbol Currency symbol or identifier to be displayed. + * @returns {string} Formatted number. + * + * + * @example +   <doc:example> +     <doc:source> +       <script> +         function Ctrl($scope) { +           $scope.amount = 1234.56; +         } +       </script> +       <div ng-controller="Ctrl"> +         <input type="number" ng-model="amount" ng-model-instant> <br> +         default currency symbol ($): {{amount | currency}}<br> +         custom currency identifier (USD$): {{amount | currency:"USD$"}} +       </div> +     </doc:source> +     <doc:scenario> +       it('should init with 1234.56', function() { +         expect(binding('amount | currency')).toBe('$1,234.56'); +         expect(binding('amount | currency:"USD$"')).toBe('USD$1,234.56'); +       }); +       it('should update', function() { +         input('amount').enter('-1234'); +         expect(binding('amount | currency')).toBe('($1,234.00)'); +         expect(binding('amount | currency:"USD$"')).toBe('(USD$1,234.00)'); +       }); +     </doc:scenario> +   </doc:example> + */ +currencyFilter.$inject = ['$locale']; +function currencyFilter($locale) { +  var formats = $locale.NUMBER_FORMATS; +  return function(amount, currencySymbol){ +    if (isUndefined(currencySymbol)) currencySymbol = formats.CURRENCY_SYM; +    return formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, 2). +                replace(/\u00A4/g, currencySymbol); +  }; +} + +/** + * @ngdoc filter + * @name angular.module.ng.$filter.number + * @function + * + * @description + * Formats a number as text. + * + * If the input is not a number an empty string is returned. + * + * @param {number|string} number Number to format. + * @param {(number|string)=} [fractionSize=2] Number of decimal places to round the number to. + * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit. + * + * @example +   <doc:example> +     <doc:source> +       <script> +         function Ctrl($scope) { +           $scope.val = 1234.56789; +         } +       </script> +       <div ng-controller="Ctrl"> +         Enter number: <input ng-model='val' ng-model-instant><br> +         Default formatting: {{val | number}}<br> +         No fractions: {{val | number:0}}<br> +         Negative number: {{-val | number:4}} +       </div> +     </doc:source> +     <doc:scenario> +       it('should format numbers', function() { +         expect(binding('val | number')).toBe('1,234.568'); +         expect(binding('val | number:0')).toBe('1,235'); +         expect(binding('-val | number:4')).toBe('-1,234.5679'); +       }); + +       it('should update', function() { +         input('val').enter('3374.333'); +         expect(binding('val | number')).toBe('3,374.333'); +         expect(binding('val | number:0')).toBe('3,374'); +         expect(binding('-val | number:4')).toBe('-3,374.3330'); +       }); +     </doc:scenario> +   </doc:example> + */ + + +numberFilter.$inject = ['$locale']; +function numberFilter($locale) { +  var formats = $locale.NUMBER_FORMATS; +  return function(number, fractionSize) { +    return formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP, +      fractionSize); +  }; +} + +var DECIMAL_SEP = '.'; +function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { +  if (isNaN(number) || !isFinite(number)) return ''; + +  var isNegative = number < 0; +  number = Math.abs(number); +  var numStr = number + '', +      formatedText = '', +      parts = []; + +  if (numStr.indexOf('e') !== -1) { +    formatedText = numStr; +  } else { +    var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length; + +    // determine fractionSize if it is not specified +    if (isUndefined(fractionSize)) { +      fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac); +    } + +    var pow = Math.pow(10, fractionSize); +    number = Math.round(number * pow) / pow; +    var fraction = ('' + number).split(DECIMAL_SEP); +    var whole = fraction[0]; +    fraction = fraction[1] || ''; + +    var pos = 0, +        lgroup = pattern.lgSize, +        group = pattern.gSize; + +    if (whole.length >= (lgroup + group)) { +      pos = whole.length - lgroup; +      for (var i = 0; i < pos; i++) { +        if ((pos - i)%group === 0 && i !== 0) { +          formatedText += groupSep; +        } +        formatedText += whole.charAt(i); +      } +    } + +    for (i = pos; i < whole.length; i++) { +      if ((whole.length - i)%lgroup === 0 && i !== 0) { +        formatedText += groupSep; +      } +      formatedText += whole.charAt(i); +    } + +    // format fraction part. +    while(fraction.length < fractionSize) { +      fraction += '0'; +    } + +    if (fractionSize) formatedText += decimalSep + fraction.substr(0, fractionSize); +  } + +  parts.push(isNegative ? pattern.negPre : pattern.posPre); +  parts.push(formatedText); +  parts.push(isNegative ? pattern.negSuf : pattern.posSuf); +  return parts.join(''); +} + +function padNumber(num, digits, trim) { +  var neg = ''; +  if (num < 0) { +    neg =  '-'; +    num = -num; +  } +  num = '' + num; +  while(num.length < digits) num = '0' + num; +  if (trim) +    num = num.substr(num.length - digits); +  return neg + num; +} + + +function dateGetter(name, size, offset, trim) { +  return function(date) { +    var value = date['get' + name](); +    if (offset > 0 || value > -offset) +      value += offset; +    if (value === 0 && offset == -12 ) value = 12; +    return padNumber(value, size, trim); +  }; +} + +function dateStrGetter(name, shortForm) { +  return function(date, formats) { +    var value = date['get' + name](); +    var get = uppercase(shortForm ? ('SHORT' + name) : name); + +    return formats[get][value]; +  }; +} + +function timeZoneGetter(date) { +  var offset = date.getTimezoneOffset(); +  return padNumber(offset / 60, 2) + padNumber(Math.abs(offset % 60), 2); +} + +function ampmGetter(date, formats) { +  return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1]; +} + +var DATE_FORMATS = { +  yyyy: dateGetter('FullYear', 4), +    yy: dateGetter('FullYear', 2, 0, true), +     y: dateGetter('FullYear', 1), +  MMMM: dateStrGetter('Month'), +   MMM: dateStrGetter('Month', true), +    MM: dateGetter('Month', 2, 1), +     M: dateGetter('Month', 1, 1), +    dd: dateGetter('Date', 2), +     d: dateGetter('Date', 1), +    HH: dateGetter('Hours', 2), +     H: dateGetter('Hours', 1), +    hh: dateGetter('Hours', 2, -12), +     h: dateGetter('Hours', 1, -12), +    mm: dateGetter('Minutes', 2), +     m: dateGetter('Minutes', 1), +    ss: dateGetter('Seconds', 2), +     s: dateGetter('Seconds', 1), +  EEEE: dateStrGetter('Day'), +   EEE: dateStrGetter('Day', true), +     a: ampmGetter, +     Z: timeZoneGetter +}; + +var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/, +    NUMBER_STRING = /^\d+$/; + +/** + * @ngdoc filter + * @name angular.module.ng.$filter.date + * @function + * + * @description + *   Formats `date` to a string based on the requested `format`. + * + *   `format` string can be composed of the following elements: + * + *   * `'yyyy'`: 4 digit representation of year (e.g. AD 1 => 0001, AD 2010 => 2010) + *   * `'yy'`: 2 digit representation of year, padded (00-99). (e.g. AD 2001 => 01, AD 2010 => 10) + *   * `'y'`: 1 digit representation of year, e.g. (AD 1 => 1, AD 199 => 199) + *   * `'MMMM'`: Month in year (January-December) + *   * `'MMM'`: Month in year (Jan-Dec) + *   * `'MM'`: Month in year, padded (01-12) + *   * `'M'`: Month in year (1-12) + *   * `'dd'`: Day in month, padded (01-31) + *   * `'d'`: Day in month (1-31) + *   * `'EEEE'`: Day in Week,(Sunday-Saturday) + *   * `'EEE'`: Day in Week, (Sun-Sat) + *   * `'HH'`: Hour in day, padded (00-23) + *   * `'H'`: Hour in day (0-23) + *   * `'hh'`: Hour in am/pm, padded (01-12) + *   * `'h'`: Hour in am/pm, (1-12) + *   * `'mm'`: Minute in hour, padded (00-59) + *   * `'m'`: Minute in hour (0-59) + *   * `'ss'`: Second in minute, padded (00-59) + *   * `'s'`: Second in minute (0-59) + *   * `'a'`: am/pm marker + *   * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-1200) + * + *   `format` string can also be one of the following predefined + *   {@link guide/dev_guide.i18n localizable formats}: + * + *   * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale + *     (e.g. Sep 3, 2010 12:05:08 pm) + *   * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US  locale (e.g. 9/3/10 12:05 pm) + *   * `'fullDate'`: equivalent to `'EEEE, MMMM d,y'` for en_US  locale + *     (e.g. Friday, September 3, 2010) + *   * `'longDate'`: equivalent to `'MMMM d, y'` for en_US  locale (e.g. September 3, 2010 + *   * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US  locale (e.g. Sep 3, 2010) + *   * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10) + *   * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 pm) + *   * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 pm) + * + *   `format` string can contain literal values. These need to be quoted with single quotes (e.g. + *   `"h 'in the morning'"`). In order to output single quote, use two single quotes in a sequence + *   (e.g. `"h o''clock"`). + * + * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or + *    number) or ISO 8601 extended datetime string (yyyy-MM-ddTHH:mm:ss.SSSZ). + * @param {string=} format Formatting rules (see Description). If not specified, + *    `mediumDate` is used. + * @returns {string} Formatted string or the input if input is not recognized as date/millis. + * + * @example +   <doc:example> +     <doc:source> +       <span ng-non-bindable>{{1288323623006 | date:'medium'}}</span>: +           {{1288323623006 | date:'medium'}}<br> +       <span ng-non-bindable>{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}</span>: +          {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}<br> +       <span ng-non-bindable>{{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}</span>: +          {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}<br> +     </doc:source> +     <doc:scenario> +       it('should format date', function() { +         expect(binding("1288323623006 | date:'medium'")). +            toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/); +         expect(binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")). +            toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} \-?\d{4}/); +         expect(binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")). +            toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); +       }); +     </doc:scenario> +   </doc:example> + */ +dateFilter.$inject = ['$locale']; +function dateFilter($locale) { +  return function(date, format) { +    var text = '', +        parts = [], +        fn, match; + +    format = format || 'mediumDate' +    format = $locale.DATETIME_FORMATS[format] || format; +    if (isString(date)) { +      if (NUMBER_STRING.test(date)) { +        date = int(date); +      } else { +        date = jsonStringToDate(date); +      } +    } + +    if (isNumber(date)) { +      date = new Date(date); +    } + +    if (!isDate(date)) { +      return date; +    } + +    while(format) { +      match = DATE_FORMATS_SPLIT.exec(format); +      if (match) { +        parts = concat(parts, match, 1); +        format = parts.pop(); +      } else { +        parts.push(format); +        format = null; +      } +    } + +    forEach(parts, function(value){ +      fn = DATE_FORMATS[value]; +      text += fn ? fn(date, $locale.DATETIME_FORMATS) +                 : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); +    }); + +    return text; +  }; +} + + +/** + * @ngdoc filter + * @name angular.module.ng.$filter.json + * @function + * + * @description + *   Allows you to convert a JavaScript object into JSON string. + * + *   This filter is mostly useful for debugging. When using the double curly {{value}} notation + *   the binding is automatically converted to JSON. + * + * @param {*} object Any JavaScript object (including arrays and primitive types) to filter. + * @returns {string} JSON string. + * + * @css ng-monospace Always applied to the encapsulating element. + * + * @example: +   <doc:example> +     <doc:source> +       <pre>{{ {'name':'value'} | json }}</pre> +     </doc:source> +     <doc:scenario> +       it('should jsonify filtered objects', function() { +         expect(binding("{'name':'value'}")).toBe('{\n  "name":"value"}'); +       }); +     </doc:scenario> +   </doc:example> + * + */ +function jsonFilter() { +  return function(object) { +    return toJson(object, true); +  }; +} + + +/** + * @ngdoc filter + * @name angular.module.ng.$filter.lowercase + * @function + * @description + * Converts string to lowercase. + * @see angular.lowercase + */ +var lowercaseFilter = valueFn(lowercase); + + +/** + * @ngdoc filter + * @name angular.module.ng.$filter.uppercase + * @function + * @description + * Converts string to uppercase. + * @see angular.uppercase + */ +var uppercaseFilter = valueFn(uppercase); + + +/** + * @ngdoc filter + * @name angular.module.ng.$filter.linky + * @function + * + * @description + *   Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and + *   plain email address links. + * + * @param {string} text Input text. + * @returns {string} Html-linkified text. + * + * @example +   <doc:example> +     <doc:source> +       <script> +         function Ctrl($scope) { +           $scope.snippet = +             'Pretty text with some links:\n'+ +             'http://angularjs.org/,\n'+ +             'mailto:us@somewhere.org,\n'+ +             'another@somewhere.org,\n'+ +             'and one more: ftp://127.0.0.1/.'; +         } +       </script> +       <div ng-controller="Ctrl"> +       Snippet: <textarea ng-model="snippet" ng-model-instant cols="60" rows="3"></textarea> +       <table> +         <tr> +           <td>Filter</td> +           <td>Source</td> +           <td>Rendered</td> +         </tr> +         <tr id="linky-filter"> +           <td>linky filter</td> +           <td> +             <pre><div ng-bind-html="snippet | linky"><br></div></pre> +           </td> +           <td> +             <div ng-bind-html="snippet | linky"></div> +           </td> +         </tr> +         <tr id="escaped-html"> +           <td>no filter</td> +           <td><pre><div ng-bind="snippet"><br></div></pre></td> +           <td><div ng-bind="snippet"></div></td> +         </tr> +       </table> +     </doc:source> +     <doc:scenario> +       it('should linkify the snippet with urls', function() { +         expect(using('#linky-filter').binding('snippet | linky')). +           toBe('Pretty text with some links:
' + +                '<a href="http://angularjs.org/">http://angularjs.org/</a>,
' + +                '<a href="mailto:us@somewhere.org">us@somewhere.org</a>,
' + +                '<a href="mailto:another@somewhere.org">another@somewhere.org</a>,
' + +                'and one more: <a href="ftp://127.0.0.1/">ftp://127.0.0.1/</a>.'); +       }); + +       it ('should not linkify snippet without the linky filter', function() { +         expect(using('#escaped-html').binding('snippet')). +           toBe("Pretty text with some links:\n" + +                "http://angularjs.org/,\n" + +                "mailto:us@somewhere.org,\n" + +                "another@somewhere.org,\n" + +                "and one more: ftp://127.0.0.1/."); +       }); + +       it('should update', function() { +         input('snippet').enter('new http://link.'); +         expect(using('#linky-filter').binding('snippet | linky')). +           toBe('new <a href="http://link">http://link</a>.'); +         expect(using('#escaped-html').binding('snippet')).toBe('new http://link.'); +       }); +     </doc:scenario> +   </doc:example> + */ +function linkyFilter() { +  var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/, +      MAILTO_REGEXP = /^mailto:/; + +  return function(text) { +    if (!text) return text; +    var match; +    var raw = text; +    var html = []; +    var writer = htmlSanitizeWriter(html); +    var url; +    var i; +    while ((match = raw.match(LINKY_URL_REGEXP))) { +      // We can not end in these as they are sometimes found at the end of the sentence +      url = match[0]; +      // if we did not match ftp/http/mailto then assume mailto +      if (match[2] == match[3]) url = 'mailto:' + url; +      i = match.index; +      writer.chars(raw.substr(0, i)); +      writer.start('a', {href:url}); +      writer.chars(match[0].replace(MAILTO_REGEXP, '')); +      writer.end('a'); +      raw = raw.substring(i + match[0].length); +    } +    writer.chars(raw); +    return html.join(''); +  }; +}; diff --git a/src/ng/filter/limitTo.js b/src/ng/filter/limitTo.js new file mode 100644 index 00000000..4928fb9a --- /dev/null +++ b/src/ng/filter/limitTo.js @@ -0,0 +1,87 @@ +'use strict'; + +/** + * @ngdoc function + * @name angular.module.ng.$filter.limitTo + * @function + * + * @description + * Creates a new array containing only a specified number of elements in an array. The elements + * are taken from either the beginning or the end of the source array, as specified by the + * value and sign (positive or negative) of `limit`. + * + * Note: This function is used to augment the `Array` type in Angular expressions. See + * {@link angular.module.ng.$filter} for more information about Angular arrays. + * + * @param {Array} array Source array to be limited. + * @param {string|Number} limit The length of the returned array. If the `limit` number is + *     positive, `limit` number of items from the beginning of the source array are copied. + *     If the number is negative, `limit` number  of items from the end of the source array are + *     copied. The `limit` will be trimmed if it exceeds `array.length` + * @returns {Array} A new sub-array of length `limit` or less if input array had less than `limit` + *     elements. + * + * @example +   <doc:example> +     <doc:source> +       <script> +         function Ctrl($scope) { +           $scope.numbers = [1,2,3,4,5,6,7,8,9]; +           $scope.limit = 3; +         } +       </script> +       <div ng-controller="Ctrl"> +         Limit {{numbers}} to: <input type="integer" ng-model="limit"/> +         <p>Output: {{ numbers | limitTo:limit | json }}</p> +       </div> +     </doc:source> +     <doc:scenario> +       it('should limit the numer array to first three items', function() { +         expect(element('.doc-example-live input[ng-model=limit]').val()).toBe('3'); +         expect(binding('numbers | limitTo:limit | json')).toEqual('[1,2,3]'); +       }); + +       it('should update the output when -3 is entered', function() { +         input('limit').enter(-3); +         expect(binding('numbers | limitTo:limit | json')).toEqual('[7,8,9]'); +       }); + +       it('should not exceed the maximum size of input array', function() { +         input('limit').enter(100); +         expect(binding('numbers | limitTo:limit | json')).toEqual('[1,2,3,4,5,6,7,8,9]'); +       }); +     </doc:scenario> +   </doc:example> + */ +function limitToFilter(){ +  return function(array, limit) { +    if (!(array instanceof Array)) return array; +    limit = int(limit); +    var out = [], +      i, n; + +    // check that array is iterable +    if (!array || !(array instanceof Array)) +      return out; + +    // if abs(limit) exceeds maximum length, trim it +    if (limit > array.length) +      limit = array.length; +    else if (limit < -array.length) +      limit = -array.length; + +    if (limit > 0) { +      i = 0; +      n = limit; +    } else { +      i = array.length + limit; +      n = array.length; +    } + +    for (; i<n; i++) { +      out.push(array[i]); +    } + +    return out; +  } +} diff --git a/src/ng/filter/orderBy.js b/src/ng/filter/orderBy.js new file mode 100644 index 00000000..3f4fe395 --- /dev/null +++ b/src/ng/filter/orderBy.js @@ -0,0 +1,137 @@ +'use strict'; + +/** + * @ngdoc function + * @name angular.module.ng.$filter.orderBy + * @function + * + * @description + * Orders a specified `array` by the `expression` predicate. + * + * Note: this function is used to augment the `Array` type in Angular expressions. See + * {@link angular.module.ng.$filter} for more informaton about Angular arrays. + * + * @param {Array} array The array to sort. + * @param {function(*)|string|Array.<(function(*)|string)>} expression A predicate to be + *    used by the comparator to determine the order of elements. + * + *    Can be one of: + * + *    - `function`: Getter function. The result of this function will be sorted using the + *      `<`, `=`, `>` operator. + *    - `string`: An Angular expression which evaluates to an object to order by, such as 'name' + *      to sort by a property called 'name'. Optionally prefixed with `+` or `-` to control + *      ascending or descending sort order (for example, +name or -name). + *    - `Array`: An array of function or string predicates. The first predicate in the array + *      is used for sorting, but when two items are equivalent, the next predicate is used. + * + * @param {boolean=} reverse Reverse the order the array. + * @returns {Array} Sorted copy of the source array. + * + * @example +   <doc:example> +     <doc:source> +       <script> +         function Ctrl($scope) { +           $scope.friends = +               [{name:'John', phone:'555-1212', age:10}, +                {name:'Mary', phone:'555-9876', age:19}, +                {name:'Mike', phone:'555-4321', age:21}, +                {name:'Adam', phone:'555-5678', age:35}, +                {name:'Julie', phone:'555-8765', age:29}] +           $scope.predicate = '-age'; +         } +       </script> +       <div ng-controller="Ctrl"> +         <pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre> +         <hr/> +         [ <a href="" ng-click="predicate=''">unsorted</a> ] +         <table class="friend"> +           <tr> +             <th><a href="" ng-click="predicate = 'name'; reverse=false">Name</a> +                 (<a href ng-click="predicate = '-name'; reverse=false">^</a>)</th> +             <th><a href="" ng-click="predicate = 'phone'; reverse=!reverse">Phone Number</a></th> +             <th><a href="" ng-click="predicate = 'age'; reverse=!reverse">Age</a></th> +           <tr> +           <tr ng-repeat="friend in friends | orderBy:predicate:reverse"> +             <td>{{friend.name}}</td> +             <td>{{friend.phone}}</td> +             <td>{{friend.age}}</td> +           <tr> +         </table> +       </div> +     </doc:source> +     <doc:scenario> +       it('should be reverse ordered by aged', function() { +         expect(binding('predicate')).toBe('-age'); +         expect(repeater('table.friend', 'friend in friends').column('friend.age')). +           toEqual(['35', '29', '21', '19', '10']); +         expect(repeater('table.friend', 'friend in friends').column('friend.name')). +           toEqual(['Adam', 'Julie', 'Mike', 'Mary', 'John']); +       }); + +       it('should reorder the table when user selects different predicate', function() { +         element('.doc-example-live a:contains("Name")').click(); +         expect(repeater('table.friend', 'friend in friends').column('friend.name')). +           toEqual(['Adam', 'John', 'Julie', 'Mary', 'Mike']); +         expect(repeater('table.friend', 'friend in friends').column('friend.age')). +           toEqual(['35', '10', '29', '19', '21']); + +         element('.doc-example-live a:contains("Phone")').click(); +         expect(repeater('table.friend', 'friend in friends').column('friend.phone')). +           toEqual(['555-9876', '555-8765', '555-5678', '555-4321', '555-1212']); +         expect(repeater('table.friend', 'friend in friends').column('friend.name')). +           toEqual(['Mary', 'Julie', 'Adam', 'Mike', 'John']); +       }); +     </doc:scenario> +   </doc:example> + */ +orderByFilter.$inject = ['$parse']; +function orderByFilter($parse){ +  return function(array, sortPredicate, reverseOrder) { +    if (!(array instanceof Array)) return array; +    if (!sortPredicate) return array; +    sortPredicate = isArray(sortPredicate) ? sortPredicate: [sortPredicate]; +    sortPredicate = map(sortPredicate, function(predicate){ +      var descending = false, get = predicate || identity; +      if (isString(predicate)) { +        if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) { +          descending = predicate.charAt(0) == '-'; +          predicate = predicate.substring(1); +        } +        get = $parse(predicate); +      } +      return reverseComparator(function(a,b){ +        return compare(get(a),get(b)); +      }, descending); +    }); +    var arrayCopy = []; +    for ( var i = 0; i < array.length; i++) { arrayCopy.push(array[i]); } +    return arrayCopy.sort(reverseComparator(comparator, reverseOrder)); + +    function comparator(o1, o2){ +      for ( var i = 0; i < sortPredicate.length; i++) { +        var comp = sortPredicate[i](o1, o2); +        if (comp !== 0) return comp; +      } +      return 0; +    } +    function reverseComparator(comp, descending) { +      return toBoolean(descending) +          ? function(a,b){return comp(b,a);} +          : comp; +    } +    function compare(v1, v2){ +      var t1 = typeof v1; +      var t2 = typeof v2; +      if (t1 == t2) { +        if (t1 == "string") v1 = v1.toLowerCase(); +        if (t1 == "string") v2 = v2.toLowerCase(); +        if (v1 === v2) return 0; +        return v1 < v2 ? -1 : 1; +      } else { +        return t1 < t2 ? -1 : 1; +      } +    } +  } +} diff --git a/src/ng/http.js b/src/ng/http.js new file mode 100644 index 00000000..c2cbd161 --- /dev/null +++ b/src/ng/http.js @@ -0,0 +1,743 @@ +'use strict'; +'use strict'; + +/** + * Parse headers into key value object + * + * @param {string} headers Raw headers as a string + * @returns {Object} Parsed headers as key value object + */ +function parseHeaders(headers) { +  var parsed = {}, key, val, i; + +  if (!headers) return parsed; + +  forEach(headers.split('\n'), function(line) { +    i = line.indexOf(':'); +    key = lowercase(trim(line.substr(0, i))); +    val = trim(line.substr(i + 1)); + +    if (key) { +      if (parsed[key]) { +        parsed[key] += ', ' + val; +      } else { +        parsed[key] = val; +      } +    } +  }); + +  return parsed; +} + + +/** + * Returns a function that provides access to parsed headers. + * + * Headers are lazy parsed when first requested. + * @see parseHeaders + * + * @param {(string|Object)} headers Headers to provide access to. + * @returns {function(string=)} Returns a getter function which if called with: + * + *   - if called with single an argument returns a single header value or null + *   - if called with no arguments returns an object containing all headers. + */ +function headersGetter(headers) { +  var headersObj = isObject(headers) ? headers : undefined; + +  return function(name) { +    if (!headersObj) headersObj =  parseHeaders(headers); + +    if (name) { +      return headersObj[lowercase(name)] || null; +    } + +    return headersObj; +  }; +} + + +/** + * Chain all given functions + * + * This function is used for both request and response transforming + * + * @param {*} data Data to transform. + * @param {function(string=)} headers Http headers getter fn. + * @param {(function|Array.<function>)} fns Function or an array of functions. + * @returns {*} Transformed data. + */ +function transformData(data, headers, fns) { +  if (isFunction(fns)) +    return fns(data, headers); + +  forEach(fns, function(fn) { +    data = fn(data, headers); +  }); + +  return data; +} + + +function isSuccess(status) { +  return 200 <= status && status < 300; +} + + +function $HttpProvider() { +  var JSON_START = /^\s*(\[|\{[^\{])/, +      JSON_END = /[\}\]]\s*$/, +      PROTECTION_PREFIX = /^\)\]\}',?\n/; + +  var $config = this.defaults = { +    // transform incoming response data +    transformResponse: function(data) { +      if (isString(data)) { +        // strip json vulnerability protection prefix +        data = data.replace(PROTECTION_PREFIX, ''); +        if (JSON_START.test(data) && JSON_END.test(data)) +          data = fromJson(data, true); +      } +      return data; +    }, + +    // transform outgoing request data +    transformRequest: function(d) { +      return isObject(d) && !isFile(d) ? toJson(d) : d; +    }, + +    // default headers +    headers: { +      common: { +        'Accept': 'application/json, text/plain, */*', +        'X-Requested-With': 'XMLHttpRequest' +      }, +      post: {'Content-Type': 'application/json'}, +      put:  {'Content-Type': 'application/json'} +    } +  }; + +  var providerResponseInterceptors = this.responseInterceptors = []; + +  this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', +      function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { + +    var defaultCache = $cacheFactory('$http'), +        responseInterceptors = []; + +    forEach(providerResponseInterceptors, function(interceptor) { +      responseInterceptors.push( +          isString(interceptor) +              ? $injector.get(interceptor) +              : $injector.invoke(interceptor) +      ); +    }); + + +    /** +     * @ngdoc function +     * @name angular.module.ng.$http +     * @requires $httpBacked +     * @requires $browser +     * @requires $cacheFactory +     * @requires $rootScope +     * @requires $q +     * @requires $injector +     * +     * @description +     * The `$http` service is a core Angular service that facilitates communication with the remote +     * HTTP servers via browser's {@link https://developer.mozilla.org/en/xmlhttprequest +     * XMLHttpRequest} object or via {@link http://en.wikipedia.org/wiki/JSONP JSONP}. +     * +     * For unit testing applications that use `$http` service, see +     * {@link angular.module.ngMock.$httpBackend $httpBackend mock}. +     * +     * For a higher level of abstraction, please check out the {@link angular.module.ng.$resource +     * $resource} service. +     * +     * The $http API is based on the {@link angular.module.ng.$q deferred/promise APIs} exposed by +     * the $q service. While for simple usage patters this doesn't matter much, for advanced usage, +     * it is important to familiarize yourself with these apis and guarantees they provide. +     * +     * +     * # General usage +     * The `$http` service is a function which takes a single argument — a configuration object — +     * that is used to generate an http request and returns  a {@link angular.module.ng.$q promise} +     * with two $http specific methods: `success` and `error`. +     * +     * <pre> +     *   $http({method: 'GET', url: '/someUrl'}). +     *     success(function(data, status, headers, config) { +     *       // this callback will be called asynchronously +     *       // when the response is available +     *     }). +     *     error(function(data, status, headers, config) { +     *       // called asynchronously if an error occurs +     *       // or server returns response with status +     *       // code outside of the <200, 400) range +     *     }); +     * </pre> +     * +     * Since the returned value of calling the $http function is a Promise object, you can also use +     * the `then` method to register callbacks, and these callbacks will receive a single argument – +     * an object representing the response. See the api signature and type info below for more +     * details. +     * +     * +     * # Shortcut methods +     * +     * Since all invocation of the $http service require definition of the http method and url and +     * POST and PUT requests require response body/data to be provided as well, shortcut methods +     * were created to simplify using the api: +     * +     * <pre> +     *   $http.get('/someUrl').success(successCallback); +     *   $http.post('/someUrl', data).success(successCallback); +     * </pre> +     * +     * Complete list of shortcut methods: +     * +     * - {@link angular.module.ng.$http#get $http.get} +     * - {@link angular.module.ng.$http#head $http.head} +     * - {@link angular.module.ng.$http#post $http.post} +     * - {@link angular.module.ng.$http#put $http.put} +     * - {@link angular.module.ng.$http#delete $http.delete} +     * - {@link angular.module.ng.$http#jsonp $http.jsonp} +     * +     * +     * # Setting HTTP Headers +     * +     * The $http service will automatically add certain http headers to all requests. These defaults +     * can be fully configured by accessing the `$httpProvider.defaults.headers` configuration +     * object, which currently contains this default configuration: +     * +     * - `$httpProvider.defaults.headers.common` (headers that are common for all requests): +     *   - `Accept: application/json, text/plain, * / *` +     *   - `X-Requested-With: XMLHttpRequest` +     * - `$httpProvider.defaults.headers.post`: (header defaults for HTTP POST requests) +     *   - `Content-Type: application/json` +     * - `$httpProvider.defaults.headers.put` (header defaults for HTTP PUT requests) +     *   - `Content-Type: application/json` +     * +     * To add or overwrite these defaults, simply add or remove a property from this configuration +     * objects. To add headers for an HTTP method other than POST or PUT, simply add a new object +     * with name equal to the lower-cased http method name, e.g. +     * `$httpProvider.defaults.headers.get['My-Header']='value'`. +     * +     * +     * # Transforming Requests and Responses +     * +     * Both requests and responses can be transformed using transform functions. By default, Angular +     * applies these transformations: +     * +     * Request transformations: +     * +     * - if the `data` property of the request config object contains an object, serialize it into +     *   JSON format. +     * +     * Response transformations: +     * +     *  - if XSRF prefix is detected, strip it (see Security Considerations section below) +     *  - if json response is detected, deserialize it using a JSON parser +     * +     * To override these transformation locally, specify transform functions as `transformRequest` +     * and/or `transformResponse` properties of the config object. To globally override the default +     * transforms, override the `$httpProvider.defaults.transformRequest` and +     * `$httpProvider.defaults.transformResponse` properties of the `$httpProvider`. +     * +     * +     * # Caching +     * +     * To enable caching set the configuration property `cache` to `true`. When the cache is +     * enabled, `$http` stores the response from the server in local cache. Next time the +     * response is served from the cache without sending a request to the server. +     * +     * Note that even if the response is served from cache, delivery of the data is asynchronous in +     * the same way that real requests are. +     * +     * If there are multiple GET requests for the same url that should be cached using the same +     * cache, but the cache is not populated yet, only one request to the server will be made and +     * the remaining requests will be fulfilled using the response for the first request. +     * +     * +     * # Response interceptors +     * +     * Before you start creating interceptors, be sure to understand the +     * {@link angular.module.ng.$q $q and deferred/promise APIs}. +     * +     * For purposes of global error handling, authentication or any kind of synchronous or +     * asynchronous preprocessing of received responses, it is desirable to be able to intercept +     * responses for http requests before they are handed over to the application code that +     * initiated these requests. The response interceptors leverage the {@link angular.module.ng.$q +     * promise apis} to fulfil this need for both synchronous and asynchronous preprocessing. +     * +     * The interceptors are service factories that are registered with the $httpProvider by +     * adding them to the `$httpProvider.responseInterceptors` array. The factory is called and +     * injected with dependencies (if specified) and returns the interceptor  — a function that +     * takes a {@link angular.module.ng.$q promise} and returns the original or a new promise. +     * +     * <pre> +     *   // register the interceptor as a service +     *   $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) { +     *     return function(promise) { +     *       return promise.then(function(response) { +     *         // do something on success +     *       }, function(response) { +     *         // do something on error +     *         if (canRecover(response)) { +     *           return responseOrNewPromise +     *         } +     *         return $q.reject(response); +     *       }); +     *     } +     *   }); +     * +     *   $httpProvider.responseInterceptors.push('myHttpInterceptor'); +     * +     * +     *   // register the interceptor via an anonymous factory +     *   $httpProvider.responseInterceptors.push(function($q, dependency1, dependency2) { +     *     return function(promise) { +     *       // same as above +     *     } +     *   }); +     * </pre> +     * +     * +     * # Security Considerations +     * +     * When designing web applications, consider security threats from: +     * +     * - {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx +     *   JSON Vulnerability} +     * - {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF} +     * +     * Both server and the client must cooperate in order to eliminate these threats. Angular comes +     * pre-configured with strategies that address these issues, but for this to work backend server +     * cooperation is required. +     * +     * ## JSON Vulnerability Protection +     * +     * A {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx +     * JSON Vulnerability} allows third party web-site to turn your JSON resource URL into +     * {@link http://en.wikipedia.org/wiki/JSON#JSONP JSONP} request under some conditions. To +     * counter this your server can prefix all JSON requests with following string `")]}',\n"`. +     * Angular will automatically strip the prefix before processing it as JSON. +     * +     * For example if your server needs to return: +     * <pre> +     * ['one','two'] +     * </pre> +     * +     * which is vulnerable to attack, your server can return: +     * <pre> +     * )]}', +     * ['one','two'] +     * </pre> +     * +     * Angular will strip the prefix, before processing the JSON. +     * +     * +     * ## Cross Site Request Forgery (XSRF) Protection +     * +     * {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF} is a technique by which +     * an unauthorized site can gain your user's private data. Angular provides following mechanism +     * to counter XSRF. When performing XHR requests, the $http service reads a token from a cookie +     * called `XSRF-TOKEN` and sets it as the HTTP header `X-XSRF-TOKEN`. Since only JavaScript that +     * runs on your domain could read the cookie, your server can be assured that the XHR came from +     * JavaScript running on your domain. +     * +     * To take advantage of this, your server needs to set a token in a JavaScript readable session +     * cookie called `XSRF-TOKEN` on first HTTP GET request. On subsequent non-GET requests the +     * server can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure +     * that only JavaScript running on your domain could have read the token. The token must be +     * unique for each user and must be verifiable by the server (to prevent the JavaScript making +     * up its own tokens). We recommend that the token is a digest of your site's authentication +     * cookie with {@link http://en.wikipedia.org/wiki/Rainbow_table salt for added security}. +     * +     * +     * @param {object} config Object describing the request to be made and how it should be +     *    processed. The object has following properties: +     * +     *    - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) +     *    - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. +     *    - **params** – `{Object.<string|Object>}` – Map of strings or objects which will be turned to +     *      `?key1=value1&key2=value2` after the url. If the value is not a string, it will be JSONified. +     *    - **data** – `{string|Object}` – Data to be sent as the request message data. +     *    - **headers** – `{Object}` – Map of strings representing HTTP headers to send to the server. +     *    - **transformRequest** – `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` – +     *      transform function or an array of such functions. The transform function takes the http +     *      request body and headers and returns its transformed (typically serialized) version. +     *    - **transformResponse** – `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` – +     *      transform function or an array of such functions. The transform function takes the http +     *      response body and headers and returns its transformed (typically deserialized) version. +     *    - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the +     *      GET request, otherwise if a cache instance built with +     *      {@link angular.module.ng.$cacheFactory $cacheFactory}, this cache will be used for +     *      caching. +     *    - **timeout** – `{number}` – timeout in milliseconds. +     * +     * @returns {HttpPromise} Returns a {@link angular.module.ng.$q promise} object with the +     *   standard `then` method and two http specific methods: `success` and `error`. The `then` +     *   method takes two arguments a success and an error callback which will be called with a +     *   response object. The `success` and `error` methods take a single argument - a function that +     *   will be called when the request succeeds or fails respectively. The arguments passed into +     *   these functions are destructured representation of the response object passed into the +     *   `then` method. The response object has these properties: +     * +     *   - **data** – `{string|Object}` – The response body transformed with the transform functions. +     *   - **status** – `{number}` – HTTP status code of the response. +     *   - **headers** – `{function([headerName])}` – Header getter function. +     *   - **config** – `{Object}` – The configuration object that was used to generate the request. +     * +     * @property {Array.<Object>} pendingRequests Array of config objects for currently pending +     *   requests. This is primarily meant to be used for debugging purposes. +     * +     * +     * @example +        <doc:example> +          <doc:source jsfiddle="false"> +            <script> +              function FetchCtrl($scope, $http) { +                $scope.method = 'GET'; +                $scope.url = 'examples/http-hello.html'; + +                $scope.fetch = function() { +                  $scope.code = null; +                  $scope.response = null; + +                  $http({method: $scope.method, url: $scope.url}). +                    success(function(data, status) { +                      $scope.status = status; +                      $scope.data = data; +                    }). +                    error(function(data, status) { +                      $scope.data = data || "Request failed"; +                      $scope.status = status; +                  }); +                }; + +                $scope.updateModel = function(method, url) { +                  $scope.method = method; +                  $scope.url = url; +                }; +              } +            </script> +            <div ng-controller="FetchCtrl"> +              <select ng-model="method"> +                <option>GET</option> +                <option>JSONP</option> +              </select> +              <input type="text" ng-model="url" size="80"/> +              <button ng-click="fetch()">fetch</button><br> +              <button ng-click="updateModel('GET', 'examples/http-hello.html')">Sample GET</button> +              <button ng-click="updateModel('JSONP', 'http://angularjs.org/greet.php?callback=JSON_CALLBACK&name=Super%20Hero')">Sample JSONP</button> +              <button ng-click="updateModel('JSONP', 'http://angularjs.org/doesntexist&callback=JSON_CALLBACK')">Invalid JSONP</button> +              <pre>http status code: {{status}}</pre> +              <pre>http response data: {{data}}</pre> +            </div> +          </doc:source> +          <doc:scenario> +            it('should make an xhr GET request', function() { +              element(':button:contains("Sample GET")').click(); +              element(':button:contains("fetch")').click(); +              expect(binding('status')).toBe('200'); +              expect(binding('data')).toBe('Hello, $http!\n'); +            }); + +            it('should make a JSONP request to angularjs.org', function() { +              element(':button:contains("Sample JSONP")').click(); +              element(':button:contains("fetch")').click(); +              expect(binding('status')).toBe('200'); +              expect(binding('data')).toMatch(/Super Hero!/); +            }); + +            it('should make JSONP request to invalid URL and invoke the error handler', +                function() { +              element(':button:contains("Invalid JSONP")').click(); +              element(':button:contains("fetch")').click(); +              expect(binding('status')).toBe('0'); +              expect(binding('data')).toBe('Request failed'); +            }); +          </doc:scenario> +        </doc:example> +     */ +    function $http(config) { +      config.method = uppercase(config.method); + +      var reqTransformFn = config.transformRequest || $config.transformRequest, +          respTransformFn = config.transformResponse || $config.transformResponse, +          defHeaders = $config.headers, +          reqHeaders = extend({'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN']}, +              defHeaders.common, defHeaders[lowercase(config.method)], config.headers), +          reqData = transformData(config.data, headersGetter(reqHeaders), reqTransformFn), +          promise; + +      // strip content-type if data is undefined +      if (isUndefined(config.data)) { +        delete reqHeaders['Content-Type']; +      } + +      // send request +      promise = sendReq(config, reqData, reqHeaders); + + +      // transform future response +      promise = promise.then(transformResponse, transformResponse); + +      // apply interceptors +      forEach(responseInterceptors, function(interceptor) { +        promise = interceptor(promise); +      }); + +      promise.success = function(fn) { +        promise.then(function(response) { +          fn(response.data, response.status, response.headers, config); +        }); +        return promise; +      }; + +      promise.error = function(fn) { +        promise.then(null, function(response) { +          fn(response.data, response.status, response.headers, config); +        }); +        return promise; +      }; + +      return promise; + +      function transformResponse(response) { +        // make a copy since the response must be cacheable +        var resp = extend({}, response, { +          data: transformData(response.data, response.headers, respTransformFn) +        }); +        return (isSuccess(response.status)) +          ? resp +          : $q.reject(resp); +      } +    } + +    $http.pendingRequests = []; + +    /** +     * @ngdoc method +     * @name angular.module.ng.$http#get +     * @methodOf angular.module.ng.$http +     * +     * @description +     * Shortcut method to perform `GET` request +     * +     * @param {string} url Relative or absolute URL specifying the destination of the request +     * @param {Object=} config Optional configuration object +     * @returns {HttpPromise} Future object +     */ + +    /** +     * @ngdoc method +     * @name angular.module.ng.$http#delete +     * @methodOf angular.module.ng.$http +     * +     * @description +     * Shortcut method to perform `DELETE` request +     * +     * @param {string} url Relative or absolute URL specifying the destination of the request +     * @param {Object=} config Optional configuration object +     * @returns {HttpPromise} Future object +     */ + +    /** +     * @ngdoc method +     * @name angular.module.ng.$http#head +     * @methodOf angular.module.ng.$http +     * +     * @description +     * Shortcut method to perform `HEAD` request +     * +     * @param {string} url Relative or absolute URL specifying the destination of the request +     * @param {Object=} config Optional configuration object +     * @returns {XhrFuture} Future object +     */ + +    /** +     * @ngdoc method +     * @name angular.module.ng.$http#jsonp +     * @methodOf angular.module.ng.$http +     * +     * @description +     * Shortcut method to perform `JSONP` request +     * +     * @param {string} url Relative or absolute URL specifying the destination of the request. +     *                     Should contain `JSON_CALLBACK` string. +     * @param {Object=} config Optional configuration object +     * @returns {XhrFuture} Future object +     */ +    createShortMethods('get', 'delete', 'head', 'jsonp'); + +    /** +     * @ngdoc method +     * @name angular.module.ng.$http#post +     * @methodOf angular.module.ng.$http +     * +     * @description +     * Shortcut method to perform `POST` request +     * +     * @param {string} url Relative or absolute URL specifying the destination of the request +     * @param {*} data Request content +     * @param {Object=} config Optional configuration object +     * @returns {HttpPromise} Future object +     */ + +    /** +     * @ngdoc method +     * @name angular.module.ng.$http#put +     * @methodOf angular.module.ng.$http +     * +     * @description +     * Shortcut method to perform `PUT` request +     * +     * @param {string} url Relative or absolute URL specifying the destination of the request +     * @param {*} data Request content +     * @param {Object=} config Optional configuration object +     * @returns {XhrFuture} Future object +     */ +    createShortMethodsWithData('post', 'put'); + + +    return $http; + + +    function createShortMethods(names) { +      forEach(arguments, function(name) { +        $http[name] = function(url, config) { +          return $http(extend(config || {}, { +            method: name, +            url: url +          })); +        }; +      }); +    } + + +    function createShortMethodsWithData(name) { +      forEach(arguments, function(name) { +        $http[name] = function(url, data, config) { +          return $http(extend(config || {}, { +            method: name, +            url: url, +            data: data +          })); +        }; +      }); +    } + + +    /** +     * Makes the request +     * +     * !!! ACCESSES CLOSURE VARS: +     * $httpBackend, $config, $log, $rootScope, defaultCache, $http.pendingRequests +     */ +    function sendReq(config, reqData, reqHeaders) { +      var deferred = $q.defer(), +          promise = deferred.promise, +          cache, +          cachedResp, +          url = buildUrl(config.url, config.params); + +      $http.pendingRequests.push(config); +      promise.then(removePendingReq, removePendingReq); + + +      if (config.cache && config.method == 'GET') { +        cache = isObject(config.cache) ? config.cache : defaultCache; +      } + +      if (cache) { +        cachedResp = cache.get(url); +        if (cachedResp) { +          if (cachedResp.then) { +            // cached request has already been sent, but there is no response yet +            cachedResp.then(removePendingReq, removePendingReq); +            return cachedResp; +          } else { +            // serving from cache +            if (isArray(cachedResp)) { +              resolvePromise(cachedResp[1], cachedResp[0], copy(cachedResp[2])); +            } else { +              resolvePromise(cachedResp, 200, {}); +            } +          } +        } else { +          // put the promise for the non-transformed response into cache as a placeholder +          cache.put(url, promise); +        } +      } + +      // if we won't have the response in cache, send the request to the backend +      if (!cachedResp) { +        $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout); +      } + +      return promise; + + +      /** +       * Callback registered to $httpBackend(): +       *  - caches the response if desired +       *  - resolves the raw $http promise +       *  - calls $apply +       */ +      function done(status, response, headersString) { +        if (cache) { +          if (isSuccess(status)) { +            cache.put(url, [status, response, parseHeaders(headersString)]); +          } else { +            // remove promise from the cache +            cache.remove(url); +          } +        } + +        resolvePromise(response, status, headersString); +        $rootScope.$apply(); +      } + + +      /** +       * Resolves the raw $http promise. +       */ +      function resolvePromise(response, status, headers) { +        // normalize internal statuses to 0 +        status = Math.max(status, 0); + +        (isSuccess(status) ? deferred.resolve : deferred.reject)({ +          data: response, +          status: status, +          headers: headersGetter(headers), +          config: config +        }); +      } + + +      function removePendingReq() { +        var idx = indexOf($http.pendingRequests, config); +        if (idx !== -1) $http.pendingRequests.splice(idx, 1); +      } +    } + + +    function buildUrl(url, params) { +          if (!params) return url; +          var parts = []; +          forEachSorted(params, function(value, key) { +            if (value == null || value == undefined) return; +            if (isObject(value)) { +              value = toJson(value); +            } +            parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); +          }); +          return url + ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); +        } + + +  }]; +} diff --git a/src/ng/httpBackend.js b/src/ng/httpBackend.js new file mode 100644 index 00000000..201d1a87 --- /dev/null +++ b/src/ng/httpBackend.js @@ -0,0 +1,99 @@ +var XHR = window.XMLHttpRequest || function() { +  try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) {} +  try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) {} +  try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {} +  throw new Error("This browser does not support XMLHttpRequest."); +}; + + +/** + * @ngdoc object + * @name angular.module.ng.$httpBackend + * @requires $browser + * @requires $window + * @requires $document + * + * @description + * HTTP backend used by the {@link angular.module.ng.$http service} that delegates to + * XMLHttpRequest object or JSONP and deals with browser incompatibilities. + * + * You should never need to use this service directly, instead use the higher-level abstractions: + * {@link angular.module.ng.$http $http} or {@link angular.module.ng.$resource $resource}. + * + * During testing this implementation is swapped with {@link angular.module.ngMock.$httpBackend mock + * $httpBackend} which can be trained with responses. + */ +function $HttpBackendProvider() { +  this.$get = ['$browser', '$window', '$document', function($browser, $window, $document) { +    return createHttpBackend($browser, XHR, $browser.defer, $window.angular.callbacks, +        $document[0].body, $window.location.protocol.replace(':', '')); +  }]; +} + +function createHttpBackend($browser, XHR, $browserDefer, callbacks, body, locationProtocol) { +  // TODO(vojta): fix the signature +  return function(method, url, post, callback, headers, timeout) { +    $browser.$$incOutstandingRequestCount(); +    url = url || $browser.url(); + +    if (lowercase(method) == 'jsonp') { +      var callbackId = '_' + (callbacks.counter++).toString(36); +      callbacks[callbackId] = function(data) { +        callbacks[callbackId].data = data; +      }; + +      var script = $browser.addJs(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId), +          function() { +        if (callbacks[callbackId].data) { +          completeRequest(callback, 200, callbacks[callbackId].data); +        } else { +          completeRequest(callback, -2); +        } +        delete callbacks[callbackId]; +        body.removeChild(script); +      }); +    } else { +      var xhr = new XHR(); +      xhr.open(method, url, true); +      forEach(headers, function(value, key) { +        if (value) xhr.setRequestHeader(key, value); +      }); + +      var status; + +      // In IE6 and 7, this might be called synchronously when xhr.send below is called and the +      // response is in the cache. the promise api will ensure that to the app code the api is +      // always async +      xhr.onreadystatechange = function() { +        if (xhr.readyState == 4) { +          completeRequest( +              callback, status || xhr.status, xhr.responseText, xhr.getAllResponseHeaders()); +        } +      }; + +      xhr.send(post || ''); + +      if (timeout > 0) { +        $browserDefer(function() { +          status = -1; +          xhr.abort(); +        }, timeout); +      } +    } + + +    function completeRequest(callback, status, response, headersString) { +      // URL_MATCH is defined in src/service/location.js +      var protocol = (url.match(URL_MATCH) || ['', locationProtocol])[1]; + +      // fix status code for file protocol (it's always 0) +      status = (protocol == 'file') ? (response ? 200 : 404) : status; + +      // normalize IE bug (http://bugs.jquery.com/ticket/1450) +      status = status == 1223 ? 204 : status; + +      callback(status, response, headersString); +      $browser.$$completeOutstandingRequest(noop); +    } +  }; +} diff --git a/src/ng/interpolate.js b/src/ng/interpolate.js new file mode 100644 index 00000000..6d3ae868 --- /dev/null +++ b/src/ng/interpolate.js @@ -0,0 +1,145 @@ +'use strict'; + +/** + * @ngdoc function + * @name angular.module.ng.$interpolateProvider + * @function + * + * @description + * + * Used for configuring the interpolation markup. Deafults to `{{` and `}}`. + */ +function $InterpolateProvider() { +  var startSymbol = '{{'; +  var endSymbol = '}}'; + +  /** +   * @ngdoc method +   * @name angular.module.ng.$interpolateProvider#startSymbol +   * @methodOf angular.module.ng.$interpolateProvider +   * @description +   * Symbol to denote start of expression in the interpolated string. Defaults to `{{`. +   * +   * @prop {string=} value new value to set the starting symbol to. +   */ +  this.startSymbol = function(value){ +    if (value) { +      startSymbol = value; +      return this; +    } else { +      return startSymbol; +    } +  }; + +  /** +   * @ngdoc method +   * @name angular.module.ng.$interpolateProvider#endSymbol +   * @methodOf angular.module.ng.$interpolateProvider +   * @description +   * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`. +   * +   * @prop {string=} value new value to set the ending symbol to. +   */ +  this.endSymbol = function(value){ +    if (value) { +      endSymbol = value; +      return this; +    } else { +      return startSymbol; +    } +  }; + + +  this.$get = ['$parse', function($parse) { +    var startSymbolLength = startSymbol.length, +        endSymbolLength = endSymbol.length; + +    /** +     * @ngdoc function +     * @name angular.module.ng.$interpolate +     * @function +     * +     * @requires $parse +     * +     * @description +     * +     * Compiles a string with markup into an interpolation function. This service is used by the +     * HTML {@link angular.module.ng.$compile $compile} service for data binding. See +     * {@link angular.module.ng.$interpolateProvider $interpolateProvider} for configuring the +     * interpolation markup. +     * +     * +       <pre> +         var $interpolate = ...; // injected +         var exp = $interpolate('Hello {{name}}!'); +         expect(exp({name:'Angular'}).toEqual('Hello Angular!'); +       </pre> +     * +     * +     * @param {string} text The text with markup to interpolate. +     * @param {boolean=} mustHaveExpression if set to true then the interpolation string must have +     *    embedded expression in order to return an interpolation function. Strings with no +     *    embedded expression will return null for the interpolation function. +     * @returns {function(context)} an interpolation function which is used to compute the interpolated +     *    string. The function has these parameters: +     * +     *    * `context`: an object against which any expressions embedded in the strings are evaluated +     *      against. +     * +     */ +    return function(text, mustHaveExpression) { +      var startIndex, +          endIndex, +          index = 0, +          parts = [], +          length = text.length, +          hasInterpolation = false, +          fn, +          exp, +          concat = []; + +      while(index < length) { +        if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) && +             ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) { +          (index != startIndex) && parts.push(text.substring(index, startIndex)); +          parts.push(fn = $parse(exp = text.substring(startIndex + startSymbolLength, endIndex))); +          fn.exp = exp; +          index = endIndex + endSymbolLength; +          hasInterpolation = true; +        } else { +          // we did not find anything, so we have to add the remainder to the parts array +          (index != length) && parts.push(text.substring(index)); +          index = length; +        } +      } + +      if (!(length = parts.length)) { +        // we added, nothing, must have been an empty string. +        parts.push(''); +        length = 1; +      } + +      if (!mustHaveExpression  || hasInterpolation) { +        concat.length = length; +        fn = function(context) { +          for(var i = 0, ii = length, part; i<ii; i++) { +            if (typeof (part = parts[i]) == 'function') { +              part = part(context); +              if (part == null || part == undefined) { +                part = ''; +              } else if (typeof part != 'string') { +                part = toJson(part); +              } +            } +            concat[i] = part; +          } +          return concat.join(''); +        }; +        fn.exp = text; +        fn.parts = parts; +        return fn; +      } +    }; +  }]; +} + diff --git a/src/ng/locale.js b/src/ng/locale.js new file mode 100644 index 00000000..4c9a989d --- /dev/null +++ b/src/ng/locale.js @@ -0,0 +1,72 @@ +'use strict'; + +/** + * @ngdoc object + * @name angular.module.ng.$locale + * + * @description + * $locale service provides localization rules for various Angular components. As of right now the + * only public api is: + * + * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) + */ +function $LocaleProvider(){ +  this.$get = function() { +    return { +      id: 'en-us', + +      NUMBER_FORMATS: { +        DECIMAL_SEP: '.', +        GROUP_SEP: ',', +        PATTERNS: [ +          { // Decimal Pattern +            minInt: 1, +            minFrac: 0, +            maxFrac: 3, +            posPre: '', +            posSuf: '', +            negPre: '-', +            negSuf: '', +            gSize: 3, +            lgSize: 3 +          },{ //Currency Pattern +            minInt: 1, +            minFrac: 2, +            maxFrac: 2, +            posPre: '\u00A4', +            posSuf: '', +            negPre: '(\u00A4', +            negSuf: ')', +            gSize: 3, +            lgSize: 3 +          } +        ], +        CURRENCY_SYM: '$' +      }, + +      DATETIME_FORMATS: { +        MONTH: 'January,February,March,April,May,June,July,August,September,October,November,December' +                .split(','), +        SHORTMONTH:  'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(','), +        DAY: 'Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday'.split(','), +        SHORTDAY: 'Sun,Mon,Tue,Wed,Thu,Fri,Sat'.split(','), +        AMPMS: ['AM','PM'], +        medium: 'MMM d, y h:mm:ss a', +        short: 'M/d/yy h:mm a', +        fullDate: 'EEEE, MMMM d, y', +        longDate: 'MMMM d, y', +        mediumDate: 'MMM d, y', +        shortDate: 'M/d/yy', +        mediumTime: 'h:mm:ss a', +        shortTime: 'h:mm a' +      }, + +      pluralCat: function(num) { +        if (num === 1) { +          return 'one'; +        } +        return 'other'; +      } +    }; +  }; +} diff --git a/src/ng/location.js b/src/ng/location.js new file mode 100644 index 00000000..1accb993 --- /dev/null +++ b/src/ng/location.js @@ -0,0 +1,556 @@ +'use strict'; + +var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?(\/[^\?#]*)?(\?([^#]*))?(#(.*))?$/, +    PATH_MATCH = /^([^\?#]*)?(\?([^#]*))?(#(.*))?$/, +    HASH_MATCH = PATH_MATCH, +    DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; + + +/** + * Encode path using encodeUriSegment, ignoring forward slashes + * + * @param {string} path Path to encode + * @returns {string} + */ +function encodePath(path) { +  var segments = path.split('/'), +      i = segments.length; + +  while (i--) { +    segments[i] = encodeUriSegment(segments[i]); +  } + +  return segments.join('/'); +} + + +function matchUrl(url, obj) { +  var match = URL_MATCH.exec(url); + +  match = { +      protocol: match[1], +      host: match[3], +      port: int(match[5]) || DEFAULT_PORTS[match[1]] || null, +      path: match[6] || '/', +      search: match[8], +      hash: match[10] +    }; + +  if (obj) { +    obj.$$protocol = match.protocol; +    obj.$$host = match.host; +    obj.$$port = match.port; +  } + +  return match; +} + + +function composeProtocolHostPort(protocol, host, port) { +  return protocol + '://' + host + (port == DEFAULT_PORTS[protocol] ? '' : ':' + port); +} + + +function pathPrefixFromBase(basePath) { +  return basePath.substr(0, basePath.lastIndexOf('/')); +} + + +function convertToHtml5Url(url, basePath, hashPrefix) { +  var match = matchUrl(url); + +  // already html5 url +  if (decodeURIComponent(match.path) != basePath || isUndefined(match.hash) || +      match.hash.indexOf(hashPrefix) !== 0) { +    return url; +  // convert hashbang url -> html5 url +  } else { +    return composeProtocolHostPort(match.protocol, match.host, match.port) + +           pathPrefixFromBase(basePath) + match.hash.substr(hashPrefix.length); +  } +} + + +function convertToHashbangUrl(url, basePath, hashPrefix) { +  var match = matchUrl(url); + +  // already hashbang url +  if (decodeURIComponent(match.path) == basePath) { +    return url; +  // convert html5 url -> hashbang url +  } else { +    var search = match.search && '?' + match.search || '', +        hash = match.hash && '#' + match.hash || '', +        pathPrefix = pathPrefixFromBase(basePath), +        path = match.path.substr(pathPrefix.length); + +    if (match.path.indexOf(pathPrefix) !== 0) { +      throw 'Invalid url "' + url + '", missing path prefix "' + pathPrefix + '" !'; +    } + +    return composeProtocolHostPort(match.protocol, match.host, match.port) + basePath + +           '#' + hashPrefix + path + search + hash; +  } +} + + +/** + * LocationUrl represents an url + * This object is exposed as $location service when HTML5 mode is enabled and supported + * + * @constructor + * @param {string} url HTML5 url + * @param {string} pathPrefix + */ +function LocationUrl(url, pathPrefix) { +  pathPrefix = pathPrefix || ''; + +  /** +   * Parse given html5 (regular) url string into properties +   * @param {string} url HTML5 url +   * @private +   */ +  this.$$parse = function(url) { +    var match = matchUrl(url, this); + +    if (match.path.indexOf(pathPrefix) !== 0) { +      throw 'Invalid url "' + url + '", missing path prefix "' + pathPrefix + '" !'; +    } + +    this.$$path = decodeURIComponent(match.path.substr(pathPrefix.length)); +    this.$$search = parseKeyValue(match.search); +    this.$$hash = match.hash && decodeURIComponent(match.hash) || ''; + +    this.$$compose(); +  }; + +  /** +   * Compose url and update `absUrl` property +   * @private +   */ +  this.$$compose = function() { +    var search = toKeyValue(this.$$search), +        hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; + +    this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; +    this.$$absUrl = composeProtocolHostPort(this.$$protocol, this.$$host, this.$$port) + +                    pathPrefix + this.$$url; +  }; + +  this.$$parse(url); +} + + +/** + * LocationHashbangUrl represents url + * This object is exposed as $location service when html5 history api is disabled or not supported + * + * @constructor + * @param {string} url Legacy url + * @param {string} hashPrefix Prefix for hash part (containing path and search) + */ +function LocationHashbangUrl(url, hashPrefix) { +  var basePath; + +  /** +   * Parse given hashbang url into properties +   * @param {string} url Hashbang url +   * @private +   */ +  this.$$parse = function(url) { +    var match = matchUrl(url, this); + +    if (match.hash && match.hash.indexOf(hashPrefix) !== 0) { +      throw 'Invalid url "' + url + '", missing hash prefix "' + hashPrefix + '" !'; +    } + +    basePath = match.path + (match.search ? '?' + match.search : ''); +    match = HASH_MATCH.exec((match.hash || '').substr(hashPrefix.length)); +    if (match[1]) { +      this.$$path = (match[1].charAt(0) == '/' ? '' : '/') + decodeURIComponent(match[1]); +    } else { +      this.$$path = ''; +    } + +    this.$$search = parseKeyValue(match[3]); +    this.$$hash = match[5] && decodeURIComponent(match[5]) || ''; + +    this.$$compose(); +  }; + +  /** +   * Compose hashbang url and update `absUrl` property +   * @private +   */ +  this.$$compose = function() { +    var search = toKeyValue(this.$$search), +        hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; + +    this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; +    this.$$absUrl = composeProtocolHostPort(this.$$protocol, this.$$host, this.$$port) + +                    basePath + (this.$$url ? '#' + hashPrefix + this.$$url : ''); +  }; + +  this.$$parse(url); +} + + +LocationUrl.prototype = { + +  /** +   * Has any change been replacing ? +   * @private +   */ +  $$replace: false, + +  /** +   * @ngdoc method +   * @name angular.module.ng.$location#absUrl +   * @methodOf angular.module.ng.$location +   * +   * @description +   * This method is getter only. +   * +   * Return full url representation with all segments encoded according to rules specified in +   * {@link http://www.ietf.org/rfc/rfc3986.txt RFC 3986}. +   * +   * @return {string} +   */ +  absUrl: locationGetter('$$absUrl'), + +  /** +   * @ngdoc method +   * @name angular.module.ng.$location#url +   * @methodOf angular.module.ng.$location +   * +   * @description +   * This method is getter / setter. +   * +   * Return url (e.g. `/path?a=b#hash`) when called without any parameter. +   * +   * Change path, search and hash, when called with parameter and return `$location`. +   * +   * @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`) +   * @return {string} +   */ +  url: function(url, replace) { +    if (isUndefined(url)) +      return this.$$url; + +    var match = PATH_MATCH.exec(url); +    if (match[1]) this.path(decodeURIComponent(match[1])); +    if (match[2] || match[1]) this.search(match[3] || ''); +    this.hash(match[5] || '', replace); + +    return this; +  }, + +  /** +   * @ngdoc method +   * @name angular.module.ng.$location#protocol +   * @methodOf angular.module.ng.$location +   * +   * @description +   * This method is getter only. +   * +   * Return protocol of current url. +   * +   * @return {string} +   */ +  protocol: locationGetter('$$protocol'), + +  /** +   * @ngdoc method +   * @name angular.module.ng.$location#host +   * @methodOf angular.module.ng.$location +   * +   * @description +   * This method is getter only. +   * +   * Return host of current url. +   * +   * @return {string} +   */ +  host: locationGetter('$$host'), + +  /** +   * @ngdoc method +   * @name angular.module.ng.$location#port +   * @methodOf angular.module.ng.$location +   * +   * @description +   * This method is getter only. +   * +   * Return port of current url. +   * +   * @return {Number} +   */ +  port: locationGetter('$$port'), + +  /** +   * @ngdoc method +   * @name angular.module.ng.$location#path +   * @methodOf angular.module.ng.$location +   * +   * @description +   * This method is getter / setter. +   * +   * Return path of current url when called without any parameter. +   * +   * Change path when called with parameter and return `$location`. +   * +   * Note: Path should always begin with forward slash (/), this method will add the forward slash +   * if it is missing. +   * +   * @param {string=} path New path +   * @return {string} +   */ +  path: locationGetterSetter('$$path', function(path) { +    return path.charAt(0) == '/' ? path : '/' + path; +  }), + +  /** +   * @ngdoc method +   * @name angular.module.ng.$location#search +   * @methodOf angular.module.ng.$location +   * +   * @description +   * This method is getter / setter. +   * +   * Return search part (as object) of current url when called without any parameter. +   * +   * Change search part when called with parameter and return `$location`. +   * +   * @param {string|object<string,string>=} search New search params - string or hash object +   * @param {string=} paramValue If `search` is a string, then `paramValue` will override only a +   *    single search parameter. If the value is `null`, the parameter will be deleted. +   * +   * @return {string} +   */ +  search: function(search, paramValue) { +    if (isUndefined(search)) +      return this.$$search; + +    if (isDefined(paramValue)) { +      if (paramValue === null) { +        delete this.$$search[search]; +      } else { +        this.$$search[search] = encodeUriQuery(paramValue); +      } +    } else { +      this.$$search = isString(search) ? parseKeyValue(search) : search; +    } + +    this.$$compose(); +    return this; +  }, + +  /** +   * @ngdoc method +   * @name angular.module.ng.$location#hash +   * @methodOf angular.module.ng.$location +   * +   * @description +   * This method is getter / setter. +   * +   * Return hash fragment when called without any parameter. +   * +   * Change hash fragment when called with parameter and return `$location`. +   * +   * @param {string=} hash New hash fragment +   * @return {string} +   */ +  hash: locationGetterSetter('$$hash', identity), + +  /** +   * @ngdoc method +   * @name angular.module.ng.$location#replace +   * @methodOf angular.module.ng.$location +   * +   * @description +   * If called, all changes to $location during current `$digest` will be replacing current history +   * record, instead of adding new one. +   */ +  replace: function() { +    this.$$replace = true; +    return this; +  } +}; + +LocationHashbangUrl.prototype = inherit(LocationUrl.prototype); + +function locationGetter(property) { +  return function() { +    return this[property]; +  }; +} + + +function locationGetterSetter(property, preprocess) { +  return function(value) { +    if (isUndefined(value)) +      return this[property]; + +    this[property] = preprocess(value); +    this.$$compose(); + +    return this; +  }; +} + + +/** + * @ngdoc object + * @name angular.module.ng.$location + * + * @requires $browser + * @requires $sniffer + * @requires $document + * + * @description + * The $location service parses the URL in the browser address bar (based on the {@link https://developer.mozilla.org/en/window.location window.location}) and makes the URL available to your application. Changes to the URL in the address bar are reflected into $location service and changes to $location are reflected into the browser address bar. + * + * **The $location service:** + * + * - Exposes the current URL in the browser address bar, so you can + *   - Watch and observe the URL. + *   - Change the URL. + * - Synchronizes the URL with the browser when the user + *   - Changes the address bar. + *   - Clicks the back or forward button (or clicks a History link). + *   - Clicks on a link. + * - Represents the URL object as a set of methods (protocol, host, port, path, search, hash). + * + * For more information see {@link guide/dev_guide.services.$location Developer Guide: Angular Services: Using $location} + */ + +/** + * @ngdoc object + * @name angular.module.ng.$locationProvider + * @description + * Use the `$locationProvider` to configure how the application deep linking paths are stored. + */ +function $LocationProvider(){ +  var hashPrefix = '', +      html5Mode = false; + +  /** +   * @ngdoc property +   * @name angular.module.ng.$locationProvider#hashPrefix +   * @methodOf angular.module.ng.$locationProvider +   * @description +   * @param {string=} prefix Prefix for hash part (containing path and search) +   * @returns {*} current value if used as getter or itself (chaining) if used as setter +   */ +  this.hashPrefix = function(prefix) { +    if (isDefined(prefix)) { +      hashPrefix = prefix; +      return this; +    } else { +      return hashPrefix; +    } +  } + +  /** +   * @ngdoc property +   * @name angular.module.ng.$locationProvider#html5Mode +   * @methodOf angular.module.ng.$locationProvider +   * @description +   * @param {string=} mode Use HTML5 strategy if available. +   * @returns {*} current value if used as getter or itself (chaining) if used as setter +   */ +  this.html5Mode = function(mode) { +    if (isDefined(mode)) { +      html5Mode = mode; +      return this; +    } else { +      return html5Mode; +    } +  }; + +  this.$get = ['$rootScope', '$browser', '$sniffer', '$document', +      function( $rootScope,   $browser,   $sniffer,   $document) { +    var currentUrl, +        basePath = $browser.baseHref() || '/', +        pathPrefix = pathPrefixFromBase(basePath), +        initUrl = $browser.url(); + +    if (html5Mode) { +      if ($sniffer.history) { +        currentUrl = new LocationUrl(convertToHtml5Url(initUrl, basePath, hashPrefix), pathPrefix); +      } else { +        currentUrl = new LocationHashbangUrl(convertToHashbangUrl(initUrl, basePath, hashPrefix), +                                             hashPrefix); +      } + +      // link rewriting +      var u = currentUrl, +          absUrlPrefix = composeProtocolHostPort(u.protocol(), u.host(), u.port()) + pathPrefix; + +      $document.bind('click', function(event) { +        // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) +        // currently we open nice url link and redirect then + +        if (event.ctrlKey || event.metaKey || event.which == 2) return; + +        var elm = jqLite(event.target); + +        // traverse the DOM up to find first A tag +        while (elm.length && lowercase(elm[0].nodeName) !== 'a') { +          elm = elm.parent(); +        } + +        var href = elm.attr('href'); +        if (!href || isDefined(elm.attr('ng-ext-link')) || elm.attr('target')) return; + +        // remove same domain from full url links (IE7 always returns full hrefs) +        href = href.replace(absUrlPrefix, ''); + +        // link to different domain (or base path) +        if (href.substr(0, 4) == 'http') return; + +        // remove pathPrefix from absolute links +        href = href.indexOf(pathPrefix) === 0 ? href.substr(pathPrefix.length) : href; + +        currentUrl.url(href); +        $rootScope.$apply(); +        event.preventDefault(); +        // hack to work around FF6 bug 684208 when scenario runner clicks on links +        window.angular['ff-684208-preventDefault'] = true; +      }); +    } else { +      currentUrl = new LocationHashbangUrl(initUrl, hashPrefix); +    } + +    // rewrite hashbang url <> html5 url +    if (currentUrl.absUrl() != initUrl) { +      $browser.url(currentUrl.absUrl(), true); +    } + +    // update $location when $browser url changes +    $browser.onUrlChange(function(newUrl) { +      if (currentUrl.absUrl() != newUrl) { +        $rootScope.$evalAsync(function() { +          currentUrl.$$parse(newUrl); +        }); +        if (!$rootScope.$$phase) $rootScope.$digest(); +      } +    }); + +    // update browser +    var changeCounter = 0; +    $rootScope.$watch(function $locationWatch() { +      if ($browser.url() != currentUrl.absUrl()) { +        changeCounter++; +        $rootScope.$evalAsync(function() { +          $browser.url(currentUrl.absUrl(), currentUrl.$$replace); +          currentUrl.$$replace = false; +        }); +      } + +      return changeCounter; +    }); + +    return currentUrl; +}]; +} diff --git a/src/ng/log.js b/src/ng/log.js new file mode 100644 index 00000000..d9d8994d --- /dev/null +++ b/src/ng/log.js @@ -0,0 +1,116 @@ +'use strict'; + +/** + * @ngdoc object + * @name angular.module.ng.$log + * @requires $window + * + * @description + * Simple service for logging. Default implementation writes the message + * into the browser's console (if present). + * + * The main purpose of this service is to simplify debugging and troubleshooting. + * + * @example +    <doc:example> +      <doc:source> +         <script> +           function LogCtrl($log) { +             this.$log = $log; +             this.message = 'Hello World!'; +           } +         </script> +         <div ng-controller="LogCtrl"> +           <p>Reload this page with open console, enter text and hit the log button...</p> +           Message: +           <input type="text" ng-model="message"/> +           <button ng-click="$log.log(message)">log</button> +           <button ng-click="$log.warn(message)">warn</button> +           <button ng-click="$log.info(message)">info</button> +           <button ng-click="$log.error(message)">error</button> +         </div> +      </doc:source> +      <doc:scenario> +      </doc:scenario> +    </doc:example> + */ + +function $LogProvider(){ +  this.$get = ['$window', function($window){ +    return { +      /** +       * @ngdoc method +       * @name angular.module.ng.$log#log +       * @methodOf angular.module.ng.$log +       * +       * @description +       * Write a log message +       */ +      log: consoleLog('log'), + +      /** +       * @ngdoc method +       * @name angular.module.ng.$log#warn +       * @methodOf angular.module.ng.$log +       * +       * @description +       * Write a warning message +       */ +      warn: consoleLog('warn'), + +      /** +       * @ngdoc method +       * @name angular.module.ng.$log#info +       * @methodOf angular.module.ng.$log +       * +       * @description +       * Write an information message +       */ +      info: consoleLog('info'), + +      /** +       * @ngdoc method +       * @name angular.module.ng.$log#error +       * @methodOf angular.module.ng.$log +       * +       * @description +       * Write an error message +       */ +      error: consoleLog('error') +    }; + +    function formatError(arg) { +      if (arg instanceof Error) { +        if (arg.stack) { +          arg = (arg.message && arg.stack.indexOf(arg.message) === -1) +              ? 'Error: ' + arg.message + '\n' + arg.stack +              : arg.stack; +        } else if (arg.sourceURL) { +          arg = arg.message + '\n' + arg.sourceURL + ':' + arg.line; +        } +      } +      return arg; +    } + +    function consoleLog(type) { +      var console = $window.console || {}, +          logFn = console[type] || console.log || noop; + +      if (logFn.apply) { +        return function() { +          var args = []; +          forEach(arguments, function(arg) { +            args.push(formatError(arg)); +          }); +          return logFn.apply(console, args); +        }; +      } + +      // we are IE which either doesn't have window.console => this is noop and we do nothing, +      // or we are IE where console.log doesn't have apply so we log at least first 2 args +      return function(arg1, arg2) { +        logFn(arg1, arg2); +      } +    } +  }]; +} diff --git a/src/ng/parse.js b/src/ng/parse.js new file mode 100644 index 00000000..47c5188e --- /dev/null +++ b/src/ng/parse.js @@ -0,0 +1,760 @@ +'use strict'; + +var OPERATORS = { +    'null':function(){return null;}, +    'true':function(){return true;}, +    'false':function(){return false;}, +    undefined:noop, +    '+':function(self, locals, a,b){a=a(self, locals); b=b(self, locals); return (isDefined(a)?a:0)+(isDefined(b)?b:0);}, +    '-':function(self, locals, a,b){a=a(self, locals); b=b(self, locals); return (isDefined(a)?a:0)-(isDefined(b)?b:0);}, +    '*':function(self, locals, a,b){return a(self, locals)*b(self, locals);}, +    '/':function(self, locals, a,b){return a(self, locals)/b(self, locals);}, +    '%':function(self, locals, a,b){return a(self, locals)%b(self, locals);}, +    '^':function(self, locals, a,b){return a(self, locals)^b(self, locals);}, +    '=':noop, +    '==':function(self, locals, a,b){return a(self, locals)==b(self, locals);}, +    '!=':function(self, locals, a,b){return a(self, locals)!=b(self, locals);}, +    '<':function(self, locals, a,b){return a(self, locals)<b(self, locals);}, +    '>':function(self, locals, a,b){return a(self, locals)>b(self, locals);}, +    '<=':function(self, locals, a,b){return a(self, locals)<=b(self, locals);}, +    '>=':function(self, locals, a,b){return a(self, locals)>=b(self, locals);}, +    '&&':function(self, locals, a,b){return a(self, locals)&&b(self, locals);}, +    '||':function(self, locals, a,b){return a(self, locals)||b(self, locals);}, +    '&':function(self, locals, a,b){return a(self, locals)&b(self, locals);}, +//    '|':function(self, locals, a,b){return a|b;}, +    '|':function(self, locals, a,b){return b(self, locals)(self, locals, a(self, locals));}, +    '!':function(self, locals, a){return !a(self, locals);} +}; +var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; + +function lex(text){ +  var tokens = [], +      token, +      index = 0, +      json = [], +      ch, +      lastCh = ':'; // can start regexp + +  while (index < text.length) { +    ch = text.charAt(index); +    if (is('"\'')) { +      readString(ch); +    } else if (isNumber(ch) || is('.') && isNumber(peek())) { +      readNumber(); +    } else if (isIdent(ch)) { +      readIdent(); +      // identifiers can only be if the preceding char was a { or , +      if (was('{,') && json[0]=='{' && +         (token=tokens[tokens.length-1])) { +        token.json = token.text.indexOf('.') == -1; +      } +    } else if (is('(){}[].,;:')) { +      tokens.push({ +        index:index, +        text:ch, +        json:(was(':[,') && is('{[')) || is('}]:,') +      }); +      if (is('{[')) json.unshift(ch); +      if (is('}]')) json.shift(); +      index++; +    } else if (isWhitespace(ch)) { +      index++; +      continue; +    } else { +      var ch2 = ch + peek(), +          fn = OPERATORS[ch], +          fn2 = OPERATORS[ch2]; +      if (fn2) { +        tokens.push({index:index, text:ch2, fn:fn2}); +        index += 2; +      } else if (fn) { +        tokens.push({index:index, text:ch, fn:fn, json: was('[,:') && is('+-')}); +        index += 1; +      } else { +        throwError("Unexpected next character ", index, index+1); +      } +    } +    lastCh = ch; +  } +  return tokens; + +  function is(chars) { +    return chars.indexOf(ch) != -1; +  } + +  function was(chars) { +    return chars.indexOf(lastCh) != -1; +  } + +  function peek() { +    return index + 1 < text.length ? text.charAt(index + 1) : false; +  } +  function isNumber(ch) { +    return '0' <= ch && ch <= '9'; +  } +  function isWhitespace(ch) { +    return ch == ' ' || ch == '\r' || ch == '\t' || +           ch == '\n' || ch == '\v' || ch == '\u00A0'; // IE treats non-breaking space as \u00A0 +  } +  function isIdent(ch) { +    return 'a' <= ch && ch <= 'z' || +           'A' <= ch && ch <= 'Z' || +           '_' == ch || ch == '$'; +  } +  function isExpOperator(ch) { +    return ch == '-' || ch == '+' || isNumber(ch); +  } + +  function throwError(error, start, end) { +    end = end || index; +    throw Error("Lexer Error: " + error + " at column" + +        (isDefined(start) +            ? "s " + start +  "-" + index + " [" + text.substring(start, end) + "]" +            : " " + end) + +        " in expression [" + text + "]."); +  } + +  function readNumber() { +    var number = ""; +    var start = index; +    while (index < text.length) { +      var ch = lowercase(text.charAt(index)); +      if (ch == '.' || isNumber(ch)) { +        number += ch; +      } else { +        var peekCh = peek(); +        if (ch == 'e' && isExpOperator(peekCh)) { +          number += ch; +        } else if (isExpOperator(ch) && +            peekCh && isNumber(peekCh) && +            number.charAt(number.length - 1) == 'e') { +          number += ch; +        } else if (isExpOperator(ch) && +            (!peekCh || !isNumber(peekCh)) && +            number.charAt(number.length - 1) == 'e') { +          throwError('Invalid exponent'); +        } else { +          break; +        } +      } +      index++; +    } +    number = 1 * number; +    tokens.push({index:start, text:number, json:true, +      fn:function() {return number;}}); +  } +  function readIdent() { +    var ident = "", +        start = index, +        lastDot, peekIndex, methodName; + +    while (index < text.length) { +      var ch = text.charAt(index); +      if (ch == '.' || isIdent(ch) || isNumber(ch)) { +        if (ch == '.') lastDot = index; +        ident += ch; +      } else { +        break; +      } +      index++; +    } + +    //check if this is not a method invocation and if it is back out to last dot +    if (lastDot) { +      peekIndex = index +      while(peekIndex < text.length) { +        var ch = text.charAt(peekIndex); +        if (ch == '(') { +          methodName = ident.substr(lastDot - start + 1); +          ident = ident.substr(0, lastDot - start); +          index = peekIndex; +          break; +        } +        if(isWhitespace(ch)) { +          peekIndex++; +        } else { +          break; +        } +      } +    } + + +    var token = { +      index:start, +      text:ident +    }; + +    if (OPERATORS.hasOwnProperty(ident)) { +      token.fn = token.json = OPERATORS[ident]; +    } else { +      var getter = getterFn(ident); +      token.fn = extend(function(self, locals) { +        return (getter(self, locals)); +      }, { +        assign: function(self, value) { +          return setter(self, ident, value); +        } +      }); +    } + +    tokens.push(token); + +    if (methodName) { +      tokens.push({ +        index:lastDot, +        text: '.', +        json: false +      }); +      tokens.push({ +        index: lastDot + 1, +        text: methodName, +        json: false +      }); +    } +  } + +  function readString(quote) { +    var start = index; +    index++; +    var string = ""; +    var rawString = quote; +    var escape = false; +    while (index < text.length) { +      var ch = text.charAt(index); +      rawString += ch; +      if (escape) { +        if (ch == 'u') { +          var hex = text.substring(index + 1, index + 5); +          if (!hex.match(/[\da-f]{4}/i)) +            throwError( "Invalid unicode escape [\\u" + hex + "]"); +          index += 4; +          string += String.fromCharCode(parseInt(hex, 16)); +        } else { +          var rep = ESCAPE[ch]; +          if (rep) { +            string += rep; +          } else { +            string += ch; +          } +        } +        escape = false; +      } else if (ch == '\\') { +        escape = true; +      } else if (ch == quote) { +        index++; +        tokens.push({ +          index:start, +          text:rawString, +          string:string, +          json:true, +          fn:function() { return string; } +        }); +        return; +      } else { +        string += ch; +      } +      index++; +    } +    throwError("Unterminated quote", start); +  } +} + +///////////////////////////////////////// + +function parser(text, json, $filter){ +  var ZERO = valueFn(0), +      value, +      tokens = lex(text), +      assignment = _assignment, +      functionCall = _functionCall, +      fieldAccess = _fieldAccess, +      objectIndex = _objectIndex, +      filterChain = _filterChain +  if(json){ +    // The extra level of aliasing is here, just in case the lexer misses something, so that +    // we prevent any accidental execution in JSON. +    assignment = logicalOR; +    functionCall = +      fieldAccess = +      objectIndex = +      filterChain = +        function() { throwError("is not valid json", {text:text, index:0}); }; +    value = primary(); +  } else { +    value = statements(); +  } +  if (tokens.length !== 0) { +    throwError("is an unexpected token", tokens[0]); +  } +  return value; + +  /////////////////////////////////// +  function throwError(msg, token) { +    throw Error("Syntax Error: Token '" + token.text + +      "' " + msg + " at column " + +      (token.index + 1) + " of the expression [" + +      text + "] starting at [" + text.substring(token.index) + "]."); +  } + +  function peekToken() { +    if (tokens.length === 0) +      throw Error("Unexpected end of expression: " + text); +    return tokens[0]; +  } + +  function peek(e1, e2, e3, e4) { +    if (tokens.length > 0) { +      var token = tokens[0]; +      var t = token.text; +      if (t==e1 || t==e2 || t==e3 || t==e4 || +          (!e1 && !e2 && !e3 && !e4)) { +        return token; +      } +    } +    return false; +  } + +  function expect(e1, e2, e3, e4){ +    var token = peek(e1, e2, e3, e4); +    if (token) { +      if (json && !token.json) { +        throwError("is not valid json", token); +      } +      tokens.shift(); +      return token; +    } +    return false; +  } + +  function consume(e1){ +    if (!expect(e1)) { +      throwError("is unexpected, expecting [" + e1 + "]", peek()); +    } +  } + +  function unaryFn(fn, right) { +    return function(self, locals) { +      return fn(self, locals, right); +    }; +  } + +  function binaryFn(left, fn, right) { +    return function(self, locals) { +      return fn(self, locals, left, right); +    }; +  } + +  function hasTokens () { +    return tokens.length > 0; +  } + +  function statements() { +    var statements = []; +    while(true) { +      if (tokens.length > 0 && !peek('}', ')', ';', ']')) +        statements.push(filterChain()); +      if (!expect(';')) { +        // optimize for the common case where there is only one statement. +        // TODO(size): maybe we should not support multiple statements? +        return statements.length == 1 +          ? statements[0] +          : function(self, locals){ +            var value; +            for ( var i = 0; i < statements.length; i++) { +              var statement = statements[i]; +              if (statement) +                value = statement(self, locals); +            } +            return value; +          }; +      } +    } +  } + +  function _filterChain() { +    var left = expression(); +    var token; +    while(true) { +      if ((token = expect('|'))) { +        left = binaryFn(left, token.fn, filter()); +      } else { +        return left; +      } +    } +  } + +  function filter() { +    var token = expect(); +    var fn = $filter(token.text); +    var argsFn = []; +    while(true) { +      if ((token = expect(':'))) { +        argsFn.push(expression()); +      } else { +        var fnInvoke = function(self, locals, input){ +          var args = [input]; +          for ( var i = 0; i < argsFn.length; i++) { +            args.push(argsFn[i](self, locals)); +          } +          return fn.apply(self, args); +        }; +        return function() { +          return fnInvoke; +        }; +      } +    } +  } + +  function expression() { +    return assignment(); +  } + +  function _assignment() { +    var left = logicalOR(); +    var right; +    var token; +    if ((token = expect('='))) { +      if (!left.assign) { +        throwError("implies assignment but [" + +          text.substring(0, token.index) + "] can not be assigned to", token); +      } +      right = logicalOR(); +      return function(self, locals){ +        return left.assign(self, right(self, locals), locals); +      }; +    } else { +      return left; +    } +  } + +  function logicalOR() { +    var left = logicalAND(); +    var token; +    while(true) { +      if ((token = expect('||'))) { +        left = binaryFn(left, token.fn, logicalAND()); +      } else { +        return left; +      } +    } +  } + +  function logicalAND() { +    var left = equality(); +    var token; +    if ((token = expect('&&'))) { +      left = binaryFn(left, token.fn, logicalAND()); +    } +    return left; +  } + +  function equality() { +    var left = relational(); +    var token; +    if ((token = expect('==','!='))) { +      left = binaryFn(left, token.fn, equality()); +    } +    return left; +  } + +  function relational() { +    var left = additive(); +    var token; +    if ((token = expect('<', '>', '<=', '>='))) { +      left = binaryFn(left, token.fn, relational()); +    } +    return left; +  } + +  function additive() { +    var left = multiplicative(); +    var token; +    while ((token = expect('+','-'))) { +      left = binaryFn(left, token.fn, multiplicative()); +    } +    return left; +  } + +  function multiplicative() { +    var left = unary(); +    var token; +    while ((token = expect('*','/','%'))) { +      left = binaryFn(left, token.fn, unary()); +    } +    return left; +  } + +  function unary() { +    var token; +    if (expect('+')) { +      return primary(); +    } else if ((token = expect('-'))) { +      return binaryFn(ZERO, token.fn, unary()); +    } else if ((token = expect('!'))) { +      return unaryFn(token.fn, unary()); +    } else { +      return primary(); +    } +  } + +  function _functionIdent(fnScope) { +    var token = expect(); +    var element = token.text.split('.'); +    var instance = fnScope; +    var key; +    for ( var i = 0; i < element.length; i++) { +      key = element[i]; +      if (instance) +        instance = instance[key]; +    } +    if (!isFunction(instance)) { +      throwError("should be a function", token); +    } +    return instance; +  } + +  function primary() { +    var primary; +    if (expect('(')) { +      primary = filterChain(); +      consume(')'); +    } else if (expect('[')) { +      primary = arrayDeclaration(); +    } else if (expect('{')) { +      primary = object(); +    } else { +      var token = expect(); +      primary = token.fn; +      if (!primary) { +        throwError("not a primary expression", token); +      } +    } + +    var next, context; +    while ((next = expect('(', '[', '.'))) { +      if (next.text === '(') { +        primary = functionCall(primary, context); +        context = null; +      } else if (next.text === '[') { +        context = primary; +        primary = objectIndex(primary); +      } else if (next.text === '.') { +        context = primary; +        primary = fieldAccess(primary); +      } else { +        throwError("IMPOSSIBLE"); +      } +    } +    return primary; +  } + +  function _fieldAccess(object) { +    var field = expect().text; +    var getter = getterFn(field); +    return extend( +        function(self, locals) { +          return getter(object(self, locals), locals); +        }, +        { +          assign:function(self, value, locals) { +            return setter(object(self, locals), field, value); +          } +        } +    ); +  } + +  function _objectIndex(obj) { +    var indexFn = expression(); +    consume(']'); +    return extend( +      function(self, locals){ +        var o = obj(self, locals), +            i = indexFn(self, locals), +            v, p; + +        if (!o) return undefined; +        v = o[i]; +        if (v && v.then) { +          p = v; +          if (!('$$v' in v)) { +            p.$$v = undefined; +            p.then(function(val) { p.$$v = val; }); +          } +          v = v.$$v; +        } +        return v; +      }, { +        assign:function(self, value, locals){ +          return obj(self, locals)[indexFn(self, locals)] = value; +        } +      }); +  } + +  function _functionCall(fn, contextGetter) { +    var argsFn = []; +    if (peekToken().text != ')') { +      do { +        argsFn.push(expression()); +      } while (expect(',')); +    } +    consume(')'); +    return function(self, locals){ +      var args = [], +          context = contextGetter ? contextGetter(self, locals) : self; + +      for ( var i = 0; i < argsFn.length; i++) { +        args.push(argsFn[i](self, locals)); +      } +      var fnPtr = fn(self, locals) || noop; +      // IE stupidity! +      return fnPtr.apply +          ? fnPtr.apply(context, args) +          : fnPtr(args[0], args[1], args[2], args[3], args[4]); +    }; +  } + +  // This is used with json array declaration +  function arrayDeclaration () { +    var elementFns = []; +    if (peekToken().text != ']') { +      do { +        elementFns.push(expression()); +      } while (expect(',')); +    } +    consume(']'); +    return function(self, locals){ +      var array = []; +      for ( var i = 0; i < elementFns.length; i++) { +        array.push(elementFns[i](self, locals)); +      } +      return array; +    }; +  } + +  function object () { +    var keyValues = []; +    if (peekToken().text != '}') { +      do { +        var token = expect(), +        key = token.string || token.text; +        consume(":"); +        var value = expression(); +        keyValues.push({key:key, value:value}); +      } while (expect(',')); +    } +    consume('}'); +    return function(self, locals){ +      var object = {}; +      for ( var i = 0; i < keyValues.length; i++) { +        var keyValue = keyValues[i]; +        var value = keyValue.value(self, locals); +        object[keyValue.key] = value; +      } +      return object; +    }; +  } +} + +////////////////////////////////////////////////// +// Parser helper functions +////////////////////////////////////////////////// + +function setter(obj, path, setValue) { +  var element = path.split('.'); +  for (var i = 0; element.length > 1; i++) { +    var key = element.shift(); +    var propertyObj = obj[key]; +    if (!propertyObj) { +      propertyObj = {}; +      obj[key] = propertyObj; +    } +    obj = propertyObj; +  } +  obj[element.shift()] = setValue; +  return setValue; +} + +/** + * Return the value accesible from the object by path. Any undefined traversals are ignored + * @param {Object} obj starting object + * @param {string} path path to traverse + * @param {boolean=true} bindFnToScope + * @returns value as accesbile by path + */ +//TODO(misko): this function needs to be removed +function getter(obj, path, bindFnToScope) { +  if (!path) return obj; +  var keys = path.split('.'); +  var key; +  var lastInstance = obj; +  var len = keys.length; + +  for (var i = 0; i < len; i++) { +    key = keys[i]; +    if (obj) { +      obj = (lastInstance = obj)[key]; +    } +  } +  if (!bindFnToScope && isFunction(obj)) { +    return bind(lastInstance, obj); +  } +  return obj; +} + +var getterFnCache = {}; + +function getterFn(path) { +  if (getterFnCache.hasOwnProperty(path)) { +    return getterFnCache[path]; +  } + +  var fn, code = 'var l, fn, p;\n'; +  forEach(path.split('.'), function(key, index) { +    code += 'if(!s) return s;\n' + +            'l=s;\n' + +            's='+ (index +                    // we simply direference 's' on any .dot notation +                    ? 's' +                    // but if we are first then we check locals firs, and if so read it first +                    : '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' + +            'if (s && s.then) {\n' + +              ' if (!("$$v" in s)) {\n' + +                ' p=s;\n' + +                ' p.$$v = undefined;\n' + +                ' p.then(function(v) {p.$$v=v;});\n' + +                '}\n' + +              ' s=s.$$v\n' + +            '}\n'; +  }); +  code += 'return s;'; +  fn = Function('s', 'k', code); +  fn.toString = function() { return code; }; + +  return getterFnCache[path] = fn; +} + +/////////////////////////////////// + +function $ParseProvider() { +  var cache = {}; +  this.$get = ['$filter', function($filter) { +    return function(exp) { +      switch(typeof exp) { +        case 'string': +          return cache.hasOwnProperty(exp) +            ? cache[exp] +            : cache[exp] =  parser(exp, false, $filter); +        case 'function': +          return exp; +        default: +          return noop; +      } +    }; +  }]; +} + + +// This is a special access for JSON parser which bypasses the injector +var parseJson = function(json) { +  return parser(json, true); +}; diff --git a/src/ng/q.js b/src/ng/q.js new file mode 100644 index 00000000..074acd1d --- /dev/null +++ b/src/ng/q.js @@ -0,0 +1,391 @@ +'use strict'; + +/** + * @ngdoc service + * @name angular.module.ng.$q + * @requires $rootScope + * + * @description + * A promise/deferred implementation inspired by [Kris Kowal's Q](https://github.com/kriskowal/q). + * + * [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an + * interface for interacting with an object that represents the result of an action that is + * performed asynchronously, and may or may not be finished at any given point in time. + * + * From the perspective of dealing with error handling, deferred and promise apis are to + * asynchronous programing what `try`, `catch` and `throw` keywords are to synchronous programing. + * + * <pre> + *   // for the purpose of this example let's assume that variables `$q` and `scope` are + *   // available in the current lexical scope (they could have been injected or passed in). + * + *   function asyncGreet(name) { + *     var deferred = $q.defer(); + * + *     setTimeout(function() { + *       // since this fn executes async in a future turn of the event loop, we need to wrap + *       // our code into an $apply call so that the model changes are properly observed. + *       scope.$apply(function() { + *         if (okToGreet(name)) { + *           deferred.resolve('Hello, ' + name + '!'); + *         } else { + *           deferred.reject('Greeting ' + name + ' is not allowed.'); + *         } + *       }); + *     }, 1000); + * + *     return deferred.promise; + *   } + * + *   var promise = asyncGreet('Robin Hood'); + *   promise.then(function(greeting) { + *     alert('Success: ' + greeting); + *   }, function(reason) { + *     alert('Failed: ' + reason); + *   ); + * </pre> + * + * At first it might not be obvious why this extra complexity is worth the trouble. The payoff + * comes in the way of + * [guarantees that promise and deferred apis make](https://github.com/kriskowal/uncommonjs/blob/master/promises/specification.md). + * + * Additionally the promise api allows for composition that is very hard to do with the + * traditional callback ([CPS](http://en.wikipedia.org/wiki/Continuation-passing_style)) approach. + * For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the + * section on serial or parallel joining of promises. + * + * + * # The Deferred API + * + * A new instance of deferred is constructed by calling `$q.defer()`. + * + * The purpose of the deferred object is to expose the associated Promise instance as well as apis + * that can be used for signaling the successful or unsuccessful completion of the task. + * + * **Methods** + * + * - `resolve(value)` – resolves the derived promise with the `value`. If the value is a rejection + *   constructed via `$q.reject`, the promise will be rejected instead. + * - `reject(reason)` – rejects the derived promise with the `reason`. This is equivalent to + *   resolving it with a rejection constructed via `$q.reject`. + * + * **Properties** + * + * - promise – `{Promise}` – promise object associated with this deferred. + * + * + * # The Promise API + * + * A new promise instance is created when a deferred instance is created and can be retrieved by + * calling `deferred.promise`. + * + * The purpose of the promise object is to allow for interested parties to get access to the result + * of the deferred task when it completes. + * + * **Methods** + * + * - `then(successCallback, errorCallback)` – regardless of when the promise was or will be resolved + *   or rejected calls one of the success or error callbacks asynchronously as soon as the result + *   is available. The callbacks are called with a single argument the result or rejection reason. + * + *   This method *returns a new promise* which is resolved or rejected via the return value of the + *   `successCallback` or `errorCallback`. + * + * + * # Chaining promises + * + * Because calling `then` api of a promise returns a new derived promise, it is easily possible + * to create a chain of promises: + * + * <pre> + *   promiseB = promiseA.then(function(result) { + *     return result + 1; + *   }); + * + *   // promiseB will be resolved immediately after promiseA is resolved and it's value will be + *   // the result of promiseA incremented by 1 + * </pre> + * + * It is possible to create chains of any length and since a promise can be resolved with another + * promise (which will defer its resolution further), it is possible to pause/defer resolution of + * the promises at any point in the chain. This makes it possible to implement powerful apis like + * $http's response interceptors. + * + * + * # Differences between Kris Kowal's Q and $q + * + *  There are three main differences: + * + * - $q is integrated with the {@link angular.module.ng.$rootScope.Scope} Scope model observation + *   mechanism in angular, which means faster propagation of resolution or rejection into your + *   models and avoiding unnecessary browser repaints, which would result in flickering UI. + * - $q promises are recognized by the templating engine in angular, which means that in templates + *   you can treat promises attached to a scope as if they were the resulting values. + * - Q has many more features that $q, but that comes at a cost of bytes. $q is tiny, but contains + *   all the important functionality needed for common async tasks. + */ +function $QProvider() { + +  this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { +    return qFactory(function(callback) { +      $rootScope.$evalAsync(callback); +    }, $exceptionHandler); +  }]; +} + + +/** + * Constructs a promise manager. + * + * @param {function(function)} nextTick Function for executing functions in the next turn. + * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for + *     debugging purposes. + * @returns {object} Promise manager. + */ +function qFactory(nextTick, exceptionHandler) { + +  /** +   * @ngdoc +   * @name angular.module.ng.$q#defer +   * @methodOf angular.module.ng.$q +   * @description +   * Creates a `Deferred` object which represents a task which will finish in the future. +   * +   * @returns {Deferred} Returns a new instance of deferred. +   */ +  var defer = function() { +    var pending = [], +        value, deferred; + +    deferred = { + +      resolve: function(val) { +        if (pending) { +          var callbacks = pending; +          pending = undefined; +          value = ref(val); + +          if (callbacks.length) { +            nextTick(function() { +              var callback; +              for (var i = 0, ii = callbacks.length; i < ii; i++) { +                callback = callbacks[i]; +                value.then(callback[0], callback[1]); +              } +            }); +          } +        } +      }, + + +      reject: function(reason) { +        deferred.resolve(reject(reason)); +      }, + + +      promise: { +        then: function(callback, errback) { +          var result = defer(); + +          var wrappedCallback = function(value) { +            try { +              result.resolve((callback || defaultCallback)(value)); +            } catch(e) { +              exceptionHandler(e); +              result.reject(e); +            } +          }; + +          var wrappedErrback = function(reason) { +            try { +              result.resolve((errback || defaultErrback)(reason)); +            } catch(e) { +              exceptionHandler(e); +              result.reject(e); +            } +          }; + +          if (pending) { +            pending.push([wrappedCallback, wrappedErrback]); +          } else { +            value.then(wrappedCallback, wrappedErrback); +          } + +          return result.promise; +        } +      } +    }; + +    return deferred; +  }; + + +  var ref = function(value) { +    if (value && value.then) return value; +    return { +      then: function(callback) { +        var result = defer(); +        nextTick(function() { +          result.resolve(callback(value)); +        }); +        return result.promise; +      } +    }; +  }; + + +  /** +   * @ngdoc +   * @name angular.module.ng.$q#reject +   * @methodOf angular.module.ng.$q +   * @description +   * Creates a promise that is resolved as rejected with the specified `reason`. This api should be +   * used to forward rejection in a chain of promises. If you are dealing with the last promise in +   * a promise chain, you don't need to worry about it. +   * +   * When comparing deferreds/promises to the familiar behavior of try/catch/throw, think of +   * `reject` as the `throw` keyword in JavaScript. This also means that if you "catch" an error via +   * a promise error callback and you want to forward the error to the promise derived from the +   * current promise, you have to "rethrow" the error by returning a rejection constructed via +   * `reject`. +   * +   * <pre> +   *   promiseB = promiseA.then(function(result) { +   *     // success: do something and resolve promiseB +   *     //          with the old or a new result +   *     return result; +   *   }, function(reason) { +   *     // error: handle the error if possible and +   *     //        resolve promiseB with newPromiseOrValue, +   *     //        otherwise forward the rejection to promiseB +   *     if (canHandle(reason)) { +   *      // handle the error and recover +   *      return newPromiseOrValue; +   *     } +   *     return $q.reject(reason); +   *   }); +   * </pre> +   * +   * @param {*} reason Constant, message, exception or an object representing the rejection reason. +   * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. +   */ +  var reject = function(reason) { +    return { +      then: function(callback, errback) { +        var result = defer(); +        nextTick(function() { +          result.resolve(errback(reason)); +        }); +        return result.promise; +      } +    }; +  }; + + +  /** +   * @ngdoc +   * @name angular.module.ng.$q#when +   * @methodOf angular.module.ng.$q +   * @description +   * Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. +   * This is useful when you are dealing with on object that might or might not be a promise, or if +   * the promise comes from a source that can't be trusted. +   * +   * @param {*} value Value or a promise +   * @returns {Promise} Returns a single promise that will be resolved with an array of values, +   *   each value coresponding to the promise at the same index in the `promises` array. If any of +   *   the promises is resolved with a rejection, this resulting promise will be resolved with the +   *   same rejection. +   */ +  var when = function(value, callback, errback) { +    var result = defer(), +        done; + +    var wrappedCallback = function(value) { +      try { +        return (callback || defaultCallback)(value); +      } catch (e) { +        exceptionHandler(e); +        return reject(e); +      } +    }; + +    var wrappedErrback = function(reason) { +      try { +        return (errback || defaultErrback)(reason); +      } catch (e) { +        exceptionHandler(e); +        return reject(e); +      } +    }; + +    nextTick(function() { +      ref(value).then(function(value) { +        if (done) return; +        done = true; +        result.resolve(ref(value).then(wrappedCallback, wrappedErrback)); +      }, function(reason) { +        if (done) return; +        done = true; +        result.resolve(wrappedErrback(reason)); +      }); +    }); + +    return result.promise; +  }; + + +  function defaultCallback(value) { +    return value; +  } + + +  function defaultErrback(reason) { +    return reject(reason); +  } + + +  /** +   * @ngdoc +   * @name angular.module.ng.$q#all +   * @methodOf angular.module.ng.$q +   * @description +   * Combines multiple promises into a single promise that is resolved when all of the input +   * promises are resolved. +   * +   * @param {Array.<Promise>} promises An array of promises. +   * @returns {Promise} Returns a single promise that will be resolved with an array of values, +   *   each value coresponding to the promise at the same index in the `promises` array. If any of +   *   the promises is resolved with a rejection, this resulting promise will be resolved with the +   *   same rejection. +   */ +  function all(promises) { +    var deferred = defer(), +        counter = promises.length, +        results = []; + +    if (counter) { +      forEach(promises, function(promise, index) { +        ref(promise).then(function(value) { +          if (index in results) return; +          results[index] = value; +          if (!(--counter)) deferred.resolve(results); +        }, function(reason) { +          if (index in results) return; +          deferred.reject(reason); +        }); +      }); +    } else { +      deferred.resolve(results); +    } + +    return deferred.promise; +  } + +  return { +    defer: defer, +    reject: reject, +    when: when, +    all: all +  }; +} diff --git a/src/ng/resource.js b/src/ng/resource.js new file mode 100644 index 00000000..3aa48e74 --- /dev/null +++ b/src/ng/resource.js @@ -0,0 +1,368 @@ +'use strict'; + +/** + * @ngdoc object + * @name angular.module.ng.$resource + * @requires $http + * + * @description + * A factory which creates a resource object that lets you interact with + * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. + * + * The returned resource object has action methods which provide high-level behaviors without + * the need to interact with the low level {@link angular.module.ng.$http $http} service. + * + * @param {string} url A parameterized URL template with parameters prefixed by `:` as in + *   `/user/:username`. + * + * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in + *   `actions` methods. + * + *   Each key value in the parameter object is first bound to url template if present and then any + *   excess keys are appended to the url search query after the `?`. + * + *   Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in + *   URL `/path/greet?salutation=Hello`. + * + *   If the parameter value is prefixed with `@` then the value of that parameter is extracted from + *   the data object (useful for non-GET operations). + * + * @param {Object.<Object>=} actions Hash with declaration of custom action that should extend the + *   default set of resource actions. The declaration should be created in the following format: + * + *       {action1: {method:?, params:?, isArray:?}, + *        action2: {method:?, params:?, isArray:?}, + *        ...} + * + *   Where: + * + *   - `action` – {string} – The name of action. This name becomes the name of the method on your + *     resource object. + *   - `method` – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`, + *     and `JSONP` + *   - `params` – {object=} – Optional set of pre-bound parameters for this action. + *   - isArray – {boolean=} – If true then the returned object for this action is an array, see + *     `returns` section. + * + * @returns {Object} A resource "class" object with methods for the default set of resource actions + *   optionally extended with custom `actions`. The default set contains these actions: + * + *       { 'get':    {method:'GET'}, + *         'save':   {method:'POST'}, + *         'query':  {method:'GET', isArray:true}, + *         'remove': {method:'DELETE'}, + *         'delete': {method:'DELETE'} }; + * + *   Calling these methods invoke an {@link angular.module.ng.$http} with the specified http method, + *   destination and parameters. When the data is returned from the server then the object is an + *   instance of the resource class `save`, `remove` and `delete` actions are available on it as + *   methods with the `$` prefix. This allows you to easily perform CRUD operations (create, read, + *   update, delete) on server-side data like this: + *   <pre> +        var User = $resource('/user/:userId', {userId:'@id'}); +        var user = User.get({userId:123}, function() { +          user.abc = true; +          user.$save(); +        }); +     </pre> + * + *   It is important to realize that invoking a $resource object method immediately returns an + *   empty reference (object or array depending on `isArray`). Once the data is returned from the + *   server the existing reference is populated with the actual data. This is a useful trick since + *   usually the resource is assigned to a model which is then rendered by the view. Having an empty + *   object results in no rendering, once the data arrives from the server then the object is + *   populated with the data and the view automatically re-renders itself showing the new data. This + *   means that in most case one never has to write a callback function for the action methods. + * + *   The action methods on the class object or instance object can be invoked with the following + *   parameters: + * + *   - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` + *   - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` + *   - non-GET instance actions:  `instance.$action([parameters], [success], [error])` + * + * + * @example + * + * # Credit card resource + * + * <pre> +     // Define CreditCard class +     var CreditCard = $resource('/user/:userId/card/:cardId', +      {userId:123, cardId:'@id'}, { +       charge: {method:'POST', params:{charge:true}} +      }); + +     // We can retrieve a collection from the server +     var cards = CreditCard.query(); +     // GET: /user/123/card +     // server returns: [ {id:456, number:'1234', name:'Smith'} ]; + +     var card = cards[0]; +     // each item is an instance of CreditCard +     expect(card instanceof CreditCard).toEqual(true); +     card.name = "J. Smith"; +     // non GET methods are mapped onto the instances +     card.$save(); +     // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'} +     // server returns: {id:456, number:'1234', name: 'J. Smith'}; + +     // our custom method is mapped as well. +     card.$charge({amount:9.99}); +     // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'} +     // server returns: {id:456, number:'1234', name: 'J. Smith'}; + +     // we can create an instance as well +     var newCard = new CreditCard({number:'0123'}); +     newCard.name = "Mike Smith"; +     newCard.$save(); +     // POST: /user/123/card {number:'0123', name:'Mike Smith'} +     // server returns: {id:789, number:'01234', name: 'Mike Smith'}; +     expect(newCard.id).toEqual(789); + * </pre> + * + * The object returned from this function execution is a resource "class" which has "static" method + * for each action in the definition. + * + * Calling these methods invoke `$http` on the `url` template with the given `method` and `params`. + * When the data is returned from the server then the object is an instance of the resource type and + * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD + * operations (create, read, update, delete) on server-side data. + +   <pre> +     var User = $resource('/user/:userId', {userId:'@id'}); +     var user = User.get({userId:123}, function() { +       user.abc = true; +       user.$save(); +     }); +   </pre> + * + *     It's worth noting that the success callback for `get`, `query` and other method gets passed + *     in the response that came from the server as well as $http header getter function, so one + *     could rewrite the above example and get access to http headers as: + * +   <pre> +     var User = $resource('/user/:userId', {userId:'@id'}); +     User.get({userId:123}, function(u, getResponseHeaders){ +       u.abc = true; +       u.$save(function(u, putResponseHeaders) { +         //u => saved user object +         //putResponseHeaders => $http header getter +       }); +     }); +   </pre> + + * # Buzz client + +   Let's look at what a buzz client created with the `$resource` service looks like: +    <doc:example> +      <doc:source jsfiddle="false"> +       <script> +         function BuzzController($resource) { +           this.userId = 'googlebuzz'; +           this.Activity = $resource( +             'https://www.googleapis.com/buzz/v1/activities/:userId/:visibility/:activityId/:comments', +             {alt:'json', callback:'JSON_CALLBACK'}, +             {get:{method:'JSONP', params:{visibility:'@self'}}, replies: {method:'JSONP', params:{visibility:'@self', comments:'@comments'}}} +           ); +         } + +         BuzzController.prototype = { +           fetch: function() { +             this.activities = this.Activity.get({userId:this.userId}); +           }, +           expandReplies: function(activity) { +             activity.replies = this.Activity.replies({userId:this.userId, activityId:activity.id}); +           } +         }; +         BuzzController.$inject = ['$resource']; +       </script> + +       <div ng-controller="BuzzController"> +         <input ng-model="userId"/> +         <button ng-click="fetch()">fetch</button> +         <hr/> +         <div ng-repeat="item in activities.data.items"> +           <h1 style="font-size: 15px;"> +             <img src="{{item.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/> +             <a href="{{item.actor.profileUrl}}">{{item.actor.name}}</a> +             <a href ng-click="expandReplies(item)" style="float: right;">Expand replies: {{item.links.replies[0].count}}</a> +           </h1> +           {{item.object.content | html}} +           <div ng-repeat="reply in item.replies.data.items" style="margin-left: 20px;"> +             <img src="{{reply.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/> +             <a href="{{reply.actor.profileUrl}}">{{reply.actor.name}}</a>: {{reply.content | html}} +           </div> +         </div> +       </div> +      </doc:source> +      <doc:scenario> +      </doc:scenario> +    </doc:example> + */ +function $ResourceProvider() { +  this.$get = ['$http', function($http) { +    var DEFAULT_ACTIONS = { +      'get':    {method:'GET'}, +      'save':   {method:'POST'}, +      'query':  {method:'GET', isArray:true}, +      'remove': {method:'DELETE'}, +      'delete': {method:'DELETE'} +    }; + + +    function Route(template, defaults) { +      this.template = template = template + '#'; +      this.defaults = defaults || {}; +      var urlParams = this.urlParams = {}; +      forEach(template.split(/\W/), function(param){ +        if (param && template.match(new RegExp("[^\\\\]:" + param + "\\W"))) { +          urlParams[param] = true; +        } +      }); +      this.template = template.replace(/\\:/g, ':'); +    } + +    Route.prototype = { +      url: function(params) { +        var self = this, +            url = this.template, +            encodedVal; + +        params = params || {}; +        forEach(this.urlParams, function(_, urlParam){ +          encodedVal = encodeUriSegment(params[urlParam] || self.defaults[urlParam] || ""); +          url = url.replace(new RegExp(":" + urlParam + "(\\W)"), encodedVal + "$1"); +        }); +        url = url.replace(/\/?#$/, ''); +        var query = []; +        forEachSorted(params, function(value, key){ +          if (!self.urlParams[key]) { +            query.push(encodeUriQuery(key) + '=' + encodeUriQuery(value)); +          } +        }); +        url = url.replace(/\/*$/, ''); +        return url + (query.length ? '?' + query.join('&') : ''); +      } +    }; + + +    function ResourceFactory(url, paramDefaults, actions) { +      var route = new Route(url); + +      actions = extend({}, DEFAULT_ACTIONS, actions); + +      function extractParams(data){ +        var ids = {}; +        forEach(paramDefaults || {}, function(value, key){ +          ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value; +        }); +        return ids; +      } + +      function Resource(value){ +        copy(value || {}, this); +      } + +      forEach(actions, function(action, name) { +        var isPostOrPut = action.method == 'POST' || action.method == 'PUT'; +        Resource[name] = function(a1, a2, a3, a4) { +          var params = {}; +          var data; +          var success = noop; +          var error = null; +          switch(arguments.length) { +          case 4: +            error = a4; +            success = a3; +            //fallthrough +          case 3: +          case 2: +            if (isFunction(a2)) { +              if (isFunction(a1)) { +                success = a1; +                error = a2; +                break; +              } + +              success = a2; +              error = a3; +              //fallthrough +            } else { +              params = a1; +              data = a2; +              success = a3; +              break; +            } +          case 1: +            if (isFunction(a1)) success = a1; +            else if (isPostOrPut) data = a1; +            else params = a1; +            break; +          case 0: break; +          default: +            throw "Expected between 0-4 arguments [params, data, success, error], got " + +              arguments.length + " arguments."; +          } + +          var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data)); +          $http({ +            method: action.method, +            url: route.url(extend({}, extractParams(data), action.params || {}, params)), +            data: data +          }).then(function(response) { +              var data = response.data; + +              if (data) { +                if (action.isArray) { +                  value.length = 0; +                  forEach(data, function(item) { +                    value.push(new Resource(item)); +                  }); +                } else { +                  copy(data, value); +                } +              } +              (success||noop)(value, response.headers); +            }, error); + +          return value; +        }; + + +        Resource.bind = function(additionalParamDefaults){ +          return ResourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); +        }; + + +        Resource.prototype['$' + name] = function(a1, a2, a3) { +          var params = extractParams(this), +              success = noop, +              error; + +          switch(arguments.length) { +          case 3: params = a1; success = a2; error = a3; break; +          case 2: +          case 1: +            if (isFunction(a1)) { +              success = a1; +              error = a2; +            } else { +              params = a1; +              success = a2 || noop; +            } +          case 0: break; +          default: +            throw "Expected between 1-3 arguments [params, success, error], got " + +              arguments.length + " arguments."; +          } +          var data = isPostOrPut ? this : undefined; +          Resource[name].call(this, params, data, success, error); +        }; +      }); +      return Resource; +    } + +    return ResourceFactory; +  }]; +} diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js new file mode 100644 index 00000000..4cf6a3e0 --- /dev/null +++ b/src/ng/rootScope.js @@ -0,0 +1,771 @@ +'use strict'; + +/** + * DESIGN NOTES + * + * The design decisions behind the scope ware heavily favored for speed and memory consumption. + * + * The typical use of scope is to watch the expressions, which most of the time return the same + * value as last time so we optimize the operation. + * + * Closures construction is expensive from speed as well as memory: + *   - no closures, instead ups prototypical inheritance for API + *   - Internal state needs to be stored on scope directly, which means that private state is + *     exposed as $$____ properties + * + * Loop operations are optimized by using while(count--) { ... } + *   - this means that in order to keep the same order of execution as addition we have to add + *     items to the array at the begging (shift) instead of at the end (push) + * + * Child scopes are created and removed often + *   - Using array would be slow since inserts in meddle are expensive so we use linked list + * + * There are few watches then a lot of observers. This is why you don't want the observer to be + * implemented in the same way as watch. Watch requires return of initialization function which + * are expensive to construct. + */ + + +/** + * @ngdoc object + * @name angular.module.ng.$rootScopeProvider + * @description + * + * Provider for the $rootScope service. + */ + +/** + * @ngdoc function + * @name angular.module.ng.$rootScopeProvider#digestTtl + * @methodOf angular.module.ng.$rootScopeProvider + * @description + * + * Sets the number of digest iteration the scope should attempt to execute before giving up and + * assuming that the model is unstable. + * + * The current default is 10 iterations. + * + * @param {number} limit The number of digest iterations. + */ + + +/** + * @ngdoc object + * @name angular.module.ng.$rootScope + * @description + * + * Every application has a single root {@link angular.module.ng.$rootScope.Scope scope}. + * All other scopes are child scopes of the root scope. Scopes provide mechanism for watching the model and provide + * event processing life-cycle. See {@link guide/dev_guide.scopes developer guide on scopes}. + */ +function $RootScopeProvider(){ +  var TTL = 10; + +  this.digestTtl = function(value) { +    if (arguments.length) { +      TTL = value; +    } +    return TTL; +  } + +  this.$get = ['$injector', '$exceptionHandler', '$parse', +      function( $injector,   $exceptionHandler,   $parse) { + +    /** +     * @ngdoc function +     * @name angular.module.ng.$rootScope.Scope +     * +     * @description +     * A root scope can be retrieved using the {@link angular.module.ng.$rootScope $rootScope} key from the +     * {@link angular.module.AUTO.$injector $injector}. Child scopes are created using the +     * {@link angular.module.ng.$rootScope.Scope#$new $new()} method. (Most scopes are created automatically when +     * compiled HTML template is executed.) +     * +     * Here is a simple scope snippet to show how you can interact with the scope. +     * <pre> +        angular.injector(['ng']).invoke(function($rootScope) { +           var scope = $rootScope.$new(); +           scope.salutation = 'Hello'; +           scope.name = 'World'; + +           expect(scope.greeting).toEqual(undefined); + +           scope.$watch('name', function() { +             this.greeting = this.salutation + ' ' + this.name + '!'; +           }); // initialize the watch + +           expect(scope.greeting).toEqual(undefined); +           scope.name = 'Misko'; +           // still old value, since watches have not been called yet +           expect(scope.greeting).toEqual(undefined); + +           scope.$digest(); // fire all  the watches +           expect(scope.greeting).toEqual('Hello Misko!'); +        }); +     * </pre> +     * +     * # Inheritance +     * A scope can inherit from a parent scope, as in this example: +     * <pre> +         var parent = $rootScope; +         var child = parent.$new(); + +         parent.salutation = "Hello"; +         child.name = "World"; +         expect(child.salutation).toEqual('Hello'); + +         child.salutation = "Welcome"; +         expect(child.salutation).toEqual('Welcome'); +         expect(parent.salutation).toEqual('Hello'); +     * </pre> +     * +     * # Dependency Injection +     * See {@link guide/dev_guide.di dependency injection}. +     * +     * +     * @param {Object.<string, function()>=} providers Map of service factory which need to be provided +     *     for the current scope. Defaults to {@link angular.module.ng}. +     * @param {Object.<string, *>=} instanceCache Provides pre-instantiated services which should +     *     append/override services provided by `providers`. This is handy when unit-testing and having +     *     the need to override a default service. +     * @returns {Object} Newly created scope. +     * +     */ +    function Scope() { +      this.$id = nextUid(); +      this.$$phase = this.$parent = this.$$watchers = +                     this.$$nextSibling = this.$$prevSibling = +                     this.$$childHead = this.$$childTail = null; +      this['this'] = this.$root =  this; +      this.$$asyncQueue = []; +      this.$$listeners = {}; +    } + +    /** +     * @ngdoc property +     * @name angular.module.ng.$rootScope.Scope#$id +     * @propertyOf angular.module.ng.$rootScope.Scope +     * @returns {number} Unique scope ID (monotonically increasing alphanumeric sequence) useful for +     *   debugging. +     */ + + +    Scope.prototype = { +      /** +       * @ngdoc function +       * @name angular.module.ng.$rootScope.Scope#$new +       * @methodOf angular.module.ng.$rootScope.Scope +       * @function +       * +       * @description +       * Creates a new child {@link angular.module.ng.$rootScope.Scope scope}. +       * +       * The parent scope will propagate the {@link angular.module.ng.$rootScope.Scope#$digest $digest()} and +       * {@link angular.module.ng.$rootScope.Scope#$digest $digest()} events. The scope can be removed from the scope +       * hierarchy using {@link angular.module.ng.$rootScope.Scope#$destroy $destroy()}. +       * +       * {@link angular.module.ng.$rootScope.Scope#$destroy $destroy()} must be called on a scope when it is desired for +       * the scope and its child scopes to be permanently detached from the parent and thus stop +       * participating in model change detection and listener notification by invoking. +       * +       * @params {boolean} isolate if true then the scoped does not prototypically inherit from the +       *         parent scope. The scope is isolated, as it can not se parent scope properties. +       *         When creating widgets it is useful for the widget to not accidently read parent +       *         state. +       * +       * @returns {Object} The newly created child scope. +       * +       */ +      $new: function(isolate) { +        var Child, +            child; + +        if (isFunction(isolate)) { +          // TODO: remove at some point +          throw Error('API-CHANGE: Use $controller to instantiate controllers.'); +        } +        if (isolate) { +          child = new Scope(); +          child.$root = this.$root; +        } else { +          Child = function() {}; // should be anonymous; This is so that when the minifier munges +            // the name it does not become random set of chars. These will then show up as class +            // name in the debugger. +          Child.prototype = this; +          child = new Child(); +          child.$id = nextUid(); +        } +        child['this'] = child; +        child.$$listeners = {}; +        child.$parent = this; +        child.$$asyncQueue = []; +        child.$$watchers = child.$$nextSibling = child.$$childHead = child.$$childTail = null; +        child.$$prevSibling = this.$$childTail; +        if (this.$$childHead) { +          this.$$childTail.$$nextSibling = child; +          this.$$childTail = child; +        } else { +          this.$$childHead = this.$$childTail = child; +        } +        return child; +      }, + +      /** +       * @ngdoc function +       * @name angular.module.ng.$rootScope.Scope#$watch +       * @methodOf angular.module.ng.$rootScope.Scope +       * @function +       * +       * @description +       * Registers a `listener` callback to be executed whenever the `watchExpression` changes. +       * +       * - The `watchExpression` is called on every call to {@link angular.module.ng.$rootScope.Scope#$digest $digest()} and +       *   should return the value which will be watched. (Since {@link angular.module.ng.$rootScope.Scope#$digest $digest()} +       *   reruns when it detects changes the `watchExpression` can execute multiple times per +       *   {@link angular.module.ng.$rootScope.Scope#$digest $digest()} and should be idempotent.) +       * - The `listener` is called only when the value from the current `watchExpression` and the +       *   previous call to `watchExpression' are not equal (with the exception of the initial run +       *   see below). The inequality is determined according to +       *   {@link angular.equals} function. To save the value of the object for later comparison +       *   {@link angular.copy} function is used. It also means that watching complex options will +       *   have adverse memory and performance implications. +       * - The watch `listener` may change the model, which may trigger other `listener`s to fire. This +       *   is achieved by rerunning the watchers until no changes are detected. The rerun iteration +       *   limit is 100 to prevent infinity loop deadlock. +       * +       * +       * If you want to be notified whenever {@link angular.module.ng.$rootScope.Scope#$digest $digest} is called, +       * you can register an `watchExpression` function with no `listener`. (Since `watchExpression`, +       * can execute multiple times per {@link angular.module.ng.$rootScope.Scope#$digest $digest} cycle when a change is +       * detected, be prepared for multiple calls to your listener.) +       * +       * After a watcher is registered with the scope, the `listener` fn is called asynchronously +       * (via {@link angular.module.ng.$rootScope.Scope#$evalAsync $evalAsync}) to initialize the +       * watcher. In rare cases, this is undesirable because the listener is called when the result +       * of `watchExpression` didn't change. To detect this scenario within the `listener` fn, you +       * can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the +       * listener was called due to initialization. +       * +       * +       * # Example +         <pre> +           // let's assume that scope was dependency injected as the $rootScope +           var scope = $rootScope; +           scope.name = 'misko'; +           scope.counter = 0; + +           expect(scope.counter).toEqual(0); +           scope.$watch('name', function(newValue, oldValue) { counter = counter + 1; }); +           expect(scope.counter).toEqual(0); + +           scope.$digest(); +           // no variable change +           expect(scope.counter).toEqual(0); + +           scope.name = 'adam'; +           scope.$digest(); +           expect(scope.counter).toEqual(1); +         </pre> +       * +       * +       * +       * @param {(function()|string)} watchExpression Expression that is evaluated on each +       *    {@link angular.module.ng.$rootScope.Scope#$digest $digest} cycle. A change in the return value triggers a +       *    call to the `listener`. +       * +       *    - `string`: Evaluated as {@link guide/dev_guide.expressions expression} +       *    - `function(scope)`: called with current `scope` as a parameter. +       * @param {(function()|string)=} listener Callback called whenever the return value of +       *   the `watchExpression` changes. +       * +       *    - `string`: Evaluated as {@link guide/dev_guide.expressions expression} +       *    - `function(newValue, oldValue, scope)`: called with current and previous values as parameters. +       * +       * @param {boolean=} objectEquality Compare object for equality rather then for refference. +       * @returns {function()} Returns a deregistration function for this listener. +       */ +      $watch: function(watchExp, listener, objectEquality) { +        var scope = this, +            get = compileToFn(watchExp, 'watch'), +            array = scope.$$watchers, +            watcher = { +              fn: listener, +              last: initWatchVal, +              get: get, +              exp: watchExp, +              eq: !!objectEquality +            }; + +        // in the case user pass string, we need to compile it, do we really need this ? +        if (!isFunction(listener)) { +          var listenFn = compileToFn(listener || noop, 'listener'); +          watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);}; +        } + +        if (!array) { +          array = scope.$$watchers = []; +        } +        // we use unshift since we use a while loop in $digest for speed. +        // the while loop reads in reverse order. +        array.unshift(watcher); + +        return function() { +          arrayRemove(array, watcher); +        }; +      }, + +      /** +       * @ngdoc function +       * @name angular.module.ng.$rootScope.Scope#$digest +       * @methodOf angular.module.ng.$rootScope.Scope +       * @function +       * +       * @description +       * Process all of the {@link angular.module.ng.$rootScope.Scope#$watch watchers} of the current scope and its children. +       * Because a {@link angular.module.ng.$rootScope.Scope#$watch watcher}'s listener can change the model, the +       * `$digest()` keeps calling the {@link angular.module.ng.$rootScope.Scope#$watch watchers} until no more listeners are +       * firing. This means that it is possible to get into an infinite loop. This function will throw +       * `'Maximum iteration limit exceeded.'` if the number of iterations exceeds 100. +       * +       * Usually you don't call `$digest()` directly in +       * {@link angular.module.ng.$compileProvider.directive.ng-controller controllers} or in +       * {@link angular.module.ng.$compileProvider.directive directives}. +       * Instead a call to {@link angular.module.ng.$rootScope.Scope#$apply $apply()} (typically from within a +       * {@link angular.module.ng.$compileProvider.directive directives}) will force a `$digest()`. +       * +       * If you want to be notified whenever `$digest()` is called, +       * you can register a `watchExpression` function  with {@link angular.module.ng.$rootScope.Scope#$watch $watch()} +       * with no `listener`. +       * +       * You may have a need to call `$digest()` from within unit-tests, to simulate the scope +       * life-cycle. +       * +       * # Example +         <pre> +           var scope = ...; +           scope.name = 'misko'; +           scope.counter = 0; + +           expect(scope.counter).toEqual(0); +           scope.$watch('name', function(scope, newValue, oldValue) { +             counter = counter + 1; +           }); +           expect(scope.counter).toEqual(0); + +           scope.$digest(); +           // no variable change +           expect(scope.counter).toEqual(0); + +           scope.name = 'adam'; +           scope.$digest(); +           expect(scope.counter).toEqual(1); +         </pre> +       * +       */ +      $digest: function() { +        var watch, value, last, +            watchers, +            asyncQueue, +            length, +            dirty, ttl = TTL, +            next, current, target = this, +            watchLog = [], +            logIdx, logMsg; + +        flagPhase(target, '$digest'); + +        do { +          dirty = false; +          current = target; +          do { +            asyncQueue = current.$$asyncQueue; +            while(asyncQueue.length) { +              try { +                current.$eval(asyncQueue.shift()); +              } catch (e) { +                $exceptionHandler(e); +              } +            } +            if ((watchers = current.$$watchers)) { +              // process our watches +              length = watchers.length; +              while (length--) { +                try { +                  watch = watchers[length]; +                  // Most common watches are on primitives, in which case we can short +                  // circuit it with === operator, only when === fails do we use .equals +                  if ((value = watch.get(current)) !== (last = watch.last) && +                      !(watch.eq +                          ? equals(value, last) +                          : (typeof value == 'number' && typeof last == 'number' +                             && isNaN(value) && isNaN(last)))) { +                    dirty = true; +                    watch.last = watch.eq ? copy(value) : value; +                    watch.fn(value, ((last === initWatchVal) ? value : last), current); +                    if (ttl < 5) { +                      logIdx = 4 - ttl; +                      if (!watchLog[logIdx]) watchLog[logIdx] = []; +                      logMsg = (isFunction(watch.exp)) +                          ? 'fn: ' + (watch.exp.name || watch.exp.toString()) +                          : watch.exp; +                      logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last); +                      watchLog[logIdx].push(logMsg); +                    } +                  } +                } catch (e) { +                  $exceptionHandler(e); +                } +              } +            } + +            // Insanity Warning: scope depth-first traversal +            // yes, this code is a bit crazy, but it works and we have tests to prove it! +            // this piece should be kept in sync with the traversal in $broadcast +            if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) { +              while(current !== target && !(next = current.$$nextSibling)) { +                current = current.$parent; +              } +            } +          } while ((current = next)); + +          if(dirty && !(ttl--)) { +            throw Error(TTL + ' $digest() iterations reached. Aborting!\n' + +                'Watchers fired in the last 5 iterations: ' + toJson(watchLog)); +          } +        } while (dirty || asyncQueue.length); + +        this.$root.$$phase = null; +      }, + + +      /** +       * @ngdoc event +       * @name angular.module.$rootScope.Scope#$destroy +       * @eventOf angular.module.ng.$rootScope.Scope +       * @eventType broadcast on scope being destroyed +       * +       * @description +       * Broadcasted when a scope and its children are being destroyed. +       */ + +      /** +       * @ngdoc function +       * @name angular.module.ng.$rootScope.Scope#$destroy +       * @methodOf angular.module.ng.$rootScope.Scope +       * @function +       * +       * @description +       * Remove the current scope (and all of its children) from the parent scope. Removal implies +       * that calls to {@link angular.module.ng.$rootScope.Scope#$digest $digest()} will no longer +       * propagate to the current scope and its children. Removal also implies that the current +       * scope is eligible for garbage collection. +       * +       * The `$destroy()` is usually used by directives such as +       * {@link angular.module.ng.$compileProvider.directive.ng-repeat ng-repeat} for managing the +       * unrolling of the loop. +       * +       * Just before a scope is destroyed a `$destroy` event is broadcasted on this scope. +       * Application code can register a `$destroy` event handler that will give it chance to +       * perform any necessary cleanup. +       */ +      $destroy: function() { +        if (this.$root == this) return; // we can't remove the root node; +        var parent = this.$parent; + +        this.$broadcast('$destroy'); + +        if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; +        if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; +        if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; +        if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; +      }, + +      /** +       * @ngdoc function +       * @name angular.module.ng.$rootScope.Scope#$eval +       * @methodOf angular.module.ng.$rootScope.Scope +       * @function +       * +       * @description +       * Executes the `expression` on the current scope returning the result. Any exceptions in the +       * expression are propagated (uncaught). This is useful when evaluating engular expressions. +       * +       * # Example +         <pre> +           var scope = angular.module.ng.$rootScope.Scope(); +           scope.a = 1; +           scope.b = 2; + +           expect(scope.$eval('a+b')).toEqual(3); +           expect(scope.$eval(function(scope){ return scope.a + scope.b; })).toEqual(3); +         </pre> +       * +       * @param {(string|function())=} expression An angular expression to be executed. +       * +       *    - `string`: execute using the rules as defined in  {@link guide/dev_guide.expressions expression}. +       *    - `function(scope, locals)`: execute the function with the current `scope` parameter. +       * @param {Object=} locals Hash object of local variables for the expression. +       * +       * @returns {*} The result of evaluating the expression. +       */ +      $eval: function(expr, locals) { +        return $parse(expr)(this, locals); +      }, + +      /** +       * @ngdoc function +       * @name angular.module.ng.$rootScope.Scope#$evalAsync +       * @methodOf angular.module.ng.$rootScope.Scope +       * @function +       * +       * @description +       * Executes the expression on the current scope at a later point in time. +       * +       * The `$evalAsync` makes no guarantees as to when the `expression` will be executed, only that: +       * +       *   - it will execute in the current script execution context (before any DOM rendering). +       *   - at least one {@link angular.module.ng.$rootScope.Scope#$digest $digest cycle} will be performed after +       *     `expression` execution. +       * +       * Any exceptions from the execution of the expression are forwarded to the +       * {@link angular.module.ng.$exceptionHandler $exceptionHandler} service. +       * +       * @param {(string|function())=} expression An angular expression to be executed. +       * +       *    - `string`: execute using the rules as defined in  {@link guide/dev_guide.expressions expression}. +       *    - `function(scope)`: execute the function with the current `scope` parameter. +       * +       */ +      $evalAsync: function(expr) { +        this.$$asyncQueue.push(expr); +      }, + +      /** +       * @ngdoc function +       * @name angular.module.ng.$rootScope.Scope#$apply +       * @methodOf angular.module.ng.$rootScope.Scope +       * @function +       * +       * @description +       * `$apply()` is used to execute an expression in angular from outside of the angular framework. +       * (For example from browser DOM events, setTimeout, XHR or third party libraries). +       * Because we are calling into the angular framework we need to perform proper scope life-cycle +       * of {@link angular.module.ng.$exceptionHandler exception handling}, +       * {@link angular.module.ng.$rootScope.Scope#$digest executing watches}. +       * +       * ## Life cycle +       * +       * # Pseudo-Code of `$apply()` +          function $apply(expr) { +            try { +              return $eval(expr); +            } catch (e) { +              $exceptionHandler(e); +            } finally { +              $root.$digest(); +            } +          } +       * +       * +       * Scope's `$apply()` method transitions through the following stages: +       * +       * 1. The {@link guide/dev_guide.expressions expression} is executed using the +       *    {@link angular.module.ng.$rootScope.Scope#$eval $eval()} method. +       * 2. Any exceptions from the execution of the expression are forwarded to the +       *    {@link angular.module.ng.$exceptionHandler $exceptionHandler} service. +       * 3. The {@link angular.module.ng.$rootScope.Scope#$watch watch} listeners are fired immediately after the expression +       *    was executed using the {@link angular.module.ng.$rootScope.Scope#$digest $digest()} method. +       * +       * +       * @param {(string|function())=} exp An angular expression to be executed. +       * +       *    - `string`: execute using the rules as defined in {@link guide/dev_guide.expressions expression}. +       *    - `function(scope)`: execute the function with current `scope` parameter. +       * +       * @returns {*} The result of evaluating the expression. +       */ +      $apply: function(expr) { +        try { +          flagPhase(this, '$apply'); +          return this.$eval(expr); +        } catch (e) { +          $exceptionHandler(e); +        } finally { +          this.$root.$$phase = null; +          this.$root.$digest(); +        } +      }, + +      /** +       * @ngdoc function +       * @name angular.module.ng.$rootScope.Scope#$on +       * @methodOf angular.module.ng.$rootScope.Scope +       * @function +       * +       * @description +       * Listen on events of a given type. See {@link angular.module.ng.$rootScope.Scope#$emit $emit} for discussion of +       * event life cycle. +       * +       * @param {string} name Event name to listen on. +       * @param {function(event)} listener Function to call when the event is emitted. +       * @returns {function()} Returns a deregistration function for this listener. +       * +       * The event listener function format is: `function(event)`. The `event` object passed into the +       * listener has the following attributes +       * +       *   - `targetScope` - {Scope}: the scope on which the event was `$emit`-ed or `$broadcast`-ed. +       *   - `currentScope` - {Scope}: the current scope which is handling the event. +       *   - `name` - {string}: Name of the event. +       *   - `cancel` - {function=}: calling `cancel` function will cancel further event propagation +       *     (available only for events that were `$emit`-ed). +       *   - `cancelled` - {boolean}: Whether the event was cancelled. +       */ +      $on: function(name, listener) { +        var namedListeners = this.$$listeners[name]; +        if (!namedListeners) { +          this.$$listeners[name] = namedListeners = []; +        } +        namedListeners.push(listener); + +        return function() { +          arrayRemove(namedListeners, listener); +        }; +      }, + + +      /** +       * @ngdoc function +       * @name angular.module.ng.$rootScope.Scope#$emit +       * @methodOf angular.module.ng.$rootScope.Scope +       * @function +       * +       * @description +       * Dispatches an event `name` upwards through the scope hierarchy notifying the +       * registered {@link angular.module.ng.$rootScope.Scope#$on} listeners. +       * +       * The event life cycle starts at the scope on which `$emit` was called. All +       * {@link angular.module.ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get notified. +       * Afterwards, the event traverses upwards toward the root scope and calls all registered +       * listeners along the way. The event will stop propagating if one of the listeners cancels it. +       * +       * Any exception emmited from the {@link angular.module.ng.$rootScope.Scope#$on listeners} will be passed +       * onto the {@link angular.module.ng.$exceptionHandler $exceptionHandler} service. +       * +       * @param {string} name Event name to emit. +       * @param {...*} args Optional set of arguments which will be passed onto the event listeners. +       * @return {Object} Event object, see {@link angular.module.ng.$rootScope.Scope#$on} +       */ +      $emit: function(name, args) { +        var empty = [], +            namedListeners, +            scope = this, +            event = { +              name: name, +              targetScope: scope, +              cancel: function() {event.cancelled = true;}, +              cancelled: false +            }, +            listenerArgs = concat([event], arguments, 1), +            i, length; + +        do { +          namedListeners = scope.$$listeners[name] || empty; +          event.currentScope = scope; +          for (i=0, length=namedListeners.length; i<length; i++) { +            try { +              namedListeners[i].apply(null, listenerArgs); +              if (event.cancelled) return event; +            } catch (e) { +              $exceptionHandler(e); +            } +          } +          //traverse upwards +          scope = scope.$parent; +        } while (scope); + +        return event; +      }, + + +      /** +       * @ngdoc function +       * @name angular.module.ng.$rootScope.Scope#$broadcast +       * @methodOf angular.module.ng.$rootScope.Scope +       * @function +       * +       * @description +       * Dispatches an event `name` downwards to all child scopes (and their children) notifying the +       * registered {@link angular.module.ng.$rootScope.Scope#$on} listeners. +       * +       * The event life cycle starts at the scope on which `$broadcast` was called. All +       * {@link angular.module.ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get notified. +       * Afterwards, the event propagates to all direct and indirect scopes of the current scope and +       * calls all registered listeners along the way. The event cannot be canceled. +       * +       * Any exception emmited from the {@link angular.module.ng.$rootScope.Scope#$on listeners} will be passed +       * onto the {@link angular.module.ng.$exceptionHandler $exceptionHandler} service. +       * +       * @param {string} name Event name to emit. +       * @param {...*} args Optional set of arguments which will be passed onto the event listeners. +       * @return {Object} Event object, see {@link angular.module.ng.$rootScope.Scope#$on} +       */ +      $broadcast: function(name, args) { +        var target = this, +            current = target, +            next = target, +            event = { name: name, +                      targetScope: target }, +            listenerArgs = concat([event], arguments, 1); + +        //down while you can, then up and next sibling or up and next sibling until back at root +        do { +          current = next; +          event.currentScope = current; +          forEach(current.$$listeners[name], function(listener) { +            try { +              listener.apply(null, listenerArgs); +            } catch(e) { +              $exceptionHandler(e); +            } +          }); + +          // Insanity Warning: scope depth-first traversal +          // yes, this code is a bit crazy, but it works and we have tests to prove it! +          // this piece should be kept in sync with the traversal in $digest +          if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) { +            while(current !== target && !(next = current.$$nextSibling)) { +              current = current.$parent; +            } +          } +        } while ((current = next)); + +        return event; +      } +    }; + + +    function flagPhase(scope, phase) { +      var root = scope.$root; + +      if (root.$$phase) { +        throw Error(root.$$phase + ' already in progress'); +      } + +      root.$$phase = phase; +    } + +    return new Scope(); + +    function compileToFn(exp, name) { +      var fn = $parse(exp); +      assertArgFn(fn, name); +      return fn; +    } + +    /** +     * function used as an initial value for watchers. +     * because it's uniqueue we can easily tell it apart from other values +     */ +    function initWatchVal() {} +  }]; +} diff --git a/src/ng/route.js b/src/ng/route.js new file mode 100644 index 00000000..2b9d187a --- /dev/null +++ b/src/ng/route.js @@ -0,0 +1,351 @@ +'use strict'; + + +/** + * @ngdoc object + * @name angular.module.ng.$routeProvider + * @function + * + * @description + * + * Used for configuring routes. See {@link angular.module.ng.$route $route} for an example. + */ +function $RouteProvider(){ +  var routes = {}; + +  /** +   * @ngdoc method +   * @name angular.module.ng.$routeProvider#when +   * @methodOf angular.module.ng.$routeProvider +   * +   * @param {string} path Route path (matched against `$location.path`). If `$location.path` +   *    contains redudant trailing slash or is missing one, the route will still match and the +   *    `$location.path` will be updated to add or drop the trailing slash to exacly match the +   *    route definition. +   * @param {Object} route Mapping information to be assigned to `$route.current` on route +   *    match. +   * +   *    Object properties: +   * +   *    - `controller` – `{function()=}` – Controller fn that should be associated with newly +   *      created scope. +   *    - `template` – `{string=}` – path to an html template that should be used by +   *      {@link angular.module.ng.$compileProvider.directive.ng-view ng-view} or +   *      {@link angular.module.ng.$compileProvider.directive.ng-include ng-include} directives. +   *    - `redirectTo` – {(string|function())=} – value to update +   *      {@link angular.module.ng.$location $location} path with and trigger route redirection. +   * +   *      If `redirectTo` is a function, it will be called with the following parameters: +   * +   *      - `{Object.<string>}` - route parameters extracted from the current +   *        `$location.path()` by applying the current route template. +   *      - `{string}` - current `$location.path()` +   *      - `{Object}` - current `$location.search()` +   * +   *      The custom `redirectTo` function is expected to return a string which will be used +   *      to update `$location.path()` and `$location.search()`. +   * +   *    - `[reloadOnSearch=true]` - {boolean=} - reload route when only $location.search() +   *    changes. +   * +   *      If the option is set to `false` and url in the browser changes, then +   *      `$routeUpdate` event is broadcasted on the root scope. +   * +   * @returns {Object} route object +   * +   * @description +   * Adds a new route definition to the `$route` service. +   */ +  this.when = function(path, route) { +    var routeDef = routes[path]; +    if (!routeDef) routeDef = routes[path] = {reloadOnSearch: true}; +    if (route) extend(routeDef, route); // TODO(im): what the heck? merge two route definitions? + +    // create redirection for trailing slashes +    if (path) { +      var redirectPath = (path[path.length-1] == '/') +          ? path.substr(0, path.length-1) +          : path +'/'; + +      routes[redirectPath] = {redirectTo: path}; +    } + +    return routeDef; +  }; + +  /** +   * @ngdoc method +   * @name angular.module.ng.$routeProvider#otherwise +   * @methodOf angular.module.ng.$routeProvider +   * +   * @description +   * Sets route definition that will be used on route change when no other route definition +   * is matched. +   * +   * @param {Object} params Mapping information to be assigned to `$route.current`. +   */ +  this.otherwise = function(params) { +    this.when(null, params); +  }; + + +  this.$get = ['$rootScope', '$location', '$routeParams', +      function( $rootScope,  $location,  $routeParams) { + +    /** +     * @ngdoc object +     * @name angular.module.ng.$route +     * @requires $location +     * @requires $routeParams +     * +     * @property {Object} current Reference to the current route definition. +     * @property {Array.<Object>} routes Array of all configured routes. +     * +     * @description +     * Is used for deep-linking URLs to controllers and views (HTML partials). +     * It watches `$location.url()` and tries to map the path to an existing route definition. +     * +     * You can define routes through {@link angular.module.ng.$routeProvider $routeProvider}'s API. +     * +     * The `$route` service is typically used in conjunction with {@link angular.module.ng.$compileProvider.directive.ng-view ng-view} +     * directive and the {@link angular.module.ng.$routeParams $routeParams} service. +     * +     * @example +       This example shows how changing the URL hash causes the `$route` to match a route against the +       URL, and the `ng-view` pulls in the partial. + +       Note that this example is using {@link angular.module.ng.$compileProvider.directive.script inlined templates} +       to get it working on jsfiddle as well. + +      <doc:example module="route"> +        <doc:source> +          <script type="text/ng-template" id="examples/book.html"> +            controller: {{name}}<br /> +            Book Id: {{params.bookId}}<br /> +          </script> + +          <script type="text/ng-template" id="examples/chapter.html"> +            controller: {{name}}<br /> +            Book Id: {{params.bookId}}<br /> +            Chapter Id: {{params.chapterId}} +          </script> + +          <script> +            angular.module('route', [], function($routeProvider, $locationProvider) { +              $routeProvider.when('/Book/:bookId', {template: 'examples/book.html', controller: BookCntl}); +              $routeProvider.when('/Book/:bookId/ch/:chapterId', {template: 'examples/chapter.html', controller: ChapterCntl}); + +              // configure html5 to get links working on jsfiddle +              $locationProvider.html5Mode(true); +            }); + +            function MainCntl($scope, $route, $routeParams, $location) { +              $scope.$route = $route; +              $scope.$location = $location; +              $scope.$routeParams = $routeParams; +            } + +            function BookCntl($scope, $routeParams) { +              $scope.name = "BookCntl"; +              $scope.params = $routeParams; +            } + +            function ChapterCntl($scope, $routeParams) { +              $scope.name = "ChapterCntl"; +              $scope.params = $routeParams; +            } +          </script> + +          <div ng-controller="MainCntl"> +            Choose: +            <a href="/Book/Moby">Moby</a> | +            <a href="/Book/Moby/ch/1">Moby: Ch1</a> | +            <a href="/Book/Gatsby">Gatsby</a> | +            <a href="/Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a> | +            <a href="/Book/Scarlet">Scarlet Letter</a><br/> + +            <div ng-view></div> +            <hr /> + +            <pre>$location.path() = {{$location.path()}}</pre> +            <pre>$route.current.template = {{$route.current.template}}</pre> +            <pre>$route.current.params = {{$route.current.params}}</pre> +            <pre>$route.current.scope.name = {{$route.current.scope.name}}</pre> +            <pre>$routeParams = {{$routeParams}}</pre> +          </div> +        </doc:source> +        <doc:scenario> +          it('should load and compile correct template', function() { +            element('a:contains("Moby: Ch1")').click(); +            var content = element('.doc-example-live [ng-view]').text(); +            expect(content).toMatch(/controller\: ChapterCntl/); +            expect(content).toMatch(/Book Id\: Moby/); +            expect(content).toMatch(/Chapter Id\: 1/); + +            element('a:contains("Scarlet")').click(); +            content = element('.doc-example-live [ng-view]').text(); +            expect(content).toMatch(/controller\: BookCntl/); +            expect(content).toMatch(/Book Id\: Scarlet/); +          }); +        </doc:scenario> +      </doc:example> +     */ + +    /** +     * @ngdoc event +     * @name angular.module.ng.$route#$beforeRouteChange +     * @eventOf angular.module.ng.$route +     * @eventType broadcast on root scope +     * @description +     * Broadcasted before a route change. +     * +     * @param {Route} next Future route information. +     * @param {Route} current Current route information. +     */ + +    /** +     * @ngdoc event +     * @name angular.module.ng.$route#$afterRouteChange +     * @eventOf angular.module.ng.$route +     * @eventType broadcast on root scope +     * @description +     * Broadcasted after a route change. +     * +     * @param {Route} current Current route information. +     * @param {Route} previous Previous route information. +     */ + +    /** +     * @ngdoc event +     * @name angular.module.ng.$route#$routeUpdate +     * @eventOf angular.module.ng.$route +     * @eventType broadcast on root scope +     * @description +     * +     * The `reloadOnSearch` property has been set to false, and we are reusing the same +     * instance of the Controller. +     */ + +    var matcher = switchRouteMatcher, +        dirty = 0, +        forceReload = false, +        $route = { +          routes: routes, + +          /** +           * @ngdoc method +           * @name angular.module.ng.$route#reload +           * @methodOf angular.module.ng.$route +           * +           * @description +           * Causes `$route` service to reload the current route even if +           * {@link angular.module.ng.$location $location} hasn't changed. +           * +           * As a result of that, {@link angular.module.ng.$compileProvider.directive.ng-view ng-view} +           * creates new scope, reinstantiates the controller. +           */ +          reload: function() { +            dirty++; +            forceReload = true; +          } +        }; + +    $rootScope.$watch(function() { return dirty + $location.url(); }, updateRoute); + +    return $route; + +    ///////////////////////////////////////////////////// + +    function switchRouteMatcher(on, when) { +      // TODO(i): this code is convoluted and inefficient, we should construct the route matching +      //   regex only once and then reuse it +      var regex = '^' + when.replace(/([\.\\\(\)\^\$])/g, "\\$1") + '$', +          params = [], +          dst = {}; +      forEach(when.split(/\W/), function(param) { +        if (param) { +          var paramRegExp = new RegExp(":" + param + "([\\W])"); +          if (regex.match(paramRegExp)) { +            regex = regex.replace(paramRegExp, "([^\\/]*)$1"); +            params.push(param); +          } +        } +      }); +      var match = on.match(new RegExp(regex)); +      if (match) { +        forEach(params, function(name, index) { +          dst[name] = match[index + 1]; +        }); +      } +      return match ? dst : null; +    } + +    function updateRoute() { +      var next = parseRoute(), +          last = $route.current; + +      if (next && last && next.$route === last.$route +          && equals(next.pathParams, last.pathParams) && !next.reloadOnSearch && !forceReload) { +        last.params = next.params; +        copy(last.params, $routeParams); +        $rootScope.$broadcast('$routeUpdate', last); +      } else if (next || last) { +        forceReload = false; +        $rootScope.$broadcast('$beforeRouteChange', next, last); +        $route.current = next; +        if (next) { +          if (next.redirectTo) { +            if (isString(next.redirectTo)) { +              $location.path(interpolate(next.redirectTo, next.params)).search(next.params) +                       .replace(); +            } else { +              $location.url(next.redirectTo(next.pathParams, $location.path(), $location.search())) +                       .replace(); +            } +          } else { +            copy(next.params, $routeParams); +          } +        } +        $rootScope.$broadcast('$afterRouteChange', next, last); +      } +    } + + +    /** +     * @returns the current active route, by matching it against the URL +     */ +    function parseRoute() { +      // Match a route +      var params, match; +      forEach(routes, function(route, path) { +        if (!match && (params = matcher($location.path(), path))) { +          match = inherit(route, { +            params: extend({}, $location.search(), params), +            pathParams: params}); +          match.$route = route; +        } +      }); +      // No route matched; fallback to "otherwise" route +      return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}}); +    } + +    /** +     * @returns interpolation of the redirect path with the parametrs +     */ +    function interpolate(string, params) { +      var result = []; +      forEach((string||'').split(':'), function(segment, i) { +        if (i == 0) { +          result.push(segment); +        } else { +          var segmentMatch = segment.match(/(\w+)(.*)/); +          var key = segmentMatch[1]; +          result.push(params[key]); +          result.push(segmentMatch[2] || ''); +          delete params[key]; +        } +      }); +      return result.join(''); +    } +  }]; +} diff --git a/src/ng/routeParams.js b/src/ng/routeParams.js new file mode 100644 index 00000000..949bc22d --- /dev/null +++ b/src/ng/routeParams.js @@ -0,0 +1,30 @@ +'use strict'; + +/** + * @ngdoc object + * @name angular.module.ng.$routeParams + * @requires $route + * + * @description + * Current set of route parameters. The route parameters are a combination of the + * {@link angular.module.ng.$location $location} `search()`, and `path()`. The `path` parameters + * are extracted when the {@link angular.module.ng.$route $route} path is matched. + * + * In case of parameter name collision, `path` params take precedence over `search` params. + * + * The service guarantees that the identity of the `$routeParams` object will remain unchanged + * (but its properties will likely change) even when a route change occurs. + * + * @example + * <pre> + *  // Given: + *  // URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby + *  // Route: /Chapter/:chapterId/Section/:sectionId + *  // + *  // Then + *  $routeParams ==> {chapterId:1, sectionId:2, search:'moby'} + * </pre> + */ +function $RouteParamsProvider() { +  this.$get = valueFn({}); +} diff --git a/src/ng/sanitize.js b/src/ng/sanitize.js new file mode 100644 index 00000000..7ca0711a --- /dev/null +++ b/src/ng/sanitize.js @@ -0,0 +1,381 @@ +'use strict'; + +/* + * HTML Parser By Misko Hevery (misko@hevery.com) + * based on:  HTML Parser By John Resig (ejohn.org) + * Original code by Erik Arvidsson, Mozilla Public License + * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js + * + * // Use like so: + * htmlParser(htmlString, { + *     start: function(tag, attrs, unary) {}, + *     end: function(tag) {}, + *     chars: function(text) {}, + *     comment: function(text) {} + * }); + * + */ + + + +/** + * @ngdoc service + * @name angular.module.ng.$sanitize + * @function + * + * @description + *   The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are + *   then serialized back to properly escaped html string. This means that no unsafe input can make + *   it into the returned string, however, since our parser is more strict than a typical browser + *   parser, it's possible that some obscure input, which would be recognized as valid HTML by a + *   browser, won't make it through the sanitizer. + * + * @param {string} html Html input. + * @returns {string} Sanitized html. + * + * @example +   <doc:example> +     <doc:source> +       <script> +         function Ctrl($scope) { +           $scope.snippet = +             '<p style="color:blue">an html\n' + +             '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' + +             'snippet</p>'; +         } +       </script> +       <div ng-controller="Ctrl"> +          Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea> +           <table> +             <tr> +               <td>Filter</td> +               <td>Source</td> +               <td>Rendered</td> +             </tr> +             <tr id="html-filter"> +               <td>html filter</td> +               <td> +                 <pre><div ng-bind-html="snippet"><br/></div></pre> +               </td> +               <td> +                 <div ng-bind-html="snippet"></div> +               </td> +             </tr> +             <tr id="escaped-html"> +               <td>no filter</td> +               <td><pre><div ng-bind="snippet"><br/></div></pre></td> +               <td><div ng-bind="snippet"></div></td> +             </tr> +             <tr id="html-unsafe-filter"> +               <td>unsafe html filter</td> +               <td><pre><div ng-bind-html-unsafe="snippet"><br/></div></pre></td> +               <td><div ng-bind-html-unsafe="snippet"></div></td> +             </tr> +           </table> +         </div> +     </doc:source> +     <doc:scenario> +       it('should sanitize the html snippet ', function() { +         expect(using('#html-filter').element('div').html()). +           toBe('<p>an html\n<em>click here</em>\nsnippet</p>'); +       }); + +       it('should escape snippet without any filter', function() { +         expect(using('#escaped-html').element('div').html()). +           toBe("<p style=\"color:blue\">an html\n" + +                "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + +                "snippet</p>"); +       }); + +       it('should inline raw snippet if filtered as unsafe', function() { +         expect(using('#html-unsafe-filter').element("div").html()). +           toBe("<p style=\"color:blue\">an html\n" + +                "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + +                "snippet</p>"); +       }); + +       it('should update', function() { +         input('snippet').enter('new <b>text</b>'); +         expect(using('#html-filter').binding('snippet')).toBe('new <b>text</b>'); +         expect(using('#escaped-html').element('div').html()).toBe("new <b>text</b>"); +         expect(using('#html-unsafe-filter').binding("snippet")).toBe('new <b>text</b>'); +       }); +     </doc:scenario> +   </doc:example> + */ + +function $SanitizeProvider() { +  this.$get = valueFn(function(html) { +    var buf = []; +    htmlParser(html, htmlSanitizeWriter(buf)); +    return buf.join(''); +  }); +}; + +// Regular Expressions for parsing tags and attributes +var START_TAG_REGEXP = /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/, +  END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/, +  ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, +  BEGIN_TAG_REGEXP = /^</, +  BEGING_END_TAGE_REGEXP = /^<\s*\//, +  COMMENT_REGEXP = /<!--(.*?)-->/g, +  CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g, +  URI_REGEXP = /^((ftp|https?):\/\/|mailto:|#)/, +  NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; // Match everything outside of normal chars and " (quote character) + + +// Good source of info about elements and attributes +// http://dev.w3.org/html5/spec/Overview.html#semantics +// http://simon.html5.org/html-elements + +// Safe Void Elements - HTML5 +// http://dev.w3.org/html5/spec/Overview.html#void-elements +var voidElements = makeMap("area,br,col,hr,img,wbr"); + +// Elements that you can, intentionally, leave open (and which close themselves) +// http://dev.w3.org/html5/spec/Overview.html#optional-tags +var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), +    optionalEndTagInlineElements = makeMap("rp,rt"), +    optionalEndTagElements = extend({}, optionalEndTagInlineElements, optionalEndTagBlockElements); + +// Safe Block Elements - HTML5 +var blockElements = extend({}, optionalEndTagBlockElements, makeMap("address,article,aside," + +        "blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6," + +        "header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); + +// Inline Elements - HTML5 +var inlineElements = extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b,bdi,bdo," + +        "big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small," + +        "span,strike,strong,sub,sup,time,tt,u,var")); + + +// Special Elements (can contain anything) +var specialElements = makeMap("script,style"); + +var validElements = extend({}, voidElements, blockElements, inlineElements, optionalEndTagElements); + +//Attributes that have href and hence need to be sanitized +var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap"); +var validAttrs = extend({}, uriAttrs, makeMap( +    'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+ +    'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+ +    'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+ +    'scope,scrolling,shape,span,start,summary,target,title,type,'+ +    'valign,value,vspace,width')); + +/** + * @example + * htmlParser(htmlString, { + *     start: function(tag, attrs, unary) {}, + *     end: function(tag) {}, + *     chars: function(text) {}, + *     comment: function(text) {} + * }); + * + * @param {string} html string + * @param {object} handler + */ +function htmlParser( html, handler ) { +  var index, chars, match, stack = [], last = html; +  stack.last = function() { return stack[ stack.length - 1 ]; }; + +  while ( html ) { +    chars = true; + +    // Make sure we're not in a script or style element +    if ( !stack.last() || !specialElements[ stack.last() ] ) { + +      // Comment +      if ( html.indexOf("<!--") === 0 ) { +        index = html.indexOf("-->"); + +        if ( index >= 0 ) { +          if (handler.comment) handler.comment( html.substring( 4, index ) ); +          html = html.substring( index + 3 ); +          chars = false; +        } + +      // end tag +      } else if ( BEGING_END_TAGE_REGEXP.test(html) ) { +        match = html.match( END_TAG_REGEXP ); + +        if ( match ) { +          html = html.substring( match[0].length ); +          match[0].replace( END_TAG_REGEXP, parseEndTag ); +          chars = false; +        } + +      // start tag +      } else if ( BEGIN_TAG_REGEXP.test(html) ) { +        match = html.match( START_TAG_REGEXP ); + +        if ( match ) { +          html = html.substring( match[0].length ); +          match[0].replace( START_TAG_REGEXP, parseStartTag ); +          chars = false; +        } +      } + +      if ( chars ) { +        index = html.indexOf("<"); + +        var text = index < 0 ? html : html.substring( 0, index ); +        html = index < 0 ? "" : html.substring( index ); + +        if (handler.chars) handler.chars( decodeEntities(text) ); +      } + +    } else { +      html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), function(all, text){ +        text = text. +          replace(COMMENT_REGEXP, "$1"). +          replace(CDATA_REGEXP, "$1"); + +        if (handler.chars) handler.chars( decodeEntities(text) ); + +        return ""; +      }); + +      parseEndTag( "", stack.last() ); +    } + +    if ( html == last ) { +      throw "Parse Error: " + html; +    } +    last = html; +  } + +  // Clean up any remaining tags +  parseEndTag(); + +  function parseStartTag( tag, tagName, rest, unary ) { +    tagName = lowercase(tagName); +    if ( blockElements[ tagName ] ) { +      while ( stack.last() && inlineElements[ stack.last() ] ) { +        parseEndTag( "", stack.last() ); +      } +    } + +    if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) { +      parseEndTag( "", tagName ); +    } + +    unary = voidElements[ tagName ] || !!unary; + +    if ( !unary ) +      stack.push( tagName ); + +    var attrs = {}; + +    rest.replace(ATTR_REGEXP, function(match, name, doubleQuotedValue, singleQoutedValue, unqoutedValue) { +      var value = doubleQuotedValue +        || singleQoutedValue +        || unqoutedValue +        || ''; + +      attrs[name] = decodeEntities(value); +    }); +    if (handler.start) handler.start( tagName, attrs, unary ); +  } + +  function parseEndTag( tag, tagName ) { +    var pos = 0, i; +    tagName = lowercase(tagName); +    if ( tagName ) +      // Find the closest opened tag of the same type +      for ( pos = stack.length - 1; pos >= 0; pos-- ) +        if ( stack[ pos ] == tagName ) +          break; + +    if ( pos >= 0 ) { +      // Close all the open elements, up the stack +      for ( i = stack.length - 1; i >= pos; i-- ) +        if (handler.end) handler.end( stack[ i ] ); + +      // Remove the open elements from the stack +      stack.length = pos; +    } +  } +} + +/** + * decodes all entities into regular string + * @param value + * @returns {string} A string with decoded entities. + */ +var hiddenPre=document.createElement("pre"); +function decodeEntities(value) { +  hiddenPre.innerHTML=value.replace(/</g,"<"); +  return hiddenPre.innerText || hiddenPre.textContent || ''; +} + +/** + * Escapes all potentially dangerous characters, so that the + * resulting string can be safely inserted into attribute or + * element text. + * @param value + * @returns escaped text + */ +function encodeEntities(value) { +  return value. +    replace(/&/g, '&'). +    replace(NON_ALPHANUMERIC_REGEXP, function(value){ +      return '&#' + value.charCodeAt(0) + ';'; +    }). +    replace(/</g, '<'). +    replace(/>/g, '>'); +} + +/** + * create an HTML/XML writer which writes to buffer + * @param {Array} buf use buf.jain('') to get out sanitized html string + * @returns {object} in the form of { + *     start: function(tag, attrs, unary) {}, + *     end: function(tag) {}, + *     chars: function(text) {}, + *     comment: function(text) {} + * } + */ +function htmlSanitizeWriter(buf){ +  var ignore = false; +  var out = bind(buf, buf.push); +  return { +    start: function(tag, attrs, unary){ +      tag = lowercase(tag); +      if (!ignore && specialElements[tag]) { +        ignore = tag; +      } +      if (!ignore && validElements[tag] == true) { +        out('<'); +        out(tag); +        forEach(attrs, function(value, key){ +          var lkey=lowercase(key); +          if (validAttrs[lkey]==true && (uriAttrs[lkey]!==true || value.match(URI_REGEXP))) { +            out(' '); +            out(key); +            out('="'); +            out(encodeEntities(value)); +            out('"'); +          } +        }); +        out(unary ? '/>' : '>'); +      } +    }, +    end: function(tag){ +        tag = lowercase(tag); +        if (!ignore && validElements[tag] == true) { +          out('</'); +          out(tag); +          out('>'); +        } +        if (tag == ignore) { +          ignore = false; +        } +      }, +    chars: function(chars){ +        if (!ignore) { +          out(encodeEntities(chars)); +        } +      } +  }; +} diff --git a/src/ng/sniffer.js b/src/ng/sniffer.js new file mode 100644 index 00000000..eebb2903 --- /dev/null +++ b/src/ng/sniffer.js @@ -0,0 +1,24 @@ +'use strict'; + +/** + * !!! This is an undocumented "private" service !!! + * + * @name angular.module.ng.$sniffer + * @requires $window + * + * @property {boolean} history Does the browser support html5 history api ? + * @property {boolean} hashchange Does the browser support hashchange event ? + * + * @description + * This is very simple implementation of testing browser's features. + */ +function $SnifferProvider(){ +  this.$get = ['$window', function($window){ +    return { +      history: !!($window.history && $window.history.pushState), +      hashchange: 'onhashchange' in $window && +                  // IE8 compatible mode lies +                  (!$window.document.documentMode || $window.document.documentMode > 7) +    }; +  }]; +} diff --git a/src/ng/window.js b/src/ng/window.js new file mode 100644 index 00000000..d7adea03 --- /dev/null +++ b/src/ng/window.js @@ -0,0 +1,28 @@ +'use strict'; + +/** + * @ngdoc object + * @name angular.module.ng.$window + * + * @description + * A reference to the browser's `window` object. While `window` + * is globally available in JavaScript, it causes testability problems, because + * it is a global variable. In angular we always refer to it through the + * `$window` service, so it may be overriden, removed or mocked for testing. + * + * All expressions are evaluated with respect to current scope so they don't + * suffer from window globality. + * + * @example +   <doc:example> +     <doc:source> +       <input ng-init="$window = $service('$window'); greeting='Hello World!'" type="text" ng-model="greeting" /> +       <button ng-click="$window.alert(greeting)">ALERT</button> +     </doc:source> +     <doc:scenario> +     </doc:scenario> +   </doc:example> + */ +function $WindowProvider(){ +  this.$get = valueFn(window); +} | 
