From 2430f52bb97fa9d682e5f028c977c5bf94c5ec38 Mon Sep 17 00:00:00 2001
From: Misko Hevery
Date: Fri, 23 Mar 2012 14:03:24 -0700
Subject: chore(module): move files around in preparation for more modules
---
src/ng/anchorScroll.js | 66 ++
src/ng/browser.js | 413 ++++++++++++
src/ng/cacheFactory.js | 159 +++++
src/ng/compiler.js | 1014 +++++++++++++++++++++++++++++
src/ng/controller.js | 68 ++
src/ng/cookieStore.js | 64 ++
src/ng/cookies.js | 94 +++
src/ng/defer.js | 45 ++
src/ng/directive/a.js | 29 +
src/ng/directive/booleanAttrDirs.js | 314 +++++++++
src/ng/directive/directives.js | 11 +
src/ng/directive/form.js | 267 ++++++++
src/ng/directive/input.js | 1194 +++++++++++++++++++++++++++++++++++
src/ng/directive/ngBind.js | 155 +++++
src/ng/directive/ngClass.js | 143 +++++
src/ng/directive/ngCloak.js | 61 ++
src/ng/directive/ngController.js | 103 +++
src/ng/directive/ngEventDirs.js | 222 +++++++
src/ng/directive/ngInclude.js | 131 ++++
src/ng/directive/ngInit.js | 37 ++
src/ng/directive/ngNonBindable.js | 33 +
src/ng/directive/ngPluralize.js | 204 ++++++
src/ng/directive/ngRepeat.js | 181 ++++++
src/ng/directive/ngShowHide.js | 80 +++
src/ng/directive/ngStyle.js | 42 ++
src/ng/directive/ngSwitch.js | 112 ++++
src/ng/directive/ngTransclude.js | 58 ++
src/ng/directive/ngView.js | 170 +++++
src/ng/directive/script.js | 43 ++
src/ng/directive/select.js | 449 +++++++++++++
src/ng/directive/style.js | 6 +
src/ng/document.js | 16 +
src/ng/exceptionHandler.js | 26 +
src/ng/filter.js | 104 +++
src/ng/filter/filter.js | 164 +++++
src/ng/filter/filters.js | 527 ++++++++++++++++
src/ng/filter/limitTo.js | 87 +++
src/ng/filter/orderBy.js | 137 ++++
src/ng/http.js | 743 ++++++++++++++++++++++
src/ng/httpBackend.js | 99 +++
src/ng/interpolate.js | 145 +++++
src/ng/locale.js | 72 +++
src/ng/location.js | 556 ++++++++++++++++
src/ng/log.js | 116 ++++
src/ng/parse.js | 760 ++++++++++++++++++++++
src/ng/q.js | 391 ++++++++++++
src/ng/resource.js | 368 +++++++++++
src/ng/rootScope.js | 771 ++++++++++++++++++++++
src/ng/route.js | 351 ++++++++++
src/ng/routeParams.js | 30 +
src/ng/sanitize.js | 381 +++++++++++
src/ng/sniffer.js | 24 +
src/ng/window.js | 28 +
53 files changed, 11864 insertions(+)
create mode 100644 src/ng/anchorScroll.js
create mode 100644 src/ng/browser.js
create mode 100644 src/ng/cacheFactory.js
create mode 100644 src/ng/compiler.js
create mode 100644 src/ng/controller.js
create mode 100644 src/ng/cookieStore.js
create mode 100644 src/ng/cookies.js
create mode 100644 src/ng/defer.js
create mode 100644 src/ng/directive/a.js
create mode 100644 src/ng/directive/booleanAttrDirs.js
create mode 100644 src/ng/directive/directives.js
create mode 100644 src/ng/directive/form.js
create mode 100644 src/ng/directive/input.js
create mode 100644 src/ng/directive/ngBind.js
create mode 100644 src/ng/directive/ngClass.js
create mode 100644 src/ng/directive/ngCloak.js
create mode 100644 src/ng/directive/ngController.js
create mode 100644 src/ng/directive/ngEventDirs.js
create mode 100644 src/ng/directive/ngInclude.js
create mode 100644 src/ng/directive/ngInit.js
create mode 100644 src/ng/directive/ngNonBindable.js
create mode 100644 src/ng/directive/ngPluralize.js
create mode 100644 src/ng/directive/ngRepeat.js
create mode 100644 src/ng/directive/ngShowHide.js
create mode 100644 src/ng/directive/ngStyle.js
create mode 100644 src/ng/directive/ngSwitch.js
create mode 100644 src/ng/directive/ngTransclude.js
create mode 100644 src/ng/directive/ngView.js
create mode 100644 src/ng/directive/script.js
create mode 100644 src/ng/directive/select.js
create mode 100644 src/ng/directive/style.js
create mode 100644 src/ng/document.js
create mode 100644 src/ng/exceptionHandler.js
create mode 100644 src/ng/filter.js
create mode 100644 src/ng/filter/filter.js
create mode 100644 src/ng/filter/filters.js
create mode 100644 src/ng/filter/limitTo.js
create mode 100644 src/ng/filter/orderBy.js
create mode 100644 src/ng/http.js
create mode 100644 src/ng/httpBackend.js
create mode 100644 src/ng/interpolate.js
create mode 100644 src/ng/locale.js
create mode 100644 src/ng/location.js
create mode 100644 src/ng/log.js
create mode 100644 src/ng/parse.js
create mode 100644 src/ng/q.js
create mode 100644 src/ng/resource.js
create mode 100644 src/ng/rootScope.js
create mode 100644 src/ng/route.js
create mode 100644 src/ng/routeParams.js
create mode 100644 src/ng/sanitize.js
create mode 100644 src/ng/sniffer.js
create mode 100644 src/ng/window.js
(limited to 'src/ng')
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:
+ *
+ *
cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify it
+ *
cookies(name, value) -> set name to value, if value is undefined delete the cookie
+ *
cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that way)
+ *
+ *
+ * @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
+ * (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.
+ *
+
+
+
+
+
+
+
+
+
+
+ it('should auto compile', function() {
+ expect(element('div[compile]').text()).toBe('Hello Angular');
+ input('html').enter('{{name}}!');
+ expect(element('div[compile]').text()).toBe('Angular!');
+ });
+
+
+
+ *
+ *
+ * @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: `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.
+ *
+ * var element = $compile('
{{total}}
')(scope);
+ *
+ *
+ * - 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:
+ *
+ * var templateHTML = angular.element('
{{total}}
'),
+ * 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`
+ *
+ *
+ *
+ * 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 = /\<\\>/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
+ forEach(templateElement, function(node, index){
+ if (node.nodeType == 3 /* text node */) {
+ templateElement[index] = jqLite(node).wrap('').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
+ 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(
+ ''))[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 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; iSave
+ */
+var htmlAnchorDirective = valueFn({
+ restrict: 'E',
+ compile: function(element, attr) {
+ // turn link 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 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:
+ *
+ *
+ *
+ *
+ * The correct way to write it:
+ *
+ *
+ *
+ *
+ * @element A
+ * @param {template} ng-href any string which can contain `{{}}` markup.
+ *
+ * @example
+ * This example uses `link` variable inside `href` attribute:
+
+
+
+ link 1 (link, don't reload)
+ link 2 (link, don't reload)
+ link 3 (link, reload!)
+ anchor (link, don't reload)
+ anchor (no link)
+ link (link, change hash)
+
+
+ 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');
+ });
+
+
+ */
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng-src
+ * @restrict A
+ *
+ * @description
+ * Using markup like `{{hash}}` in a `src` attribute doesn't
+ * work right: The browser will fetch from the URL with the literal
+ * text `{{hash}}` until 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:
+ *
+ *
+ *
+ *
+ * The correct way to write it:
+ *
+ *
+ *
+ *
+ * @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:
+ *
+ *
+ *
+ *
+ *
+ *
+ * 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
+
+
+ Click me to toggle:
+
+
+
+ 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();
+ });
+
+
+ *
+ * @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
+
+
+ Check me to check both:
+
+
+
+ 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();
+ });
+
+
+ *
+ * @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
+
+
+ Check me check multiple:
+
+
+
+ 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();
+ });
+
+
+ *
+ * @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
+
+
+ Check me to make text readonly:
+
+
+
+ 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();
+ });
+
+
+ *
+ * @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
+
+
+ Check me to select:
+
+
+
+ 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();
+ });
+
+
+ *
+ * @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 `