diff --git a/.gitignore b/.gitignore index 1377554..0f72e6a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ *.swp +.ruby-version +.ruby-gemset diff --git a/angular_app/index.html b/angular_app/index.html index e45f871..c0fbe50 100644 --- a/angular_app/index.html +++ b/angular_app/index.html @@ -1,18 +1,19 @@ - + + - Ticket Tracker Demo in Angular + Todo Tracker Demo in Angular
-

Ticket Tracker Demo: Angular

-
- +

Todo Tracker Demo: Angular

+
+ @@ -21,12 +22,12 @@

Ticket Tracker Demo: Angular

- + - + diff --git a/angular_app/js/angular-resource.js b/angular_app/js/angular-resource.js new file mode 100644 index 0000000..712b32e --- /dev/null +++ b/angular_app/js/angular-resource.js @@ -0,0 +1,610 @@ +/** + * @license AngularJS v1.2.17 + * (c) 2010-2014 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window, angular, undefined) {'use strict'; + +var $resourceMinErr = angular.$$minErr('$resource'); + +// Helper functions and regex to lookup a dotted path on an object +// stopping at undefined/null. The path must be composed of ASCII +// identifiers (just like $parse) +var MEMBER_NAME_REGEX = /^(\.[a-zA-Z_$][0-9a-zA-Z_$]*)+$/; + +function isValidDottedPath(path) { + return (path != null && path !== '' && path !== 'hasOwnProperty' && + MEMBER_NAME_REGEX.test('.' + path)); +} + +function lookupDottedPath(obj, path) { + if (!isValidDottedPath(path)) { + throw $resourceMinErr('badmember', 'Dotted member path "@{0}" is invalid.', path); + } + var keys = path.split('.'); + for (var i = 0, ii = keys.length; i < ii && obj !== undefined; i++) { + var key = keys[i]; + obj = (obj !== null) ? obj[key] : undefined; + } + return obj; +} + +/** + * Create a shallow copy of an object and clear other fields from the destination + */ +function shallowClearAndCopy(src, dst) { + dst = dst || {}; + + angular.forEach(dst, function(value, key){ + delete dst[key]; + }); + + for (var key in src) { + if (src.hasOwnProperty(key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { + dst[key] = src[key]; + } + } + + return dst; +} + +/** + * @ngdoc module + * @name ngResource + * @description + * + * # ngResource + * + * The `ngResource` module provides interaction support with RESTful services + * via the $resource service. + * + * + *
+ * + * See {@link ngResource.$resource `$resource`} for usage. + */ + +/** + * @ngdoc service + * @name $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 ng.$http $http} service. + * + * Requires the {@link ngResource `ngResource`} module to be installed. + * + * @param {string} url A parametrized URL template with parameters prefixed by `:` as in + * `/user/:username`. If you are using a URL with a port number (e.g. + * `http://example.com:8080/api`), it will be respected. + * + * If you are using a url with a suffix, just add the suffix, like this: + * `$resource('http://example.com/resource.json')` or `$resource('http://example.com/:id.json')` + * or even `$resource('http://example.com/resource/:resource_id.:format')` + * If the parameter before the suffix is empty, :resource_id in this case, then the `/.` will be + * collapsed down to a single `.`. If you need this sequence to appear and not collapse then you + * can escape it with `/\.`. + * + * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in + * `actions` methods. If any of the parameter value is a function, it will be executed every time + * when a param value needs to be obtained for a request (unless the param was overridden). + * + * 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 will be taken + * from the corresponding key on the data object (useful for non-GET operations). + * + * @param {Object.=} actions Hash with declaration of custom action that should extend + * the default set of resource actions. The declaration should be created in the format of {@link + * ng.$http#usage_parameters $http.config}: + * + * {action1: {method:?, params:?, isArray:?, headers:?, ...}, + * action2: {method:?, params:?, isArray:?, headers:?, ...}, + * ...} + * + * 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. If any of + * the parameter value is a function, it will be executed every time when a param value needs to + * be obtained for a request (unless the param was overridden). + * - **`url`** – {string} – action specific `url` override. The url templating is supported just + * like for the resource-level urls. + * - **`isArray`** – {boolean=} – If true then the returned object for this action is an array, + * see `returns` section. + * - **`transformRequest`** – + * `{function(data, headersGetter)|Array.}` – + * 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.}` – + * 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 ng.$cacheFactory $cacheFactory}, this cache will be used for + * caching. + * - **`timeout`** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} that + * should abort the request when resolved. + * - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the + * XHR object. See + * [requests with credentials](https://developer.mozilla.org/en/http_access_control#section_5) + * for more information. + * - **`responseType`** - `{string}` - see + * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType). + * - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods - + * `response` and `responseError`. Both `response` and `responseError` interceptors get called + * with `http response` object. See {@link ng.$http $http interceptors}. + * + * @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: + * ```js + * { 'get': {method:'GET'}, + * 'save': {method:'POST'}, + * 'query': {method:'GET', isArray:true}, + * 'remove': {method:'DELETE'}, + * 'delete': {method:'DELETE'} }; + * ``` + * + * Calling these methods invoke an {@link 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. The actions `save`, `remove` and `delete` 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: + * ```js + * var User = $resource('/user/:userId', {userId:'@id'}); + * var user = User.get({userId:123}, function() { + * user.abc = true; + * user.$save(); + * }); + * ``` + * + * 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 cases 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])` + * + * Success callback is called with (value, responseHeaders) arguments. Error callback is called + * with (httpResponse) argument. + * + * Class actions return empty instance (with additional properties below). + * Instance actions return promise of the action. + * + * The Resource instances and collection have these additional properties: + * + * - `$promise`: the {@link ng.$q promise} of the original server interaction that created this + * instance or collection. + * + * On success, the promise is resolved with the same resource instance or collection object, + * updated with data from server. This makes it easy to use in + * {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view + * rendering until the resource(s) are loaded. + * + * On failure, the promise is resolved with the {@link ng.$http http response} object, without + * the `resource` property. + * + * If an interceptor object was provided, the promise will instead be resolved with the value + * returned by the interceptor. + * + * - `$resolved`: `true` after first server interaction is completed (either with success or + * rejection), `false` before that. Knowing if the Resource has been resolved is useful in + * data-binding. + * + * @example + * + * # Credit card resource + * + * ```js + // 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(function() { + // 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'} + }); + + // 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:'0123', name: 'Mike Smith'}; + expect(newCard.id).toEqual(789); + * ``` + * + * 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`, `params` and + * `headers`. + * 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. + + ```js + var User = $resource('/user/:userId', {userId:'@id'}); + User.get({userId:123}, function(user) { + user.abc = true; + user.$save(); + }); + ``` + * + * It's worth noting that the success callback for `get`, `query` and other methods 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: + * + ```js + 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 + }); + }); + ``` + * + * You can also access the raw `$http` promise via the `$promise` property on the object returned + * + ``` + var User = $resource('/user/:userId', {userId:'@id'}); + User.get({userId:123}) + .$promise.then(function(user) { + $scope.user = user; + }); + ``` + + * # Creating a custom 'PUT' request + * In this example we create a custom method on our resource to make a PUT request + * ```js + * var app = angular.module('app', ['ngResource', 'ngRoute']); + * + * // Some APIs expect a PUT request in the format URL/object/ID + * // Here we are creating an 'update' method + * app.factory('Notes', ['$resource', function($resource) { + * return $resource('/notes/:id', null, + * { + * 'update': { method:'PUT' } + * }); + * }]); + * + * // In our controller we get the ID from the URL using ngRoute and $routeParams + * // We pass in $routeParams and our Notes factory along with $scope + * app.controller('NotesCtrl', ['$scope', '$routeParams', 'Notes', + function($scope, $routeParams, Notes) { + * // First get a note object from the factory + * var note = Notes.get({ id:$routeParams.id }); + * $id = note.id; + * + * // Now call update passing in the ID first then the object you are updating + * Notes.update({ id:$id }, note); + * + * // This will PUT /notes/ID with the note object in the request payload + * }]); + * ``` + */ +angular.module('ngResource', ['ng']). + factory('$resource', ['$http', '$q', function($http, $q) { + + var DEFAULT_ACTIONS = { + 'get': {method:'GET'}, + 'save': {method:'POST'}, + 'query': {method:'GET', isArray:true}, + 'remove': {method:'DELETE'}, + 'delete': {method:'DELETE'} + }; + var noop = angular.noop, + forEach = angular.forEach, + extend = angular.extend, + copy = angular.copy, + isFunction = angular.isFunction; + + /** + * We need our custom method because encodeURIComponent is too aggressive and doesn't follow + * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path + * segments: + * segment = *pchar + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + * pct-encoded = "%" HEXDIG HEXDIG + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + */ + function encodeUriSegment(val) { + return encodeUriQuery(val, true). + replace(/%26/gi, '&'). + replace(/%3D/gi, '='). + replace(/%2B/gi, '+'); + } + + + /** + * This method is intended for encoding *key* or *value* parts of query component. We need a + * custom method because encodeURIComponent is too aggressive and encodes stuff that doesn't + * have to be encoded per http://tools.ietf.org/html/rfc3986: + * query = *( pchar / "/" / "?" ) + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * pct-encoded = "%" HEXDIG HEXDIG + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + */ + function encodeUriQuery(val, pctEncodeSpaces) { + return encodeURIComponent(val). + replace(/%40/gi, '@'). + replace(/%3A/gi, ':'). + replace(/%24/g, '$'). + replace(/%2C/gi, ','). + replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); + } + + function Route(template, defaults) { + this.template = template; + this.defaults = defaults || {}; + this.urlParams = {}; + } + + Route.prototype = { + setUrlParams: function(config, params, actionUrl) { + var self = this, + url = actionUrl || self.template, + val, + encodedVal; + + var urlParams = self.urlParams = {}; + forEach(url.split(/\W/), function(param){ + if (param === 'hasOwnProperty') { + throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name."); + } + if (!(new RegExp("^\\d+$").test(param)) && param && + (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) { + urlParams[param] = true; + } + }); + url = url.replace(/\\:/g, ':'); + + params = params || {}; + forEach(self.urlParams, function(_, urlParam){ + val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam]; + if (angular.isDefined(val) && val !== null) { + encodedVal = encodeUriSegment(val); + url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), function(match, p1) { + return encodedVal + p1; + }); + } else { + url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W|$)", "g"), function(match, + leadingSlashes, tail) { + if (tail.charAt(0) == '/') { + return tail; + } else { + return leadingSlashes + tail; + } + }); + } + }); + + // strip trailing slashes and set the url + url = url.replace(/\/+$/, '') || '/'; + // then replace collapse `/.` if found in the last URL path segment before the query + // E.g. `http://url.com/id./format?q=x` becomes `http://url.com/id.format?q=x` + url = url.replace(/\/\.(?=\w+($|\?))/, '.'); + // replace escaped `/\.` with `/.` + config.url = url.replace(/\/\\\./, '/.'); + + + // set params - delegate param encoding to $http + forEach(params, function(value, key){ + if (!self.urlParams[key]) { + config.params = config.params || {}; + config.params[key] = value; + } + }); + } + }; + + + function resourceFactory(url, paramDefaults, actions) { + var route = new Route(url); + + actions = extend({}, DEFAULT_ACTIONS, actions); + + function extractParams(data, actionParams){ + var ids = {}; + actionParams = extend({}, paramDefaults, actionParams); + forEach(actionParams, function(value, key){ + if (isFunction(value)) { value = value(); } + ids[key] = value && value.charAt && value.charAt(0) == '@' ? + lookupDottedPath(data, value.substr(1)) : value; + }); + return ids; + } + + function defaultResponseInterceptor(response) { + return response.resource; + } + + function Resource(value){ + shallowClearAndCopy(value || {}, this); + } + + forEach(actions, function(action, name) { + var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method); + + Resource[name] = function(a1, a2, a3, a4) { + var params = {}, data, success, error; + + /* jshint -W086 */ /* (purposefully fall through case statements) */ + 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 (hasBody) data = a1; + else params = a1; + break; + case 0: break; + default: + throw $resourceMinErr('badargs', + "Expected up to 4 arguments [params, data, success, error], got {0} arguments", + arguments.length); + } + /* jshint +W086 */ /* (purposefully fall through case statements) */ + + var isInstanceCall = this instanceof Resource; + var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data)); + var httpConfig = {}; + var responseInterceptor = action.interceptor && action.interceptor.response || + defaultResponseInterceptor; + var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || + undefined; + + forEach(action, function(value, key) { + if (key != 'params' && key != 'isArray' && key != 'interceptor') { + httpConfig[key] = copy(value); + } + }); + + if (hasBody) httpConfig.data = data; + route.setUrlParams(httpConfig, + extend({}, extractParams(data, action.params || {}), params), + action.url); + + var promise = $http(httpConfig).then(function(response) { + var data = response.data, + promise = value.$promise; + + if (data) { + // Need to convert action.isArray to boolean in case it is undefined + // jshint -W018 + if (angular.isArray(data) !== (!!action.isArray)) { + throw $resourceMinErr('badcfg', 'Error in resource configuration. Expected ' + + 'response to contain an {0} but got an {1}', + action.isArray?'array':'object', angular.isArray(data)?'array':'object'); + } + // jshint +W018 + if (action.isArray) { + value.length = 0; + forEach(data, function(item) { + value.push(new Resource(item)); + }); + } else { + shallowClearAndCopy(data, value); + value.$promise = promise; + } + } + + value.$resolved = true; + + response.resource = value; + + return response; + }, function(response) { + value.$resolved = true; + + (error||noop)(response); + + return $q.reject(response); + }); + + promise = promise.then( + function(response) { + var value = responseInterceptor(response); + (success||noop)(value, response.headers); + return value; + }, + responseErrorInterceptor); + + if (!isInstanceCall) { + // we are creating instance / collection + // - set the initial promise + // - return the instance / collection + value.$promise = promise; + value.$resolved = false; + + return value; + } + + // instance call + return promise; + }; + + + Resource.prototype['$' + name] = function(params, success, error) { + if (isFunction(params)) { + error = success; success = params; params = {}; + } + var result = Resource[name].call(this, params, this, success, error); + return result.$promise || result; + }; + }); + + Resource.bind = function(additionalParamDefaults){ + return resourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); + }; + + return Resource; + } + + return resourceFactory; + }]); + + +})(window, window.angular); diff --git a/angular_app/js/angular.js b/angular_app/js/angular.js index 719bc64..e3e6ffd 100644 --- a/angular_app/js/angular.js +++ b/angular_app/js/angular.js @@ -1,29 +1,204 @@ /** - * @license AngularJS v1.0.8 - * (c) 2010-2012 Google, Inc. http://angularjs.org + * @license AngularJS v1.2.17 + * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ -(function(window, document, undefined) { -'use strict'; +(function(window, document, undefined) {'use strict'; + +/** + * @description + * + * This object provides a utility for producing rich Error messages within + * Angular. It can be called as follows: + * + * var exampleMinErr = minErr('example'); + * throw exampleMinErr('one', 'This {0} is {1}', foo, bar); + * + * The above creates an instance of minErr in the example namespace. The + * resulting error will have a namespaced error code of example.one. The + * resulting error will replace {0} with the value of foo, and {1} with the + * value of bar. The object is not restricted in the number of arguments it can + * take. + * + * If fewer arguments are specified than necessary for interpolation, the extra + * interpolation markers will be preserved in the final string. + * + * Since data will be parsed statically during a build step, some restrictions + * are applied with respect to how minErr instances are created and called. + * Instances should have names of the form namespaceMinErr for a minErr created + * using minErr('namespace') . Error codes, namespaces and template strings + * should all be static strings, not variables or general expressions. + * + * @param {string} module The namespace to use for the new minErr instance. + * @returns {function(code:string, template:string, ...templateArgs): Error} minErr instance + */ + +function minErr(module) { + return function () { + var code = arguments[0], + prefix = '[' + (module ? module + ':' : '') + code + '] ', + template = arguments[1], + templateArgs = arguments, + stringify = function (obj) { + if (typeof obj === 'function') { + return obj.toString().replace(/ \{[\s\S]*$/, ''); + } else if (typeof obj === 'undefined') { + return 'undefined'; + } else if (typeof obj !== 'string') { + return JSON.stringify(obj); + } + return obj; + }, + message, i; + + message = prefix + template.replace(/\{\d+\}/g, function (match) { + var index = +match.slice(1, -1), arg; + + if (index + 2 < templateArgs.length) { + arg = templateArgs[index + 2]; + if (typeof arg === 'function') { + return arg.toString().replace(/ ?\{[\s\S]*$/, ''); + } else if (typeof arg === 'undefined') { + return 'undefined'; + } else if (typeof arg !== 'string') { + return toJson(arg); + } + return arg; + } + return match; + }); + + message = message + '\nhttp://errors.angularjs.org/1.2.17/' + + (module ? module + '/' : '') + code; + for (i = 2; i < arguments.length; i++) { + message = message + (i == 2 ? '?' : '&') + 'p' + (i-2) + '=' + + encodeURIComponent(stringify(arguments[i])); + } + + return new Error(message); + }; +} + +/* We need to tell jshint what variables are being exported */ +/* global + -angular, + -msie, + -jqLite, + -jQuery, + -slice, + -push, + -toString, + -ngMinErr, + -angularModule, + -nodeName_, + -uid, + + -lowercase, + -uppercase, + -manualLowercase, + -manualUppercase, + -nodeName_, + -isArrayLike, + -forEach, + -sortedKeys, + -forEachSorted, + -reverseParams, + -nextUid, + -setHashKey, + -extend, + -int, + -inherit, + -noop, + -identity, + -valueFn, + -isUndefined, + -isDefined, + -isObject, + -isString, + -isNumber, + -isDate, + -isArray, + -isFunction, + -isRegExp, + -isWindow, + -isScope, + -isFile, + -isBlob, + -isBoolean, + -trim, + -isElement, + -makeMap, + -map, + -size, + -includes, + -indexOf, + -arrayRemove, + -isLeafNode, + -copy, + -shallowCopy, + -equals, + -csp, + -concat, + -sliceArgs, + -bind, + -toJsonReplacer, + -toJson, + -fromJson, + -toBoolean, + -startingTag, + -tryDecodeURIComponent, + -parseKeyValue, + -toKeyValue, + -encodeUriSegment, + -encodeUriQuery, + -angularInit, + -bootstrap, + -snake_case, + -bindJQuery, + -assertArg, + -assertArgFn, + -assertNotHasOwnProperty, + -getter, + -getBlockElements, + -hasOwnProperty, + +*/ //////////////////////////////////// +/** + * @ngdoc module + * @name ng + * @module ng + * @description + * + * # ng (core module) + * The ng module is loaded by default when an AngularJS application is started. The module itself + * contains the essential components for an AngularJS application to function. The table below + * lists a high level breakdown of each of the services/factories, filters, directives and testing + * components available within this core module. + * + *
+ */ + /** * @ngdoc function * @name angular.lowercase - * @function + * @module ng + * @kind function * * @description Converts the specified string to lowercase. * @param {string} string String to be converted to lowercase. * @returns {string} Lowercased string. */ var lowercase = function(string){return isString(string) ? string.toLowerCase() : string;}; - +var hasOwnProperty = Object.prototype.hasOwnProperty; /** * @ngdoc function * @name angular.uppercase - * @function + * @module ng + * @kind function * * @description Converts the specified string to uppercase. * @param {string} string String to be converted to uppercase. @@ -33,11 +208,13 @@ var uppercase = function(string){return isString(string) ? string.toUpperCase() var manualLowercase = function(s) { + /* jshint bitwise: false */ return isString(s) ? s.replace(/[A-Z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) | 32);}) : s; }; var manualUppercase = function(s) { + /* jshint bitwise: false */ return isString(s) ? s.replace(/[a-z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) & ~32);}) : s; @@ -54,12 +231,13 @@ if ('i' !== 'I'.toLowerCase()) { var /** holds major version number for IE or NaN for real browsers */ - msie = int((/msie (\d+)/.exec(lowercase(navigator.userAgent)) || [])[1]), + msie, jqLite, // delay binding since jQuery could be loaded after us. jQuery, // delay binding slice = [].slice, push = [].push, toString = Object.prototype.toString, + ngMinErr = minErr('ng'), /** @name angular */ angular = window.angular || (window.angular = {}), @@ -67,33 +245,42 @@ var /** holds major version number for IE or NaN for real browsers */ nodeName_, uid = ['0', '0', '0']; +/** + * IE 11 changed the format of the UserAgent string. + * See http://msdn.microsoft.com/en-us/library/ms537503.aspx + */ +msie = int((/msie (\d+)/.exec(lowercase(navigator.userAgent)) || [])[1]); +if (isNaN(msie)) { + msie = int((/trident\/.*; rv:(\d+)/.exec(lowercase(navigator.userAgent)) || [])[1]); +} + /** * @private * @param {*} obj - * @return {boolean} Returns true if `obj` is an array or array-like object (NodeList, Arguments, ...) + * @return {boolean} Returns true if `obj` is an array or array-like object (NodeList, Arguments, + * String ...) */ function isArrayLike(obj) { - if (!obj || (typeof obj.length !== 'number')) return false; + if (obj == null || isWindow(obj)) { + return false; + } - // We have on object which has length property. Should we treat it as array? - if (typeof obj.hasOwnProperty != 'function' && - typeof obj.constructor != 'function') { - // This is here for IE8: it is a bogus object treat it as array; + var length = obj.length; + + if (obj.nodeType === 1 && length) { return true; - } else { - return obj instanceof JQLite || // JQLite - (jQuery && obj instanceof jQuery) || // jQuery - toString.call(obj) !== '[object Object]' || // some browser native object - typeof obj.callee === 'function'; // arguments (on IE8 looks like regular obj) } -} + return isString(obj) || isArray(obj) || length === 0 || + typeof length === 'number' && length > 0 && (length - 1) in obj; +} /** * @ngdoc function * @name angular.forEach - * @function + * @module ng + * @kind function * * @description * Invokes the `iterator` function once for each item in `obj` collection, which can be either an @@ -101,16 +288,17 @@ function isArrayLike(obj) { * is the value of an object property or an array element and `key` is the object property key or * array element index. Specifying a `context` for the function is optional. * - * Note: this function was previously known as `angular.foreach`. + * It is worth noting that `.forEach` does not iterate over inherited properties because it filters + * using the `hasOwnProperty` method. * -
+   ```js
      var values = {name: 'misko', gender: 'male'};
      var log = [];
-     angular.forEach(values, function(value, key){
+     angular.forEach(values, function(value, key) {
        this.push(key + ': ' + value);
      }, log);
-     expect(log).toEqual(['name: misko', 'gender:male']);
-   
+ expect(log).toEqual(['name: misko', 'gender: male']); + ``` * * @param {Object|Array} obj Object to iterate over. * @param {Function} iterator Iterator function. @@ -120,9 +308,11 @@ function isArrayLike(obj) { function forEach(obj, iterator, context) { var key; if (obj) { - if (isFunction(obj)){ + if (isFunction(obj)) { for (key in obj) { - if (key != 'prototype' && key != 'length' && key != 'name' && obj.hasOwnProperty(key)) { + // Need to check if hasOwnProperty exists, + // as on IE8 the result of querySelectorAll is an object without a hasOwnProperty function + if (key != 'prototype' && key != 'length' && key != 'name' && (!obj.hasOwnProperty || obj.hasOwnProperty(key))) { iterator.call(context, obj[key], key); } } @@ -167,7 +357,7 @@ function forEachSorted(obj, iterator, context) { * @returns {function(*, string)} */ function reverseParams(iteratorFn) { - return function(value, key) { iteratorFn(key, value) }; + return function(value, key) { iteratorFn(key, value); }; } /** @@ -176,7 +366,7 @@ function reverseParams(iteratorFn) { * the number string gets longer over time, and it can also overflow, where as the nextId * will grow much slower, it is a string, and it will never overflow. * - * @returns an unique alpha-numeric string + * @returns {string} an unique alpha-numeric string */ function nextUid() { var index = uid.length; @@ -203,7 +393,7 @@ function nextUid() { /** * Set or clear the hashkey for an object. - * @param obj object + * @param obj object * @param h the hashkey (!truthy to delete the hashkey) */ function setHashKey(obj, h) { @@ -218,7 +408,8 @@ function setHashKey(obj, h) { /** * @ngdoc function * @name angular.extend - * @function + * @module ng + * @kind function * * @description * Extends the destination object `dst` by copying all of the properties from the `src` object(s) @@ -230,9 +421,9 @@ function setHashKey(obj, h) { */ function extend(dst) { var h = dst.$$hashKey; - forEach(arguments, function(obj){ + forEach(arguments, function(obj) { if (obj !== dst) { - forEach(obj, function(value, key){ + forEach(obj, function(value, key) { dst[key] = value; }); } @@ -251,21 +442,21 @@ function inherit(parent, extra) { return extend(new (extend(function() {}, {prototype:parent}))(), extra); } - /** * @ngdoc function * @name angular.noop - * @function + * @module ng + * @kind function * * @description * A function that performs no operations. This function can be useful when writing code in the * functional style. -
+   ```js
      function foo(callback) {
        var result = calculateResult();
        (callback || angular.noop)(result);
      }
-   
+ ``` */ function noop() {} noop.$inject = []; @@ -274,17 +465,18 @@ noop.$inject = []; /** * @ngdoc function * @name angular.identity - * @function + * @module ng + * @kind function * * @description * A function that returns its first argument. This function is useful when writing code in the * functional style. * -
+   ```js
      function transformer(transformationFn, value) {
        return (transformationFn || angular.identity)(value);
      };
-   
+ ``` */ function identity($) {return $;} identity.$inject = []; @@ -295,7 +487,8 @@ function valueFn(value) {return function() {return value;};} /** * @ngdoc function * @name angular.isUndefined - * @function + * @module ng + * @kind function * * @description * Determines if a reference is undefined. @@ -303,13 +496,14 @@ function valueFn(value) {return function() {return value;};} * @param {*} value Reference to check. * @returns {boolean} True if `value` is undefined. */ -function isUndefined(value){return typeof value == 'undefined';} +function isUndefined(value){return typeof value === 'undefined';} /** * @ngdoc function * @name angular.isDefined - * @function + * @module ng + * @kind function * * @description * Determines if a reference is defined. @@ -317,28 +511,30 @@ function isUndefined(value){return typeof value == 'undefined';} * @param {*} value Reference to check. * @returns {boolean} True if `value` is defined. */ -function isDefined(value){return typeof value != 'undefined';} +function isDefined(value){return typeof value !== 'undefined';} /** * @ngdoc function * @name angular.isObject - * @function + * @module ng + * @kind function * * @description * Determines if a reference is an `Object`. Unlike `typeof` in JavaScript, `null`s are not - * considered to be objects. + * considered to be objects. Note that JavaScript arrays are objects. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Object` but not `null`. */ -function isObject(value){return value != null && typeof value == 'object';} +function isObject(value){return value != null && typeof value === 'object';} /** * @ngdoc function * @name angular.isString - * @function + * @module ng + * @kind function * * @description * Determines if a reference is a `String`. @@ -346,13 +542,14 @@ function isObject(value){return value != null && typeof value == 'object';} * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `String`. */ -function isString(value){return typeof value == 'string';} +function isString(value){return typeof value === 'string';} /** * @ngdoc function * @name angular.isNumber - * @function + * @module ng + * @kind function * * @description * Determines if a reference is a `Number`. @@ -360,13 +557,14 @@ function isString(value){return typeof value == 'string';} * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Number`. */ -function isNumber(value){return typeof value == 'number';} +function isNumber(value){return typeof value === 'number';} /** * @ngdoc function * @name angular.isDate - * @function + * @module ng + * @kind function * * @description * Determines if a value is a date. @@ -374,15 +572,16 @@ function isNumber(value){return typeof value == 'number';} * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Date`. */ -function isDate(value){ - return toString.apply(value) == '[object Date]'; +function isDate(value) { + return toString.call(value) === '[object Date]'; } /** * @ngdoc function * @name angular.isArray - * @function + * @module ng + * @kind function * * @description * Determines if a reference is an `Array`. @@ -391,14 +590,15 @@ function isDate(value){ * @returns {boolean} True if `value` is an `Array`. */ function isArray(value) { - return toString.apply(value) == '[object Array]'; + return toString.call(value) === '[object Array]'; } /** * @ngdoc function * @name angular.isFunction - * @function + * @module ng + * @kind function * * @description * Determines if a reference is a `Function`. @@ -406,7 +606,7 @@ function isArray(value) { * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Function`. */ -function isFunction(value){return typeof value == 'function';} +function isFunction(value){return typeof value === 'function';} /** @@ -417,7 +617,7 @@ function isFunction(value){return typeof value == 'function';} * @returns {boolean} True if `value` is a `RegExp`. */ function isRegExp(value) { - return toString.apply(value) == '[object RegExp]'; + return toString.call(value) === '[object RegExp]'; } @@ -439,12 +639,17 @@ function isScope(obj) { function isFile(obj) { - return toString.apply(obj) === '[object File]'; + return toString.call(obj) === '[object File]'; +} + + +function isBlob(obj) { + return toString.call(obj) === '[object Blob]'; } function isBoolean(value) { - return typeof value == 'boolean'; + return typeof value === 'boolean'; } @@ -454,7 +659,7 @@ var trim = (function() { // TODO: we should move this into IE/ES5 polyfill if (!String.prototype.trim) { return function(value) { - return isString(value) ? value.replace(/^\s*/, '').replace(/\s*$/, '') : value; + return isString(value) ? value.replace(/^\s\s*/, '').replace(/\s\s*$/, '') : value; }; } return function(value) { @@ -466,7 +671,8 @@ var trim = (function() { /** * @ngdoc function * @name angular.isElement - * @function + * @module ng + * @kind function * * @description * Determines if a reference is a DOM element (or wrapped jQuery element). @@ -475,16 +681,16 @@ var trim = (function() { * @returns {boolean} True if `value` is a DOM element (or wrapped jQuery element). */ function isElement(node) { - return node && + return !!(node && (node.nodeName // we are a direct element - || (node.bind && node.find)); // we have a bind and find method part of jQuery API + || (node.prop && node.attr && node.find))); // we have an on and find method part of jQuery API } /** * @param str 'key1,key2,...' * @returns {object} in the form of {key1:true, key2:true, ...} */ -function makeMap(str){ +function makeMap(str) { var obj = {}, items = str.split(","), i; for ( i = 0; i < items.length; i++ ) obj[ items[i] ] = true; @@ -527,17 +733,17 @@ function map(obj, iterator, context) { * @returns {number} The size of `obj` or `0` if `obj` is neither an object nor an array. */ function size(obj, ownPropsOnly) { - var size = 0, key; + var count = 0, key; if (isArray(obj) || isString(obj)) { return obj.length; - } else if (isObject(obj)){ + } else if (isObject(obj)) { for (key in obj) if (!ownPropsOnly || obj.hasOwnProperty(key)) - size++; + count++; } - return size; + return count; } @@ -548,7 +754,7 @@ function includes(array, obj) { function indexOf(array, obj) { if (array.indexOf) return array.indexOf(obj); - for ( var i = 0; i < array.length; i++) { + for (var i = 0; i < array.length; i++) { if (obj === array[i]) return i; } return -1; @@ -576,7 +782,8 @@ function isLeafNode (node) { /** * @ngdoc function * @name angular.copy - * @function + * @module ng + * @kind function * * @description * Creates a deep copy of `source`, which should be an object or an array. @@ -584,88 +791,161 @@ function isLeafNode (node) { * * If no destination is supplied, a copy of the object or array is created. * * If a destination is provided, all of its elements (for array) or properties (for objects) * are deleted and then all elements/properties from the source are copied to it. - * * If `source` is not an object or array, `source` is returned. - * - * Note: this function is used to augment the Object type in Angular expressions. See - * {@link ng.$filter} for more information about Angular arrays. + * * If `source` is not an object or array (inc. `null` and `undefined`), `source` is returned. + * * If `source` is identical to 'destination' an exception will be thrown. * * @param {*} source The source that will be used to make a copy. * Can be any type, including primitives, `null`, and `undefined`. * @param {(Object|Array)=} destination Destination into which the source is copied. If * provided, must be of the same type as `source`. * @returns {*} The copy or updated `destination`, if `destination` was specified. + * + * @example + + +
+
+ Name:
+ E-mail:
+ Gender: male + female
+ + + +
form = {{user | json}}
+
master = {{master | json}}
+
+ + +
+
*/ -function copy(source, destination){ - if (isWindow(source) || isScope(source)) throw Error("Can't copy Window or Scope"); +function copy(source, destination, stackSource, stackDest) { + if (isWindow(source) || isScope(source)) { + throw ngMinErr('cpws', + "Can't copy! Making copies of Window or Scope instances is not supported."); + } + if (!destination) { destination = source; if (source) { if (isArray(source)) { - destination = copy(source, []); + destination = copy(source, [], stackSource, stackDest); } else if (isDate(source)) { destination = new Date(source.getTime()); } else if (isRegExp(source)) { destination = new RegExp(source.source); } else if (isObject(source)) { - destination = copy(source, {}); + destination = copy(source, {}, stackSource, stackDest); } } } else { - if (source === destination) throw Error("Can't copy equivalent objects or arrays"); + if (source === destination) throw ngMinErr('cpi', + "Can't copy! Source and destination are identical."); + + stackSource = stackSource || []; + stackDest = stackDest || []; + + if (isObject(source)) { + var index = indexOf(stackSource, source); + if (index !== -1) return stackDest[index]; + + stackSource.push(source); + stackDest.push(destination); + } + + var result; if (isArray(source)) { destination.length = 0; for ( var i = 0; i < source.length; i++) { - destination.push(copy(source[i])); + result = copy(source[i], null, stackSource, stackDest); + if (isObject(source[i])) { + stackSource.push(source[i]); + stackDest.push(result); + } + destination.push(result); } } else { var h = destination.$$hashKey; - forEach(destination, function(value, key){ + forEach(destination, function(value, key) { delete destination[key]; }); for ( var key in source) { - destination[key] = copy(source[key]); + result = copy(source[key], null, stackSource, stackDest); + if (isObject(source[key])) { + stackSource.push(source[key]); + stackDest.push(result); + } + destination[key] = result; } setHashKey(destination,h); } + } return destination; } /** - * Create a shallow copy of an object + * Creates a shallow copy of an object, an array or a primitive */ function shallowCopy(src, dst) { - dst = dst || {}; + if (isArray(src)) { + dst = dst || []; + + for ( var i = 0; i < src.length; i++) { + dst[i] = src[i]; + } + } else if (isObject(src)) { + dst = dst || {}; - for(var key in src) { - if (src.hasOwnProperty(key) && key.substr(0, 2) !== '$$') { - dst[key] = src[key]; + for (var key in src) { + if (hasOwnProperty.call(src, key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { + dst[key] = src[key]; + } } } - return dst; + return dst || src; } /** * @ngdoc function * @name angular.equals - * @function + * @module ng + * @kind function * * @description - * Determines if two objects or two values are equivalent. Supports value types, regular expressions, arrays and - * objects. + * Determines if two objects or two values are equivalent. Supports value types, regular + * expressions, arrays and objects. * * Two objects or values are considered equivalent if at least one of the following is true: * * * Both objects or values pass `===` comparison. - * * Both objects or values are of the same type and all of their properties pass `===` comparison. - * * Both values are NaN. (In JavasScript, NaN == NaN => false. But we consider two NaN as equal) - * * Both values represent the same regular expression (In JavasScript, + * * Both objects or values are of the same type and all of their properties are equal by + * comparing them with `angular.equals`. + * * Both values are NaN. (In JavaScript, NaN == NaN => false. But we consider two NaN as equal) + * * Both values represent the same regular expression (In JavaScript, * /abc/ == /abc/ => false. But we consider two regular expressions as equal when their textual * representation matches). * - * During a property comparision, properties of `function` type and properties with names + * During a property comparison, properties of `function` type and properties with names * that begin with `$` are ignored. * * Scope and DOMWindow objects are being compared only by identify (`===`). @@ -702,7 +982,7 @@ function equals(o1, o2) { keySet[key] = true; } for(key in o2) { - if (!keySet[key] && + if (!keySet.hasOwnProperty(key) && key.charAt(0) !== '$' && o2[key] !== undefined && !isFunction(o2[key])) return false; @@ -715,6 +995,13 @@ function equals(o1, o2) { } +function csp() { + return (document.securityPolicy && document.securityPolicy.isActive) || + (document.querySelector && + !!(document.querySelector('[ng-csp]') || document.querySelector('[data-ng-csp]'))); +} + + function concat(array1, array2, index) { return array1.concat(slice.call(array2, index)); } @@ -724,21 +1011,25 @@ function sliceArgs(args, startIndex) { } +/* jshint -W101 */ /** * @ngdoc function * @name angular.bind - * @function + * @module ng + * @kind function * * @description * Returns a function which calls function `fn` bound to `self` (`self` becomes the `this` for * `fn`). You can supply optional `args` that are prebound to the function. This feature is also - * known as [function currying](http://en.wikipedia.org/wiki/Currying). + * known as [partial application](http://en.wikipedia.org/wiki/Partial_application), as + * distinguished from [function currying](http://en.wikipedia.org/wiki/Currying#Contrast_with_partial_function_application). * * @param {Object} self Context which `fn` should be evaluated in. * @param {function()} fn Function to be bound. * @param {...*} args Optional arguments to be prebound to the `fn` function call. * @returns {function()} Function that wraps the `fn` with all the specified bindings. */ +/* jshint +W101 */ function bind(self, fn) { var curryArgs = arguments.length > 2 ? sliceArgs(arguments, 2) : []; if (isFunction(fn) && !(fn instanceof RegExp)) { @@ -763,7 +1054,7 @@ function bind(self, fn) { function toJsonReplacer(key, value) { var val = value; - if (/^\$+/.test(key)) { + if (typeof key === 'string' && key.charAt(0) === '$') { val = undefined; } else if (isWindow(value)) { val = '$WINDOW'; @@ -780,7 +1071,8 @@ function toJsonReplacer(key, value) { /** * @ngdoc function * @name angular.toJson - * @function + * @module ng + * @kind function * * @description * Serializes input into a JSON-formatted string. Properties with leading $ characters will be @@ -788,7 +1080,7 @@ function toJsonReplacer(key, value) { * * @param {Object|Array|Date|string|number} obj Input to be serialized into JSON. * @param {boolean=} pretty If set to true, the JSON output will contain newlines and whitespace. - * @returns {string|undefined} Jsonified string representing `obj`. + * @returns {string|undefined} JSON-ified string representing `obj`. */ function toJson(obj, pretty) { if (typeof obj === 'undefined') return undefined; @@ -799,13 +1091,14 @@ function toJson(obj, pretty) { /** * @ngdoc function * @name angular.fromJson - * @function + * @module ng + * @kind function * * @description * Deserializes a JSON string. * * @param {string} json JSON string to deserialize. - * @returns {Object|Array|Date|string|number} Deserialized thingy. + * @returns {Object|Array|string|number} Deserialized thingy. */ function fromJson(json) { return isString(json) @@ -815,7 +1108,9 @@ function fromJson(json) { function toBoolean(value) { - if (value && value.length !== 0) { + if (typeof value === 'function') { + value = true; + } else if (value && value.length !== 0) { var v = lowercase("" + value); value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == 'n' || v == '[]'); } else { @@ -832,7 +1127,7 @@ function startingTag(element) { try { // turns out IE does not let you set .html() on elements which // are not allowed to have children. So we just ignore it. - element.html(''); + element.empty(); } catch(e) {} // As Per DOM Standards var TEXT_NODE = 3; @@ -870,16 +1165,23 @@ function tryDecodeURIComponent(value) { /** * Parses an escaped url query string into key-value pairs. - * @returns Object.<(string|boolean)> + * @returns {Object.} */ function parseKeyValue(/**string*/keyValue) { var obj = {}, key_value, key; - forEach((keyValue || "").split('&'), function(keyValue){ + forEach((keyValue || "").split('&'), function(keyValue) { if ( keyValue ) { key_value = keyValue.split('='); key = tryDecodeURIComponent(key_value[0]); if ( isDefined(key) ) { - obj[key] = isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true; + var val = isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true; + if (!obj[key]) { + obj[key] = val; + } else if(isArray(obj[key])) { + obj[key].push(val); + } else { + obj[key] = [obj[key],val]; + } } } }); @@ -889,14 +1191,22 @@ function parseKeyValue(/**string*/keyValue) { function toKeyValue(obj) { var parts = []; forEach(obj, function(value, key) { - parts.push(encodeUriQuery(key, true) + (value === true ? '' : '=' + encodeUriQuery(value, true))); + if (isArray(value)) { + forEach(value, function(arrayValue) { + parts.push(encodeUriQuery(key, true) + + (arrayValue === true ? '' : '=' + encodeUriQuery(arrayValue, true))); + }); + } else { + parts.push(encodeUriQuery(key, true) + + (value === true ? '' : '=' + encodeUriQuery(value, true))); + } }); return parts.length ? parts.join('&') : ''; } /** - * We need our custom method because encodeURIComponent is too agressive and doesn't follow + * We need our custom method because encodeURIComponent is too aggressive and doesn't follow * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path * segments: * segment = *pchar @@ -916,7 +1226,7 @@ function encodeUriSegment(val) { /** * This method is intended for encoding *key* or *value* parts of query component. We need a custom - * method becuase encodeURIComponent is too agressive and encodes stuff that doesn't have to be + * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be * encoded per http://tools.ietf.org/html/rfc3986: * query = *( pchar / "/" / "?" ) * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" @@ -937,7 +1247,8 @@ function encodeUriQuery(val, pctEncodeSpaces) { /** * @ngdoc directive - * @name ng.directive:ngApp + * @name ngApp + * @module ng * * @element ANY * @param {angular.Module} ngApp an optional application @@ -945,26 +1256,39 @@ function encodeUriQuery(val, pctEncodeSpaces) { * * @description * - * Use this directive to auto-bootstrap an application. Only - * one ngApp directive can be used per HTML document. The directive - * designates the root of the application and is typically placed - * at the root of the page. - * - * The first ngApp found in the document will be auto-bootstrapped. To use multiple applications in an - * HTML document you must manually bootstrap them using {@link angular.bootstrap}. - * Applications cannot be nested. - * - * In the example below if the `ngApp` directive would not be placed - * on the `html` element then the document would not be compiled - * and the `{{ 1+2 }}` would not be resolved to `3`. - * - * `ngApp` is the easiest way to bootstrap an application. - * - - - I can add: 1 + 2 = {{ 1+2 }} - - + * Use this directive to **auto-bootstrap** an AngularJS application. The `ngApp` directive + * designates the **root element** of the application and is typically placed near the root element + * of the page - e.g. on the `` or `` tags. + * + * Only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp` + * found in the document will be used to define the root element to auto-bootstrap as an + * application. To run multiple applications in an HTML document you must manually bootstrap them using + * {@link angular.bootstrap} instead. AngularJS applications cannot be nested within each other. + * + * You can specify an **AngularJS module** to be used as the root module for the application. This + * module will be loaded into the {@link auto.$injector} when the application is bootstrapped and + * should contain the application code needed or have dependencies on other modules that will + * contain the code. See {@link angular.module} for more information. + * + * In the example below if the `ngApp` directive were not placed on the `html` element then the + * document would not be compiled, the `AppController` would not be instantiated and the `{{ a+b }}` + * would not be resolved to `3`. + * + * `ngApp` is the easiest, and most common, way to bootstrap an application. + * + + +
+ I can add: {{a}} + {{b}} = {{ a+b }} +
+
+ + angular.module('ngAppDemo', []).controller('ngAppDemoController', function($scope) { + $scope.a = 1; + $scope.b = 2; + }); + +
* */ function angularInit(element, bootstrap) { @@ -1014,29 +1338,74 @@ function angularInit(element, bootstrap) { /** * @ngdoc function * @name angular.bootstrap + * @module ng * @description * Use this function to manually start up angular application. * * See: {@link guide/bootstrap Bootstrap} * * Note that ngScenario-based end-to-end tests cannot use this function to bootstrap manually. - * They must use {@link api/ng.directive:ngApp ngApp}. + * They must use {@link ng.directive:ngApp ngApp}. + * + * Angular will detect if it has been loaded into the browser more than once and only allow the + * first loaded script to be bootstrapped and will report a warning to the browser console for + * each of the subsequent scripts. This prevents strange results in applications, where otherwise + * multiple instances of Angular try to work on the DOM. + * + * + * + * + *
+ *
Priority Tools
- {{abbreviation(ticket)}} - {{ticket.name}} + {{abbreviation(todo)}} + {{todo.name}} {{ticket.priority}}{{todo.priority}}
+ * + * + * + * + * + * + *
{{heading}}
{{fill}}
+ *
+ * + * + * var app = angular.module('multi-bootstrap', []) + * + * .controller('BrokenTable', function($scope) { + * $scope.headings = ['One', 'Two', 'Three']; + * $scope.fillings = [[1, 2, 3], ['A', 'B', 'C'], [7, 8, 9]]; + * }); + * + * + * it('should only insert one table cell for each item in $scope.fillings', function() { + * expect(element.all(by.css('td')).count()) + * .toBe(9); + * }); + * + * * - * @param {Element} element DOM element which is the root of angular application. - * @param {Array=} modules an array of module declarations. See: {@link angular.module modules} - * @returns {AUTO.$injector} Returns the newly created injector for this app. + * @param {DOMElement} element DOM element which is the root of angular application. + * @param {Array=} modules an array of modules to load into the application. + * Each item in the array should be the name of a predefined module or a (DI annotated) + * function that will be invoked by the injector as a run block. + * See: {@link angular.module modules} + * @returns {auto.$injector} Returns the newly created injector for this app. */ function bootstrap(element, modules) { var doBootstrap = function() { element = jqLite(element); + + if (element.injector()) { + var tag = (element[0] === document) ? 'document' : startingTag(element); + throw ngMinErr('btstrpd', "App Already Bootstrapped with this Element '{0}'", tag); + } + modules = modules || []; modules.unshift(['$provide', function($provide) { $provide.value('$rootElement', element); }]); modules.unshift('ng'); var injector = createInjector(modules); - injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', - function(scope, element, compile, injector) { + injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate', + function(scope, element, compile, injector, animate) { scope.$apply(function() { element.data('$injector', injector); compile(element)(scope); @@ -1062,7 +1431,7 @@ function bootstrap(element, modules) { } var SNAKE_CASE_REGEXP = /[A-Z]/g; -function snake_case(name, separator){ +function snake_case(name, separator) { separator = separator || '_'; return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) { return (pos ? separator : '') + letter.toLowerCase(); @@ -1072,19 +1441,22 @@ function snake_case(name, separator){ function bindJQuery() { // bind to jQuery if present; jQuery = window.jQuery; - // reset to jQuery or default to us. - if (jQuery) { + // Use jQuery if it exists with proper functionality, otherwise default to us. + // Angular 1.2+ requires jQuery 1.7.1+ for on()/off() support. + if (jQuery && jQuery.fn.on) { jqLite = jQuery; extend(jQuery.fn, { scope: JQLitePrototype.scope, + isolateScope: JQLitePrototype.isolateScope, controller: JQLitePrototype.controller, injector: JQLitePrototype.injector, inheritedData: JQLitePrototype.inheritedData }); - // Method signature: JQLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) - JQLitePatchJQueryRemove('remove', true, true, false); - JQLitePatchJQueryRemove('empty', false, false, false); - JQLitePatchJQueryRemove('html', false, false, true); + // Method signature: + // jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) + jqLitePatchJQueryRemove('remove', true, true, false); + jqLitePatchJQueryRemove('empty', false, false, false); + jqLitePatchJQueryRemove('html', false, false, true); } else { jqLite = JQLite; } @@ -1096,7 +1468,7 @@ function bindJQuery() { */ function assertArg(arg, name, reason) { if (!arg) { - throw new Error("Argument '" + (name || '?') + "' is " + (reason || "required")); + throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required")); } return arg; } @@ -1111,12 +1483,23 @@ function assertArgFn(arg, name, acceptArrayAnnotation) { return arg; } +/** + * throw error if the name given is hasOwnProperty + * @param {String} name the name to test + * @param {String} context the context in which the name is used, such as module or directive + */ +function assertNotHasOwnProperty(name, context) { + if (name === 'hasOwnProperty') { + throw ngMinErr('badname', "hasOwnProperty is not a valid {0} name", context); + } +} + /** * Return the value accessible 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 accessible by path + * @param {String} path path to traverse + * @param {boolean} [bindFnToScope=true] + * @returns {Object} value as accessible by path */ //TODO(misko): this function needs to be removed function getter(obj, path, bindFnToScope) { @@ -1139,8 +1522,33 @@ function getter(obj, path, bindFnToScope) { } /** - * @ngdoc interface + * Return the DOM siblings between the first and last node in the given array. + * @param {Array} array like object + * @returns {DOMElement} object containing the elements + */ +function getBlockElements(nodes) { + var startNode = nodes[0], + endNode = nodes[nodes.length - 1]; + if (startNode === endNode) { + return jqLite(startNode); + } + + var element = startNode; + var elements = [element]; + + do { + element = element.nextSibling; + if (!element) break; + elements.push(element); + } while (element !== endNode); + + return jqLite(elements); +} + +/** + * @ngdoc type * @name angular.Module + * @module ng * @description * * Interface for configuring angular {@link angular.module modules}. @@ -1148,30 +1556,43 @@ function getter(obj, path, bindFnToScope) { function setupModuleLoader(window) { + var $injectorMinErr = minErr('$injector'); + var ngMinErr = minErr('ng'); + function ensure(obj, name, factory) { return obj[name] || (obj[name] = factory()); } - return ensure(ensure(window, 'angular', Object), 'module', function() { + var angular = ensure(window, 'angular', Object); + + // We need to expose `angular.$$minErr` to modules such as `ngResource` that reference it during bootstrap + angular.$$minErr = angular.$$minErr || minErr; + + return ensure(angular, 'module', function() { /** @type {Object.} */ var modules = {}; /** * @ngdoc function * @name angular.module + * @module ng * @description * - * The `angular.module` is a global place for creating and registering Angular modules. All - * modules (angular core or 3rd party) that should be available to an application must be + * The `angular.module` is a global place for creating, registering and retrieving Angular + * modules. + * All modules (angular core or 3rd party) that should be available to an application must be * registered using this mechanism. * + * When passed two or more arguments, a new module is created. If passed only one argument, an + * existing module (the name passed as the first argument to `module`) is retrieved. + * * * # Module * * A module is a collection of services, directives, filters, and configuration information. - * `angular.module` is used to configure the {@link AUTO.$injector $injector}. + * `angular.module` is used to configure the {@link auto.$injector $injector}. * - *
+     * ```js
      * // Create a new module
      * var myModule = angular.module('myModule', []);
      *
@@ -1179,36 +1600,45 @@ function setupModuleLoader(window) {
      * myModule.value('appName', 'MyCoolApp');
      *
      * // configure existing services inside initialization blocks.
-     * myModule.config(function($locationProvider) {
+     * myModule.config(['$locationProvider', function($locationProvider) {
      *   // Configure existing providers
      *   $locationProvider.hashPrefix('!');
-     * });
-     * 
+ * }]); + * ``` * * Then you can create an injector and load your modules like this: * - *
-     * var injector = angular.injector(['ng', 'MyModule'])
-     * 
+ * ```js + * var injector = angular.injector(['ng', 'myModule']) + * ``` * * However it's more likely that you'll just use * {@link ng.directive:ngApp ngApp} or * {@link angular.bootstrap} to simplify this process for you. * * @param {!string} name The name of the module to create or retrieve. - * @param {Array.=} requires If specified then new module is being created. If unspecified then the - * the module is being retrieved for further configuration. +<<<<<* @param {!Array.=} requires If specified then new module is being created. If +>>>>>* unspecified then the module is being retrieved for further configuration. * @param {Function} configFn Optional configuration function for the module. Same as * {@link angular.Module#config Module#config()}. * @returns {module} new module with the {@link angular.Module} api. */ return function module(name, requires, configFn) { + var assertNotHasOwnProperty = function(name, context) { + if (name === 'hasOwnProperty') { + throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context); + } + }; + + assertNotHasOwnProperty(name, 'module'); if (requires && modules.hasOwnProperty(name)) { modules[name] = null; } return ensure(modules, name, function() { if (!requires) { - throw Error('No module: ' + name); + throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " + + "the module name or forgot to load it. If registering a module ensure that you " + + "specify the dependencies as the second argument.", name); } /** @type {!Array.>} */ @@ -1228,17 +1658,18 @@ function setupModuleLoader(window) { /** * @ngdoc property * @name angular.Module#requires - * @propertyOf angular.Module + * @module ng * @returns {Array.} List of module names which must be loaded before this module. * @description - * Holds the list of modules which the injector will load before the current module is loaded. + * Holds the list of modules which the injector will load before the current module is + * loaded. */ requires: requires, /** * @ngdoc property * @name angular.Module#name - * @propertyOf angular.Module + * @module ng * @returns {string} Name of the module. * @description */ @@ -1248,63 +1679,98 @@ function setupModuleLoader(window) { /** * @ngdoc method * @name angular.Module#provider - * @methodOf angular.Module + * @module ng * @param {string} name service name - * @param {Function} providerType Construction function for creating new instance of the service. + * @param {Function} providerType Construction function for creating new instance of the + * service. * @description - * See {@link AUTO.$provide#provider $provide.provider()}. + * See {@link auto.$provide#provider $provide.provider()}. */ provider: invokeLater('$provide', 'provider'), /** * @ngdoc method * @name angular.Module#factory - * @methodOf angular.Module + * @module ng * @param {string} name service name * @param {Function} providerFunction Function for creating new instance of the service. * @description - * See {@link AUTO.$provide#factory $provide.factory()}. + * See {@link auto.$provide#factory $provide.factory()}. */ factory: invokeLater('$provide', 'factory'), /** * @ngdoc method * @name angular.Module#service - * @methodOf angular.Module + * @module ng * @param {string} name service name * @param {Function} constructor A constructor function that will be instantiated. * @description - * See {@link AUTO.$provide#service $provide.service()}. + * See {@link auto.$provide#service $provide.service()}. */ service: invokeLater('$provide', 'service'), /** * @ngdoc method * @name angular.Module#value - * @methodOf angular.Module + * @module ng * @param {string} name service name * @param {*} object Service instance object. * @description - * See {@link AUTO.$provide#value $provide.value()}. + * See {@link auto.$provide#value $provide.value()}. */ value: invokeLater('$provide', 'value'), /** * @ngdoc method * @name angular.Module#constant - * @methodOf angular.Module + * @module ng * @param {string} name constant name * @param {*} object Constant value. * @description * Because the constant are fixed, they get applied before other provide methods. - * See {@link AUTO.$provide#constant $provide.constant()}. + * See {@link auto.$provide#constant $provide.constant()}. */ constant: invokeLater('$provide', 'constant', 'unshift'), + /** + * @ngdoc method + * @name angular.Module#animation + * @module ng + * @param {string} name animation name + * @param {Function} animationFactory Factory function for creating new instance of an + * animation. + * @description + * + * **NOTE**: animations take effect only if the **ngAnimate** module is loaded. + * + * + * Defines an animation hook that can be later used with + * {@link ngAnimate.$animate $animate} service and directives that use this service. + * + * ```js + * module.animation('.animation-name', function($inject1, $inject2) { + * return { + * eventName : function(element, done) { + * //code to run the animation + * //once complete, then run done() + * return function cancellationFunction(element) { + * //code to cancel the animation + * } + * } + * } + * }) + * ``` + * + * See {@link ngAnimate.$animateProvider#register $animateProvider.register()} and + * {@link ngAnimate ngAnimate module} for more information. + */ + animation: invokeLater('$animateProvider', 'register'), + /** * @ngdoc method * @name angular.Module#filter - * @methodOf angular.Module + * @module ng * @param {string} name Filter name. * @param {Function} filterFactory Factory function for creating new instance of filter. * @description @@ -1315,8 +1781,9 @@ function setupModuleLoader(window) { /** * @ngdoc method * @name angular.Module#controller - * @methodOf angular.Module - * @param {string} name Controller name. + * @module ng + * @param {string|Object} name Controller name, or an object map of controllers where the + * keys are the names and the values are the constructors. * @param {Function} constructor Controller constructor function. * @description * See {@link ng.$controllerProvider#register $controllerProvider.register()}. @@ -1326,8 +1793,9 @@ function setupModuleLoader(window) { /** * @ngdoc method * @name angular.Module#directive - * @methodOf angular.Module - * @param {string} name directive name + * @module ng + * @param {string|Object} name Directive name, or an object map of directives where the + * keys are the names and the values are the factories. * @param {Function} directiveFactory Factory function for creating new instance of * directives. * @description @@ -1338,18 +1806,20 @@ function setupModuleLoader(window) { /** * @ngdoc method * @name angular.Module#config - * @methodOf angular.Module + * @module ng * @param {Function} configFn Execute this function on module load. Useful for service * configuration. * @description * Use this method to register work which needs to be performed on module loading. + * For more about how to configure services, see + * {@link providers#providers_provider-recipe Provider Recipe}. */ config: config, /** * @ngdoc method * @name angular.Module#run - * @methodOf angular.Module + * @module ng * @param {Function} initializationFn Execute this function after injector creation. * Useful for application initialization. * @description @@ -1378,7 +1848,7 @@ function setupModuleLoader(window) { return function() { invokeQueue[insertMethod || 'push']([provider, method, arguments]); return moduleInstance; - } + }; } }); }; @@ -1386,9 +1856,88 @@ function setupModuleLoader(window) { } +/* global + angularModule: true, + version: true, + + $LocaleProvider, + $CompileProvider, + + htmlAnchorDirective, + inputDirective, + inputDirective, + formDirective, + scriptDirective, + selectDirective, + styleDirective, + optionDirective, + ngBindDirective, + ngBindHtmlDirective, + ngBindTemplateDirective, + ngClassDirective, + ngClassEvenDirective, + ngClassOddDirective, + ngCspDirective, + ngCloakDirective, + ngControllerDirective, + ngFormDirective, + ngHideDirective, + ngIfDirective, + ngIncludeDirective, + ngIncludeFillContentDirective, + ngInitDirective, + ngNonBindableDirective, + ngPluralizeDirective, + ngRepeatDirective, + ngShowDirective, + ngStyleDirective, + ngSwitchDirective, + ngSwitchWhenDirective, + ngSwitchDefaultDirective, + ngOptionsDirective, + ngTranscludeDirective, + ngModelDirective, + ngListDirective, + ngChangeDirective, + requiredDirective, + requiredDirective, + ngValueDirective, + ngAttributeAliasDirectives, + ngEventDirectives, + + $AnchorScrollProvider, + $AnimateProvider, + $BrowserProvider, + $CacheFactoryProvider, + $ControllerProvider, + $DocumentProvider, + $ExceptionHandlerProvider, + $FilterProvider, + $InterpolateProvider, + $IntervalProvider, + $HttpProvider, + $HttpBackendProvider, + $LocationProvider, + $LogProvider, + $ParseProvider, + $RootScopeProvider, + $QProvider, + $$SanitizeUriProvider, + $SceProvider, + $SceDelegateProvider, + $SnifferProvider, + $TemplateCacheProvider, + $TimeoutProvider, + $$RAFProvider, + $$AsyncCallbackProvider, + $WindowProvider +*/ + + /** - * @ngdoc property + * @ngdoc object * @name angular.version + * @module ng * @description * An object that contains information about the current AngularJS version. This object has the * following properties: @@ -1400,11 +1949,11 @@ function setupModuleLoader(window) { * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". */ var version = { - full: '1.0.8', // all of these placeholder strings will be replaced by grunt's + full: '1.2.17', // all of these placeholder strings will be replaced by grunt's major: 1, // package task - minor: 0, - dot: 8, - codeName: 'bubble-burst' + minor: 2, + dot: 17, + codeName: 'quantum-disentanglement' }; @@ -1434,7 +1983,9 @@ function publishExternalAPI(angular){ 'isDate': isDate, 'lowercase': lowercase, 'uppercase': uppercase, - 'callbacks': {counter: 0} + 'callbacks': {counter: 0}, + '$$minErr': minErr, + '$$csp': csp }); angularModule = setupModuleLoader(window); @@ -1446,6 +1997,10 @@ function publishExternalAPI(angular){ angularModule('ng', ['ngLocale'], ['$provide', function ngModule($provide) { + // $$sanitizeUriProvider needs to be before $compileProvider as it is used by it. + $provide.provider({ + $$sanitizeUri: $$SanitizeUriProvider + }); $provide.provider('$compile', $CompileProvider). directive({ a: htmlAnchorDirective, @@ -1457,16 +2012,16 @@ function publishExternalAPI(angular){ style: styleDirective, option: optionDirective, ngBind: ngBindDirective, - ngBindHtmlUnsafe: ngBindHtmlUnsafeDirective, + ngBindHtml: ngBindHtmlDirective, ngBindTemplate: ngBindTemplateDirective, ngClass: ngClassDirective, ngClassEven: ngClassEvenDirective, ngClassOdd: ngClassOddDirective, - ngCsp: ngCspDirective, ngCloak: ngCloakDirective, ngController: ngControllerDirective, ngForm: ngFormDirective, ngHide: ngHideDirective, + ngIf: ngIfDirective, ngInclude: ngIncludeDirective, ngInit: ngInitDirective, ngNonBindable: ngNonBindableDirective, @@ -1478,7 +2033,6 @@ function publishExternalAPI(angular){ ngSwitchWhen: ngSwitchWhenDirective, ngSwitchDefault: ngSwitchDefaultDirective, ngOptions: ngOptionsDirective, - ngView: ngViewDirective, ngTransclude: ngTranscludeDirective, ngModel: ngModelDirective, ngList: ngListDirective, @@ -1487,10 +2041,14 @@ function publishExternalAPI(angular){ ngRequired: requiredDirective, ngValue: ngValueDirective }). + directive({ + ngInclude: ngIncludeFillContentDirective + }). directive(ngAttributeAliasDirectives). directive(ngEventDirectives); $provide.provider({ $anchorScroll: $AnchorScrollProvider, + $animate: $AnimateProvider, $browser: $BrowserProvider, $cacheFactory: $CacheFactoryProvider, $controller: $ControllerProvider, @@ -1498,24 +2056,35 @@ function publishExternalAPI(angular){ $exceptionHandler: $ExceptionHandlerProvider, $filter: $FilterProvider, $interpolate: $InterpolateProvider, + $interval: $IntervalProvider, $http: $HttpProvider, $httpBackend: $HttpBackendProvider, $location: $LocationProvider, $log: $LogProvider, $parse: $ParseProvider, - $route: $RouteProvider, - $routeParams: $RouteParamsProvider, $rootScope: $RootScopeProvider, $q: $QProvider, + $sce: $SceProvider, + $sceDelegate: $SceDelegateProvider, $sniffer: $SnifferProvider, $templateCache: $TemplateCacheProvider, $timeout: $TimeoutProvider, - $window: $WindowProvider + $window: $WindowProvider, + $$rAF: $$RAFProvider, + $$asyncCallback : $$AsyncCallbackProvider }); } ]); } +/* global + + -JQLitePrototype, + -addEventListenerFn, + -removeEventListenerFn, + -BOOLEAN_ATTR +*/ + ////////////////////////////////// //JQLite ////////////////////////////////// @@ -1523,74 +2092,82 @@ function publishExternalAPI(angular){ /** * @ngdoc function * @name angular.element - * @function + * @module ng + * @kind function * * @description * Wraps a raw DOM element or HTML string as a [jQuery](http://jquery.com) element. - * `angular.element` can be either an alias for [jQuery](http://api.jquery.com/jQuery/) function, if - * jQuery is available, or a function that wraps the element or string in Angular's jQuery lite - * implementation (commonly referred to as jqLite). * - * Real jQuery always takes precedence over jqLite, provided it was loaded before `DOMContentLoaded` - * event fired. + * If jQuery is available, `angular.element` is an alias for the + * [jQuery](http://api.jquery.com/jQuery/) function. If jQuery is not available, `angular.element` + * delegates to Angular's built-in subset of jQuery, called "jQuery lite" or "jqLite." * - * jqLite is a tiny, API-compatible subset of jQuery that allows - * Angular to manipulate the DOM. jqLite implements only the most commonly needed functionality - * within a very small footprint, so only a subset of the jQuery API - methods, arguments and - * invocation styles - are supported. + *
jqLite is a tiny, API-compatible subset of jQuery that allows + * Angular to manipulate the DOM in a cross-browser compatible way. **jqLite** implements only the most + * commonly needed functionality with the goal of having a very small footprint.
* - * Note: All element references in Angular are always wrapped with jQuery or jqLite; they are never - * raw DOM references. + * To use jQuery, simply load it before `DOMContentLoaded` event fired. + * + *
**Note:** all element references in Angular are always wrapped with jQuery or + * jqLite; they are never raw DOM references.
* * ## Angular's jqLite - * Angular's lite version of jQuery provides only the following jQuery methods: - * - * - [addClass()](http://api.jquery.com/addClass/) - * - [after()](http://api.jquery.com/after/) - * - [append()](http://api.jquery.com/append/) - * - [attr()](http://api.jquery.com/attr/) - * - [bind()](http://api.jquery.com/bind/) - Does not support namespaces - * - [children()](http://api.jquery.com/children/) - Does not support selectors - * - [clone()](http://api.jquery.com/clone/) - * - [contents()](http://api.jquery.com/contents/) - * - [css()](http://api.jquery.com/css/) - * - [data()](http://api.jquery.com/data/) - * - [eq()](http://api.jquery.com/eq/) - * - [find()](http://api.jquery.com/find/) - Limited to lookups by tag name - * - [hasClass()](http://api.jquery.com/hasClass/) - * - [html()](http://api.jquery.com/html/) - * - [next()](http://api.jquery.com/next/) - Does not support selectors - * - [parent()](http://api.jquery.com/parent/) - Does not support selectors - * - [prepend()](http://api.jquery.com/prepend/) - * - [prop()](http://api.jquery.com/prop/) - * - [ready()](http://api.jquery.com/ready/) - * - [remove()](http://api.jquery.com/remove/) - * - [removeAttr()](http://api.jquery.com/removeAttr/) - * - [removeClass()](http://api.jquery.com/removeClass/) - * - [removeData()](http://api.jquery.com/removeData/) - * - [replaceWith()](http://api.jquery.com/replaceWith/) - * - [text()](http://api.jquery.com/text/) - * - [toggleClass()](http://api.jquery.com/toggleClass/) - * - [triggerHandler()](http://api.jquery.com/triggerHandler/) - Doesn't pass native event objects to handlers. - * - [unbind()](http://api.jquery.com/unbind/) - Does not support namespaces - * - [val()](http://api.jquery.com/val/) - * - [wrap()](http://api.jquery.com/wrap/) + * jqLite provides only the following jQuery methods: + * + * - [`addClass()`](http://api.jquery.com/addClass/) + * - [`after()`](http://api.jquery.com/after/) + * - [`append()`](http://api.jquery.com/append/) + * - [`attr()`](http://api.jquery.com/attr/) + * - [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData + * - [`children()`](http://api.jquery.com/children/) - Does not support selectors + * - [`clone()`](http://api.jquery.com/clone/) + * - [`contents()`](http://api.jquery.com/contents/) + * - [`css()`](http://api.jquery.com/css/) + * - [`data()`](http://api.jquery.com/data/) + * - [`empty()`](http://api.jquery.com/empty/) + * - [`eq()`](http://api.jquery.com/eq/) + * - [`find()`](http://api.jquery.com/find/) - Limited to lookups by tag name + * - [`hasClass()`](http://api.jquery.com/hasClass/) + * - [`html()`](http://api.jquery.com/html/) + * - [`next()`](http://api.jquery.com/next/) - Does not support selectors + * - [`on()`](http://api.jquery.com/on/) - Does not support namespaces, selectors or eventData + * - [`off()`](http://api.jquery.com/off/) - Does not support namespaces or selectors + * - [`one()`](http://api.jquery.com/one/) - Does not support namespaces or selectors + * - [`parent()`](http://api.jquery.com/parent/) - Does not support selectors + * - [`prepend()`](http://api.jquery.com/prepend/) + * - [`prop()`](http://api.jquery.com/prop/) + * - [`ready()`](http://api.jquery.com/ready/) + * - [`remove()`](http://api.jquery.com/remove/) + * - [`removeAttr()`](http://api.jquery.com/removeAttr/) + * - [`removeClass()`](http://api.jquery.com/removeClass/) + * - [`removeData()`](http://api.jquery.com/removeData/) + * - [`replaceWith()`](http://api.jquery.com/replaceWith/) + * - [`text()`](http://api.jquery.com/text/) + * - [`toggleClass()`](http://api.jquery.com/toggleClass/) + * - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers. + * - [`unbind()`](http://api.jquery.com/unbind/) - Does not support namespaces + * - [`val()`](http://api.jquery.com/val/) + * - [`wrap()`](http://api.jquery.com/wrap/) * * ## jQuery/jqLite Extras * Angular also provides the following additional methods and events to both jQuery and jqLite: * * ### Events * - `$destroy` - AngularJS intercepts all jqLite/jQuery's DOM destruction apis and fires this event - * on all DOM nodes being removed. This can be used to clean up and 3rd party bindings to the DOM + * on all DOM nodes being removed. This can be used to clean up any 3rd party bindings to the DOM * element before it is removed. + * * ### Methods * - `controller(name)` - retrieves the controller of the current element or its parent. By default * retrieves controller associated with the `ngController` directive. If `name` is provided as * camelCase directive name, then the controller for this directive will be retrieved (e.g. * `'ngModel'`). * - `injector()` - retrieves the injector of the current element or its parent. - * - `scope()` - retrieves the {@link api/ng.$rootScope.Scope scope} of the current + * - `scope()` - retrieves the {@link ng.$rootScope.Scope scope} of the current * element or its parent. + * - `isolateScope()` - retrieves an isolate {@link ng.$rootScope.Scope scope} if one is attached directly to the + * current element. This getter should be used only on elements that contain a directive which starts a new isolate + * scope. Calling `scope()` on this element always returns the original non-isolate scope. * - `inheritedData()` - same as `data()`, but walks up the DOM until a value is found or the top * parent element is reached. * @@ -1599,7 +2176,7 @@ function publishExternalAPI(angular){ */ var jqCache = JQLite.cache = {}, - jqName = JQLite.expando = 'ng-' + new Date().getTime(), + jqName = JQLite.expando = 'ng' + new Date().getTime(), jqId = 1, addEventListenerFn = (window.document.addEventListener ? function(element, type, fn) {element.addEventListener(type, fn, false);} @@ -1608,11 +2185,20 @@ var jqCache = JQLite.cache = {}, ? function(element, type, fn) {element.removeEventListener(type, fn, false); } : function(element, type, fn) {element.detachEvent('on' + type, fn); }); +/* + * !!! This is an undocumented "private" function !!! + */ +var jqData = JQLite._data = function(node) { + //jQuery always returns an object on cache miss + return this.cache[node[this.expando]] || {}; +}; + function jqNextId() { return ++jqId; } var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; var MOZ_HACK_REGEXP = /^moz([A-Z])/; +var jqLiteMinErr = minErr('jqLite'); /** * Converts snake_case to camelCase. @@ -1635,13 +2221,14 @@ function camelCase(name) { // ///////////////////////////////////////////// -function JQLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) { +function jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) { var originalJqFn = jQuery.fn[name]; originalJqFn = originalJqFn.$original || originalJqFn; removePatch.$original = originalJqFn; jQuery.fn[name] = removePatch; function removePatch(param) { + // jshint -W040 var list = filterElems && param ? [this.filter(param)] : [this], fireEvent = dispatchThis, set, setIndex, setLength, @@ -1669,45 +2256,115 @@ function JQLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArgu } } +var SINGLE_TAG_REGEXP = /^<(\w+)\s*\/?>(?:<\/\1>|)$/; +var HTML_REGEXP = /<|&#?\w+;/; +var TAG_NAME_REGEXP = /<([\w:]+)/; +var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi; + +var wrapMap = { + 'option': [1, ''], + + 'thead': [1, '', '
'], + 'col': [2, '', '
'], + 'tr': [2, '', '
'], + 'td': [3, '', '
'], + '_default': [0, "", ""] +}; + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +function jqLiteIsTextNode(html) { + return !HTML_REGEXP.test(html); +} + +function jqLiteBuildFragment(html, context) { + var elem, tmp, tag, wrap, + fragment = context.createDocumentFragment(), + nodes = [], i, j, jj; + + if (jqLiteIsTextNode(html)) { + // Convert non-html into a text node + nodes.push(context.createTextNode(html)); + } else { + tmp = fragment.appendChild(context.createElement('div')); + // Convert html into DOM nodes + tag = (TAG_NAME_REGEXP.exec(html) || ["", ""])[1].toLowerCase(); + wrap = wrapMap[tag] || wrapMap._default; + tmp.innerHTML = '
 
' + + wrap[1] + html.replace(XHTML_TAG_REGEXP, "<$1>") + wrap[2]; + tmp.removeChild(tmp.firstChild); + + // Descend through wrappers to the right content + i = wrap[0]; + while (i--) { + tmp = tmp.lastChild; + } + + for (j=0, jj=tmp.childNodes.length; j -1); } -function JQLiteRemoveClass(element, cssClasses) { - if (cssClasses) { +function jqLiteRemoveClass(element, cssClasses) { + if (cssClasses && element.setAttribute) { forEach(cssClasses.split(' '), function(cssClass) { - element.className = trim( - (" " + element.className + " ") + element.setAttribute('class', trim( + (" " + (element.getAttribute('class') || '') + " ") .replace(/[\n\t]/g, " ") - .replace(" " + trim(cssClass) + " ", " ") + .replace(" " + trim(cssClass) + " ", " ")) ); }); } } -function JQLiteAddClass(element, cssClasses) { - if (cssClasses) { +function jqLiteAddClass(element, cssClasses) { + if (cssClasses && element.setAttribute) { + var existingClasses = (' ' + (element.getAttribute('class') || '') + ' ') + .replace(/[\n\t]/g, " "); + forEach(cssClasses.split(' '), function(cssClass) { - if (!JQLiteHasClass(element, cssClass)) { - element.className = trim(element.className + ' ' + trim(cssClass)); + cssClass = trim(cssClass); + if (existingClasses.indexOf(' ' + cssClass + ' ') === -1) { + existingClasses += cssClass + ' '; } }); + + element.setAttribute('class', trim(existingClasses)); } } -function JQLiteAddNodes(root, elements) { +function jqLiteAddNodes(root, elements) { if (elements) { elements = (!elements.nodeName && isDefined(elements.length) && !isWindow(elements)) ? elements @@ -1819,11 +2490,11 @@ function JQLiteAddNodes(root, elements) { } } -function JQLiteController(element, name) { - return JQLiteInheritedData(element, '$' + (name || 'ngController' ) + 'Controller'); +function jqLiteController(element, name) { + return jqLiteInheritedData(element, '$' + (name || 'ngController' ) + 'Controller'); } -function JQLiteInheritedData(element, name, value) { +function jqLiteInheritedData(element, name, value) { element = jqLite(element); // if element is the document object work with the html element instead @@ -1831,10 +2502,27 @@ function JQLiteInheritedData(element, name, value) { if(element[0].nodeType == 9) { element = element.find('html'); } + var names = isArray(name) ? name : [name]; while (element.length) { - if (value = element.data(name)) return value; - element = element.parent(); + var node = element[0]; + for (var i = 0, ii = names.length; i < ii; i++) { + if ((value = element.data(names[i])) !== undefined) return value; + } + + // If dealing with a document fragment node with a host element, and no parent, use the host + // element as the parent. This enables directives within a Shadow DOM or polyfilled Shadow DOM + // to lookup parent controllers. + element = jqLite(node.parentNode || (node.nodeType === 11 && node.host)); + } +} + +function jqLiteEmpty(element) { + for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { + jqLiteDealoc(childNodes[i]); + } + while (element.firstChild) { + element.removeChild(element.firstChild); } } @@ -1851,9 +2539,16 @@ var JQLitePrototype = JQLite.prototype = { fn(); } - this.bind('DOMContentLoaded', trigger); // works for modern browsers and IE9 - // we can not use jqLite since we are not done loading and jQuery could be loaded later. - JQLite(window).bind('load', trigger); // fallback to window.onload for others + // check if document already is loaded + if (document.readyState === 'complete'){ + setTimeout(trigger); + } else { + this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9 + // we can not use jqLite since we are not done loading and jQuery could be loaded later. + // jshint -W064 + JQLite(window).on('load', trigger); // fallback to window.onload for others + // jshint +W064 + } }, toString: function() { var value = []; @@ -1877,11 +2572,11 @@ var JQLitePrototype = JQLite.prototype = { // value on get. ////////////////////////////////////////// var BOOLEAN_ATTR = {}; -forEach('multiple,selected,checked,disabled,readOnly,required'.split(','), function(value) { +forEach('multiple,selected,checked,disabled,readOnly,required,open'.split(','), function(value) { BOOLEAN_ATTR[lowercase(value)] = value; }); var BOOLEAN_ELEMENTS = {}; -forEach('input,select,option,textarea,button,form'.split(','), function(value) { +forEach('input,select,option,textarea,button,form,details'.split(','), function(value) { BOOLEAN_ELEMENTS[uppercase(value)] = true; }); @@ -1894,24 +2589,30 @@ function getBooleanAttrName(element, name) { } forEach({ - data: JQLiteData, - inheritedData: JQLiteInheritedData, + data: jqLiteData, + inheritedData: jqLiteInheritedData, scope: function(element) { - return JQLiteInheritedData(element, '$scope'); + // Can't use jqLiteData here directly so we stay compatible with jQuery! + return jqLite(element).data('$scope') || jqLiteInheritedData(element.parentNode || element, ['$isolateScope', '$scope']); + }, + + isolateScope: function(element) { + // Can't use jqLiteData here directly so we stay compatible with jQuery! + return jqLite(element).data('$isolateScope') || jqLite(element).data('$isolateScopeNoTemplate'); }, - controller: JQLiteController , + controller: jqLiteController, injector: function(element) { - return JQLiteInheritedData(element, '$injector'); + return jqLiteInheritedData(element, '$injector'); }, removeAttr: function(element,name) { element.removeAttribute(name); }, - hasClass: JQLiteHasClass, + hasClass: jqLiteHasClass, css: function(element, name, value) { name = camelCase(name); @@ -1974,24 +2675,26 @@ forEach({ } }, - text: extend((msie < 9) - ? function(element, value) { - if (element.nodeType == 1 /** Element */) { - if (isUndefined(value)) - return element.innerText; - element.innerText = value; - } else { - if (isUndefined(value)) - return element.nodeValue; - element.nodeValue = value; - } + text: (function() { + var NODE_TYPE_TEXT_PROPERTY = []; + if (msie < 9) { + NODE_TYPE_TEXT_PROPERTY[1] = 'innerText'; /** Element **/ + NODE_TYPE_TEXT_PROPERTY[3] = 'nodeValue'; /** Text **/ + } else { + NODE_TYPE_TEXT_PROPERTY[1] = /** Element **/ + NODE_TYPE_TEXT_PROPERTY[3] = 'textContent'; /** Text **/ + } + getText.$dv = ''; + return getText; + + function getText(element, value) { + var textProp = NODE_TYPE_TEXT_PROPERTY[element.nodeType]; + if (isUndefined(value)) { + return textProp ? element[textProp] : ''; } - : function(element, value) { - if (isUndefined(value)) { - return element.textContent; - } - element.textContent = value; - }, {$dv:''}), + element[textProp] = value; + } + })(), val: function(element, value) { if (isUndefined(value)) { @@ -2014,10 +2717,12 @@ forEach({ return element.innerHTML; } for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { - JQLiteDealoc(childNodes[i]); + jqLiteDealoc(childNodes[i]); } element.innerHTML = value; - } + }, + + empty: jqLiteEmpty }, function(fn, name){ /** * Properties: writes return selection, reads return first value @@ -2025,14 +2730,16 @@ forEach({ JQLite.prototype[name] = function(arg1, arg2) { var i, key; - // JQLiteHasClass has only two arguments, but is a getter-only fn, so we need to special-case it + // jqLiteHasClass has only two arguments, but is a getter-only fn, so we need to special-case it // in a way that survives minification. - if (((fn.length == 2 && (fn !== JQLiteHasClass && fn !== JQLiteController)) ? arg1 : arg2) === undefined) { + // jqLiteEmpty takes no arguments but is a setter. + if (fn !== jqLiteEmpty && + (((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2) === undefined)) { if (isObject(arg1)) { // we are a write, but the object properties are the key/values - for(i=0; i < this.length; i++) { - if (fn === JQLiteData) { + for (i = 0; i < this.length; i++) { + if (fn === jqLiteData) { // data() takes the whole object in jQuery fn(this[i], arg1); } else { @@ -2045,18 +2752,23 @@ forEach({ return this; } else { // we are a read, so read the first child. - if (this.length) - return fn(this[0], arg1, arg2); + var value = fn.$dv; + // Only if we have $dv do we iterate over all, otherwise it is just the first element. + var jj = (value === undefined) ? Math.min(this.length, 1) : this.length; + for (var j = 0; j < jj; j++) { + var nodeValue = fn(this[j], arg1, arg2); + value = value ? value + nodeValue : nodeValue; + } + return value; } } else { // we are a write, so apply to all children - for(i=0; i < this.length; i++) { + for (i = 0; i < this.length; i++) { fn(this[i], arg1, arg2); } // return self for chaining return this; } - return fn.$dv; }; }); @@ -2088,10 +2800,13 @@ function createEventHandler(element, events) { } event.isDefaultPrevented = function() { - return event.defaultPrevented; + return event.defaultPrevented || event.returnValue === false; }; - forEach(events[type || event.type], function(fn) { + // Copy event handlers in case event handlers array is modified during execution. + var eventHandlersCopy = shallowCopy(events[type || event.type] || []); + + forEach(eventHandlersCopy, function(fn) { fn.call(element, event); }); @@ -2119,16 +2834,18 @@ function createEventHandler(element, events) { // selector. ////////////////////////////////////////// forEach({ - removeData: JQLiteRemoveData, + removeData: jqLiteRemoveData, - dealoc: JQLiteDealoc, + dealoc: jqLiteDealoc, - bind: function bindFn(element, type, fn){ - var events = JQLiteExpandoStore(element, 'events'), - handle = JQLiteExpandoStore(element, 'handle'); + on: function onFn(element, type, fn, unsupported){ + if (isDefined(unsupported)) throw jqLiteMinErr('onargs', 'jqLite#on() does not support the `selector` or `eventData` parameters'); - if (!events) JQLiteExpandoStore(element, 'events', events = {}); - if (!handle) JQLiteExpandoStore(element, 'handle', handle = createEventHandler(element, events)); + var events = jqLiteExpandoStore(element, 'events'), + handle = jqLiteExpandoStore(element, 'handle'); + + if (!events) jqLiteExpandoStore(element, 'events', events = {}); + if (!handle) jqLiteExpandoStore(element, 'handle', handle = createEventHandler(element, events)); forEach(type.split(' '), function(type){ var eventFns = events[type]; @@ -2137,6 +2854,7 @@ forEach({ if (type == 'mouseenter' || type == 'mouseleave') { var contains = document.body.contains || document.body.compareDocumentPosition ? function( a, b ) { + // jshint bitwise: false var adown = a.nodeType === 9 ? a.documentElement : a, bup = b && b.parentNode; return a === bup || !!( bup && bup.nodeType === 1 && ( @@ -2154,39 +2872,52 @@ forEach({ } } return false; - }; + }; events[type] = []; - - // Refer to jQuery's implementation of mouseenter & mouseleave + + // Refer to jQuery's implementation of mouseenter & mouseleave // Read about mouseenter and mouseleave: // http://www.quirksmode.org/js/events_mouse.html#link8 - var eventmap = { mouseleave : "mouseout", mouseenter : "mouseover"} - bindFn(element, eventmap[type], function(event) { - var ret, target = this, related = event.relatedTarget; + var eventmap = { mouseleave : "mouseout", mouseenter : "mouseover"}; + + onFn(element, eventmap[type], function(event) { + var target = this, related = event.relatedTarget; // For mousenter/leave call the handler if related is outside the target. // NB: No relatedTarget if the mouse left/entered the browser window if ( !related || (related !== target && !contains(target, related)) ){ handle(event, type); - } - + } }); } else { addEventListenerFn(element, type, handle); events[type] = []; } - eventFns = events[type] + eventFns = events[type]; } eventFns.push(fn); }); }, - unbind: JQLiteUnbind, + off: jqLiteOff, + + one: function(element, type, fn) { + element = jqLite(element); + + //add the listener twice so that when it is called + //you can remove the original function and still be + //able to call element.off(ev, fn) normally + element.on(type, function onFn() { + element.off(type, fn); + element.off(type, onFn); + }); + element.on(type, fn); + }, replaceWith: function(element, replaceNode) { var index, parent = element.parentNode; - JQLiteDealoc(element); + jqLiteDealoc(element); forEach(new JQLite(replaceNode), function(node){ if (index) { parent.insertBefore(node, index.nextSibling); @@ -2207,13 +2938,14 @@ forEach({ }, contents: function(element) { - return element.childNodes || []; + return element.contentDocument || element.childNodes || []; }, append: function(element, node) { forEach(new JQLite(node), function(child){ - if (element.nodeType === 1) + if (element.nodeType === 1 || element.nodeType === 11) { element.appendChild(child); + } }); }, @@ -2236,7 +2968,7 @@ forEach({ }, remove: function(element) { - JQLiteDealoc(element); + jqLiteDealoc(element); var parent = element.parentNode; if (parent) parent.removeChild(element); }, @@ -2249,14 +2981,19 @@ forEach({ }); }, - addClass: JQLiteAddClass, - removeClass: JQLiteRemoveClass, + addClass: jqLiteAddClass, + removeClass: jqLiteRemoveClass, toggleClass: function(element, selector, condition) { - if (isUndefined(condition)) { - condition = !JQLiteHasClass(element, selector); + if (selector) { + forEach(selector.split(' '), function(className){ + var classCondition = condition; + if (isUndefined(classCondition)) { + classCondition = !jqLiteHasClass(element, className); + } + (classCondition ? jqLiteAddClass : jqLiteRemoveClass)(element, className); + }); } - (condition ? JQLiteAddClass : JQLiteRemoveClass)(element, selector); }, parent: function(element) { @@ -2278,37 +3015,52 @@ forEach({ }, find: function(element, selector) { - return element.getElementsByTagName(selector); + if (element.getElementsByTagName) { + return element.getElementsByTagName(selector); + } else { + return []; + } }, - clone: JQLiteClone, + clone: jqLiteClone, + + triggerHandler: function(element, eventName, eventData) { + var eventFns = (jqLiteExpandoStore(element, 'events') || {})[eventName]; - triggerHandler: function(element, eventName) { - var eventFns = (JQLiteExpandoStore(element, 'events') || {})[eventName]; + eventData = eventData || []; + + var event = [{ + preventDefault: noop, + stopPropagation: noop + }]; forEach(eventFns, function(fn) { - fn.call(element, null); + fn.apply(element, event.concat(eventData)); }); } }, function(fn, name){ /** * chaining functions */ - JQLite.prototype[name] = function(arg1, arg2) { + JQLite.prototype[name] = function(arg1, arg2, arg3) { var value; for(var i=0; i < this.length; i++) { - if (value == undefined) { - value = fn(this[i], arg1, arg2); - if (value !== undefined) { + if (isUndefined(value)) { + value = fn(this[i], arg1, arg2, arg3); + if (isDefined(value)) { // any function which returns a value needs to be wrapped value = jqLite(value); } } else { - JQLiteAddNodes(value, fn(this[i], arg1, arg2)); + jqLiteAddNodes(value, fn(this[i], arg1, arg2, arg3)); } } - return value == undefined ? this : value; + return isDefined(value) ? value : this; }; + + // bind legacy bind/unbind to on/off + JQLite.prototype.bind = JQLite.prototype.on; + JQLite.prototype.unbind = JQLite.prototype.off; }); /** @@ -2359,7 +3111,7 @@ HashMap.prototype = { /** * @param key - * @returns the value for the key + * @returns {Object} the value for the key */ get: function(key) { return this[hashKey(key)]; @@ -2376,54 +3128,11 @@ HashMap.prototype = { } }; -/** - * A map where multiple values can be added to the same key such that they form a queue. - * @returns {HashQueueMap} - */ -function HashQueueMap() {} -HashQueueMap.prototype = { - /** - * Same as array push, but using an array as the value for the hash - */ - push: function(key, value) { - var array = this[key = hashKey(key)]; - if (!array) { - this[key] = [value]; - } else { - array.push(value); - } - }, - - /** - * Same as array shift, but using an array as the value for the hash - */ - shift: function(key) { - var array = this[key = hashKey(key)]; - if (array) { - if (array.length == 1) { - delete this[key]; - return array[0]; - } else { - return array.shift(); - } - } - }, - - /** - * return the first item without deleting it - */ - peek: function(key) { - var array = this[hashKey(key)]; - if (array) { - return array[0]; - } - } -}; - /** * @ngdoc function + * @module ng * @name angular.injector - * @function + * @kind function * * @description * Creates an injector function that can be used for retrieving services as well as for @@ -2432,11 +3141,11 @@ HashQueueMap.prototype = { * @param {Array.} modules A list of module functions or their aliases. See * {@link angular.module}. The `ng` module must be explicitly added. - * @returns {function()} Injector function. See {@link AUTO.$injector $injector}. + * @returns {function()} Injector function. See {@link auto.$injector $injector}. * * @example * Typical usage - *
+ * ```js
  *   // create an injector
  *   var $injector = angular.injector(['ng']);
  *
@@ -2446,22 +3155,45 @@ HashQueueMap.prototype = {
  *     $compile($document)($rootScope);
  *     $rootScope.$digest();
  *   });
- * 
+ * ``` + * + * Sometimes you want to get access to the injector of a currently running Angular app + * from outside Angular. Perhaps, you want to inject and compile some markup after the + * application has been bootstrapped. You can do this using the extra `injector()` added + * to JQuery/jqLite elements. See {@link angular.element}. + * + * *This is fairly rare but could be the case if a third party library is injecting the + * markup.* + * + * In the following example a new block of HTML containing a `ng-controller` + * directive is added to the end of the document body by JQuery. We then compile and link + * it into the current AngularJS scope. + * + * ```js + * var $div = $('
{{content.label}}
'); + * $(document.body).append($div); + * + * angular.element(document).injector().invoke(function($compile) { + * var scope = angular.element($div).scope(); + * $compile($div)(scope); + * }); + * ``` */ /** - * @ngdoc overview - * @name AUTO + * @ngdoc module + * @name auto * @description * - * Implicit module which gets automatically added to each {@link AUTO.$injector $injector}. + * Implicit module which gets automatically added to each {@link auto.$injector $injector}. */ var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; var FN_ARG_SPLIT = /,/; var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; +var $injectorMinErr = minErr('$injector'); function annotate(fn) { var $inject, fnText, @@ -2471,13 +3203,15 @@ function annotate(fn) { if (typeof fn == 'function') { if (!($inject = fn.$inject)) { $inject = []; - fnText = fn.toString().replace(STRIP_COMMENTS, ''); - argDecl = fnText.match(FN_ARGS); - forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){ - arg.replace(FN_ARG, function(all, underscore, name){ - $inject.push(name); + if (fn.length) { + fnText = fn.toString().replace(STRIP_COMMENTS, ''); + argDecl = fnText.match(FN_ARGS); + forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){ + arg.replace(FN_ARG, function(all, underscore, name){ + $inject.push(name); + }); }); - }); + } fn.$inject = $inject; } } else if (isArray(fn)) { @@ -2493,32 +3227,32 @@ function annotate(fn) { /////////////////////////////////////// /** - * @ngdoc object - * @name AUTO.$injector - * @function + * @ngdoc service + * @name $injector + * @kind function * * @description * * `$injector` is used to retrieve object instances as defined by - * {@link AUTO.$provide provider}, instantiate types, invoke methods, + * {@link auto.$provide provider}, instantiate types, invoke methods, * and load modules. * * The following always holds true: * - *
+ * ```js
  *   var $injector = angular.injector();
  *   expect($injector.get('$injector')).toBe($injector);
  *   expect($injector.invoke(function($injector){
  *     return $injector;
  *   }).toBe($injector);
- * 
+ * ``` * * # Injection Function Annotation * * JavaScript does not have annotations, and annotations are needed for dependency injection. The * following are all valid ways of annotating function with injection arguments and are equivalent. * - *
+ * ```js
  *   // inferred (only works if code not minified/obfuscated)
  *   $injector.invoke(function(serviceA){});
  *
@@ -2529,16 +3263,16 @@ function annotate(fn) {
  *
  *   // inline
  *   $injector.invoke(['serviceA', function(serviceA){}]);
- * 
+ * ``` * * ## Inference * - * In JavaScript calling `toString()` on a function returns the function definition. The definition can then be - * parsed and the function arguments can be extracted. *NOTE:* This does not work with minification, and obfuscation - * tools since these tools change the argument names. + * In JavaScript calling `toString()` on a function returns the function definition. The definition + * can then be parsed and the function arguments can be extracted. *NOTE:* This does not work with + * minification, and obfuscation tools since these tools change the argument names. * * ## `$inject` Annotation - * By adding a `$inject` property onto a function the injection parameters can be specified. + * By adding an `$inject` property onto a function the injection parameters can be specified. * * ## Inline * As an array of injection names, where the last item in the array is the function to call. @@ -2546,8 +3280,7 @@ function annotate(fn) { /** * @ngdoc method - * @name AUTO.$injector#get - * @methodOf AUTO.$injector + * @name $injector#get * * @description * Return an instance of the service. @@ -2558,48 +3291,60 @@ function annotate(fn) { /** * @ngdoc method - * @name AUTO.$injector#invoke - * @methodOf AUTO.$injector + * @name $injector#invoke * * @description * Invoke the method and supply the method arguments from the `$injector`. * - * @param {!function} fn The function to invoke. The function arguments come form the function annotation. + * @param {!Function} fn The function to invoke. Function parameters are injected according to the + * {@link guide/di $inject Annotation} rules. * @param {Object=} self The `this` for the invoked method. - * @param {Object=} locals Optional object. If preset then any argument names are read from this object first, before - * the `$injector` is consulted. + * @param {Object=} locals Optional object. If preset then any argument names are read from this + * object first, before the `$injector` is consulted. * @returns {*} the value returned by the invoked `fn` function. */ /** * @ngdoc method - * @name AUTO.$injector#instantiate - * @methodOf AUTO.$injector + * @name $injector#has + * * @description - * Create a new instance of JS type. The method takes a constructor function invokes the new operator and supplies - * all of the arguments to the constructor function as specified by the constructor annotation. + * Allows the user to query if the particular service exists. * - * @param {function} Type Annotated constructor function. - * @param {Object=} locals Optional object. If preset then any argument names are read from this object first, before - * the `$injector` is consulted. - * @returns {Object} new instance of `Type`. + * @param {string} Name of the service to query. + * @returns {boolean} returns true if injector has given service. */ /** * @ngdoc method - * @name AUTO.$injector#annotate - * @methodOf AUTO.$injector - * + * @name $injector#instantiate * @description - * Returns an array of service names which the function is requesting for injection. This API is used by the injector - * to determine which services need to be injected into the function when the function is invoked. There are three - * ways in which the function can be annotated with the needed dependencies. + * Create a new instance of JS type. The method takes a constructor function, invokes the new + * operator, and supplies all of the arguments to the constructor function as specified by the + * constructor annotation. * - * # Argument names + * @param {Function} Type Annotated constructor function. + * @param {Object=} locals Optional object. If preset then any argument names are read from this + * object first, before the `$injector` is consulted. + * @returns {Object} new instance of `Type`. + */ + +/** + * @ngdoc method + * @name $injector#annotate * - * The simplest form is to extract the dependencies from the arguments of the function. This is done by converting - * the function into a string using `toString()` method and extracting the argument names. - *
+ * @description
+ * Returns an array of service names which the function is requesting for injection. This API is
+ * used by the injector to determine which services need to be injected into the function when the
+ * function is invoked. There are three ways in which the function can be annotated with the needed
+ * dependencies.
+ *
+ * # Argument names
+ *
+ * The simplest form is to extract the dependencies from the arguments of the function. This is done
+ * by converting the function into a string using `toString()` method and extracting the argument
+ * names.
+ * ```js
  *   // Given
  *   function MyController($scope, $route) {
  *     // ...
@@ -2607,34 +3352,34 @@ function annotate(fn) {
  *
  *   // Then
  *   expect(injector.annotate(MyController)).toEqual(['$scope', '$route']);
- * 
+ * ``` * - * This method does not work with code minfication / obfuscation. For this reason the following annotation strategies - * are supported. + * This method does not work with code minification / obfuscation. For this reason the following + * annotation strategies are supported. * * # The `$inject` property * - * If a function has an `$inject` property and its value is an array of strings, then the strings represent names of - * services to be injected into the function. - *
+ * If a function has an `$inject` property and its value is an array of strings, then the strings
+ * represent names of services to be injected into the function.
+ * ```js
  *   // Given
  *   var MyController = function(obfuscatedScope, obfuscatedRoute) {
  *     // ...
  *   }
  *   // Define function dependencies
- *   MyController.$inject = ['$scope', '$route'];
+ *   MyController['$inject'] = ['$scope', '$route'];
  *
  *   // Then
  *   expect(injector.annotate(MyController)).toEqual(['$scope', '$route']);
- * 
+ * ``` * * # The array notation * - * It is often desirable to inline Injected functions and that's when setting the `$inject` property is very - * inconvenient. In these situations using the array notation to specify the dependencies in a way that survives - * minification is a better choice: + * It is often desirable to inline Injected functions and that's when setting the `$inject` property + * is very inconvenient. In these situations using the array notation to specify the dependencies in + * a way that survives minification is a better choice: * - *
+ * ```js
  *   // We wish to write this (not minification / obfuscation safe)
  *   injector.invoke(function($compile, $rootScope) {
  *     // ...
@@ -2656,10 +3401,10 @@ function annotate(fn) {
  *   expect(injector.annotate(
  *      ['$compile', '$rootScope', function(obfus_$compile, obfus_$rootScope) {}])
  *    ).toEqual(['$compile', '$rootScope']);
- * 
+ * ``` * - * @param {function|Array.} fn Function for which dependent service names need to be retrieved as described - * above. + * @param {Function|Array.} fn Function for which dependent service names need to + * be retrieved as described above. * * @returns {Array.} The names of the services which the function requires. */ @@ -2669,147 +3414,304 @@ function annotate(fn) { /** * @ngdoc object - * @name AUTO.$provide + * @name $provide * * @description * - * Use `$provide` to register new providers with the `$injector`. The providers are the factories for the instance. - * The providers share the same name as the instance they create with `Provider` suffixed to them. - * - * A provider is an object with a `$get()` method. The injector calls the `$get` method to create a new instance of - * a service. The Provider can have additional methods which would allow for configuration of the provider. - * - *
- *   function GreetProvider() {
- *     var salutation = 'Hello';
- *
- *     this.salutation = function(text) {
- *       salutation = text;
- *     };
- *
- *     this.$get = function() {
- *       return function (name) {
- *         return salutation + ' ' + name + '!';
- *       };
- *     };
- *   }
- *
- *   describe('Greeter', function(){
- *
- *     beforeEach(module(function($provide) {
- *       $provide.provider('greet', GreetProvider);
- *     }));
- *
- *     it('should greet', inject(function(greet) {
- *       expect(greet('angular')).toEqual('Hello angular!');
- *     }));
- *
- *     it('should allow configuration of salutation', function() {
- *       module(function(greetProvider) {
- *         greetProvider.salutation('Ahoj');
- *       });
- *       inject(function(greet) {
- *         expect(greet('angular')).toEqual('Ahoj angular!');
- *       });
- *     });
- * 
+ * The {@link auto.$provide $provide} service has a number of methods for registering components + * with the {@link auto.$injector $injector}. Many of these functions are also exposed on + * {@link angular.Module}. + * + * An Angular **service** is a singleton object created by a **service factory**. These **service + * factories** are functions which, in turn, are created by a **service provider**. + * The **service providers** are constructor functions. When instantiated they must contain a + * property called `$get`, which holds the **service factory** function. + * + * When you request a service, the {@link auto.$injector $injector} is responsible for finding the + * correct **service provider**, instantiating it and then calling its `$get` **service factory** + * function to get the instance of the **service**. + * + * Often services have no configuration options and there is no need to add methods to the service + * provider. The provider will be no more than a constructor function with a `$get` property. For + * these cases the {@link auto.$provide $provide} service has additional helper methods to register + * services without specifying a provider. + * + * * {@link auto.$provide#provider provider(provider)} - registers a **service provider** with the + * {@link auto.$injector $injector} + * * {@link auto.$provide#constant constant(obj)} - registers a value/object that can be accessed by + * providers and services. + * * {@link auto.$provide#value value(obj)} - registers a value/object that can only be accessed by + * services, not providers. + * * {@link auto.$provide#factory factory(fn)} - registers a service **factory function**, `fn`, + * that will be wrapped in a **service provider** object, whose `$get` property will contain the + * given factory function. + * * {@link auto.$provide#service service(class)} - registers a **constructor function**, `class` + * that will be wrapped in a **service provider** object, whose `$get` property will instantiate + * a new object using the given constructor function. + * + * See the individual methods for more information and examples. */ /** * @ngdoc method - * @name AUTO.$provide#provider - * @methodOf AUTO.$provide + * @name $provide#provider * @description * - * Register a provider for a service. The providers can be retrieved and can have additional configuration methods. + * Register a **provider function** with the {@link auto.$injector $injector}. Provider functions + * are constructor functions, whose instances are responsible for "providing" a factory for a + * service. * - * @param {string} name The name of the instance. NOTE: the provider will be available under `name + 'Provider'` key. + * Service provider names start with the name of the service they provide followed by `Provider`. + * For example, the {@link ng.$log $log} service has a provider called + * {@link ng.$logProvider $logProvider}. + * + * Service provider objects can have additional methods which allow configuration of the provider + * and its service. Importantly, you can configure what kind of service is created by the `$get` + * method, or how that service will act. For example, the {@link ng.$logProvider $logProvider} has a + * method {@link ng.$logProvider#debugEnabled debugEnabled} + * which lets you specify whether the {@link ng.$log $log} service will log debug messages to the + * console or not. + * + * @param {string} name The name of the instance. NOTE: the provider will be available under `name + + 'Provider'` key. * @param {(Object|function())} provider If the provider is: * * - `Object`: then it should have a `$get` method. The `$get` method will be invoked using - * {@link AUTO.$injector#invoke $injector.invoke()} when an instance needs to be created. + * {@link auto.$injector#invoke $injector.invoke()} when an instance needs to be created. * - `Constructor`: a new instance of the provider will be created using - * {@link AUTO.$injector#instantiate $injector.instantiate()}, then treated as `object`. + * {@link auto.$injector#instantiate $injector.instantiate()}, then treated as `object`. * * @returns {Object} registered provider instance + + * @example + * + * The following example shows how to create a simple event tracking service and register it using + * {@link auto.$provide#provider $provide.provider()}. + * + * ```js + * // Define the eventTracker provider + * function EventTrackerProvider() { + * var trackingUrl = '/track'; + * + * // A provider method for configuring where the tracked events should been saved + * this.setTrackingUrl = function(url) { + * trackingUrl = url; + * }; + * + * // The service factory function + * this.$get = ['$http', function($http) { + * var trackedEvents = {}; + * return { + * // Call this to track an event + * event: function(event) { + * var count = trackedEvents[event] || 0; + * count += 1; + * trackedEvents[event] = count; + * return count; + * }, + * // Call this to save the tracked events to the trackingUrl + * save: function() { + * $http.post(trackingUrl, trackedEvents); + * } + * }; + * }]; + * } + * + * describe('eventTracker', function() { + * var postSpy; + * + * beforeEach(module(function($provide) { + * // Register the eventTracker provider + * $provide.provider('eventTracker', EventTrackerProvider); + * })); + * + * beforeEach(module(function(eventTrackerProvider) { + * // Configure eventTracker provider + * eventTrackerProvider.setTrackingUrl('/custom-track'); + * })); + * + * it('tracks events', inject(function(eventTracker) { + * expect(eventTracker.event('login')).toEqual(1); + * expect(eventTracker.event('login')).toEqual(2); + * })); + * + * it('saves to the tracking url', inject(function(eventTracker, $http) { + * postSpy = spyOn($http, 'post'); + * eventTracker.event('login'); + * eventTracker.save(); + * expect(postSpy).toHaveBeenCalled(); + * expect(postSpy.mostRecentCall.args[0]).not.toEqual('/track'); + * expect(postSpy.mostRecentCall.args[0]).toEqual('/custom-track'); + * expect(postSpy.mostRecentCall.args[1]).toEqual({ 'login': 1 }); + * })); + * }); + * ``` */ /** * @ngdoc method - * @name AUTO.$provide#factory - * @methodOf AUTO.$provide + * @name $provide#factory * @description * - * A short hand for configuring services if only `$get` method is required. + * Register a **service factory**, which will be called to return the service instance. + * This is short for registering a service where its provider consists of only a `$get` property, + * which is the given service factory function. + * You should use {@link auto.$provide#factory $provide.factory(getFn)} if you do not need to + * configure your service in a provider. * * @param {string} name The name of the instance. - * @param {function()} $getFn The $getFn for the instance creation. Internally this is a short hand for - * `$provide.provider(name, {$get: $getFn})`. + * @param {function()} $getFn The $getFn for the instance creation. Internally this is a short hand + * for `$provide.provider(name, {$get: $getFn})`. * @returns {Object} registered provider instance + * + * @example + * Here is an example of registering a service + * ```js + * $provide.factory('ping', ['$http', function($http) { + * return function ping() { + * return $http.send('/ping'); + * }; + * }]); + * ``` + * You would then inject and use this service like this: + * ```js + * someModule.controller('Ctrl', ['ping', function(ping) { + * ping(); + * }]); + * ``` */ /** * @ngdoc method - * @name AUTO.$provide#service - * @methodOf AUTO.$provide + * @name $provide#service * @description * - * A short hand for registering service of given class. + * Register a **service constructor**, which will be invoked with `new` to create the service + * instance. + * This is short for registering a service where its provider's `$get` property is the service + * constructor function that will be used to instantiate the service instance. + * + * You should use {@link auto.$provide#service $provide.service(class)} if you define your service + * as a type/class. * * @param {string} name The name of the instance. * @param {Function} constructor A class (constructor function) that will be instantiated. * @returns {Object} registered provider instance + * + * @example + * Here is an example of registering a service using + * {@link auto.$provide#service $provide.service(class)}. + * ```js + * var Ping = function($http) { + * this.$http = $http; + * }; + * + * Ping.$inject = ['$http']; + * + * Ping.prototype.send = function() { + * return this.$http.get('/ping'); + * }; + * $provide.service('ping', Ping); + * ``` + * You would then inject and use this service like this: + * ```js + * someModule.controller('Ctrl', ['ping', function(ping) { + * ping.send(); + * }]); + * ``` */ /** * @ngdoc method - * @name AUTO.$provide#value - * @methodOf AUTO.$provide + * @name $provide#value * @description * - * A short hand for configuring services if the `$get` method is a constant. + * Register a **value service** with the {@link auto.$injector $injector}, such as a string, a + * number, an array, an object or a function. This is short for registering a service where its + * provider's `$get` property is a factory function that takes no arguments and returns the **value + * service**. + * + * Value services are similar to constant services, except that they cannot be injected into a + * module configuration function (see {@link angular.Module#config}) but they can be overridden by + * an Angular + * {@link auto.$provide#decorator decorator}. * * @param {string} name The name of the instance. * @param {*} value The value. * @returns {Object} registered provider instance + * + * @example + * Here are some examples of creating value services. + * ```js + * $provide.value('ADMIN_USER', 'admin'); + * + * $provide.value('RoleLookup', { admin: 0, writer: 1, reader: 2 }); + * + * $provide.value('halfOf', function(value) { + * return value / 2; + * }); + * ``` */ /** * @ngdoc method - * @name AUTO.$provide#constant - * @methodOf AUTO.$provide + * @name $provide#constant * @description * - * A constant value, but unlike {@link AUTO.$provide#value value} it can be injected - * into configuration function (other modules) and it is not interceptable by - * {@link AUTO.$provide#decorator decorator}. + * Register a **constant service**, such as a string, a number, an array, an object or a function, + * with the {@link auto.$injector $injector}. Unlike {@link auto.$provide#value value} it can be + * injected into a module configuration function (see {@link angular.Module#config}) and it cannot + * be overridden by an Angular {@link auto.$provide#decorator decorator}. * * @param {string} name The name of the constant. * @param {*} value The constant value. * @returns {Object} registered instance + * + * @example + * Here a some examples of creating constants: + * ```js + * $provide.constant('SHARD_HEIGHT', 306); + * + * $provide.constant('MY_COLOURS', ['red', 'blue', 'grey']); + * + * $provide.constant('double', function(value) { + * return value * 2; + * }); + * ``` */ /** * @ngdoc method - * @name AUTO.$provide#decorator - * @methodOf AUTO.$provide + * @name $provide#decorator * @description * - * Decoration of service, allows the decorator to intercept the service instance creation. The - * returned instance may be the original instance, or a new instance which delegates to the - * original instance. + * Register a **service decorator** with the {@link auto.$injector $injector}. A service decorator + * intercepts the creation of a service, allowing it to override or modify the behaviour of the + * service. The object returned by the decorator may be the original service, or a new service + * object which replaces or wraps and delegates to the original service. * * @param {string} name The name of the service to decorate. * @param {function()} decorator This function will be invoked when the service needs to be - * instantiated. The function is called using the {@link AUTO.$injector#invoke - * injector.invoke} method and is therefore fully injectable. Local injection arguments: + * instantiated and should return the decorated service instance. The function is called using + * the {@link auto.$injector#invoke injector.invoke} method and is therefore fully injectable. + * Local injection arguments: * * * `$delegate` - The original service instance, which can be monkey patched, configured, * decorated or delegated to. + * + * @example + * Here we decorate the {@link ng.$log $log} service to convert warnings to errors by intercepting + * calls to {@link ng.$log#error $log.warn()}. + * ```js + * $provide.decorator('$log', ['$delegate', function($delegate) { + * $delegate.warn = $delegate.error; + * return $delegate; + * }]); + * ``` */ @@ -2828,9 +3730,10 @@ function createInjector(modulesToLoad) { decorator: decorator } }, - providerInjector = createInternalInjector(providerCache, function() { - throw Error("Unknown provider: " + path.join(' <- ')); - }), + providerInjector = (providerCache.$injector = + createInternalInjector(providerCache, function() { + throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- ')); + })), instanceCache = {}, instanceInjector = (instanceCache.$injector = createInternalInjector(instanceCache, function(servicename) { @@ -2854,15 +3757,16 @@ function createInjector(modulesToLoad) { } else { return delegate(key, value); } - } + }; } function provider(name, provider_) { + assertNotHasOwnProperty(name, 'service'); if (isFunction(provider_) || isArray(provider_)) { provider_ = providerInjector.instantiate(provider_); } if (!provider_.$get) { - throw Error('Provider ' + name + ' must define $get factory method.'); + throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name); } return providerCache[name + providerSuffix] = provider_; } @@ -2875,9 +3779,10 @@ function createInjector(modulesToLoad) { }]); } - function value(name, value) { return factory(name, valueFn(value)); } + function value(name, val) { return factory(name, valueFn(val)); } function constant(name, value) { + assertNotHasOwnProperty(name, 'constant'); providerCache[name] = value; instanceCache[name] = value; } @@ -2896,43 +3801,43 @@ function createInjector(modulesToLoad) { // Module Loading //////////////////////////////////// function loadModules(modulesToLoad){ - var runBlocks = []; + var runBlocks = [], moduleFn, invokeQueue, i, ii; forEach(modulesToLoad, function(module) { if (loadedModules.get(module)) return; loadedModules.put(module, true); - if (isString(module)) { - var moduleFn = angularModule(module); - runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); - try { - for(var invokeQueue = moduleFn._invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) { + try { + if (isString(module)) { + moduleFn = angularModule(module); + runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); + + for(invokeQueue = moduleFn._invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) { var invokeArgs = invokeQueue[i], - provider = invokeArgs[0] == '$injector' - ? providerInjector - : providerInjector.get(invokeArgs[0]); + provider = providerInjector.get(invokeArgs[0]); provider[invokeArgs[1]].apply(provider, invokeArgs[2]); } - } catch (e) { - if (e.message) e.message += ' from ' + module; - throw e; + } else if (isFunction(module)) { + runBlocks.push(providerInjector.invoke(module)); + } else if (isArray(module)) { + runBlocks.push(providerInjector.invoke(module)); + } else { + assertArgFn(module, 'module'); } - } else if (isFunction(module)) { - try { - runBlocks.push(providerInjector.invoke(module)); - } catch (e) { - if (e.message) e.message += ' from ' + module; - throw e; + } catch (e) { + if (isArray(module)) { + module = module[module.length - 1]; } - } else if (isArray(module)) { - try { - runBlocks.push(providerInjector.invoke(module)); - } catch (e) { - if (e.message) e.message += ' from ' + String(module[module.length - 1]); - throw e; + if (e.message && e.stack && e.stack.indexOf(e.message) == -1) { + // Safari & FF's stack traces don't contain error.message content + // unlike those of Chrome and IE + // So if stack doesn't contain message, we create a new string that contains both. + // Since error.stack is read-only in Safari, I'm overriding e and not e.stack here. + /* jshint -W022 */ + e = e.message + '\n' + e.stack; } - } else { - assertArgFn(module, 'module'); + throw $injectorMinErr('modulerr', "Failed to instantiate module {0} due to:\n{1}", + module, e.stack || e.message || e); } }); return runBlocks; @@ -2945,12 +3850,9 @@ function createInjector(modulesToLoad) { function createInternalInjector(cache, factory) { function getService(serviceName) { - if (typeof serviceName !== 'string') { - throw Error('Service name expected'); - } if (cache.hasOwnProperty(serviceName)) { if (cache[serviceName] === INSTANTIATING) { - throw Error('Circular dependency: ' + path.join(' <- ')); + throw $injectorMinErr('cdep', 'Circular dependency found: {0}', path.join(' <- ')); } return cache[serviceName]; } else { @@ -2958,6 +3860,11 @@ function createInjector(modulesToLoad) { path.unshift(serviceName); cache[serviceName] = INSTANTIATING; return cache[serviceName] = factory(serviceName); + } catch (err) { + if (cache[serviceName] === INSTANTIATING) { + delete cache[serviceName]; + } + throw err; } finally { path.shift(); } @@ -2972,6 +3879,10 @@ function createInjector(modulesToLoad) { for(i = 0, length = $inject.length; i < length; i++) { key = $inject[i]; + if (typeof key !== 'string') { + throw $injectorMinErr('itkn', + 'Incorrect injection token! Expected service name as string, got {0}', key); + } args.push( locals && locals.hasOwnProperty(key) ? locals[key] @@ -2983,22 +3894,9 @@ function createInjector(modulesToLoad) { fn = fn[length]; } - - // Performance optimization: http://jsperf.com/apply-vs-call-vs-invoke - switch (self ? -1 : args.length) { - case 0: return fn(); - case 1: return fn(args[0]); - case 2: return fn(args[0], args[1]); - case 3: return fn(args[0], args[1], args[2]); - case 4: return fn(args[0], args[1], args[2], args[3]); - case 5: return fn(args[0], args[1], args[2], args[3], args[4]); - case 6: return fn(args[0], args[1], args[2], args[3], args[4], args[5]); - case 7: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6]); - case 8: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]); - case 9: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]); - case 10: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]); - default: return fn.apply(self, args); - } + // http://jsperf.com/angularjs-invoke-apply-vs-switch + // #5388 + return fn.apply(self, args); } function instantiate(Type, locals) { @@ -3011,32 +3909,69 @@ function createInjector(modulesToLoad) { instance = new Constructor(); returnedValue = invoke(Type, instance, locals); - return isObject(returnedValue) ? returnedValue : instance; + return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance; } return { invoke: invoke, instantiate: instantiate, get: getService, - annotate: annotate + annotate: annotate, + has: function(name) { + return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name); + } }; } } /** - * @ngdoc function - * @name ng.$anchorScroll + * @ngdoc service + * @name $anchorScroll + * @kind function * @requires $window * @requires $location * @requires $rootScope * * @description - * When called, it checks current value of `$location.hash()` and scroll to related element, + * When called, it checks current value of `$location.hash()` and scrolls to the related element, * according to rules specified in - * {@link http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document Html5 spec}. + * [Html5 spec](http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document). * - * It also watches the `$location.hash()` and scroll whenever it changes to match any anchor. + * It also watches the `$location.hash()` and scrolls whenever it changes to match any anchor. * This can be disabled by calling `$anchorScrollProvider.disableAutoScrolling()`. + * + * @example + + +
+ Go to bottom + You're at the bottom! +
+
+ + function ScrollCtrl($scope, $location, $anchorScroll) { + $scope.gotoBottom = function (){ + // set the location.hash to the id of + // the element you wish to scroll to. + $location.hash('bottom'); + + // call $anchorScroll() + $anchorScroll(); + }; + } + + + #scrollArea { + height: 350px; + overflow: auto; + } + + #bottom { + display: block; + margin-top: 2000px; + } + +
*/ function $AnchorScrollProvider() { @@ -3090,10 +4025,266 @@ function $AnchorScrollProvider() { }]; } +var $animateMinErr = minErr('$animate'); + +/** + * @ngdoc provider + * @name $animateProvider + * + * @description + * Default implementation of $animate that doesn't perform any animations, instead just + * synchronously performs DOM + * updates and calls done() callbacks. + * + * In order to enable animations the ngAnimate module has to be loaded. + * + * To see the functional implementation check out src/ngAnimate/animate.js + */ +var $AnimateProvider = ['$provide', function($provide) { + + + this.$$selectors = {}; + + + /** + * @ngdoc method + * @name $animateProvider#register + * + * @description + * Registers a new injectable animation factory function. The factory function produces the + * animation object which contains callback functions for each event that is expected to be + * animated. + * + * * `eventFn`: `function(Element, doneFunction)` The element to animate, the `doneFunction` + * must be called once the element animation is complete. If a function is returned then the + * animation service will use this function to cancel the animation whenever a cancel event is + * triggered. + * + * + * ```js + * return { + * eventFn : function(element, done) { + * //code to run the animation + * //once complete, then run done() + * return function cancellationFunction() { + * //code to cancel the animation + * } + * } + * } + * ``` + * + * @param {string} name The name of the animation. + * @param {Function} factory The factory function that will be executed to return the animation + * object. + */ + this.register = function(name, factory) { + var key = name + '-animation'; + if (name && name.charAt(0) != '.') throw $animateMinErr('notcsel', + "Expecting class selector starting with '.' got '{0}'.", name); + this.$$selectors[name.substr(1)] = key; + $provide.factory(key, factory); + }; + + /** + * @ngdoc method + * @name $animateProvider#classNameFilter + * + * @description + * Sets and/or returns the CSS class regular expression that is checked when performing + * an animation. Upon bootstrap the classNameFilter value is not set at all and will + * therefore enable $animate to attempt to perform an animation on any element. + * When setting the classNameFilter value, animations will only be performed on elements + * that successfully match the filter expression. This in turn can boost performance + * for low-powered devices as well as applications containing a lot of structural operations. + * @param {RegExp=} expression The className expression which will be checked against all animations + * @return {RegExp} The current CSS className expression value. If null then there is no expression value + */ + this.classNameFilter = function(expression) { + if(arguments.length === 1) { + this.$$classNameFilter = (expression instanceof RegExp) ? expression : null; + } + return this.$$classNameFilter; + }; + + this.$get = ['$timeout', '$$asyncCallback', function($timeout, $$asyncCallback) { + + function async(fn) { + fn && $$asyncCallback(fn); + } + + /** + * + * @ngdoc service + * @name $animate + * @description The $animate service provides rudimentary DOM manipulation functions to + * insert, remove and move elements within the DOM, as well as adding and removing classes. + * This service is the core service used by the ngAnimate $animator service which provides + * high-level animation hooks for CSS and JavaScript. + * + * $animate is available in the AngularJS core, however, the ngAnimate module must be included + * to enable full out animation support. Otherwise, $animate will only perform simple DOM + * manipulation operations. + * + * To learn more about enabling animation support, click here to visit the {@link ngAnimate + * ngAnimate module page} as well as the {@link ngAnimate.$animate ngAnimate $animate service + * page}. + */ + return { + + /** + * + * @ngdoc method + * @name $animate#enter + * @kind function + * @description Inserts the element into the DOM either after the `after` element or within + * the `parent` element. Once complete, the done() callback will be fired (if provided). + * @param {DOMElement} element the element which will be inserted into the DOM + * @param {DOMElement} parent the parent element which will append the element as + * a child (if the after element is not present) + * @param {DOMElement} after the sibling element which will append the element + * after itself + * @param {Function=} done callback function that will be called after the element has been + * inserted into the DOM + */ + enter : function(element, parent, after, done) { + if (after) { + after.after(element); + } else { + if (!parent || !parent[0]) { + parent = after.parent(); + } + parent.append(element); + } + async(done); + }, + + /** + * + * @ngdoc method + * @name $animate#leave + * @kind function + * @description Removes the element from the DOM. Once complete, the done() callback will be + * fired (if provided). + * @param {DOMElement} element the element which will be removed from the DOM + * @param {Function=} done callback function that will be called after the element has been + * removed from the DOM + */ + leave : function(element, done) { + element.remove(); + async(done); + }, + + /** + * + * @ngdoc method + * @name $animate#move + * @kind function + * @description Moves the position of the provided element within the DOM to be placed + * either after the `after` element or inside of the `parent` element. Once complete, the + * done() callback will be fired (if provided). + * + * @param {DOMElement} element the element which will be moved around within the + * DOM + * @param {DOMElement} parent the parent element where the element will be + * inserted into (if the after element is not present) + * @param {DOMElement} after the sibling element where the element will be + * positioned next to + * @param {Function=} done the callback function (if provided) that will be fired after the + * element has been moved to its new position + */ + move : function(element, parent, after, done) { + // Do not remove element before insert. Removing will cause data associated with the + // element to be dropped. Insert will implicitly do the remove. + this.enter(element, parent, after, done); + }, + + /** + * + * @ngdoc method + * @name $animate#addClass + * @kind function + * @description Adds the provided className CSS class value to the provided element. Once + * complete, the done() callback will be fired (if provided). + * @param {DOMElement} element the element which will have the className value + * added to it + * @param {string} className the CSS class which will be added to the element + * @param {Function=} done the callback function (if provided) that will be fired after the + * className value has been added to the element + */ + addClass : function(element, className, done) { + className = isString(className) ? + className : + isArray(className) ? className.join(' ') : ''; + forEach(element, function (element) { + jqLiteAddClass(element, className); + }); + async(done); + }, + + /** + * + * @ngdoc method + * @name $animate#removeClass + * @kind function + * @description Removes the provided className CSS class value from the provided element. + * Once complete, the done() callback will be fired (if provided). + * @param {DOMElement} element the element which will have the className value + * removed from it + * @param {string} className the CSS class which will be removed from the element + * @param {Function=} done the callback function (if provided) that will be fired after the + * className value has been removed from the element + */ + removeClass : function(element, className, done) { + className = isString(className) ? + className : + isArray(className) ? className.join(' ') : ''; + forEach(element, function (element) { + jqLiteRemoveClass(element, className); + }); + async(done); + }, + + /** + * + * @ngdoc method + * @name $animate#setClass + * @kind function + * @description Adds and/or removes the given CSS classes to and from the element. + * Once complete, the done() callback will be fired (if provided). + * @param {DOMElement} element the element which will have its CSS classes changed + * removed from it + * @param {string} add the CSS classes which will be added to the element + * @param {string} remove the CSS class which will be removed from the element + * @param {Function=} done the callback function (if provided) that will be fired after the + * CSS classes have been set on the element + */ + setClass : function(element, add, remove, done) { + forEach(element, function (element) { + jqLiteAddClass(element, add); + jqLiteRemoveClass(element, remove); + }); + async(done); + }, + + enabled : noop + }; + }]; +}]; + +function $$AsyncCallbackProvider(){ + this.$get = ['$$rAF', '$timeout', function($$rAF, $timeout) { + return $$rAF.supported + ? function(fn) { return $$rAF(fn); } + : function(fn) { + return $timeout(fn, 0, false); + }; + }]; +} + /** * ! This is a private undocumented service ! * - * @name ng.$browser + * @name $browser * @requires $log * @description * This object has two goals: @@ -3177,8 +4368,7 @@ function Browser(window, document, $log, $sniffer) { pollTimeout; /** - * @name ng.$browser#addPollFn - * @methodOf ng.$browser + * @name $browser#addPollFn * * @param {function()} fn Poll function to add * @@ -3215,11 +4405,10 @@ function Browser(window, document, $log, $sniffer) { var lastBrowserUrl = location.href, baseElement = document.find('base'), - replacedUrl = null; + newLocation = null; /** - * @name ng.$browser#url - * @methodOf ng.$browser + * @name $browser#url * * @description * GETTER: @@ -3238,6 +4427,10 @@ function Browser(window, document, $log, $sniffer) { * @param {boolean=} replace Should new url replace current history record ? */ self.url = function(url, replace) { + // Android Browser BFCache causes location, history reference to become stale. + if (location !== window.location) location = window.location; + if (history !== window.history) history = window.history; + // setter if (url) { if (lastBrowserUrl == url) return; @@ -3250,21 +4443,20 @@ function Browser(window, document, $log, $sniffer) { baseElement.attr('href', baseElement.attr('href')); } } else { + newLocation = url; if (replace) { location.replace(url); - replacedUrl = url; } else { location.href = url; - replacedUrl = null; } } return self; // getter } else { - // - the replacedUrl is a workaround for an IE8-9 issue with location.replace method that doesn't update - // location.href synchronously + // - newLocation is a workaround for an IE7-9 issue with location.replace and location.href + // methods not updating location.href synchronously. // - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172 - return replacedUrl || location.href.replace(/%27/g,"'"); + return newLocation || location.href.replace(/%27/g,"'"); } }; @@ -3272,6 +4464,7 @@ function Browser(window, document, $log, $sniffer) { urlChangeInit = false; function fireUrlChange() { + newLocation = null; if (lastBrowserUrl == self.url()) return; lastBrowserUrl = self.url(); @@ -3281,14 +4474,12 @@ function Browser(window, document, $log, $sniffer) { } /** - * @name ng.$browser#onUrlChange - * @methodOf ng.$browser - * @TODO(vojta): refactor to use node's syntax for events + * @name $browser#onUrlChange * * @description * Register callback function that will be called, when url changes. * - * It's only called when the url is changed by outside of angular: + * It's only called when the url is changed from outside of angular: * - user types different url into address bar * - user clicks on history (forward/back) button * - user clicks on a link @@ -3304,15 +4495,16 @@ function Browser(window, document, $log, $sniffer) { * @return {function(string)} Returns the registered listener fn - handy if the fn is anonymous. */ self.onUrlChange = function(callback) { + // TODO(vojta): refactor to use node's syntax for events 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); + if ($sniffer.history) jqLite(window).on('popstate', fireUrlChange); // hashchange event - if ($sniffer.hashchange) jqLite(window).bind('hashchange', fireUrlChange); + if ($sniffer.hashchange) jqLite(window).on('hashchange', fireUrlChange); // polling else self.addPollFn(fireUrlChange); @@ -3328,14 +4520,17 @@ function Browser(window, document, $log, $sniffer) { ////////////////////////////////////////////////////////////// /** + * @name $browser#baseHref + * + * @description * Returns current * (always relative - without domain) * - * @returns {string=} + * @returns {string} The current base href */ self.baseHref = function() { var href = baseElement.attr('href'); - return href ? href.replace(/^https?\:\/\/[^\/]*/, '') : ''; + return href ? href.replace(/^(https?\:)?\/\/[^\/]*/, '') : ''; }; ////////////////////////////////////////////////////////////// @@ -3346,41 +4541,45 @@ function Browser(window, document, $log, $sniffer) { var cookiePath = self.baseHref(); /** - * @name ng.$browser#cookies - * @methodOf ng.$browser + * @name $browser#cookies * * @param {string=} name Cookie name - * @param {string=} value Cokkie value + * @param {string=} value Cookie 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)
  • - *
+ * + * - 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) { + /* global escape: false, unescape: false */ var cookieLength, cookieArray, cookie, i, index; if (name) { if (value === undefined) { - rawDocument.cookie = escape(name) + "=;path=" + cookiePath + ";expires=Thu, 01 Jan 1970 00:00:00 GMT"; + rawDocument.cookie = escape(name) + "=;path=" + cookiePath + + ";expires=Thu, 01 Jan 1970 00:00:00 GMT"; } else { if (isString(value)) { - cookieLength = (rawDocument.cookie = escape(name) + '=' + escape(value) + ';path=' + cookiePath).length + 1; + cookieLength = (rawDocument.cookie = escape(name) + '=' + escape(value) + + ';path=' + cookiePath).length + 1; // per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum: // - 300 cookies // - 20 cookies per unique domain // - 4096 bytes per cookie if (cookieLength > 4096) { - $log.warn("Cookie '"+ name +"' possibly not set or overflowed because it was too large ("+ + $log.warn("Cookie '"+ name + + "' possibly not set or overflowed because it was too large ("+ cookieLength + " > 4096 bytes)!"); } } @@ -3395,7 +4594,7 @@ function Browser(window, document, $log, $sniffer) { cookie = cookieArray[i]; index = cookie.indexOf('='); if (index > 0) { //ignore nameless cookies - var name = unescape(cookie.substring(0, index)); + name = unescape(cookie.substring(0, index)); // the first value that is seen for a cookie is the most // specific one. values for the same cookie name that // follow are for less specific paths. @@ -3411,14 +4610,13 @@ function Browser(window, document, $log, $sniffer) { /** - * @name ng.$browser#defer - * @methodOf ng.$browser - * @param {function()} fn A function, who's execution should be defered. + * @name $browser#defer + * @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 `$browser.defer.cancel()`. * * @description - * Executes a fn asynchroniously via `setTimeout(fn, delay)`. + * Executes a fn asynchronously 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 @@ -3438,14 +4636,14 @@ function Browser(window, document, $log, $sniffer) { /** - * @name ng.$browser#defer.cancel - * @methodOf ng.$browser.defer + * @name $browser#defer.cancel * * @description - * Cancels a defered task identified with `deferId`. + * Cancels a deferred 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. + * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully + * canceled. */ self.defer.cancel = function(deferId) { if (pendingDeferIds[deferId]) { @@ -3467,24 +4665,26 @@ function $BrowserProvider(){ } /** - * @ngdoc object - * @name ng.$cacheFactory + * @ngdoc service + * @name $cacheFactory * * @description - * Factory that constructs cache objects and gives access to them. - * - *
- * 
+ * Factory that constructs {@link $cacheFactory.Cache Cache} objects and gives access to
+ * them.
+ *
+ * ```js
+ *
  *  var cache = $cacheFactory('cacheId');
  *  expect($cacheFactory.get('cacheId')).toBe(cache);
  *  expect($cacheFactory.get('noSuchCacheId')).not.toBeDefined();
  *
  *  cache.put("key", "value");
  *  cache.put("another key", "another value");
- * 
- *  expect(cache.info()).toEqual({id: 'cacheId', size: 2}); // Since we've specified no options on creation
- * 
- * 
+ * + * // We've specified no options on creation + * expect(cache.info()).toEqual({id: 'cacheId', size: 2}); + * + * ``` * * * @param {string} cacheId Name or id of the newly created cache. @@ -3495,12 +4695,53 @@ function $BrowserProvider(){ * @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. + * - `{{*}}` `put({string} key, {*} value)` — Puts a new key-value pair into the cache and returns + * it. * - `{{*}}` `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. * + * @example + + +
+ + + + +

Cached Values

+
+ + : + +
+ +

Cache Info

+
+ + : + +
+
+
+ + angular.module('cacheExampleApp', []). + controller('CacheController', ['$scope', '$cacheFactory', function($scope, $cacheFactory) { + $scope.keys = []; + $scope.cache = $cacheFactory('cacheId'); + $scope.put = function(key, value) { + $scope.cache.put(key, value); + $scope.keys.push(key); + }; + }]); + + + p { + margin: 10px 0 3px; + } + +
*/ function $CacheFactoryProvider() { @@ -3509,7 +4750,7 @@ function $CacheFactoryProvider() { function cacheFactory(cacheId, options) { if (cacheId in caches) { - throw Error('cacheId ' + cacheId + ' taken'); + throw minErr('$cacheFactory')('iid', "CacheId '{0}' is already taken!", cacheId); } var size = 0, @@ -3520,12 +4761,71 @@ function $CacheFactoryProvider() { freshEnd = null, staleEnd = null; + /** + * @ngdoc type + * @name $cacheFactory.Cache + * + * @description + * A cache object used to store and retrieve data, primarily used by + * {@link $http $http} and the {@link ng.directive:script script} directive to cache + * templates and other data. + * + * ```js + * angular.module('superCache') + * .factory('superCache', ['$cacheFactory', function($cacheFactory) { + * return $cacheFactory('super-cache'); + * }]); + * ``` + * + * Example test: + * + * ```js + * it('should behave like a cache', inject(function(superCache) { + * superCache.put('key', 'value'); + * superCache.put('another key', 'another value'); + * + * expect(superCache.info()).toEqual({ + * id: 'super-cache', + * size: 2 + * }); + * + * superCache.remove('another key'); + * expect(superCache.get('another key')).toBeUndefined(); + * + * superCache.removeAll(); + * expect(superCache.info()).toEqual({ + * id: 'super-cache', + * size: 0 + * }); + * })); + * ``` + */ return caches[cacheId] = { + /** + * @ngdoc method + * @name $cacheFactory.Cache#put + * @kind function + * + * @description + * Inserts a named entry into the {@link $cacheFactory.Cache Cache} object to be + * retrieved later, and incrementing the size of the cache if the key was not already + * present in the cache. If behaving like an LRU cache, it will also remove stale + * entries from the set. + * + * It will not insert undefined values into the cache. + * + * @param {string} key the key under which the cached data is stored. + * @param {*} value the value to store alongside the key. If it is undefined, the key + * will not be stored. + * @returns {*} the value stored. + */ put: function(key, value) { - var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); + if (capacity < Number.MAX_VALUE) { + var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); - refresh(lruEntry); + refresh(lruEntry); + } if (isUndefined(value)) return; if (!(key in data)) size++; @@ -3534,35 +4834,70 @@ function $CacheFactoryProvider() { if (size > capacity) { this.remove(staleEnd.key); } - }, + return value; + }, + /** + * @ngdoc method + * @name $cacheFactory.Cache#get + * @kind function + * + * @description + * Retrieves named data stored in the {@link $cacheFactory.Cache Cache} object. + * + * @param {string} key the key of the data to be retrieved + * @returns {*} the value stored. + */ get: function(key) { - var lruEntry = lruHash[key]; + if (capacity < Number.MAX_VALUE) { + var lruEntry = lruHash[key]; - if (!lruEntry) return; + if (!lruEntry) return; - refresh(lruEntry); + refresh(lruEntry); + } return data[key]; }, + /** + * @ngdoc method + * @name $cacheFactory.Cache#remove + * @kind function + * + * @description + * Removes an entry from the {@link $cacheFactory.Cache Cache} object. + * + * @param {string} key the key of the entry to be removed + */ remove: function(key) { - var lruEntry = lruHash[key]; + if (capacity < Number.MAX_VALUE) { + var lruEntry = lruHash[key]; + + if (!lruEntry) return; - if (!lruEntry) return; + if (lruEntry == freshEnd) freshEnd = lruEntry.p; + if (lruEntry == staleEnd) staleEnd = lruEntry.n; + link(lruEntry.n,lruEntry.p); - if (lruEntry == freshEnd) freshEnd = lruEntry.p; - if (lruEntry == staleEnd) staleEnd = lruEntry.n; - link(lruEntry.n,lruEntry.p); + delete lruHash[key]; + } - delete lruHash[key]; delete data[key]; size--; }, + /** + * @ngdoc method + * @name $cacheFactory.Cache#removeAll + * @kind function + * + * @description + * Clears the cache object of any entries. + */ removeAll: function() { data = {}; size = 0; @@ -3571,7 +4906,16 @@ function $CacheFactoryProvider() { }, - destroy: function() { + /** + * @ngdoc method + * @name $cacheFactory.Cache#destroy + * @kind function + * + * @description + * Destroys the {@link $cacheFactory.Cache Cache} object entirely, + * removing it from the {@link $cacheFactory $cacheFactory} set. + */ + destroy: function() { data = null; stats = null; lruHash = null; @@ -3579,6 +4923,22 @@ function $CacheFactoryProvider() { }, + /** + * @ngdoc method + * @name $cacheFactory.Cache#info + * @kind function + * + * @description + * Retrieve information regarding a particular {@link $cacheFactory.Cache Cache}. + * + * @returns {object} an object with the following properties: + *
    + *
  • **id**: the id of the cache instance
  • + *
  • **size**: the number of entries kept in the cache instance
  • + *
  • **...**: any additional properties from the options object when creating the + * cache.
  • + *
+ */ info: function() { return extend({}, stats, {size: size}); } @@ -3618,11 +4978,10 @@ function $CacheFactoryProvider() { /** * @ngdoc method - * @name ng.$cacheFactory#info - * @methodOf ng.$cacheFactory + * @name $cacheFactory#info * * @description - * Get information about all the of the caches that have been created + * Get information about all the caches that have been created * * @returns {Object} - key-value map of `cacheId` to the result of calling `cache#info` */ @@ -3637,8 +4996,7 @@ function $CacheFactoryProvider() { /** * @ngdoc method - * @name ng.$cacheFactory#get - * @methodOf ng.$cacheFactory + * @name $cacheFactory#get * * @description * Get access to a cache object by the `cacheId` used when it was created. @@ -3656,48 +5014,44 @@ function $CacheFactoryProvider() { } /** - * @ngdoc object - * @name ng.$templateCache + * @ngdoc service + * @name $templateCache * * @description - * The first time a template is used, it is loaded in the template cache for quick retrieval. You can - * load templates directly into the cache in a `script` tag, or by consuming the `$templateCache` - * service directly. - * + * The first time a template is used, it is loaded in the template cache for quick retrieval. You + * can load templates directly into the cache in a `script` tag, or by consuming the + * `$templateCache` service directly. + * * Adding via the `script` tag: - *
- * 
- * 
- * 
- * 
- *   ...
- * 
- * 
- * - * **Note:** the `script` tag containing the template does not need to be included in the `head` of the document, but - * it must be below the `ng-app` definition. - * + * + * ```html + * + * ``` + * + * **Note:** the `script` tag containing the template does not need to be included in the `head` of + * the document, but it must be below the `ng-app` definition. + * * Adding via the $templateCache service: - * - *
+ *
+ * ```js
  * var myApp = angular.module('myApp', []);
  * myApp.run(function($templateCache) {
  *   $templateCache.put('templateId.html', 'This is the content of the template');
  * });
- * 
- * + * ``` + * * To retrieve the template later, simply use it in your HTML: - *
+ * ```html
  * 
- *
- * + * ``` + * * or get it via Javascript: - *
+ * ```js
  * $templateCache.get('templateId.html')
- * 
- * + * ``` + * * See {@link ng.$cacheFactory $cacheFactory}. * */ @@ -3725,31 +5079,380 @@ function $TemplateCacheProvider() { */ -var NON_ASSIGNABLE_MODEL_EXPRESSION = 'Non-assignable model expression: '; - - /** - * @ngdoc function - * @name ng.$compile - * @function + * @ngdoc service + * @name $compile + * @kind 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 ng.$rootScope.Scope scope} and the template together. + * Compiles an HTML string or DOM into a template and produces a template function, which + * can then be used to link {@link ng.$rootScope.Scope `scope`} and the template together. + * + * The compilation is a process of walking the DOM tree and matching DOM elements to + * {@link ng.$compileProvider#directive directives}. + * + *
+ * **Note:** This document is an in-depth reference of all directive options. + * For a gentle introduction to directives with examples of common use cases, + * see the {@link guide/directive directive guide}. + *
+ * + * ## Comprehensive Directive API + * + * There are many different options for a directive. + * + * The difference resides in the return value of the factory function. + * You can either return a "Directive Definition Object" (see below) that defines the directive properties, + * or just the `postLink` function (all other properties will have the default values). + * + *
+ * **Best Practice:** It's recommended to use the "directive definition object" form. + *
+ * + * Here's an example directive declared with a Directive Definition Object: + * + * ```js + * var myModule = angular.module(...); + * + * myModule.directive('directiveName', function factory(injectables) { + * var directiveDefinitionObject = { + * priority: 0, + * template: '
', // or // function(tElement, tAttrs) { ... }, + * // or + * // templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... }, + * transclude: false, + * restrict: 'A', + * scope: false, + * controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... }, + * controllerAs: 'stringAlias', + * require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'], + * compile: function compile(tElement, tAttrs, transclude) { + * return { + * pre: function preLink(scope, iElement, iAttrs, controller) { ... }, + * post: function postLink(scope, iElement, iAttrs, controller) { ... } + * } + * // or + * // return function postLink( ... ) { ... } + * }, + * // or + * // link: { + * // pre: function preLink(scope, iElement, iAttrs, controller) { ... }, + * // post: function postLink(scope, iElement, iAttrs, controller) { ... } + * // } + * // or + * // link: function postLink( ... ) { ... } + * }; + * return directiveDefinitionObject; + * }); + * ``` + * + *
+ * **Note:** Any unspecified options will use the default value. You can see the default values below. + *
+ * + * Therefore the above can be simplified as: + * + * ```js + * var myModule = angular.module(...); + * + * myModule.directive('directiveName', function factory(injectables) { + * var directiveDefinitionObject = { + * link: function postLink(scope, iElement, iAttrs) { ... } + * }; + * return directiveDefinitionObject; + * // or + * // return function postLink(scope, iElement, iAttrs) { ... } + * }); + * ``` + * + * + * + * ### Directive Definition Object + * + * The directive definition object provides instructions to the {@link ng.$compile + * compiler}. The attributes are: + * + * #### `priority` + * When there are multiple directives defined on a single DOM element, sometimes it + * is necessary to specify the order in which the directives are applied. The `priority` is used + * to sort the directives before their `compile` functions get called. Priority is defined as a + * number. Directives with greater numerical `priority` are compiled first. Pre-link functions + * are also run in priority order, but post-link functions are run in reverse order. The order + * of directives with the same priority is undefined. The default priority is `0`. + * + * #### `terminal` + * If set to true then the current `priority` will be the last set of directives + * which will execute (any directives at the current priority will still execute + * as the order of execution on same `priority` is undefined). + * + * #### `scope` + * **If set to `true`,** then a new scope will be created for this directive. If multiple directives on the + * same element request a new scope, only one new scope is created. The new scope rule does not + * apply for the root of the template since the root of the template always gets a new scope. + * + * **If set to `{}` (object hash),** then a new "isolate" scope is created. The 'isolate' scope differs from + * normal scope in that it does not prototypically inherit from the parent scope. This is useful + * when creating reusable components, which should not accidentally read or modify data in the + * parent scope. + * + * The 'isolate' scope takes an object hash which defines a set of local scope properties + * derived from the parent scope. These local properties are useful for aliasing values for + * templates. Locals definition is a hash of local scope property to its source: + * + * * `@` or `@attr` - bind a local scope property to the value of DOM attribute. The result is + * always a string since DOM attributes are strings. If no `attr` name is specified then the + * attribute name is assumed to be the same as the local name. + * Given `` and widget definition + * of `scope: { localName:'@myAttr' }`, then widget scope property `localName` will reflect + * the interpolated value of `hello {{name}}`. As the `name` attribute changes so will the + * `localName` property on the widget scope. The `name` is read from the parent scope (not + * component scope). + * + * * `=` or `=attr` - set up bi-directional binding between a local scope property and the + * parent scope property of name defined via the value of the `attr` attribute. If no `attr` + * name is specified then the attribute name is assumed to be the same as the local name. + * Given `` and widget definition of + * `scope: { localModel:'=myAttr' }`, then widget scope property `localModel` will reflect the + * value of `parentModel` on the parent scope. Any changes to `parentModel` will be reflected + * in `localModel` and any changes in `localModel` will reflect in `parentModel`. If the parent + * scope property doesn't exist, it will throw a NON_ASSIGNABLE_MODEL_EXPRESSION exception. You + * can avoid this behavior using `=?` or `=?attr` in order to flag the property as optional. + * + * * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope. + * If no `attr` name is specified then the attribute name is assumed to be the same as the + * local name. Given `` and widget definition of + * `scope: { localFn:'&myAttr' }`, then isolate scope property `localFn` will point to + * a function wrapper for the `count = count + value` expression. Often it's desirable to + * pass data from the isolated scope via an expression and to the parent scope, this can be + * done by passing a map of local variable names and values into the expression wrapper fn. + * For example, if the expression is `increment(amount)` then we can specify the amount value + * by calling the `localFn` as `localFn({amount: 22})`. + * + * + * + * #### `controller` + * Controller constructor function. The controller is instantiated before the + * pre-linking phase and it is shared with other directives (see + * `require` attribute). This allows the directives to communicate with each other and augment + * each other's behavior. The controller is injectable (and supports bracket notation) with the following locals: + * + * * `$scope` - Current scope associated with the element + * * `$element` - Current element + * * `$attrs` - Current attributes object for the element + * * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope. + * The scope can be overridden by an optional first argument. + * `function([scope], cloneLinkingFn)`. + * + * + * #### `require` + * Require another directive and inject its controller as the fourth argument to the linking function. The + * `require` takes a string name (or array of strings) of the directive(s) to pass in. If an array is used, the + * injected argument will be an array in corresponding order. If no such directive can be + * found, or if the directive does not have a controller, then an error is raised. The name can be prefixed with: + * + * * (no prefix) - Locate the required controller on the current element. Throw an error if not found. + * * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found. + * * `^` - Locate the required controller by searching the element's parents. Throw an error if not found. + * * `?^` - Attempt to locate the required controller by searching the element's parents or pass `null` to the + * `link` fn if not found. + * + * + * #### `controllerAs` + * Controller alias at the directive scope. An alias for the controller so it + * can be referenced at the directive template. The directive needs to define a scope for this + * configuration to be used. Useful in the case when directive is used as component. + * + * + * #### `restrict` + * String of subset of `EACM` which restricts the directive to a specific directive + * declaration style. If omitted, the default (attributes only) is used. + * + * * `E` - Element name: `` + * * `A` - Attribute (default): `
` + * * `C` - Class: `
` + * * `M` - Comment: `` + * + * + * #### `template` + * replace the current element with the contents of the HTML. The replacement process + * migrates all of the attributes / classes from the old element to the new one. See the + * {@link guide/directive#creating-custom-directives_creating-directives_template-expanding-directive + * Directives Guide} for an example. * - * The compilation is a process of walking the DOM tree and trying to match DOM elements to - * {@link 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. + * You can specify `template` as a string representing the template or as a function which takes + * two arguments `tElement` and `tAttrs` (described in the `compile` function api below) and + * returns a string value representing the template. * - * The template function can then be used once to produce the view or as it is the case with - * {@link ng.directive:ngRepeat repeater} many-times, in which - * case each call results in a view that is a DOM clone of the original template. * - - + * #### `templateUrl` + * Same as `template` but the template is loaded from the specified URL. Because + * the template loading is asynchronous the compilation/linking is suspended until the template + * is loaded. + * + * You can specify `templateUrl` as a string representing the URL or as a function which takes two + * arguments `tElement` and `tAttrs` (described in the `compile` function api below) and returns + * a string value representing the url. In either case, the template URL is passed through {@link + * api/ng.$sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}. + * + * + * #### `replace` ([*DEPRECATED*!], will be removed in next major release) + * specify where the template should be inserted. Defaults to `false`. + * + * * `true` - the template will replace the current element. + * * `false` - the template will replace the contents of the current element. + * + * + * #### `transclude` + * compile the content of the element and make it available to the directive. + * Typically used with {@link ng.directive:ngTransclude + * ngTransclude}. The advantage of transclusion is that the linking function receives a + * transclusion function which is pre-bound to the correct scope. In a typical setup the widget + * creates an `isolate` scope, but the transclusion is not a child, but a sibling of the `isolate` + * scope. This makes it possible for the widget to have private state, and the transclusion to + * be bound to the parent (pre-`isolate`) scope. + * + * * `true` - transclude the content of the directive. + * * `'element'` - transclude the whole element including any directives defined at lower priority. + * + * + * #### `compile` + * + * ```js + * function compile(tElement, tAttrs, transclude) { ... } + * ``` + * + * The compile function deals with transforming the template DOM. Since most directives do not do + * template transformation, it is not used often. The compile function takes the following arguments: + * + * * `tElement` - template element - The element where the directive has been declared. It is + * safe to do template transformation on the element and child elements only. + * + * * `tAttrs` - template attributes - Normalized list of attributes declared on this element shared + * between all directive compile functions. + * + * * `transclude` - [*DEPRECATED*!] A transclude linking function: `function(scope, cloneLinkingFn)` + * + *
+ * **Note:** The template instance and the link instance may be different objects if the template has + * been cloned. For this reason it is **not** safe to do anything other than DOM transformations that + * apply to all cloned DOM nodes within the compile function. Specifically, DOM listener registration + * should be done in a linking function rather than in a compile function. + *
+ + *
+ * **Note:** The compile function cannot handle directives that recursively use themselves in their + * own templates or compile functions. Compiling these directives results in an infinite loop and a + * stack overflow errors. + * + * This can be avoided by manually using $compile in the postLink function to imperatively compile + * a directive's template instead of relying on automatic template compilation via `template` or + * `templateUrl` declaration or manual compilation inside the compile function. + *
+ * + *
+ * **Note:** The `transclude` function that is passed to the compile function is deprecated, as it + * e.g. does not know about the right outer scope. Please use the transclude function that is passed + * to the link function instead. + *
+ + * A compile function can have a return value which can be either a function or an object. + * + * * returning a (post-link) function - is equivalent to registering the linking function via the + * `link` property of the config object when the compile function is empty. + * + * * returning an object with function(s) registered via `pre` and `post` properties - allows you to + * control when a linking function should be called during the linking phase. See info about + * pre-linking and post-linking functions below. + * + * + * #### `link` + * This property is used only if the `compile` property is not defined. + * + * ```js + * function link(scope, iElement, iAttrs, controller, transcludeFn) { ... } + * ``` + * + * The link function is responsible for registering DOM listeners as well as updating the DOM. It is + * executed after the template has been cloned. This is where most of the directive logic will be + * put. + * + * * `scope` - {@link ng.$rootScope.Scope Scope} - The scope to be used by the + * directive for registering {@link ng.$rootScope.Scope#$watch watches}. + * + * * `iElement` - instance element - The element where the directive is to be used. It is safe to + * manipulate the children of the element only in `postLink` function since the children have + * already been linked. + * + * * `iAttrs` - instance attributes - Normalized list of attributes declared on this element shared + * between all directive linking functions. + * + * * `controller` - a controller instance - A controller instance if at least one directive on the + * element defines a controller. The controller is shared among all the directives, which allows + * the directives to use the controllers as a communication channel. + * + * * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope. + * The scope can be overridden by an optional first argument. This is the same as the `$transclude` + * parameter of directive controllers. + * `function([scope], cloneLinkingFn)`. + * + * + * #### Pre-linking function + * + * Executed before the child elements are linked. Not safe to do DOM transformation since the + * compiler linking function will fail to locate the correct elements for linking. + * + * #### Post-linking function + * + * Executed after the child elements are linked. It is safe to do DOM transformation in the post-linking function. + * + * + * ### Attributes + * + * The {@link ng.$compile.directive.Attributes Attributes} object - passed as a parameter in the + * `link()` or `compile()` functions. It has a variety of uses. + * + * accessing *Normalized attribute names:* + * Directives like 'ngBind' can be expressed in many ways: 'ng:bind', `data-ng-bind`, or 'x-ng-bind'. + * the attributes object allows for normalized access to + * the attributes. + * + * * *Directive inter-communication:* All directives share the same instance of the attributes + * object which allows the directives to use the attributes object as inter directive + * communication. + * + * * *Supports interpolation:* Interpolation attributes are assigned to the attribute object + * allowing other directives to read the interpolated value. + * + * * *Observing interpolated attributes:* Use `$observe` to observe the value changes of attributes + * that contain interpolation (e.g. `src="{{bar}}"`). Not only is this very efficient but it's also + * the only way to easily get the actual value because during the linking phase the interpolation + * hasn't been evaluated yet and so the value is at this time set to `undefined`. + * + * ```js + * function linkingFn(scope, elm, attrs, ctrl) { + * // get the attribute value + * console.log(attrs.ngModel); + * + * // change the attribute + * attrs.$set('ngModel', 'new value'); + * + * // observe changes to interpolated attribute + * attrs.$observe('ngModel', function(value) { + * console.log('ngModel has changed value to ' + value); + * }); + * } + * ``` + * + * Below is an example using `$compileProvider`. + * + *
+ * **Note**: Typically directives are registered with `module.directive`. The example below is + * to illustrate how `$compile` works. + *
+ * + + +
+ //demo.label// +
+
+ + it('should interpolate binding with custom symbols', function() { + expect(element(by.binding('demo.label')).getText()).toBe('This binding is brought you by // interpolation symbols.'); + }); + +
+ */ +function $InterpolateProvider() { + var startSymbol = '{{'; + var endSymbol = '}}'; + + /** + * @ngdoc method + * @name $interpolateProvider#startSymbol + * @description + * Symbol to denote start of expression in the interpolated string. Defaults to `{{`. + * + * @param {string=} value new value to set the starting symbol to. + * @returns {string|self} Returns the symbol when used as getter and self if used as setter. + */ + this.startSymbol = function(value){ + if (value) { + startSymbol = value; + return this; + } else { + return startSymbol; + } + }; + + /** + * @ngdoc method + * @name $interpolateProvider#endSymbol + * @description + * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`. + * + * @param {string=} value new value to set the ending symbol to. + * @returns {string|self} Returns the symbol when used as getter and self if used as setter. + */ + this.endSymbol = function(value){ + if (value) { + endSymbol = value; + return this; + } else { + return endSymbol; + } + }; + + + this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) { + var startSymbolLength = startSymbol.length, + endSymbolLength = endSymbol.length; + + /** + * @ngdoc service + * @name $interpolate + * @kind function + * + * @requires $parse + * @requires $sce + * + * @description + * + * Compiles a string with markup into an interpolation function. This service is used by the + * HTML {@link ng.$compile $compile} service for data binding. See + * {@link ng.$interpolateProvider $interpolateProvider} for configuring the + * interpolation markup. * -
-         var $interpolate = ...; // injected
-         var exp = $interpolate('Hello {{name}}!');
-         expect(exp({name:'Angular'}).toEqual('Hello Angular!');
-       
+ * + * ```js + * var $interpolate = ...; // injected + * var exp = $interpolate('Hello {{name | uppercase}}!'); + * expect(exp({name:'Angular'}).toEqual('Hello ANGULAR!'); + * ``` * * * @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: + * @param {string=} trustedContext when provided, the returned function passes the interpolated + * result through {@link ng.$sce#getTrusted $sce.getTrusted(interpolatedResult, + * trustedContext)} before returning it. Refer to the {@link ng.$sce $sce} service that + * provides Strict Contextual Escaping for details. + * @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. * */ - function $interpolate(text, mustHaveExpression) { + function $interpolate(text, mustHaveExpression, trustedContext) { var startIndex, endIndex, index = 0, @@ -5145,21 +8643,60 @@ function $InterpolateProvider() { length = 1; } + // Concatenating expressions makes it hard to reason about whether some combination of + // concatenated values are unsafe to use and could easily lead to XSS. By requiring that a + // single expression be used for iframe[src], object[src], etc., we ensure that the value + // that's used is assigned or constructed by some JS code somewhere that is more testable or + // make it obvious that you bound the value to some user controlled value. This helps reduce + // the load when auditing for XSS issues. + if (trustedContext && parts.length > 1) { + throw $interpolateMinErr('noconcat', + "Error while interpolating: {0}\nStrict Contextual Escaping disallows " + + "interpolations that concatenate multiple expressions when a trusted value is " + + "required. See http://docs.angularjs.org/api/ng.$sce", text); + } + if (!mustHaveExpression || hasInterpolation) { concat.length = length; fn = function(context) { - for(var i = 0, ii = length, part; i + * **Note**: Intervals created by this service must be explicitly destroyed when you are finished + * with them. In particular they are not automatically destroyed when a controller's scope or a + * directive's element are destroyed. + * You should take this into consideration and make sure to always cancel the interval at the + * appropriate moment. See the example below for more details on how and when to do this. + *
+ * + * @param {function()} fn A function that should be called repeatedly. + * @param {number} delay Number of milliseconds between each function call. + * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat + * indefinitely. + * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise + * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. + * @returns {promise} A promise which will be notified on each iteration. + * + * @example + * + * + * + * + *
+ *
+ * Date format:
+ * Current time is: + *
+ * Blood 1 : {{blood_1}} + * Blood 2 : {{blood_2}} + * + * + * + *
+ *
+ * + *
+ *
+ */ + function interval(fn, delay, count, invokeApply) { + var setInterval = $window.setInterval, + clearInterval = $window.clearInterval, + deferred = $q.defer(), + promise = deferred.promise, + iteration = 0, + skipApply = (isDefined(invokeApply) && !invokeApply); + + count = isDefined(count) ? count : 0; + + promise.then(null, null, fn); + + promise.$$intervalId = setInterval(function tick() { + deferred.notify(iteration++); + + if (count > 0 && iteration >= count) { + deferred.resolve(iteration); + clearInterval(promise.$$intervalId); + delete intervals[promise.$$intervalId]; + } + + if (!skipApply) $rootScope.$apply(); + + }, delay); + + intervals[promise.$$intervalId] = deferred; + + return promise; + } + + + /** + * @ngdoc method + * @name $interval#cancel + * + * @description + * Cancels a task associated with the `promise`. + * + * @param {promise} promise returned by the `$interval` function. + * @returns {boolean} Returns `true` if the task was successfully canceled. + */ + interval.cancel = function(promise) { + if (promise && promise.$$intervalId in intervals) { + intervals[promise.$$intervalId].reject('canceled'); + clearInterval(promise.$$intervalId); + delete intervals[promise.$$intervalId]; + return true; + } + return false; + }; + + return interval; + }]; +} + +/** + * @ngdoc service + * @name $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'; + } + }; + }; +} + +var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; +var $locationMinErr = minErr('$location'); /** @@ -5228,108 +9018,95 @@ function encodePath(path) { return segments.join('/'); } -function stripHash(url) { - return url.split('#')[0]; -} - +function parseAbsoluteUrl(absoluteUrl, locationObj, appBase) { + var parsedUrl = urlResolve(absoluteUrl, appBase); -function matchUrl(url, obj) { - var match = URL_MATCH.exec(url); + locationObj.$$protocol = parsedUrl.protocol; + locationObj.$$host = parsedUrl.hostname; + locationObj.$$port = int(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null; +} - 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; +function parseAppUrl(relativeUrl, locationObj, appBase) { + var prefixed = (relativeUrl.charAt(0) !== '/'); + if (prefixed) { + relativeUrl = '/' + relativeUrl; + } + var match = urlResolve(relativeUrl, appBase); + locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ? + match.pathname.substring(1) : match.pathname); + locationObj.$$search = parseKeyValue(match.search); + locationObj.$$hash = decodeURIComponent(match.hash); + + // make sure path starts with '/'; + if (locationObj.$$path && locationObj.$$path.charAt(0) != '/') { + locationObj.$$path = '/' + locationObj.$$path; } - - return match; } -function composeProtocolHostPort(protocol, host, port) { - return protocol + '://' + host + (port == DEFAULT_PORTS[protocol] ? '' : ':' + port); +/** + * + * @param {string} begin + * @param {string} whole + * @returns {string} returns text from whole after begin or undefined if it does not begin with + * expected string. + */ +function beginsWith(begin, whole) { + if (whole.indexOf(begin) === 0) { + return whole.substr(begin.length); + } } -function pathPrefixFromBase(basePath) { - return basePath.substr(0, basePath.lastIndexOf('/')); +function stripHash(url) { + var index = url.indexOf('#'); + return index == -1 ? url : url.substr(0, index); } -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 stripFile(url) { + return url.substr(0, stripHash(url).lastIndexOf('/') + 1); } - -function convertToHashbangUrl(url, basePath, hashPrefix) { - var match = matchUrl(url); - - // already hashbang url - if (decodeURIComponent(match.path) == basePath && !isUndefined(match.hash) && - match.hash.indexOf(hashPrefix) === 0) { - 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 Error('Invalid url "' + url + '", missing path prefix "' + pathPrefix + '" !'); - } - - return composeProtocolHostPort(match.protocol, match.host, match.port) + basePath + - '#' + hashPrefix + path + search + hash; - } +/* return the server only (scheme://host:port) */ +function serverBase(url) { + return url.substring(0, url.indexOf('/', url.indexOf('//') + 2)); } /** - * LocationUrl represents an url + * LocationHtml5Url 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 + * @param {string} appBase application base URL + * @param {string} basePrefix url path prefix */ -function LocationUrl(url, pathPrefix, appBaseUrl) { - pathPrefix = pathPrefix || ''; +function LocationHtml5Url(appBase, basePrefix) { + this.$$html5 = true; + basePrefix = basePrefix || ''; + var appBaseNoFile = stripFile(appBase); + parseAbsoluteUrl(appBase, this, appBase); + /** * Parse given html5 (regular) url string into properties * @param {string} newAbsoluteUrl HTML5 url * @private */ - this.$$parse = function(newAbsoluteUrl) { - var match = matchUrl(newAbsoluteUrl, this); - - if (match.path.indexOf(pathPrefix) !== 0) { - throw Error('Invalid url "' + newAbsoluteUrl + '", missing path prefix "' + pathPrefix + '" !'); + this.$$parse = function(url) { + var pathUrl = beginsWith(appBaseNoFile, url); + if (!isString(pathUrl)) { + throw $locationMinErr('ipthprfx', 'Invalid url "{0}", missing path prefix "{1}".', url, + appBaseNoFile); } - this.$$path = decodeURIComponent(match.path.substr(pathPrefix.length)); - this.$$search = parseKeyValue(match.search); - this.$$hash = match.hash && decodeURIComponent(match.hash) || ''; + parseAppUrl(pathUrl, this, appBase); + + if (!this.$$path) { + this.$$path = '/'; + } this.$$compose(); }; @@ -5343,32 +9120,42 @@ function LocationUrl(url, pathPrefix, appBaseUrl) { 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.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/' }; + this.$$rewrite = function(url) { + var appUrl, prevAppUrl; - this.$$rewriteAppUrl = function(absoluteLinkUrl) { - if(absoluteLinkUrl.indexOf(appBaseUrl) == 0) { - return absoluteLinkUrl; + if ( (appUrl = beginsWith(appBase, url)) !== undefined ) { + prevAppUrl = appUrl; + if ( (appUrl = beginsWith(basePrefix, appUrl)) !== undefined ) { + return appBaseNoFile + (beginsWith('/', appUrl) || appUrl); + } else { + return appBase + prevAppUrl; + } + } else if ( (appUrl = beginsWith(appBaseNoFile, url)) !== undefined ) { + return appBaseNoFile + appUrl; + } else if (appBaseNoFile == url + '/') { + return appBaseNoFile; } - } - - - this.$$parse(url); + }; } /** * LocationHashbangUrl represents url - * This object is exposed as $location service when html5 history api is disabled or not supported + * This object is exposed as $location service when developer doesn't opt into html5 mode. + * It also serves as the base class for html5 mode fallback on legacy browsers. * * @constructor - * @param {string} url Legacy url - * @param {string} hashPrefix Prefix for hash part (containing path and search) + * @param {string} appBase application base URL + * @param {string} hashPrefix hashbang prefix */ -function LocationHashbangUrl(url, hashPrefix, appBaseUrl) { - var basePath; +function LocationHashbangUrl(appBase, hashPrefix) { + var appBaseNoFile = stripFile(appBase); + + parseAbsoluteUrl(appBase, this, appBase); + /** * Parse given hashbang url into properties @@ -5376,25 +9163,56 @@ function LocationHashbangUrl(url, hashPrefix, appBaseUrl) { * @private */ this.$$parse = function(url) { - var match = matchUrl(url, this); + var withoutBaseUrl = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url); + var withoutHashUrl = withoutBaseUrl.charAt(0) == '#' + ? beginsWith(hashPrefix, withoutBaseUrl) + : (this.$$html5) + ? withoutBaseUrl + : ''; + + if (!isString(withoutHashUrl)) { + throw $locationMinErr('ihshprfx', 'Invalid url "{0}", missing hash prefix "{1}".', url, + hashPrefix); + } + parseAppUrl(withoutHashUrl, this, appBase); + this.$$path = removeWindowsDriveName(this.$$path, withoutHashUrl, appBase); - if (match.hash && match.hash.indexOf(hashPrefix) !== 0) { - throw Error('Invalid url "' + url + '", missing hash prefix "' + hashPrefix + '" !'); - } + this.$$compose(); - 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 = ''; - } + /* + * In Windows, on an anchor node on documents loaded from + * the filesystem, the browser will return a pathname + * prefixed with the drive name ('/C:/path') when a + * pathname without a drive is set: + * * a.setAttribute('href', '/foo') + * * a.pathname === '/C:/foo' //true + * + * Inside of Angular, we're always using pathnames that + * do not include drive names for routing. + */ + function removeWindowsDriveName (path, url, base) { + /* + Matches paths for file protocol on windows, + such as /C:/foo/bar, and captures only /foo/bar. + */ + var windowsFilePathExp = /^\/[A-Z]:(\/.*)/; - this.$$search = parseKeyValue(match[3]); - this.$$hash = match[5] && decodeURIComponent(match[5]) || ''; + var firstPathSegmentMatch; - this.$$compose(); + //Get the relative path from the input URL. + if (url.indexOf(base) === 0) { + url = url.replace(base, ''); + } + + // The input URL intentionally contains a first path segment that ends with a colon. + if (windowsFilePathExp.exec(url)) { + return path; + } + + firstPathSegmentMatch = windowsFilePathExp.exec(path); + return firstPathSegmentMatch ? firstPathSegmentMatch[1] : path; + } }; /** @@ -5406,22 +9224,65 @@ function LocationHashbangUrl(url, hashPrefix, appBaseUrl) { 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.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : ''); }; - this.$$rewriteAppUrl = function(absoluteLinkUrl) { - if(absoluteLinkUrl.indexOf(appBaseUrl) == 0) { - return absoluteLinkUrl; + this.$$rewrite = function(url) { + if(stripHash(appBase) == stripHash(url)) { + return url; } - } + }; +} + + +/** + * LocationHashbangUrl represents url + * This object is exposed as $location service when html5 history api is enabled but the browser + * does not support it. + * + * @constructor + * @param {string} appBase application base URL + * @param {string} hashPrefix hashbang prefix + */ +function LocationHashbangInHtml5Url(appBase, hashPrefix) { + this.$$html5 = true; + LocationHashbangUrl.apply(this, arguments); + + var appBaseNoFile = stripFile(appBase); + + this.$$rewrite = function(url) { + var appUrl; + + if ( appBase == stripHash(url) ) { + return url; + } else if ( (appUrl = beginsWith(appBaseNoFile, url)) ) { + return appBase + hashPrefix + appUrl; + } else if ( appBaseNoFile === url + '/') { + return appBaseNoFile; + } + }; + + this.$$compose = function() { + var search = toKeyValue(this.$$search), + hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; + this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; + // include hashPrefix in $$absUrl when $$url is empty so IE8 & 9 do not reload page because of removal of '#' + this.$$absUrl = appBase + hashPrefix + this.$$url; + }; - this.$$parse(url); } -LocationUrl.prototype = { +LocationHashbangInHtml5Url.prototype = + LocationHashbangUrl.prototype = + LocationHtml5Url.prototype = { + + /** + * Are we in html5 mode? + * @private + */ + $$html5: false, /** * Has any change been replacing ? @@ -5431,14 +9292,13 @@ LocationUrl.prototype = { /** * @ngdoc method - * @name ng.$location#absUrl - * @methodOf ng.$location + * @name $location#absUrl * * @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}. + * [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt). * * @return {string} full url */ @@ -5446,8 +9306,7 @@ LocationUrl.prototype = { /** * @ngdoc method - * @name ng.$location#url - * @methodOf ng.$location + * @name $location#url * * @description * This method is getter / setter. @@ -5457,6 +9316,7 @@ LocationUrl.prototype = { * 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`) + * @param {string=} replace The path that will be changed * @return {string} url */ url: function(url, replace) { @@ -5473,8 +9333,7 @@ LocationUrl.prototype = { /** * @ngdoc method - * @name ng.$location#protocol - * @methodOf ng.$location + * @name $location#protocol * * @description * This method is getter only. @@ -5487,8 +9346,7 @@ LocationUrl.prototype = { /** * @ngdoc method - * @name ng.$location#host - * @methodOf ng.$location + * @name $location#host * * @description * This method is getter only. @@ -5501,8 +9359,7 @@ LocationUrl.prototype = { /** * @ngdoc method - * @name ng.$location#port - * @methodOf ng.$location + * @name $location#port * * @description * This method is getter only. @@ -5515,8 +9372,7 @@ LocationUrl.prototype = { /** * @ngdoc method - * @name ng.$location#path - * @methodOf ng.$location + * @name $location#path * * @description * This method is getter / setter. @@ -5537,8 +9393,7 @@ LocationUrl.prototype = { /** * @ngdoc method - * @name ng.$location#search - * @methodOf ng.$location + * @name $location#search * * @description * This method is getter / setter. @@ -5547,24 +9402,58 @@ LocationUrl.prototype = { * * Change search part when called with parameter and return `$location`. * - * @param {string|object=} 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 + * ```js + * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * var searchObject = $location.search(); + * // => {foo: 'bar', baz: 'xoxo'} + * + * + * // set foo to 'yipee' + * $location.search('foo', 'yipee'); + * // => $location + * ``` + * + * @param {string|Object.|Object.>} search New search params - string or + * hash object. + * + * When called with a single argument the method acts as a setter, setting the `search` component + * of `$location` to the specified value. + * + * If the argument is a hash object containing an array of values, these values will be encoded + * as duplicate search parameters in the url. + * + * @param {(string|Array)=} paramValue If `search` is a string, then `paramValue` will + * override only a single search property. + * + * If `paramValue` is an array, it will override the property of the `search` component of + * `$location` specified via the first argument. + * + * If `paramValue` is `null`, the property specified via the first argument will be deleted. + * + * @return {Object} If called with no arguments returns the parsed `search` object. If called with + * one or more arguments returns `$location` object itself. */ search: function(search, paramValue) { - if (isUndefined(search)) - return this.$$search; - - if (isDefined(paramValue)) { - if (paramValue === null) { - delete this.$$search[search]; - } else { - this.$$search[search] = paramValue; - } - } else { - this.$$search = isString(search) ? parseKeyValue(search) : search; + switch (arguments.length) { + case 0: + return this.$$search; + case 1: + if (isString(search)) { + this.$$search = parseKeyValue(search); + } else if (isObject(search)) { + this.$$search = search; + } else { + throw $locationMinErr('isrcharg', + 'The first argument of the `$location#search()` call must be a string or an object.'); + } + break; + default: + if (isUndefined(paramValue) || paramValue === null) { + delete this.$$search[search]; + } else { + this.$$search[search] = paramValue; + } } this.$$compose(); @@ -5573,8 +9462,7 @@ LocationUrl.prototype = { /** * @ngdoc method - * @name ng.$location#hash - * @methodOf ng.$location + * @name $location#hash * * @description * This method is getter / setter. @@ -5590,8 +9478,7 @@ LocationUrl.prototype = { /** * @ngdoc method - * @name ng.$location#replace - * @methodOf ng.$location + * @name $location#replace * * @description * If called, all changes to $location during current `$digest` will be replacing current history @@ -5603,21 +9490,6 @@ LocationUrl.prototype = { } }; -LocationHashbangUrl.prototype = inherit(LocationUrl.prototype); - -function LocationHashbangInHtml5Url(url, hashPrefix, appBaseUrl, baseExtra) { - LocationHashbangUrl.apply(this, arguments); - - - this.$$rewriteAppUrl = function(absoluteLinkUrl) { - if (absoluteLinkUrl.indexOf(appBaseUrl) == 0) { - return appBaseUrl + baseExtra + '#' + hashPrefix + absoluteLinkUrl.substr(appBaseUrl.length); - } - } -} - -LocationHashbangInHtml5Url.prototype = inherit(LocationHashbangUrl.prototype); - function locationGetter(property) { return function() { return this[property]; @@ -5639,16 +9511,14 @@ function locationGetterSetter(property, preprocess) { /** - * @ngdoc object - * @name ng.$location + * @ngdoc service + * @name $location * - * @requires $browser - * @requires $sniffer * @requires $rootElement * * @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 + * [window.location](https://developer.mozilla.org/en/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. * @@ -5663,13 +9533,12 @@ function locationGetterSetter(property, preprocess) { * - 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} + * For more information see {@link guide/$location Developer Guide: Using $location} */ /** - * @ngdoc object - * @name ng.$locationProvider + * @ngdoc provider + * @name $locationProvider * @description * Use the `$locationProvider` to configure how the application deep linking paths are stored. */ @@ -5679,8 +9548,7 @@ function $LocationProvider(){ /** * @ngdoc property - * @name ng.$locationProvider#hashPrefix - * @methodOf ng.$locationProvider + * @name $locationProvider#hashPrefix * @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 @@ -5696,10 +9564,9 @@ function $LocationProvider(){ /** * @ngdoc property - * @name ng.$locationProvider#html5Mode - * @methodOf ng.$locationProvider + * @name $locationProvider#html5Mode * @description - * @param {string=} mode Use HTML5 strategy if available. + * @param {boolean=} mode Use HTML5 strategy if available. * @returns {*} current value if used as getter or itself (chaining) if used as setter */ this.html5Mode = function(mode) { @@ -5711,42 +9578,52 @@ function $LocationProvider(){ } }; + /** + * @ngdoc event + * @name $location#$locationChangeStart + * @eventType broadcast on root scope + * @description + * Broadcasted before a URL will change. This change can be prevented by calling + * `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} for more + * details about event object. Upon successful change + * {@link ng.$location#events_$locationChangeSuccess $locationChangeSuccess} is fired. + * + * @param {Object} angularEvent Synthetic event object. + * @param {string} newUrl New URL + * @param {string=} oldUrl URL that was before it was changed. + */ + + /** + * @ngdoc event + * @name $location#$locationChangeSuccess + * @eventType broadcast on root scope + * @description + * Broadcasted after a URL was changed. + * + * @param {Object} angularEvent Synthetic event object. + * @param {string} newUrl New URL + * @param {string=} oldUrl URL that was before it was changed. + */ + this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', function( $rootScope, $browser, $sniffer, $rootElement) { var $location, - basePath, - pathPrefix, - initUrl = $browser.url(), - initUrlParts = matchUrl(initUrl), - appBaseUrl; + LocationMode, + baseHref = $browser.baseHref(), // if base[href] is undefined, it defaults to '' + initialUrl = $browser.url(), + appBase; if (html5Mode) { - basePath = $browser.baseHref() || '/'; - pathPrefix = pathPrefixFromBase(basePath); - appBaseUrl = - composeProtocolHostPort(initUrlParts.protocol, initUrlParts.host, initUrlParts.port) + - pathPrefix + '/'; - - if ($sniffer.history) { - $location = new LocationUrl( - convertToHtml5Url(initUrl, basePath, hashPrefix), - pathPrefix, appBaseUrl); - } else { - $location = new LocationHashbangInHtml5Url( - convertToHashbangUrl(initUrl, basePath, hashPrefix), - hashPrefix, appBaseUrl, basePath.substr(pathPrefix.length + 1)); - } + appBase = serverBase(initialUrl) + (baseHref || '/'); + LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url; } else { - appBaseUrl = - composeProtocolHostPort(initUrlParts.protocol, initUrlParts.host, initUrlParts.port) + - (initUrlParts.path || '') + - (initUrlParts.search ? ('?' + initUrlParts.search) : '') + - '#' + hashPrefix + '/'; - - $location = new LocationHashbangUrl(initUrl, hashPrefix, appBaseUrl); + appBase = stripHash(initialUrl); + LocationMode = LocationHashbangUrl; } + $location = new LocationMode(appBase, '#' + hashPrefix); + $location.$$parse($location.$$rewrite(initialUrl)); - $rootElement.bind('click', function(event) { + $rootElement.on('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 @@ -5760,37 +9637,81 @@ function $LocationProvider(){ if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return; } - var absHref = elm.prop('href'), - rewrittenUrl = $location.$$rewriteAppUrl(absHref); + var absHref = elm.prop('href'); + + if (isObject(absHref) && absHref.toString() === '[object SVGAnimatedString]') { + // SVGAnimatedString.animVal should be identical to SVGAnimatedString.baseVal, unless during + // an animation. + absHref = urlResolve(absHref.animVal).href; + } + + // Make relative links work in HTML5 mode for legacy browsers (or at least IE8 & 9) + // The href should be a regular url e.g. /link/somewhere or link/somewhere or ../somewhere or + // somewhere#anchor or http://example.com/somewhere + if (LocationMode === LocationHashbangInHtml5Url) { + // get the actual href attribute - see + // http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx + var href = elm.attr('href') || elm.attr('xlink:href'); + + if (href.indexOf('://') < 0) { // Ignore absolute URLs + var prefix = '#' + hashPrefix; + if (href[0] == '/') { + // absolute path - replace old path + absHref = appBase + prefix + href; + } else if (href[0] == '#') { + // local anchor + absHref = appBase + prefix + ($location.path() || '/') + href; + } else { + // relative path - join with current path + var stack = $location.path().split("/"), + parts = href.split("/"); + for (var i=0; i html5 url - if ($location.absUrl() != initUrl) { + if ($location.absUrl() != initialUrl) { $browser.url($location.absUrl(), true); } // update $location when $browser url changes $browser.onUrlChange(function(newUrl) { if ($location.absUrl() != newUrl) { - if ($rootScope.$broadcast('$locationChangeStart', newUrl, $location.absUrl()).defaultPrevented) { - $browser.url($location.absUrl()); - return; - } $rootScope.$evalAsync(function() { var oldUrl = $location.absUrl(); $location.$$parse(newUrl); - afterLocationChange(oldUrl); + if ($rootScope.$broadcast('$locationChangeStart', newUrl, + oldUrl).defaultPrevented) { + $location.$$parse(oldUrl); + $browser.url(oldUrl); + } else { + afterLocationChange(oldUrl); + } }); if (!$rootScope.$$phase) $rootScope.$digest(); } @@ -5828,16 +9749,19 @@ function $LocationProvider(){ } /** - * @ngdoc object - * @name ng.$log + * @ngdoc service + * @name $log * @requires $window * * @description - * Simple service for logging. Default implementation writes the message + * Simple service for logging. Default implementation safely writes the message * into the browser's console (if present). * * The main purpose of this service is to simplify debugging and troubleshooting. * + * The default is to log `debug` messages. You can use + * {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this. + * * @example @@ -5860,13 +9784,37 @@ function $LocationProvider(){ */ +/** + * @ngdoc provider + * @name $logProvider + * @description + * Use the `$logProvider` to configure how the application logs messages + */ function $LogProvider(){ + var debug = true, + self = this; + + /** + * @ngdoc property + * @name $logProvider#debugEnabled + * @description + * @param {boolean=} flag enable or disable debug level messages + * @returns {*} current value if used as getter or itself (chaining) if used as setter + */ + this.debugEnabled = function(flag) { + if (isDefined(flag)) { + debug = flag; + return this; + } else { + return debug; + } + }; + this.$get = ['$window', function($window){ return { /** * @ngdoc method - * @name ng.$log#log - * @methodOf ng.$log + * @name $log#log * * @description * Write a log message @@ -5875,8 +9823,16 @@ function $LogProvider(){ /** * @ngdoc method - * @name ng.$log#warn - * @methodOf ng.$log + * @name $log#info + * + * @description + * Write an information message + */ + info: consoleLog('info'), + + /** + * @ngdoc method + * @name $log#warn * * @description * Write a warning message @@ -5885,23 +9841,29 @@ function $LogProvider(){ /** * @ngdoc method - * @name ng.$log#info - * @methodOf ng.$log + * @name $log#error * * @description - * Write an information message + * Write an error message */ - info: consoleLog('info'), + error: consoleLog('error'), /** * @ngdoc method - * @name ng.$log#error - * @methodOf ng.$log + * @name $log#debug * * @description - * Write an error message + * Write a debug message */ - error: consoleLog('error') + debug: (function () { + var fn = consoleLog('debug'); + + return function() { + if (debug) { + fn.apply(self, arguments); + } + }; + }()) }; function formatError(arg) { @@ -5919,9 +9881,16 @@ function $LogProvider(){ function consoleLog(type) { var console = $window.console || {}, - logFn = console[type] || console.log || noop; + logFn = console[type] || console.log || noop, + hasApply = false; + + // Note: reading logFn.apply throws an error in IE11 in IE8 document mode. + // The reason behind this is that console.log has type "object" in IE8... + try { + hasApply = !!logFn.apply; + } catch (e) {} - if (logFn.apply) { + if (hasApply) { return function() { var args = []; forEach(arguments, function(arg) { @@ -5934,13 +9903,78 @@ function $LogProvider(){ // 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); - } + logFn(arg1, arg2 == null ? '' : arg2); + }; } }]; } +var $parseMinErr = minErr('$parse'); +var promiseWarningCache = {}; +var promiseWarning; + +// Sandboxing Angular Expressions +// ------------------------------ +// Angular expressions are generally considered safe because these expressions only have direct +// access to $scope and locals. However, one can obtain the ability to execute arbitrary JS code by +// obtaining a reference to native JS functions such as the Function constructor. +// +// As an example, consider the following Angular expression: +// +// {}.toString.constructor(alert("evil JS code")) +// +// We want to prevent this type of access. For the sake of performance, during the lexing phase we +// disallow any "dotted" access to any member named "constructor". +// +// For reflective calls (a[b]) we check that the value of the lookup is not the Function constructor +// while evaluating the expression, which is a stronger but more expensive test. Since reflective +// calls are expensive anyway, this is not such a big deal compared to static dereferencing. +// +// This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits +// against the expression language, but not to prevent exploits that were enabled by exposing +// sensitive JavaScript or browser apis on Scope. Exposing such objects on a Scope is never a good +// practice and therefore we are not even trying to protect against interaction with an object +// explicitly exposed in this way. +// +// A developer could foil the name check by aliasing the Function constructor under a different +// name on the scope. +// +// In general, it is not possible to access a Window object from an angular expression unless a +// window or some DOM object that has a reference to window is published onto a Scope. + +function ensureSafeMemberName(name, fullExpression) { + if (name === "constructor") { + throw $parseMinErr('isecfld', + 'Referencing "constructor" field in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } + return name; +} + +function ensureSafeObject(obj, fullExpression) { + // nifty check if obj is Function that is fast and works across iframes and other contexts + if (obj) { + if (obj.constructor === obj) { + throw $parseMinErr('isecfn', + 'Referencing Function in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } else if (// isWindow(obj) + obj.document && obj.location && obj.alert && obj.setInterval) { + throw $parseMinErr('isecwindow', + 'Referencing the Window in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } else if (// isElement(obj) + obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) { + throw $parseMinErr('isecdom', + 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } + } + return obj; +} + var OPERATORS = { + /* jshint bitwise : false */ 'null':function(){return null;}, 'true':function(){return true;}, 'false':function(){return false;}, @@ -5954,12 +9988,17 @@ var OPERATORS = { return a; } return isDefined(b)?b:undefined;}, - '-':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) 0) { - var token = tokens[0]; + primary: function () { + var primary; + if (this.expect('(')) { + primary = this.filterChain(); + this.consume(')'); + } else if (this.expect('[')) { + primary = this.arrayDeclaration(); + } else if (this.expect('{')) { + primary = this.object(); + } else { + var token = this.expect(); + primary = token.fn; + if (!primary) { + this.throwError('not a primary expression', token); + } + primary.literal = !!token.literal; + primary.constant = !!token.constant; + } + + var next, context; + while ((next = this.expect('(', '[', '.'))) { + if (next.text === '(') { + primary = this.functionCall(primary, context); + context = null; + } else if (next.text === '[') { + context = primary; + primary = this.objectIndex(primary); + } else if (next.text === '.') { + context = primary; + primary = this.fieldAccess(primary); + } else { + this.throwError('IMPOSSIBLE'); + } + } + return primary; + }, + + throwError: function(msg, token) { + throw $parseMinErr('syntax', + 'Syntax Error: Token \'{0}\' {1} at column {2} of the expression [{3}] starting at [{4}].', + token.text, msg, (token.index + 1), this.text, this.text.substring(token.index)); + }, + + peekToken: function() { + if (this.tokens.length === 0) + throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); + return this.tokens[0]; + }, + + peek: function(e1, e2, e3, e4) { + if (this.tokens.length > 0) { + var token = this.tokens[0]; var t = token.text; - if (t==e1 || t==e2 || t==e3 || t==e4 || + 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); + expect: function(e1, e2, e3, e4){ + var token = this.peek(e1, e2, e3, e4); if (token) { - if (json && !token.json) { - throwError("is not valid json", token); - } - tokens.shift(); + this.tokens.shift(); return token; } return false; - } + }, - function consume(e1){ - if (!expect(e1)) { - throwError("is unexpected, expecting [" + e1 + "]", peek()); + consume: function(e1){ + if (!this.expect(e1)) { + this.throwError('is unexpected, expecting [' + e1 + ']', this.peek()); } - } + }, - function unaryFn(fn, right) { - return function(self, locals) { + unaryFn: function(fn, right) { + return extend(function(self, locals) { return fn(self, locals, right); - }; - } + }, { + constant:right.constant + }); + }, + + ternaryFn: function(left, middle, right){ + return extend(function(self, locals){ + return left(self, locals) ? middle(self, locals) : right(self, locals); + }, { + constant: left.constant && middle.constant && right.constant + }); + }, - function binaryFn(left, fn, right) { - return function(self, locals) { + binaryFn: function(left, fn, right) { + return extend(function(self, locals) { return fn(self, locals, left, right); - }; - } + }, { + constant:left.constant && right.constant + }); + }, - function statements() { + statements: function() { var statements = []; - while(true) { - if (tokens.length > 0 && !peek('}', ')', ';', ']')) - statements.push(filterChain()); - if (!expect(';')) { + while (true) { + if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']')) + statements.push(this.filterChain()); + if (!this.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; - }; + 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(); + filterChain: function() { + var left = this.expression(); var token; - while(true) { - if ((token = expect('|'))) { - left = binaryFn(left, token.fn, filter()); + while (true) { + if ((token = this.expect('|'))) { + left = this.binaryFn(left, token.fn, this.filter()); } else { return left; } } - } + }, - function filter() { - var token = expect(); - var fn = $filter(token.text); + filter: function() { + var token = this.expect(); + var fn = this.$filter(token.text); var argsFn = []; - while(true) { - if ((token = expect(':'))) { - argsFn.push(expression()); + while (true) { + if ((token = this.expect(':'))) { + argsFn.push(this.expression()); } else { - var fnInvoke = function(self, locals, input){ + var fnInvoke = function(self, locals, input) { var args = [input]; - for ( var i = 0; i < argsFn.length; i++) { + for (var i = 0; i < argsFn.length; i++) { args.push(argsFn[i](self, locals)); } return fn.apply(self, args); @@ -6348,258 +10473,292 @@ function parser(text, json, $filter, csp){ }; } } - } + }, - function expression() { - return assignment(); - } + expression: function() { + return this.assignment(); + }, - function _assignment() { - var left = logicalOR(); + assignment: function() { + var left = this.ternary(); var right; var token; - if ((token = expect('='))) { + if ((token = this.expect('='))) { if (!left.assign) { - throwError("implies assignment but [" + - text.substring(0, token.index) + "] can not be assigned to", token); + this.throwError('implies assignment but [' + + this.text.substring(0, token.index) + '] can not be assigned to', token); } - right = logicalOR(); - return function(scope, locals){ + right = this.ternary(); + return function(scope, locals) { return left.assign(scope, right(scope, locals), locals); }; + } + return left; + }, + + ternary: function() { + var left = this.logicalOR(); + var middle; + var token; + if ((token = this.expect('?'))) { + middle = this.ternary(); + if ((token = this.expect(':'))) { + return this.ternaryFn(left, middle, this.ternary()); + } else { + this.throwError('expected :', token); + } } else { return left; } - } + }, - function logicalOR() { - var left = logicalAND(); + logicalOR: function() { + var left = this.logicalAND(); var token; - while(true) { - if ((token = expect('||'))) { - left = binaryFn(left, token.fn, logicalAND()); + while (true) { + if ((token = this.expect('||'))) { + left = this.binaryFn(left, token.fn, this.logicalAND()); } else { return left; } } - } + }, - function logicalAND() { - var left = equality(); + logicalAND: function() { + var left = this.equality(); var token; - if ((token = expect('&&'))) { - left = binaryFn(left, token.fn, logicalAND()); + if ((token = this.expect('&&'))) { + left = this.binaryFn(left, token.fn, this.logicalAND()); } return left; - } + }, - function equality() { - var left = relational(); + equality: function() { + var left = this.relational(); var token; - if ((token = expect('==','!='))) { - left = binaryFn(left, token.fn, equality()); + if ((token = this.expect('==','!=','===','!=='))) { + left = this.binaryFn(left, token.fn, this.equality()); } return left; - } + }, - function relational() { - var left = additive(); + relational: function() { + var left = this.additive(); var token; - if ((token = expect('<', '>', '<=', '>='))) { - left = binaryFn(left, token.fn, relational()); + if ((token = this.expect('<', '>', '<=', '>='))) { + left = this.binaryFn(left, token.fn, this.relational()); } return left; - } + }, - function additive() { - var left = multiplicative(); + additive: function() { + var left = this.multiplicative(); var token; - while ((token = expect('+','-'))) { - left = binaryFn(left, token.fn, multiplicative()); + while ((token = this.expect('+','-'))) { + left = this.binaryFn(left, token.fn, this.multiplicative()); } return left; - } + }, - function multiplicative() { - var left = unary(); + multiplicative: function() { + var left = this.unary(); var token; - while ((token = expect('*','/','%'))) { - left = binaryFn(left, token.fn, unary()); + while ((token = this.expect('*','/','%'))) { + left = this.binaryFn(left, token.fn, this.unary()); } return left; - } + }, - function unary() { + unary: function() { 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()); + if (this.expect('+')) { + return this.primary(); + } else if ((token = this.expect('-'))) { + return this.binaryFn(Parser.ZERO, token.fn, this.unary()); + } else if ((token = this.expect('!'))) { + return this.unaryFn(token.fn, this.unary()); } else { - return primary(); + return this.primary(); } - } + }, + fieldAccess: function(object) { + var parser = this; + var field = this.expect().text; + var getter = getterFn(field, this.options, this.text); - 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); + return extend(function(scope, locals, self) { + return getter(self || object(scope, locals)); + }, { + assign: function(scope, value, locals) { + return setter(object(scope, locals), field, value, parser.text, parser.options); } - } + }); + }, - 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; - } + objectIndex: function(obj) { + var parser = this; - function _fieldAccess(object) { - var field = expect().text; - var getter = getterFn(field, csp); - return extend( - function(scope, locals, self) { - return getter(self || object(scope, locals), locals); - }, - { - assign:function(scope, value, locals) { - return setter(object(scope, locals), field, value); - } - } - ); - } + var indexFn = this.expression(); + this.consume(']'); - 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; + return extend(function(self, locals) { + var o = obj(self, locals), + i = indexFn(self, locals), + v, p; + + if (!o) return undefined; + v = ensureSafeObject(o[i], parser.text); + if (v && v.then && parser.options.unwrapPromises) { + 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) { + var key = indexFn(self, locals); + // prevent overwriting of Function.constructor which would break ensureSafeObject check + var safe = ensureSafeObject(obj(self, locals), parser.text); + return safe[key] = value; + } + }); + }, - function _functionCall(fn, contextGetter) { + functionCall: function(fn, contextGetter) { var argsFn = []; - if (peekToken().text != ')') { + if (this.peekToken().text !== ')') { do { - argsFn.push(expression()); - } while (expect(',')); + argsFn.push(this.expression()); + } while (this.expect(',')); } - consume(')'); - return function(scope, locals){ - var args = [], - context = contextGetter ? contextGetter(scope, locals) : scope; + this.consume(')'); + + var parser = this; - for ( var i = 0; i < argsFn.length; i++) { + return function(scope, locals) { + var args = []; + var context = contextGetter ? contextGetter(scope, locals) : scope; + + for (var i = 0; i < argsFn.length; i++) { args.push(argsFn[i](scope, locals)); } var fnPtr = fn(scope, locals, context) || noop; - // IE stupidity! - return fnPtr.apply - ? fnPtr.apply(context, args) - : fnPtr(args[0], args[1], args[2], args[3], args[4]); + + ensureSafeObject(context, parser.text); + ensureSafeObject(fnPtr, parser.text); + + // IE stupidity! (IE doesn't have apply for some native functions) + var v = fnPtr.apply + ? fnPtr.apply(context, args) + : fnPtr(args[0], args[1], args[2], args[3], args[4]); + + return ensureSafeObject(v, parser.text); }; - } + }, // This is used with json array declaration - function arrayDeclaration () { + arrayDeclaration: function () { var elementFns = []; - if (peekToken().text != ']') { + var allConstant = true; + if (this.peekToken().text !== ']') { do { - elementFns.push(expression()); - } while (expect(',')); + if (this.peek(']')) { + // Support trailing commas per ES5.1. + break; + } + var elementFn = this.expression(); + elementFns.push(elementFn); + if (!elementFn.constant) { + allConstant = false; + } + } while (this.expect(',')); } - consume(']'); - return function(self, locals){ + this.consume(']'); + + return extend(function(self, locals) { var array = []; - for ( var i = 0; i < elementFns.length; i++) { + for (var i = 0; i < elementFns.length; i++) { array.push(elementFns[i](self, locals)); } return array; - }; - } + }, { + literal: true, + constant: allConstant + }); + }, - function object () { + object: function () { var keyValues = []; - if (peekToken().text != '}') { + var allConstant = true; + if (this.peekToken().text !== '}') { do { - var token = expect(), + if (this.peek('}')) { + // Support trailing commas per ES5.1. + break; + } + var token = this.expect(), key = token.string || token.text; - consume(":"); - var value = expression(); - keyValues.push({key:key, value:value}); - } while (expect(',')); + this.consume(':'); + var value = this.expression(); + keyValues.push({key: key, value: value}); + if (!value.constant) { + allConstant = false; + } + } while (this.expect(',')); } - consume('}'); - return function(self, locals){ + this.consume('}'); + + return extend(function(self, locals) { var object = {}; - for ( var i = 0; i < keyValues.length; i++) { + for (var i = 0; i < keyValues.length; i++) { var keyValue = keyValues[i]; object[keyValue.key] = keyValue.value(self, locals); } return object; - }; + }, { + literal: true, + constant: allConstant + }); } -} +}; + ////////////////////////////////////////////////// // Parser helper functions ////////////////////////////////////////////////// -function setter(obj, path, setValue) { - var element = path.split('.'); +function setter(obj, path, setValue, fullExp, options) { + //needed? + options = options || {}; + + var element = path.split('.'), key; for (var i = 0; element.length > 1; i++) { - var key = element.shift(); + key = ensureSafeMemberName(element.shift(), fullExp); var propertyObj = obj[key]; if (!propertyObj) { propertyObj = {}; obj[key] = propertyObj; } obj = propertyObj; + if (obj.then && options.unwrapPromises) { + promiseWarning(fullExp); + if (!("$$v" in obj)) { + (function(promise) { + promise.then(function(val) { promise.$$v = val; }); } + )(obj); + } + if (obj.$$v === undefined) { + obj.$$v = {}; + } + obj = obj.$$v; + } } - obj[element.shift()] = setValue; + key = ensureSafeMemberName(element.shift(), fullExp); + obj[key] = setValue; return setValue; } @@ -6610,71 +10769,134 @@ var getterFnCache = {}; * - http://jsperf.com/angularjs-parse-getter/4 * - http://jsperf.com/path-evaluation-simplified/7 */ -function cspSafeGetterFn(key0, key1, key2, key3, key4) { - return function(scope, locals) { - var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope, - promise; +function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp, options) { + ensureSafeMemberName(key0, fullExp); + ensureSafeMemberName(key1, fullExp); + ensureSafeMemberName(key2, fullExp); + ensureSafeMemberName(key3, fullExp); + ensureSafeMemberName(key4, fullExp); - if (pathVal === null || pathVal === undefined) return pathVal; + return !options.unwrapPromises + ? function cspSafeGetter(scope, locals) { + var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope; - pathVal = pathVal[key0]; - if (pathVal && pathVal.then) { - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - if (!key1 || pathVal === null || pathVal === undefined) return pathVal; + if (pathVal == null) return pathVal; + pathVal = pathVal[key0]; - pathVal = pathVal[key1]; - if (pathVal && pathVal.then) { - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - if (!key2 || pathVal === null || pathVal === undefined) return pathVal; + if (!key1) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key1]; - pathVal = pathVal[key2]; - if (pathVal && pathVal.then) { - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - if (!key3 || pathVal === null || pathVal === undefined) return pathVal; + if (!key2) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key2]; - pathVal = pathVal[key3]; - if (pathVal && pathVal.then) { - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - if (!key4 || pathVal === null || pathVal === undefined) return pathVal; + if (!key3) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key3]; - pathVal = pathVal[key4]; - if (pathVal && pathVal.then) { - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - return pathVal; + if (!key4) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key4]; + + return pathVal; + } + : function cspSafePromiseEnabledGetter(scope, locals) { + var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope, + promise; + + if (pathVal == null) return pathVal; + + pathVal = pathVal[key0]; + if (pathVal && pathVal.then) { + promiseWarning(fullExp); + if (!("$$v" in pathVal)) { + promise = pathVal; + promise.$$v = undefined; + promise.then(function(val) { promise.$$v = val; }); + } + pathVal = pathVal.$$v; + } + + if (!key1) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key1]; + if (pathVal && pathVal.then) { + promiseWarning(fullExp); + if (!("$$v" in pathVal)) { + promise = pathVal; + promise.$$v = undefined; + promise.then(function(val) { promise.$$v = val; }); + } + pathVal = pathVal.$$v; + } + + if (!key2) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key2]; + if (pathVal && pathVal.then) { + promiseWarning(fullExp); + if (!("$$v" in pathVal)) { + promise = pathVal; + promise.$$v = undefined; + promise.then(function(val) { promise.$$v = val; }); + } + pathVal = pathVal.$$v; + } + + if (!key3) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key3]; + if (pathVal && pathVal.then) { + promiseWarning(fullExp); + if (!("$$v" in pathVal)) { + promise = pathVal; + promise.$$v = undefined; + promise.then(function(val) { promise.$$v = val; }); + } + pathVal = pathVal.$$v; + } + + if (!key4) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key4]; + if (pathVal && pathVal.then) { + promiseWarning(fullExp); + if (!("$$v" in pathVal)) { + promise = pathVal; + promise.$$v = undefined; + promise.then(function(val) { promise.$$v = val; }); + } + pathVal = pathVal.$$v; + } + return pathVal; + }; +} + +function simpleGetterFn1(key0, fullExp) { + ensureSafeMemberName(key0, fullExp); + + return function simpleGetterFn1(scope, locals) { + if (scope == null) return undefined; + return ((locals && locals.hasOwnProperty(key0)) ? locals : scope)[key0]; + }; +} + +function simpleGetterFn2(key0, key1, fullExp) { + ensureSafeMemberName(key0, fullExp); + ensureSafeMemberName(key1, fullExp); + + return function simpleGetterFn2(scope, locals) { + if (scope == null) return undefined; + scope = ((locals && locals.hasOwnProperty(key0)) ? locals : scope)[key0]; + return scope == null ? undefined : scope[key1]; }; } -function getterFn(path, csp) { +function getterFn(path, options, fullExp) { + // Check whether the cache has this getter already. + // We can use hasOwnProperty directly on the cache because we ensure, + // see below, that the cache never stores a path called 'hasOwnProperty' if (getterFnCache.hasOwnProperty(path)) { return getterFnCache[path]; } @@ -6683,60 +10905,82 @@ function getterFn(path, csp) { pathKeysLength = pathKeys.length, fn; - if (csp) { - fn = (pathKeysLength < 6) - ? cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4]) - : function(scope, locals) { - var i = 0, val; - do { - val = cspSafeGetterFn( - pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++] - )(scope, locals); - - locals = undefined; // clear after first iteration - scope = val; - } while (i < pathKeysLength); - return val; - } + // When we have only 1 or 2 tokens, use optimized special case closures. + // http://jsperf.com/angularjs-parse-getter/6 + if (!options.unwrapPromises && pathKeysLength === 1) { + fn = simpleGetterFn1(pathKeys[0], fullExp); + } else if (!options.unwrapPromises && pathKeysLength === 2) { + fn = simpleGetterFn2(pathKeys[0], pathKeys[1], fullExp); + } else if (options.csp) { + if (pathKeysLength < 6) { + fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp, + options); + } else { + fn = function(scope, locals) { + var i = 0, val; + do { + val = cspSafeGetterFn(pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], + pathKeys[i++], fullExp, options)(scope, locals); + + locals = undefined; // clear after first iteration + scope = val; + } while (i < pathKeysLength); + return val; + }; + } } else { - var code = 'var l, fn, p;\n'; + var code = 'var p;\n'; forEach(pathKeys, function(key, index) { - code += 'if(s === null || s === undefined) return s;\n' + - 'l=s;\n' + + ensureSafeMemberName(key, fullExp); + code += 'if(s == null) return undefined;\n' + 's='+ (index // we simply dereference 's' on any .dot notation ? 's' // but if we are first then we check locals first, 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'; + (options.unwrapPromises + ? 'if (s && s.then) {\n' + + ' pw("' + fullExp.replace(/(["\r\n])/g, '\\$1') + '");\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); // s=scope, k=locals - fn.toString = function() { return code; }; + + /* jshint -W054 */ + var evaledFnGetter = new Function('s', 'k', 'pw', code); // s=scope, k=locals, pw=promiseWarning + /* jshint +W054 */ + evaledFnGetter.toString = valueFn(code); + fn = options.unwrapPromises ? function(scope, locals) { + return evaledFnGetter(scope, locals, promiseWarning); + } : evaledFnGetter; } - return getterFnCache[path] = fn; + // Only cache the value if it's not going to mess up the cache object + // This is more performant that using Object.prototype.hasOwnProperty.call + if (path !== 'hasOwnProperty') { + getterFnCache[path] = fn; + } + return fn; } /////////////////////////////////// /** - * @ngdoc function - * @name ng.$parse - * @function + * @ngdoc service + * @name $parse + * @kind function * * @description * * Converts Angular {@link guide/expression expression} into a function. * - *
+ * ```js
  *   var getter = $parse('user.name');
  *   var setter = getter.assign;
  *   var context = {user:{name:'angular'}};
@@ -6746,32 +10990,163 @@ function getterFn(path, csp) {
  *   setter(context, 'newValue');
  *   expect(context.user.name).toEqual('newValue');
  *   expect(getter(context, locals)).toEqual('local');
- * 
+ * ``` * * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: * * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (tipically a scope object). + * are evaluated against (typically a scope object). * * `locals` – `{object=}` – local variables context object, useful for overriding values in * `context`. * - * The return function also has an `assign` property, if the expression is assignable, which - * allows one to set values to expressions. + * The returned function also has the following properties: + * * `literal` – `{boolean}` – whether the expression's top-level node is a JavaScript + * literal. + * * `constant` – `{boolean}` – whether the expression is made entirely of JavaScript + * constant literals. + * * `assign` – `{?function(context, value)}` – if the expression is assignable, this will be + * set to a function to change its value on the given context. + * + */ + + +/** + * @ngdoc provider + * @name $parseProvider + * @kind function * + * @description + * `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse} + * service. */ function $ParseProvider() { var cache = {}; - this.$get = ['$filter', '$sniffer', function($filter, $sniffer) { + + var $parseOptions = { + csp: false, + unwrapPromises: false, + logPromiseWarnings: true + }; + + + /** + * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. + * + * @ngdoc method + * @name $parseProvider#unwrapPromises + * @description + * + * **This feature is deprecated, see deprecation notes below for more info** + * + * If set to true (default is false), $parse will unwrap promises automatically when a promise is + * found at any part of the expression. In other words, if set to true, the expression will always + * result in a non-promise value. + * + * While the promise is unresolved, it's treated as undefined, but once resolved and fulfilled, + * the fulfillment value is used in place of the promise while evaluating the expression. + * + * **Deprecation notice** + * + * This is a feature that didn't prove to be wildly useful or popular, primarily because of the + * dichotomy between data access in templates (accessed as raw values) and controller code + * (accessed as promises). + * + * In most code we ended up resolving promises manually in controllers anyway and thus unifying + * the model access there. + * + * Other downsides of automatic promise unwrapping: + * + * - when building components it's often desirable to receive the raw promises + * - adds complexity and slows down expression evaluation + * - makes expression code pre-generation unattractive due to the amount of code that needs to be + * generated + * - makes IDE auto-completion and tool support hard + * + * **Warning Logs** + * + * If the unwrapping is enabled, Angular will log a warning about each expression that unwraps a + * promise (to reduce the noise, each expression is logged only once). To disable this logging use + * `$parseProvider.logPromiseWarnings(false)` api. + * + * + * @param {boolean=} value New value. + * @returns {boolean|self} Returns the current setting when used as getter and self if used as + * setter. + */ + this.unwrapPromises = function(value) { + if (isDefined(value)) { + $parseOptions.unwrapPromises = !!value; + return this; + } else { + return $parseOptions.unwrapPromises; + } + }; + + + /** + * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. + * + * @ngdoc method + * @name $parseProvider#logPromiseWarnings + * @description + * + * Controls whether Angular should log a warning on any encounter of a promise in an expression. + * + * The default is set to `true`. + * + * This setting applies only if `$parseProvider.unwrapPromises` setting is set to true as well. + * + * @param {boolean=} value New value. + * @returns {boolean|self} Returns the current setting when used as getter and self if used as + * setter. + */ + this.logPromiseWarnings = function(value) { + if (isDefined(value)) { + $parseOptions.logPromiseWarnings = value; + return this; + } else { + return $parseOptions.logPromiseWarnings; + } + }; + + + this.$get = ['$filter', '$sniffer', '$log', function($filter, $sniffer, $log) { + $parseOptions.csp = $sniffer.csp; + + promiseWarning = function promiseWarningFn(fullExp) { + if (!$parseOptions.logPromiseWarnings || promiseWarningCache.hasOwnProperty(fullExp)) return; + promiseWarningCache[fullExp] = true; + $log.warn('[$parse] Promise found in the expression `' + fullExp + '`. ' + + 'Automatic unwrapping of promises in Angular expressions is deprecated.'); + }; + return function(exp) { - switch(typeof exp) { + var parsedExpression; + + switch (typeof exp) { case 'string': - return cache.hasOwnProperty(exp) - ? cache[exp] - : cache[exp] = parser(exp, false, $filter, $sniffer.csp); + + if (cache.hasOwnProperty(exp)) { + return cache[exp]; + } + + var lexer = new Lexer($parseOptions); + var parser = new Parser(lexer, $filter, $parseOptions); + parsedExpression = parser.parse(exp); + + if (exp !== 'hasOwnProperty') { + // Only cache the value if it's not going to mess up the cache object + // This is more performant that using Object.prototype.hasOwnProperty.call + cache[exp] = parsedExpression; + } + + return parsedExpression; + case 'function': return exp; + default: return noop; } @@ -6781,7 +11156,7 @@ function $ParseProvider() { /** * @ngdoc service - * @name ng.$q + * @name $q * @requires $rootScope * * @description @@ -6794,9 +11169,9 @@ function $ParseProvider() { * From the perspective of dealing with error handling, deferred and promise APIs are to * asynchronous programming what `try`, `catch` and `throw` keywords are to synchronous programming. * - *
- *   // 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).
+ * ```js
+ *   // for the purpose of this example let's assume that variables `$q`, `scope` and `okToGreet`
+ *   // are available in the current lexical scope (they could have been injected or passed in).
  *
  *   function asyncGreet(name) {
  *     var deferred = $q.defer();
@@ -6805,6 +11180,8 @@ function $ParseProvider() {
  *       // 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() {
+ *         deferred.notify('About to greet ' + name + '.');
+ *
  *         if (okToGreet(name)) {
  *           deferred.resolve('Hello, ' + name + '!');
  *         } else {
@@ -6821,12 +11198,14 @@ function $ParseProvider() {
  *     alert('Success: ' + greeting);
  *   }, function(reason) {
  *     alert('Failed: ' + reason);
+ *   }, function(update) {
+ *     alert('Got notification: ' + update);
  *   });
- * 
+ * ``` * * 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). + * comes in the way of guarantees that promise and deferred APIs make, see + * 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. @@ -6839,7 +11218,8 @@ function $ParseProvider() { * 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. + * that can be used for signaling the successful or unsuccessful completion, as well as the status + * of the task. * * **Methods** * @@ -6847,6 +11227,8 @@ function $ParseProvider() { * 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`. + * - `notify(value)` - provides updates on the status of the promise's execution. This may be called + * multiple times before the promise is either resolved or rejected. * * **Properties** * @@ -6863,27 +11245,42 @@ function $ParseProvider() { * * **Methods** * - * - `then(successCallback, errorCallback)` – regardless of when the promise was or will be resolved - * or rejected, `then` 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. + * - `then(successCallback, errorCallback, notifyCallback)` – regardless of when the promise was or + * will be resolved or rejected, `then` 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. Additionally, the notify callback may be called zero or more times to + * provide a progress indication, before the promise is resolved or rejected. * * This method *returns a new promise* which is resolved or rejected via the return value of the - * `successCallback` or `errorCallback`. + * `successCallback`, `errorCallback`. It also notifies via the return value of the + * `notifyCallback` method. The promise can not be resolved or rejected from the notifyCallback + * method. * + * - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)` + * + * - `finally(callback)` – allows you to observe either the fulfillment or rejection of a promise, + * but to do so without modifying the final value. This is useful to release resources or do some + * clean-up that needs to be done whether the promise was rejected or resolved. See the [full + * specification](https://github.com/kriskowal/q/wiki/API-Reference#promisefinallycallback) for + * more information. + * + * Because `finally` is a reserved word in JavaScript and reserved keywords are not supported as + * property names by ES3, you'll need to invoke the method like `promise['finally'](callback)` to + * make your code IE8 and Android 2.x compatible. * * # Chaining promises * - * Because calling the `then` method of a promise returns a new derived promise, it is easily possible - * to create a chain of promises: + * Because calling the `then` method of a promise returns a new derived promise, it is easily + * possible to create a chain of promises: * - *
+ * ```js
  *   promiseB = promiseA.then(function(result) {
  *     return result + 1;
  *   });
  *
  *   // promiseB will be resolved immediately after promiseA is resolved and its value
  *   // will be the result of promiseA incremented by 1
- * 
+ * ``` * * 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 @@ -6893,39 +11290,37 @@ function $ParseProvider() { * * # Differences between Kris Kowal's Q and $q * - * There are three main differences: + * There are two main differences: * * - $q is integrated with the {@link 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 than $q, but that comes at a cost of bytes. $q is tiny, but contains * all the important functionality needed for common async tasks. - * + * * # Testing - * - *
+ *
+ *  ```js
  *    it('should simulate promise', inject(function($q, $rootScope) {
  *      var deferred = $q.defer();
  *      var promise = deferred.promise;
  *      var resolvedValue;
- * 
+ *
  *      promise.then(function(value) { resolvedValue = value; });
  *      expect(resolvedValue).toBeUndefined();
- * 
+ *
  *      // Simulate resolving of promise
  *      deferred.resolve(123);
  *      // Note that the 'then' function does not get called synchronously.
  *      // This is because we want the promise API to always be async, whether or not
  *      // it got called synchronously or asynchronously.
  *      expect(resolvedValue).toBeUndefined();
- * 
+ *
  *      // Propagate promise resolution to 'then' functions using $apply().
  *      $rootScope.$apply();
  *      expect(resolvedValue).toEqual(123);
- *    });
- *  
+ * })); + * ``` */ function $QProvider() { @@ -6940,7 +11335,7 @@ function $QProvider() { /** * Constructs a promise manager. * - * @param {function(function)} nextTick Function for executing functions in the next turn. + * @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. @@ -6948,9 +11343,10 @@ function $QProvider() { function qFactory(nextTick, exceptionHandler) { /** - * @ngdoc - * @name ng.$q#defer - * @methodOf ng.$q + * @ngdoc method + * @name $q#defer + * @kind function + * * @description * Creates a `Deferred` object which represents a task which will finish in the future. * @@ -6973,7 +11369,7 @@ function qFactory(nextTick, exceptionHandler) { var callback; for (var i = 0, ii = callbacks.length; i < ii; i++) { callback = callbacks[i]; - value.then(callback[0], callback[1]); + value.then(callback[0], callback[1], callback[2]); } }); } @@ -6982,17 +11378,34 @@ function qFactory(nextTick, exceptionHandler) { reject: function(reason) { - deferred.resolve(reject(reason)); + deferred.resolve(createInternalRejectedPromise(reason)); + }, + + + notify: function(progress) { + if (pending) { + var callbacks = pending; + + if (pending.length) { + nextTick(function() { + var callback; + for (var i = 0, ii = callbacks.length; i < ii; i++) { + callback = callbacks[i]; + callback[2](progress); + } + }); + } + } }, promise: { - then: function(callback, errback) { + then: function(callback, errback, progressback) { var result = defer(); var wrappedCallback = function(value) { try { - result.resolve((callback || defaultCallback)(value)); + result.resolve((isFunction(callback) ? callback : defaultCallback)(value)); } catch(e) { result.reject(e); exceptionHandler(e); @@ -7001,20 +11414,69 @@ function qFactory(nextTick, exceptionHandler) { var wrappedErrback = function(reason) { try { - result.resolve((errback || defaultErrback)(reason)); + result.resolve((isFunction(errback) ? errback : defaultErrback)(reason)); } catch(e) { result.reject(e); exceptionHandler(e); } }; + var wrappedProgressback = function(progress) { + try { + result.notify((isFunction(progressback) ? progressback : defaultCallback)(progress)); + } catch(e) { + exceptionHandler(e); + } + }; + if (pending) { - pending.push([wrappedCallback, wrappedErrback]); + pending.push([wrappedCallback, wrappedErrback, wrappedProgressback]); } else { - value.then(wrappedCallback, wrappedErrback); + value.then(wrappedCallback, wrappedErrback, wrappedProgressback); } return result.promise; + }, + + "catch": function(callback) { + return this.then(null, callback); + }, + + "finally": function(callback) { + + function makePromise(value, resolved) { + var result = defer(); + if (resolved) { + result.resolve(value); + } else { + result.reject(value); + } + return result.promise; + } + + function handleCallback(value, isResolved) { + var callbackOutput = null; + try { + callbackOutput = (callback ||defaultCallback)(); + } catch(e) { + return makePromise(e, false); + } + if (callbackOutput && isFunction(callbackOutput.then)) { + return callbackOutput.then(function() { + return makePromise(value, isResolved); + }, function(error) { + return makePromise(error, false); + }); + } else { + return makePromise(value, isResolved); + } + } + + return this.then(function(value) { + return handleCallback(value, true); + }, function(error) { + return handleCallback(error, false); + }); } } }; @@ -7024,7 +11486,7 @@ function qFactory(nextTick, exceptionHandler) { var ref = function(value) { - if (value && value.then) return value; + if (value && isFunction(value.then)) return value; return { then: function(callback) { var result = defer(); @@ -7038,9 +11500,10 @@ function qFactory(nextTick, exceptionHandler) { /** - * @ngdoc - * @name ng.$q#reject - * @methodOf ng.$q + * @ngdoc method + * @name $q#reject + * @kind function + * * @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 @@ -7052,7 +11515,7 @@ function qFactory(nextTick, exceptionHandler) { * current promise, you have to "rethrow" the error by returning a rejection constructed via * `reject`. * - *
+   * ```js
    *   promiseB = promiseA.then(function(result) {
    *     // success: do something and resolve promiseB
    *     //          with the old or a new result
@@ -7067,17 +11530,28 @@ function qFactory(nextTick, exceptionHandler) {
    *     }
    *     return $q.reject(reason);
    *   });
-   * 
+ * ``` * * @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) { + var result = defer(); + result.reject(reason); + return result.promise; + }; + + var createInternalRejectedPromise = function(reason) { return { then: function(callback, errback) { var result = defer(); nextTick(function() { - result.resolve((errback || defaultErrback)(reason)); + try { + result.resolve((isFunction(errback) ? errback : defaultErrback)(reason)); + } catch(e) { + result.reject(e); + exceptionHandler(e); + } }); return result.promise; } @@ -7086,9 +11560,10 @@ function qFactory(nextTick, exceptionHandler) { /** - * @ngdoc - * @name ng.$q#when - * @methodOf ng.$q + * @ngdoc method + * @name $q#when + * @kind function + * * @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 an object that might or might not be a promise, or if @@ -7097,25 +11572,33 @@ function qFactory(nextTick, exceptionHandler) { * @param {*} value Value or a promise * @returns {Promise} Returns a promise of the passed value or promise */ - var when = function(value, callback, errback) { + var when = function(value, callback, errback, progressback) { var result = defer(), done; var wrappedCallback = function(value) { try { - return (callback || defaultCallback)(value); + return (isFunction(callback) ? callback : defaultCallback)(value); + } catch (e) { + exceptionHandler(e); + return reject(e); + } + }; + + var wrappedErrback = function(reason) { + try { + return (isFunction(errback) ? errback : defaultErrback)(reason); } catch (e) { exceptionHandler(e); return reject(e); } }; - var wrappedErrback = function(reason) { + var wrappedProgressback = function(progress) { try { - return (errback || defaultErrback)(reason); + return (isFunction(progressback) ? progressback : defaultCallback)(progress); } catch (e) { exceptionHandler(e); - return reject(e); } }; @@ -7123,11 +11606,14 @@ function qFactory(nextTick, exceptionHandler) { ref(value).then(function(value) { if (done) return; done = true; - result.resolve(ref(value).then(wrappedCallback, wrappedErrback)); + result.resolve(ref(value).then(wrappedCallback, wrappedErrback, wrappedProgressback)); }, function(reason) { if (done) return; done = true; result.resolve(wrappedErrback(reason)); + }, function(progress) { + if (done) return; + result.notify(wrappedProgressback(progress)); }); }); @@ -7146,36 +11632,38 @@ function qFactory(nextTick, exceptionHandler) { /** - * @ngdoc - * @name ng.$q#all - * @methodOf ng.$q + * @ngdoc method + * @name $q#all + * @kind function + * * @description * Combines multiple promises into a single promise that is resolved when all of the input * promises are resolved. * - * @param {Array.} promises An array of promises. - * @returns {Promise} Returns a single promise that will be resolved with an array of values, - * each value corresponding 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. + * @param {Array.|Object.} promises An array or hash of promises. + * @returns {Promise} Returns a single promise that will be resolved with an array/hash of values, + * each value corresponding to the promise at the same index/key in the `promises` array/hash. + * If any of the promises is resolved with a rejection, this resulting promise will be rejected + * with the same rejection value. */ 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); - }); + counter = 0, + results = isArray(promises) ? [] : {}; + + forEach(promises, function(promise, key) { + counter++; + ref(promise).then(function(value) { + if (results.hasOwnProperty(key)) return; + results[key] = value; + if (!(--counter)) deferred.resolve(results); + }, function(reason) { + if (results.hasOwnProperty(key)) return; + deferred.reject(reason); }); - } else { + }); + + if (counter === 0) { deferred.resolve(results); } @@ -7190,510 +11678,38 @@ function qFactory(nextTick, exceptionHandler) { }; } -/** - * @ngdoc object - * @name ng.$routeProvider - * @function - * - * @description - * - * Used for configuring routes. See {@link ng.$route $route} for an example. - */ -function $RouteProvider(){ - var routes = {}; - - /** - * @ngdoc method - * @name ng.$routeProvider#when - * @methodOf ng.$routeProvider - * - * @param {string} path Route path (matched against `$location.path`). If `$location.path` - * contains redundant 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 exactly match the - * route definition. - * - * `path` can contain named groups starting with a colon (`:name`). All characters up to the - * next slash are matched and stored in `$routeParams` under the given `name` after the route - * is resolved. - * - * @param {Object} route Mapping information to be assigned to `$route.current` on route - * match. - * - * Object properties: - * - * - `controller` – `{(string|function()=}` – Controller fn that should be associated with newly - * created scope or the name of a {@link angular.Module#controller registered controller} - * if passed as a string. - * - `template` – `{string=}` – html template as a string that should be used by - * {@link ng.directive:ngView ngView} or - * {@link ng.directive:ngInclude ngInclude} directives. - * this property takes precedence over `templateUrl`. - * - `templateUrl` – `{string=}` – path to an html template that should be used by - * {@link ng.directive:ngView ngView}. - * - `resolve` - `{Object.=}` - An optional map of dependencies which should - * be injected into the controller. If any of these dependencies are promises, they will be - * resolved and converted to a value before the controller is instantiated and the - * `$routeChangeSuccess` event is fired. The map object is: - * - * - `key` – `{string}`: a name of a dependency to be injected into the controller. - * - `factory` - `{string|function}`: If `string` then it is an alias for a service. - * Otherwise if function, then it is {@link api/AUTO.$injector#invoke injected} - * and the return value is treated as the dependency. If the result is a promise, it is resolved - * before its value is injected into the controller. Be aware that `ngRoute.$routeParams` will - * still refer to the previous route within these resolve functions. Use `$route.current.params` - * to access the new route parameters, instead. - * - * - `redirectTo` – {(string|function())=} – value to update - * {@link ng.$location $location} path with and trigger route redirection. - * - * If `redirectTo` is a function, it will be called with the following parameters: - * - * - `{Object.}` - route parameters extracted from the current - * `$location.path()` by applying the current route templateUrl. - * - `{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} self - * - * @description - * Adds a new route definition to the `$route` service. - */ - this.when = function(path, route) { - routes[path] = extend({reloadOnSearch: true}, route); - - // 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 this; - }; - - /** - * @ngdoc method - * @name ng.$routeProvider#otherwise - * @methodOf 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`. - * @returns {Object} self - */ - this.otherwise = function(params) { - this.when(null, params); - return this; - }; - - - this.$get = ['$rootScope', '$location', '$routeParams', '$q', '$injector', '$http', '$templateCache', - function( $rootScope, $location, $routeParams, $q, $injector, $http, $templateCache) { - - /** - * @ngdoc object - * @name ng.$route - * @requires $location - * @requires $routeParams - * - * @property {Object} current Reference to the current route definition. - * The route definition contains: - * - * - `controller`: The controller constructor as define in route definition. - * - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for - * controller instantiation. The `locals` contain - * the resolved values of the `resolve` map. Additionally the `locals` also contain: - * - * - `$scope` - The current route scope. - * - `$template` - The current route template HTML. - * - * @property {Array.} 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 ng.$routeProvider $routeProvider}'s API. - * - * The `$route` service is typically used in conjunction with {@link ng.directive:ngView ngView} - * directive and the {@link 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 `ngView` pulls in the partial. - - Note that this example is using {@link ng.directive:script inlined templates} - to get it working on jsfiddle as well. - - - -
- Choose: - Moby | - Moby: Ch1 | - Gatsby | - Gatsby: Ch4 | - Scarlet Letter
- -
-
- -
$location.path() = {{$location.path()}}
-
$route.current.templateUrl = {{$route.current.templateUrl}}
-
$route.current.params = {{$route.current.params}}
-
$route.current.scope.name = {{$route.current.scope.name}}
-
$routeParams = {{$routeParams}}
-
-
- - - controller: {{name}}
- Book Id: {{params.bookId}}
-
- - - controller: {{name}}
- Book Id: {{params.bookId}}
- Chapter Id: {{params.chapterId}} -
- - - angular.module('ngView', [], function($routeProvider, $locationProvider) { - $routeProvider.when('/Book/:bookId', { - templateUrl: 'book.html', - controller: BookCntl, - resolve: { - // I will cause a 1 second delay - delay: function($q, $timeout) { - var delay = $q.defer(); - $timeout(delay.resolve, 1000); - return delay.promise; - } - } - }); - $routeProvider.when('/Book/:bookId/ch/:chapterId', { - templateUrl: '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; - } - - - - 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(); - sleep(2); // promises are not part of scenario waiting - content = element('.doc-example-live [ng-view]').text(); - expect(content).toMatch(/controller\: BookCntl/); - expect(content).toMatch(/Book Id\: Scarlet/); - }); - -
- */ - - /** - * @ngdoc event - * @name ng.$route#$routeChangeStart - * @eventOf ng.$route - * @eventType broadcast on root scope - * @description - * Broadcasted before a route change. At this point the route services starts - * resolving all of the dependencies needed for the route change to occurs. - * Typically this involves fetching the view template as well as any dependencies - * defined in `resolve` route property. Once all of the dependencies are resolved - * `$routeChangeSuccess` is fired. - * - * @param {Route} next Future route information. - * @param {Route} current Current route information. - */ - - /** - * @ngdoc event - * @name ng.$route#$routeChangeSuccess - * @eventOf ng.$route - * @eventType broadcast on root scope - * @description - * Broadcasted after a route dependencies are resolved. - * {@link ng.directive:ngView ngView} listens for the directive - * to instantiate the controller and render the view. - * - * @param {Object} angularEvent Synthetic event object. - * @param {Route} current Current route information. - * @param {Route|Undefined} previous Previous route information, or undefined if current is first route entered. - */ - - /** - * @ngdoc event - * @name ng.$route#$routeChangeError - * @eventOf ng.$route - * @eventType broadcast on root scope - * @description - * Broadcasted if any of the resolve promises are rejected. - * - * @param {Route} current Current route information. - * @param {Route} previous Previous route information. - * @param {Route} rejection Rejection of the promise. Usually the error of the failed promise. - */ - - /** - * @ngdoc event - * @name ng.$route#$routeUpdate - * @eventOf 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 forceReload = false, - $route = { - routes: routes, - - /** - * @ngdoc method - * @name ng.$route#reload - * @methodOf ng.$route - * - * @description - * Causes `$route` service to reload the current route even if - * {@link ng.$location $location} hasn't changed. - * - * As a result of that, {@link ng.directive:ngView ngView} - * creates new scope, reinstantiates the controller. - */ - reload: function() { - forceReload = true; - $rootScope.$evalAsync(updateRoute); - } - }; - - $rootScope.$on('$locationChangeSuccess', updateRoute); - - return $route; - - ///////////////////////////////////////////////////// - - /** - * @param on {string} current url - * @param when {string} route when template to match the url against - * @return {?Object} - */ - 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 - - // Escape regexp special characters. - when = '^' + when.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&") + '$'; - var regex = '', - params = [], - dst = {}; - - var re = /:(\w+)/g, - paramMatch, - lastMatchedIndex = 0; - - while ((paramMatch = re.exec(when)) !== null) { - // Find each :param in `when` and replace it with a capturing group. - // Append all other sections of when unchanged. - regex += when.slice(lastMatchedIndex, paramMatch.index); - regex += '([^\\/]*)'; - params.push(paramMatch[1]); - lastMatchedIndex = re.lastIndex; - } - // Append trailing path part. - regex += when.substr(lastMatchedIndex); - - 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('$routeChangeStart', 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(); - } - } +function $$RAFProvider(){ //rAF + this.$get = ['$window', '$timeout', function($window, $timeout) { + var requestAnimationFrame = $window.requestAnimationFrame || + $window.webkitRequestAnimationFrame || + $window.mozRequestAnimationFrame; + + var cancelAnimationFrame = $window.cancelAnimationFrame || + $window.webkitCancelAnimationFrame || + $window.mozCancelAnimationFrame || + $window.webkitCancelRequestAnimationFrame; + + var rafSupported = !!requestAnimationFrame; + var raf = rafSupported + ? function(fn) { + var id = requestAnimationFrame(fn); + return function() { + cancelAnimationFrame(id); + }; } + : function(fn) { + var timer = $timeout(fn, 16.66, false); // 1000 / 60 = 16.666 + return function() { + $timeout.cancel(timer); + }; + }; - $q.when(next). - then(function() { - if (next) { - var keys = [], - values = [], - template; - - forEach(next.resolve || {}, function(value, key) { - keys.push(key); - values.push(isString(value) ? $injector.get(value) : $injector.invoke(value)); - }); - if (isDefined(template = next.template)) { - } else if (isDefined(template = next.templateUrl)) { - template = $http.get(template, {cache: $templateCache}). - then(function(response) { return response.data; }); - } - if (isDefined(template)) { - keys.push('$template'); - values.push(template); - } - return $q.all(values).then(function(values) { - var locals = {}; - forEach(values, function(value, index) { - locals[keys[index]] = value; - }); - return locals; - }); - } - }). - // after route change - then(function(locals) { - if (next == $route.current) { - if (next) { - next.locals = locals; - copy(next.params, $routeParams); - } - $rootScope.$broadcast('$routeChangeSuccess', next, last); - } - }, function(error) { - if (next == $route.current) { - $rootScope.$broadcast('$routeChangeError', next, last, error); - } - }); - } - } - - - /** - * @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 = switchRouteMatcher($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:{}}); - } + raf.supported = rafSupported; - /** - * @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(''); - } + return raf; }]; } -/** - * @ngdoc object - * @name ng.$routeParams - * @requires $route - * - * @description - * Current set of route parameters. The route parameters are a combination of the - * {@link ng.$location $location} `search()`, and `path()`. The `path` parameters - * are extracted when the {@link 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. - * - * Note that the `$routeParams` are only updated *after* a route change completes successfully. - * This means that you cannot rely on `$routeParams` being correct in route resolve functions. - * Instead you can use `$route.current.params` to access the new route's parameters. - * - * @example - *
- *  // 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'}
- * 
- */ -function $RouteParamsProvider() { - this.$get = valueFn({}); -} - /** * DESIGN NOTES * @@ -7709,7 +11725,7 @@ function $RouteParamsProvider() { * * 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 beginning (shift) instead of at the end (push) + * items to the array at the beginning (unshift) instead of at the end (push) * * Child scopes are created and removed often * - Using an array would be slow since inserts in middle are expensive so we use linked list @@ -7721,39 +11737,50 @@ function $RouteParamsProvider() { /** - * @ngdoc object - * @name ng.$rootScopeProvider + * @ngdoc provider + * @name $rootScopeProvider * @description * * Provider for the $rootScope service. */ /** - * @ngdoc function - * @name ng.$rootScopeProvider#digestTtl - * @methodOf ng.$rootScopeProvider + * @ngdoc method + * @name $rootScopeProvider#digestTtl * @description * - * Sets the number of digest iterations the scope should attempt to execute before giving up and + * Sets the number of `$digest` iterations the scope should attempt to execute before giving up and * assuming that the model is unstable. * * The current default is 10 iterations. * + * In complex applications it's possible that the dependencies between `$watch`s will result in + * several digest iterations. However if an application needs more than the default 10 digest + * iterations for its model to stabilize then you should investigate what is causing the model to + * continuously change during the digest. + * + * Increasing the TTL could have performance implications, so you should not change it without + * proper justification. + * * @param {number} limit The number of digest iterations. */ /** - * @ngdoc object - * @name ng.$rootScope + * @ngdoc service + * @name $rootScope * @description * * Every application has a single root {@link 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/scope developer guide on scopes}. + * All other scopes are descendant scopes of the root scope. Scopes provide separation + * between the model and the view, via a mechanism for watching the model for changes. + * They also provide an event emission/broadcast and subscription facility. See the + * {@link guide/scope developer guide on scopes}. */ function $RootScopeProvider(){ var TTL = 10; + var $rootScopeMinErr = minErr('$rootScope'); + var lastDirtyWatch = null; this.digestTtl = function(value) { if (arguments.length) { @@ -7762,45 +11789,27 @@ function $RootScopeProvider(){ return TTL; }; - this.$get = ['$injector', '$exceptionHandler', '$parse', - function( $injector, $exceptionHandler, $parse) { + this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser', + function( $injector, $exceptionHandler, $parse, $browser) { /** - * @ngdoc function - * @name ng.$rootScope.Scope + * @ngdoc type + * @name $rootScope.Scope * * @description * A root scope can be retrieved using the {@link ng.$rootScope $rootScope} key from the - * {@link AUTO.$injector $injector}. Child scopes are created using the + * {@link auto.$injector $injector}. Child scopes are created using the * {@link 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. - *
-        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() {
-             scope.greeting = scope.salutation + ' ' + scope.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!');
-        });
-     * 
+ * ```html + * + * ``` * * # Inheritance * A scope can inherit from a parent scope, as in this example: - *
+     * ```js
          var parent = $rootScope;
          var child = parent.$new();
 
@@ -7811,14 +11820,15 @@ function $RootScopeProvider(){
          child.salutation = "Welcome";
          expect(child.salutation).toEqual('Welcome');
          expect(parent.salutation).toEqual('Hello');
-     * 
+ * ``` * * - * @param {Object.=} providers Map of service factory which need to be provided - * for the current scope. Defaults to {@link ng}. + * @param {Object.=} providers Map of service factory which need to be + * provided for the current scope. Defaults to {@link ng}. * @param {Object.=} 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. + * 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. * */ @@ -7830,69 +11840,74 @@ function $RootScopeProvider(){ this['this'] = this.$root = this; this.$$destroyed = false; this.$$asyncQueue = []; + this.$$postDigestQueue = []; this.$$listeners = {}; + this.$$listenerCount = {}; this.$$isolateBindings = {}; } /** * @ngdoc property - * @name ng.$rootScope.Scope#$id - * @propertyOf ng.$rootScope.Scope + * @name $rootScope.Scope#$id * @returns {number} Unique scope ID (monotonically increasing alphanumeric sequence) useful for * debugging. */ Scope.prototype = { + constructor: Scope, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$new - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$new + * @kind function * * @description * Creates a new child {@link ng.$rootScope.Scope scope}. * * The parent scope will propagate the {@link ng.$rootScope.Scope#$digest $digest()} and - * {@link ng.$rootScope.Scope#$digest $digest()} events. The scope can be removed from the scope - * hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}. + * {@link ng.$rootScope.Scope#$digest $digest()} events. The scope can be removed from the + * scope hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}. * - * {@link 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. + * {@link 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. * - * @param {boolean} isolate if true then the scope does not prototypically inherit from the + * @param {boolean} isolate If true, then the scope does not prototypically inherit from the * parent scope. The scope is isolated, as it can not see parent scope properties. - * When creating widgets it is useful for the widget to not accidentally read parent + * When creating widgets, it is useful for the widget to not accidentally read parent * state. * * @returns {Object} The newly created child scope. * */ $new: function(isolate) { - var Child, + var ChildScope, 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; + // ensure that there is just one async queue per $rootScope and its children + child.$$asyncQueue = this.$$asyncQueue; + child.$$postDigestQueue = this.$$postDigestQueue; } 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(); + // Only create a child scope class if somebody asks for one, + // but cache it to allow the VM to optimize lookups. + if (!this.$$childScopeClass) { + this.$$childScopeClass = function() { + this.$$watchers = this.$$nextSibling = + this.$$childHead = this.$$childTail = null; + this.$$listeners = {}; + this.$$listenerCount = {}; + this.$id = nextUid(); + this.$$childScopeClass = null; + }; + this.$$childScopeClass.prototype = this; + } + child = new this.$$childScopeClass(); } 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; @@ -7904,33 +11919,37 @@ function $RootScopeProvider(){ }, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$watch - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$watch + * @kind function * * @description * Registers a `listener` callback to be executed whenever the `watchExpression` changes. * - * - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#$digest $digest()} and - * should return the value which will be watched. (Since {@link ng.$rootScope.Scope#$digest $digest()} - * reruns when it detects changes the `watchExpression` can execute multiple times per + * - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#$digest + * $digest()} and should return the value that will be watched. (Since + * {@link ng.$rootScope.Scope#$digest $digest()} reruns when it detects changes the + * `watchExpression` can execute multiple times per * {@link 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, the - * {@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 10 to prevent an infinite loop deadlock. + * see below). Inequality is determined according to reference inequality, + * [strict comparison](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Comparison_Operators) + * via the `!==` Javascript operator, unless `objectEquality == true` + * (see next point) + * - When `objectEquality == true`, inequality of the `watchExpression` is determined + * according to the {@link angular.equals} function. To save the value of the object for + * later comparison, the {@link angular.copy} function is used. This therefore means that + * watching complex objects 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 10 to prevent an infinite loop deadlock. * * * If you want to be notified whenever {@link ng.$rootScope.Scope#$digest $digest} is called, * you can register a `watchExpression` function with no `listener`. (Since `watchExpression` - * can execute multiple times per {@link ng.$rootScope.Scope#$digest $digest} cycle when a change is - * detected, be prepared for multiple calls to your listener.) + * can execute multiple times per {@link 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 ng.$rootScope.Scope#$evalAsync $evalAsync}) to initialize the @@ -7939,32 +11958,70 @@ function $RootScopeProvider(){ * can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the * listener was called due to initialization. * + * The example below contains an illustration of using a function as your $watch listener + * * * # Example - *
+       * ```js
            // 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) { scope.counter = scope.counter + 1; });
+           scope.$watch('name', function(newValue, oldValue) {
+             scope.counter = scope.counter + 1;
+           });
            expect(scope.counter).toEqual(0);
 
            scope.$digest();
-           // no variable change
-           expect(scope.counter).toEqual(0);
+           // the listener is always called during the first $digest loop after it was registered
+           expect(scope.counter).toEqual(1);
 
-           scope.name = 'adam';
            scope.$digest();
+           // but now it will not be called unless the value changes
            expect(scope.counter).toEqual(1);
-       * 
+ + scope.name = 'adam'; + scope.$digest(); + expect(scope.counter).toEqual(2); + + + + // Using a listener function + var food; + scope.foodCounter = 0; + expect(scope.foodCounter).toEqual(0); + scope.$watch( + // This is the listener function + function() { return food; }, + // This is the change handler + function(newValue, oldValue) { + if ( newValue !== oldValue ) { + // Only increment the counter if the value changed + scope.foodCounter = scope.foodCounter + 1; + } + } + ); + // No digest has been run so the counter will be zero + expect(scope.foodCounter).toEqual(0); + + // Run the digest but since food has not changed count will still be zero + scope.$digest(); + expect(scope.foodCounter).toEqual(0); + + // Update food and run digest. Now the counter will increment + food = 'cheeseburger'; + scope.$digest(); + expect(scope.foodCounter).toEqual(1); + + * ``` * * * * @param {(function()|string)} watchExpression Expression that is evaluated on each - * {@link ng.$rootScope.Scope#$digest $digest} cycle. A change in the return value triggers a - * call to the `listener`. + * {@link ng.$rootScope.Scope#$digest $digest} cycle. A change in the return value triggers + * a call to the `listener`. * * - `string`: Evaluated as {@link guide/expression expression} * - `function(scope)`: called with current `scope` as a parameter. @@ -7972,9 +12029,11 @@ function $RootScopeProvider(){ * the `watchExpression` changes. * * - `string`: Evaluated as {@link guide/expression expression} - * - `function(newValue, oldValue, scope)`: called with current and previous values as parameters. + * - `function(newValue, oldValue, scope)`: called with current and previous values as + * parameters. * - * @param {boolean=} objectEquality Compare object for equality rather than for reference. + * @param {boolean=} objectEquality Compare for object equality using {@link angular.equals} instead of + * comparing for reference equality. * @returns {function()} Returns a deregistration function for this listener. */ $watch: function(watchExp, listener, objectEquality) { @@ -7989,52 +12048,239 @@ function $RootScopeProvider(){ eq: !!objectEquality }; + lastDirtyWatch = null; + // 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 (typeof watchExp == 'string' && get.constant) { + var originalFn = watcher.fn; + watcher.fn = function(newVal, oldVal, scope) { + originalFn.call(this, newVal, oldVal, scope); + arrayRemove(array, watcher); + }; + } + 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); + // 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 deregisterWatch() { + arrayRemove(array, watcher); + lastDirtyWatch = null; + }; + }, + + + /** + * @ngdoc method + * @name $rootScope.Scope#$watchCollection + * @kind function + * + * @description + * Shallow watches the properties of an object and fires whenever any of the properties change + * (for arrays, this implies watching the array items; for object maps, this implies watching + * the properties). If a change is detected, the `listener` callback is fired. + * + * - The `obj` collection is observed via standard $watch operation and is examined on every + * call to $digest() to see if any items have been added, removed, or moved. + * - The `listener` is called whenever anything within the `obj` has changed. Examples include + * adding, removing, and moving items belonging to an object or array. + * + * + * # Example + * ```js + $scope.names = ['igor', 'matias', 'misko', 'james']; + $scope.dataCount = 4; + + $scope.$watchCollection('names', function(newNames, oldNames) { + $scope.dataCount = newNames.length; + }); + + expect($scope.dataCount).toEqual(4); + $scope.$digest(); + + //still at 4 ... no changes + expect($scope.dataCount).toEqual(4); + + $scope.names.pop(); + $scope.$digest(); + + //now there's been a change + expect($scope.dataCount).toEqual(3); + * ``` + * + * + * @param {string|function(scope)} obj Evaluated as {@link guide/expression expression}. The + * expression value should evaluate to an object or an array which is observed on each + * {@link ng.$rootScope.Scope#$digest $digest} cycle. Any shallow change within the + * collection will trigger a call to the `listener`. + * + * @param {function(newCollection, oldCollection, scope)} listener a callback function called + * when a change is detected. + * - The `newCollection` object is the newly modified data obtained from the `obj` expression + * - The `oldCollection` object is a copy of the former collection data. + * Due to performance considerations, the`oldCollection` value is computed only if the + * `listener` function declares two or more arguments. + * - The `scope` argument refers to the current scope. + * + * @returns {function()} Returns a de-registration function for this listener. When the + * de-registration function is executed, the internal watch operation is terminated. + */ + $watchCollection: function(obj, listener) { + var self = this; + // the current value, updated on each dirty-check run + var newValue; + // a shallow copy of the newValue from the last dirty-check run, + // updated to match newValue during dirty-check run + var oldValue; + // a shallow copy of the newValue from when the last change happened + var veryOldValue; + // only track veryOldValue if the listener is asking for it + var trackVeryOldValue = (listener.length > 1); + var changeDetected = 0; + var objGetter = $parse(obj); + var internalArray = []; + var internalObject = {}; + var initRun = true; + var oldLength = 0; + + function $watchCollectionWatch() { + newValue = objGetter(self); + var newLength, key; + + if (!isObject(newValue)) { // if primitive + if (oldValue !== newValue) { + oldValue = newValue; + changeDetected++; + } + } else if (isArrayLike(newValue)) { + if (oldValue !== internalArray) { + // we are transitioning from something which was not an array into array. + oldValue = internalArray; + oldLength = oldValue.length = 0; + changeDetected++; + } + + newLength = newValue.length; + + if (oldLength !== newLength) { + // if lengths do not match we need to trigger change notification + changeDetected++; + oldValue.length = oldLength = newLength; + } + // copy the items to oldValue and look for changes. + for (var i = 0; i < newLength; i++) { + var bothNaN = (oldValue[i] !== oldValue[i]) && + (newValue[i] !== newValue[i]); + if (!bothNaN && (oldValue[i] !== newValue[i])) { + changeDetected++; + oldValue[i] = newValue[i]; + } + } + } else { + if (oldValue !== internalObject) { + // we are transitioning from something which was not an object into object. + oldValue = internalObject = {}; + oldLength = 0; + changeDetected++; + } + // copy the items to oldValue and look for changes. + newLength = 0; + for (key in newValue) { + if (newValue.hasOwnProperty(key)) { + newLength++; + if (oldValue.hasOwnProperty(key)) { + if (oldValue[key] !== newValue[key]) { + changeDetected++; + oldValue[key] = newValue[key]; + } + } else { + oldLength++; + oldValue[key] = newValue[key]; + changeDetected++; + } + } + } + if (oldLength > newLength) { + // we used to have more keys, need to find them and destroy them. + changeDetected++; + for(key in oldValue) { + if (oldValue.hasOwnProperty(key) && !newValue.hasOwnProperty(key)) { + oldLength--; + delete oldValue[key]; + } + } + } + } + return changeDetected; + } + + function $watchCollectionAction() { + if (initRun) { + initRun = false; + listener(newValue, newValue, self); + } else { + listener(newValue, veryOldValue, self); + } + + // make a copy for the next time a collection is changed + if (trackVeryOldValue) { + if (!isObject(newValue)) { + //primitive + veryOldValue = newValue; + } else if (isArrayLike(newValue)) { + veryOldValue = new Array(newValue.length); + for (var i = 0; i < newValue.length; i++) { + veryOldValue[i] = newValue[i]; + } + } else { // if object + veryOldValue = {}; + for (var key in newValue) { + if (hasOwnProperty.call(newValue, key)) { + veryOldValue[key] = newValue[key]; + } + } + } + } + } - return function() { - arrayRemove(array, watcher); - }; + return this.$watch($watchCollectionWatch, $watchCollectionAction); }, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$digest - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$digest + * @kind function * * @description - * Processes all of the {@link ng.$rootScope.Scope#$watch watchers} of the current scope and its children. - * Because a {@link ng.$rootScope.Scope#$watch watcher}'s listener can change the model, the - * `$digest()` keeps calling the {@link 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 10. + * Processes all of the {@link ng.$rootScope.Scope#$watch watchers} of the current scope and + * its children. Because a {@link ng.$rootScope.Scope#$watch watcher}'s listener can change + * the model, the `$digest()` keeps calling the {@link 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 10. * - * Usually you don't call `$digest()` directly in + * Usually, you don't call `$digest()` directly in * {@link ng.directive:ngController controllers} or in * {@link ng.$compileProvider#directive directives}. - * Instead a call to {@link ng.$rootScope.Scope#$apply $apply()} (typically from within a - * {@link ng.$compileProvider#directive directives}) will force a `$digest()`. + * Instead, you should call {@link ng.$rootScope.Scope#$apply $apply()} (typically from within + * a {@link ng.$compileProvider#directive directives}), which will force a `$digest()`. * * If you want to be notified whenever `$digest()` is called, - * you can register a `watchExpression` function with {@link ng.$rootScope.Scope#$watch $watch()} - * with no `listener`. + * you can register a `watchExpression` function with + * {@link 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. + * In unit tests, you may need to call `$digest()` to simulate the scope life cycle. * * # Example - *
+       * ```js
            var scope = ...;
            scope.name = 'misko';
            scope.counter = 0;
@@ -8046,39 +12292,51 @@ function $RootScopeProvider(){
            expect(scope.counter).toEqual(0);
 
            scope.$digest();
-           // no variable change
-           expect(scope.counter).toEqual(0);
+           // the listener is always called during the first $digest loop after it was registered
+           expect(scope.counter).toEqual(1);
 
-           scope.name = 'adam';
            scope.$digest();
+           // but now it will not be called unless the value changes
            expect(scope.counter).toEqual(1);
-       * 
+ + scope.name = 'adam'; + scope.$digest(); + expect(scope.counter).toEqual(2); + * ``` * */ $digest: function() { var watch, value, last, watchers, - asyncQueue, + asyncQueue = this.$$asyncQueue, + postDigestQueue = this.$$postDigestQueue, length, dirty, ttl = TTL, next, current, target = this, watchLog = [], - logIdx, logMsg; + logIdx, logMsg, asyncTask; beginPhase('$digest'); - do { + lastDirtyWatch = null; + + do { // "while dirty" loop dirty = false; current = target; - do { - asyncQueue = current.$$asyncQueue; - while(asyncQueue.length) { - try { - current.$eval(asyncQueue.shift()); - } catch (e) { - $exceptionHandler(e); - } + + while(asyncQueue.length) { + try { + asyncTask = asyncQueue.shift(); + asyncTask.scope.$eval(asyncTask.expression); + } catch (e) { + clearPhase(); + $exceptionHandler(e); } + lastDirtyWatch = null; + } + + traverseScopesLoop: + do { // "traverse the scopes" loop if ((watchers = current.$$watchers)) { // process our watches length = watchers.length; @@ -8087,25 +12345,34 @@ function $RootScopeProvider(){ 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 (watch && (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); + if (watch) { + 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; + lastDirtyWatch = watch; + watch.last = watch.eq ? copy(value, null) : 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); + } + } else if (watch === lastDirtyWatch) { + // If the most recently dirty watcher is now clean, short circuit since the remaining watchers + // have already been tested. + dirty = false; + break traverseScopesLoop; } } } catch (e) { + clearPhase(); $exceptionHandler(e); } } @@ -8114,42 +12381,54 @@ function $RootScopeProvider(){ // 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)))) { + if (!(next = (current.$$childHead || + (current !== target && current.$$nextSibling)))) { while(current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } } } while ((current = next)); - if(dirty && !(ttl--)) { + // `break traverseScopesLoop;` takes us to here + + if((dirty || asyncQueue.length) && !(ttl--)) { clearPhase(); - throw Error(TTL + ' $digest() iterations reached. Aborting!\n' + - 'Watchers fired in the last 5 iterations: ' + toJson(watchLog)); + throw $rootScopeMinErr('infdig', + '{0} $digest() iterations reached. Aborting!\n' + + 'Watchers fired in the last 5 iterations: {1}', + TTL, toJson(watchLog)); } + } while (dirty || asyncQueue.length); clearPhase(); + + while(postDigestQueue.length) { + try { + postDigestQueue.shift()(); + } catch (e) { + $exceptionHandler(e); + } + } }, /** * @ngdoc event - * @name ng.$rootScope.Scope#$destroy - * @eventOf ng.$rootScope.Scope + * @name $rootScope.Scope#$destroy * @eventType broadcast on scope being destroyed * * @description * Broadcasted when a scope and its children are being destroyed. - * + * * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to * clean up DOM bindings before an element is removed from the DOM. */ /** - * @ngdoc function - * @name ng.$rootScope.Scope#$destroy - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$destroy + * @kind function * * @description * Removes the current scope (and all of its children) from the parent scope. Removal implies @@ -8161,57 +12440,78 @@ function $RootScopeProvider(){ * {@link ng.directive:ngRepeat ngRepeat} 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 + * 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 a chance to * perform any necessary cleanup. - * + * * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to * clean up DOM bindings before an element is removed from the DOM. */ $destroy: function() { // we can't destroy the root scope or a scope that has been already destroyed - if ($rootScope == this || this.$$destroyed) return; + if (this.$$destroyed) return; var parent = this.$parent; this.$broadcast('$destroy'); this.$$destroyed = true; + if (this === $rootScope) return; + forEach(this.$$listenerCount, bind(null, decrementListenerCount, this)); + + // sever all the references to parent scopes (after this cleanup, the current scope should + // not be retained by any of our references and should be eligible for garbage collection) 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; - // This is bogus code that works around Chrome's GC leak - // see: https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 + + // All of the code below is bogus code that works around V8's memory leak via optimized code + // and inline caches. + // + // see: + // - https://code.google.com/p/v8/issues/detail?id=2073#c26 + // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909 + // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 + this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead = - this.$$childTail = null; + this.$$childTail = this.$root = null; + + // don't reset these to null in case some async task tries to register a listener/watch/task + this.$$listeners = {}; + this.$$watchers = this.$$asyncQueue = this.$$postDigestQueue = []; + + // prevent NPEs since these methods have references to properties we nulled out + this.$destroy = this.$digest = this.$apply = noop; + this.$on = this.$watch = function() { return noop; }; }, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$eval - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$eval + * @kind 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 Angular expressions. + * Executes the `expression` on the current scope and returns the result. Any exceptions in + * the expression are propagated (uncaught). This is useful when evaluating Angular + * expressions. * * # Example - *
+       * ```js
            var scope = 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);
-       * 
+ * ``` * * @param {(string|function())=} expression An angular expression to be executed. * * - `string`: execute using the rules as defined in {@link guide/expression expression}. * - `function(scope)`: execute the function with the current `scope` parameter. * + * @param {(object)=} locals Local variables object, useful for overriding values in scope. * @returns {*} The result of evaluating the expression. */ $eval: function(expr, locals) { @@ -8219,50 +12519,68 @@ function $RootScopeProvider(){ }, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$evalAsync - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$evalAsync + * @kind 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: + * 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). + * - it will execute after the function that scheduled the evaluation (preferably before DOM + * rendering). * - at least one {@link 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 ng.$exceptionHandler $exceptionHandler} service. * + * __Note:__ if this function is called outside of a `$digest` cycle, a new `$digest` cycle + * will be scheduled. However, it is encouraged to always call code that changes the model + * from within an `$apply` call. That includes code evaluated via `$evalAsync`. + * * @param {(string|function())=} expression An angular expression to be executed. * - * - `string`: execute using the rules as defined in {@link guide/expression expression}. + * - `string`: execute using the rules as defined in {@link guide/expression expression}. * - `function(scope)`: execute the function with the current `scope` parameter. * */ $evalAsync: function(expr) { - this.$$asyncQueue.push(expr); + // if we are outside of an $digest loop and this is the first time we are scheduling async + // task also schedule async auto-flush + if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) { + $browser.defer(function() { + if ($rootScope.$$asyncQueue.length) { + $rootScope.$digest(); + } + }); + } + + this.$$asyncQueue.push({scope: this, expression: expr}); + }, + + $$postDigest : function(fn) { + this.$$postDigestQueue.push(fn); }, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$apply - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$apply + * @kind 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 ng.$exceptionHandler exception handling}, + * `$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 ng.$exceptionHandler exception handling}, * {@link ng.$rootScope.Scope#$digest executing watches}. * * ## Life cycle * * # Pseudo-Code of `$apply()` - *
+       * ```js
            function $apply(expr) {
              try {
                return $eval(expr);
@@ -8272,7 +12590,7 @@ function $RootScopeProvider(){
                $root.$digest();
              }
            }
-       * 
+ * ``` * * * Scope's `$apply()` method transitions through the following stages: @@ -8281,8 +12599,8 @@ function $RootScopeProvider(){ * {@link ng.$rootScope.Scope#$eval $eval()} method. * 2. Any exceptions from the execution of the expression are forwarded to the * {@link ng.$exceptionHandler $exceptionHandler} service. - * 3. The {@link ng.$rootScope.Scope#$watch watch} listeners are fired immediately after the expression - * was executed using the {@link ng.$rootScope.Scope#$digest $digest()} method. + * 3. The {@link ng.$rootScope.Scope#$watch watch} listeners are fired immediately after the + * expression was executed using the {@link ng.$rootScope.Scope#$digest $digest()} method. * * * @param {(string|function())=} exp An angular expression to be executed. @@ -8310,28 +12628,29 @@ function $RootScopeProvider(){ }, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$on - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$on + * @kind function * * @description - * Listens on events of a given type. See {@link ng.$rootScope.Scope#$emit $emit} for discussion of - * event life cycle. + * Listens on events of a given type. See {@link ng.$rootScope.Scope#$emit $emit} for + * discussion of event life cycle. * * The event listener function format is: `function(event, args...)`. 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. + * - `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. - * - `stopPropagation` - `{function=}`: calling `stopPropagation` function will cancel further event - * propagation (available only for events that were `$emit`-ed). - * - `preventDefault` - `{function}`: calling `preventDefault` sets `defaultPrevented` flag to true. + * - `name` - `{string}`: name of the event. + * - `stopPropagation` - `{function=}`: calling `stopPropagation` function will cancel + * further event propagation (available only for events that were `$emit`-ed). + * - `preventDefault` - `{function}`: calling `preventDefault` sets `defaultPrevented` flag + * to true. * - `defaultPrevented` - `{boolean}`: true if `preventDefault` was called. * * @param {string} name Event name to listen on. - * @param {function(event, args...)} listener Function to call when the event is emitted. + * @param {function(event, ...args)} listener Function to call when the event is emitted. * @returns {function()} Returns a deregistration function for this listener. */ $on: function(name, listener) { @@ -8341,33 +12660,43 @@ function $RootScopeProvider(){ } namedListeners.push(listener); + var current = this; + do { + if (!current.$$listenerCount[name]) { + current.$$listenerCount[name] = 0; + } + current.$$listenerCount[name]++; + } while ((current = current.$parent)); + + var self = this; return function() { namedListeners[indexOf(namedListeners, listener)] = null; + decrementListenerCount(self, 1, name); }; }, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$emit - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$emit + * @kind function * * @description * Dispatches an event `name` upwards through the scope hierarchy notifying the * registered {@link ng.$rootScope.Scope#$on} listeners. * * The event life cycle starts at the scope on which `$emit` was called. All - * {@link 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. + * {@link 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 emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed * onto the {@link 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 ng.$rootScope.Scope#$on} + * @param {...*} args Optional one or more arguments which will be passed onto the event listeners. + * @return {Object} Event object (see {@link ng.$rootScope.Scope#$on}). */ $emit: function(name, args) { var empty = [], @@ -8399,12 +12728,14 @@ function $RootScopeProvider(){ continue; } try { + //allow all listeners attached to the current scope to run namedListeners[i].apply(null, listenerArgs); - if (stopPropagation) return event; } catch (e) { $exceptionHandler(e); } } + //if any listener on the current scope stops propagation, prevent bubbling + if (stopPropagation) return event; //traverse upwards scope = scope.$parent; } while (scope); @@ -8414,25 +12745,24 @@ function $RootScopeProvider(){ /** - * @ngdoc function - * @name ng.$rootScope.Scope#$broadcast - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$broadcast + * @kind function * * @description * Dispatches an event `name` downwards to all child scopes (and their children) notifying the * registered {@link ng.$rootScope.Scope#$on} listeners. * * The event life cycle starts at the scope on which `$broadcast` was called. All - * {@link 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. + * {@link 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 ng.$rootScope.Scope#$on listeners} will be passed + * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed * onto the {@link ng.$exceptionHandler $exceptionHandler} service. * * @param {string} name Event name to broadcast. - * @param {...*} args Optional set of arguments which will be passed onto the event listeners. + * @param {...*} args Optional one or more arguments which will be passed onto the event listeners. * @return {Object} Event object, see {@link ng.$rootScope.Scope#$on} */ $broadcast: function(name, args) { @@ -8451,8 +12781,7 @@ function $RootScopeProvider(){ listeners, i, length; //down while you can, then up and next sibling or up and next sibling until back at root - do { - current = next; + while ((current = next)) { event.currentScope = current; listeners = current.$$listeners[name] || []; for (i=0, length = listeners.length; i 7), - hasEvent: function(event) { - // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have - // it. In particular the event is not fired when backspace or delete key are pressed or - // when cut operation is performed. - if (event == 'input' && msie == 9) return false; - - if (isUndefined(eventSupport[event])) { - var divElm = $window.document.createElement('div'); - eventSupport[event] = 'on' + event in divElm; - } - - return eventSupport[event]; - }, - // TODO(i): currently there is no way to feature detect CSP without triggering alerts - csp: false - }; - }]; -} - -/** - * @ngdoc object - * @name 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. - * - * Expressions, like the one defined for the `ngClick` directive in the example - * below, are evaluated with respect to the current scope. Therefore, there is - * no risk of inadvertently coding in a dependency on a global value in such an - * expression. - * - * @example - - - -
- - -
-
- - it('should display the greeting in the input box', function() { - input('greeting').enter('Hello, E2E Tests'); - // If we click the button it will block the test runner - // element(':button').click(); - }); - -
- */ -function $WindowProvider(){ - this.$get = valueFn(window); -} - -/** - * 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.)} 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); + + + function beginPhase(phase) { + if ($rootScope.$$phase) { + throw $rootScopeMinErr('inprog', '{0} already in progress', $rootScope.$$phase); } - return data; - }], - // transform outgoing request data - transformRequest: [function(d) { - return isObject(d) && !isFile(d) ? toJson(d) : d; - }], + $rootScope.$$phase = phase; + } - // default headers - headers: { - common: { - 'Accept': 'application/json, text/plain, */*', - 'X-Requested-With': 'XMLHttpRequest' - }, - post: {'Content-Type': 'application/json;charset=utf-8'}, - put: {'Content-Type': 'application/json;charset=utf-8'} + function clearPhase() { + $rootScope.$$phase = null; } - }; - var providerResponseInterceptors = this.responseInterceptors = []; + function compileToFn(exp, name) { + var fn = $parse(exp); + assertArgFn(fn, name); + return fn; + } - this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', - function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { + function decrementListenerCount(current, count, name) { + do { + current.$$listenerCount[name] -= count; - var defaultCache = $cacheFactory('$http'), - responseInterceptors = []; + if (current.$$listenerCount[name] === 0) { + delete current.$$listenerCount[name]; + } + } while ((current = current.$parent)); + } - forEach(providerResponseInterceptors, function(interceptor) { - responseInterceptors.push( - isString(interceptor) - ? $injector.get(interceptor) - : $injector.invoke(interceptor) - ); - }); + /** + * function used as an initial value for watchers. + * because it's unique we can easily tell it apart from other values + */ + function initWatchVal() {} + }]; +} +/** + * @description + * Private service to sanitize uris for links and images. Used by $compile and $sanitize. + */ +function $$SanitizeUriProvider() { + var aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/, + imgSrcSanitizationWhitelist = /^\s*(https?|ftp|file):|data:image\//; - /** - * @ngdoc function - * @name ng.$http - * @requires $httpBackend - * @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 the 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 ngMock.$httpBackend $httpBackend mock}. - * - * For a higher level of abstraction, please check out the {@link ngResource.$resource - * $resource} service. - * - * The $http API is based on the {@link ng.$q deferred/promise APIs} exposed by - * the $q service. While for simple usage patterns this doesn't matter much, for advanced usage - * it is important to familiarize yourself with these APIs and the 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 ng.$q promise} - * with two $http specific methods: `success` and `error`. - * - *
-     *   $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 an error status.
-     *     });
-     * 
- * - * Since the returned value of calling the $http function is a `promise`, 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. - * - * A response status code between 200 and 299 is considered a success status and - * will result in the success callback being called. Note that if the response is a redirect, - * XMLHttpRequest will transparently follow it, meaning that the error callback will not be - * called for such responses. - * - * # Shortcut methods - * - * Since all invocations of the $http service require passing in an HTTP method and URL, and - * POST/PUT requests require request data to be provided as well, shortcut methods - * were created: - * - *
-     *   $http.get('/someUrl').success(successCallback);
-     *   $http.post('/someUrl', data).success(successCallback);
-     * 
- * - * Complete list of shortcut methods: - * - * - {@link ng.$http#get $http.get} - * - {@link ng.$http#head $http.head} - * - {@link ng.$http#post $http.post} - * - {@link ng.$http#put $http.put} - * - {@link ng.$http#delete $http.delete} - * - {@link 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 POST requests) - * - `Content-Type: application/json` - * - `$httpProvider.defaults.headers.put` (header defaults for PUT requests) - * - `Content-Type: application/json` - * - * To add or overwrite these defaults, simply add or remove a property from these configuration - * objects. To add headers for an HTTP method other than POST or PUT, simply add a new object - * with the lowercased HTTP method name as the key, e.g. - * `$httpProvider.defaults.headers.get['My-Header']='value'`. - * - * Additionally, the defaults can be set at runtime via the `$http.defaults` object in the same - * fashion. - * - * - * # 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 configuration 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 globally augment or override the default transforms, modify the `$httpProvider.defaults.transformRequest` and - * `$httpProvider.defaults.transformResponse` properties. These properties are by default an - * array of transform functions, which allows you to `push` or `unshift` a new transformation function into the - * transformation chain. You can also decide to completely override any default transformations by assigning your - * transformation functions to these properties directly without the array wrapper. - * - * Similarly, to locally override the request/response transforms, augment the `transformRequest` and/or - * `transformResponse` properties of the configuration object passed into `$http`. - * - * - * # 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 from the first request. - * - * - * # Response interceptors - * - * Before you start creating interceptors, be sure to understand the - * {@link 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 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 ng.$q promise} and returns the original or a new promise. - * - *
-     *   // 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
-     *         return response;
-     *       }, 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
-     *     }
-     *   });
-     * 
- * - * - * # 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 website to turn your JSON resource URL into - * {@link http://en.wikipedia.org/wiki/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: - *
-     * ['one','two']
-     * 
- * - * which is vulnerable to attack, your server can return: - *
-     * )]}',
-     * ['one','two']
-     * 
- * - * 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 a 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 the first HTTP GET request. On subsequent XHR 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 sent the request. The token must be - * unique for each user and must be verifiable by the server (to prevent the JavaScript from making - * up its own tokens). We recommend that the token is a digest of your site's authentication - * cookie with a {@link https://en.wikipedia.org/wiki/Salt_(cryptography) salt} for added security. - * + /** + * @description + * Retrieves or overrides the default regular expression that is used for whitelisting of safe + * urls during a[href] sanitization. + * + * The sanitization is a security measure aimed at prevent XSS attacks via html links. + * + * Any url about to be assigned to a[href] via data-binding is first normalized and turned into + * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist` + * regular expression. If a match is found, the original url is written into the dom. Otherwise, + * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. + * + * @param {RegExp=} regexp New regexp to whitelist urls with. + * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for + * chaining otherwise. + */ + this.aHrefSanitizationWhitelist = function(regexp) { + if (isDefined(regexp)) { + aHrefSanitizationWhitelist = regexp; + return this; + } + return aHrefSanitizationWhitelist; + }; + + + /** + * @description + * Retrieves or overrides the default regular expression that is used for whitelisting of safe + * urls during img[src] sanitization. + * + * The sanitization is a security measure aimed at prevent XSS attacks via html links. + * + * Any url about to be assigned to img[src] via data-binding is first normalized and turned into + * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist` + * regular expression. If a match is found, the original url is written into the dom. Otherwise, + * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. + * + * @param {RegExp=} regexp New regexp to whitelist urls with. + * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for + * chaining otherwise. + */ + this.imgSrcSanitizationWhitelist = function(regexp) { + if (isDefined(regexp)) { + imgSrcSanitizationWhitelist = regexp; + return this; + } + return imgSrcSanitizationWhitelist; + }; + + this.$get = function() { + return function sanitizeUri(uri, isImage) { + var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist; + var normalizedVal; + // NOTE: urlResolve() doesn't support IE < 8 so we don't sanitize for that case. + if (!msie || msie >= 8 ) { + normalizedVal = urlResolve(uri).href; + if (normalizedVal !== '' && !normalizedVal.match(regex)) { + return 'unsafe:'+normalizedVal; + } + } + return uri; + }; + }; +} + +var $sceMinErr = minErr('$sce'); + +var SCE_CONTEXTS = { + HTML: 'html', + CSS: 'css', + URL: 'url', + // RESOURCE_URL is a subtype of URL used in contexts where a privileged resource is sourced from a + // url. (e.g. ng-include, script src, templateUrl) + RESOURCE_URL: 'resourceUrl', + JS: 'js' +}; + +// Helper functions follow. + +// Copied from: +// http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962 +// Prereq: s is a string. +function escapeForRegexp(s) { + return s.replace(/([-()\[\]{}+?*.$\^|,:# -1) { + throw $sceMinErr('iwcard', + 'Illegal sequence *** in string matcher. String: {0}', matcher); + } + matcher = escapeForRegexp(matcher). + replace('\\*\\*', '.*'). + replace('\\*', '[^:/.?&;]*'); + return new RegExp('^' + matcher + '$'); + } else if (isRegExp(matcher)) { + // The only other type of matcher allowed is a Regexp. + // Match entire URL / disallow partial matches. + // Flags are reset (i.e. no global, ignoreCase or multiline) + return new RegExp('^' + matcher.source + '$'); + } else { + throw $sceMinErr('imatcher', + 'Matchers may only be "self", string patterns or RegExp objects'); + } +} + + +function adjustMatchers(matchers) { + var adjustedMatchers = []; + if (isDefined(matchers)) { + forEach(matchers, function(matcher) { + adjustedMatchers.push(adjustMatcher(matcher)); + }); + } + return adjustedMatchers; +} + + +/** + * @ngdoc service + * @name $sceDelegate + * @kind function + * + * @description + * + * `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict + * Contextual Escaping (SCE)} services to AngularJS. + * + * Typically, you would configure or override the {@link ng.$sceDelegate $sceDelegate} instead of + * the `$sce` service to customize the way Strict Contextual Escaping works in AngularJS. This is + * because, while the `$sce` provides numerous shorthand methods, etc., you really only need to + * override 3 core functions (`trustAs`, `getTrusted` and `valueOf`) to replace the way things + * work because `$sce` delegates to `$sceDelegate` for these operations. + * + * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} to configure this service. + * + * The default instance of `$sceDelegate` should work out of the box with little pain. While you + * can override it completely to change the behavior of `$sce`, the common case would + * involve configuring the {@link ng.$sceDelegateProvider $sceDelegateProvider} instead by setting + * your own whitelists and blacklists for trusting URLs used for loading AngularJS resources such as + * templates. Refer {@link ng.$sceDelegateProvider#resourceUrlWhitelist + * $sceDelegateProvider.resourceUrlWhitelist} and {@link + * ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} + */ + +/** + * @ngdoc provider + * @name $sceDelegateProvider + * @description + * + * The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate + * $sceDelegate} service. This allows one to get/set the whitelists and blacklists used to ensure + * that the URLs used for sourcing Angular templates are safe. Refer {@link + * ng.$sceDelegateProvider#resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist} and + * {@link ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} + * + * For the general details about this service in Angular, read the main page for {@link ng.$sce + * Strict Contextual Escaping (SCE)}. + * + * **Example**: Consider the following case. + * + * - your app is hosted at url `http://myapp.example.com/` + * - but some of your templates are hosted on other domains you control such as + * `http://srv01.assets.example.com/`,  `http://srv02.assets.example.com/`, etc. + * - and you have an open redirect at `http://myapp.example.com/clickThru?...`. + * + * Here is what a secure configuration for this scenario might look like: + * + *
+ *    angular.module('myApp', []).config(function($sceDelegateProvider) {
+ *      $sceDelegateProvider.resourceUrlWhitelist([
+ *        // Allow same origin resource loads.
+ *        'self',
+ *        // Allow loading from our assets domain.  Notice the difference between * and **.
+ *        'http://srv*.assets.example.com/**']);
+ *
+ *      // The blacklist overrides the whitelist so the open redirect here is blocked.
+ *      $sceDelegateProvider.resourceUrlBlacklist([
+ *        'http://myapp.example.com/clickThru**']);
+ *      });
+ * 
+ */ + +function $SceDelegateProvider() { + this.SCE_CONTEXTS = SCE_CONTEXTS; + + // Resource URLs can also be trusted by policy. + var resourceUrlWhitelist = ['self'], + resourceUrlBlacklist = []; + + /** + * @ngdoc method + * @name $sceDelegateProvider#resourceUrlWhitelist + * @kind function + * + * @param {Array=} whitelist When provided, replaces the resourceUrlWhitelist with the value + * provided. This must be an array or null. A snapshot of this array is used so further + * changes to the array are ignored. + * + * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items + * allowed in this array. + * + * Note: **an empty whitelist array will block all URLs**! + * + * @return {Array} the currently set whitelist array. + * + * The **default value** when no whitelist has been explicitly set is `['self']` allowing only + * same origin resource requests. + * + * @description + * Sets/Gets the whitelist of trusted resource URLs. + */ + this.resourceUrlWhitelist = function (value) { + if (arguments.length) { + resourceUrlWhitelist = adjustMatchers(value); + } + return resourceUrlWhitelist; + }; + + /** + * @ngdoc method + * @name $sceDelegateProvider#resourceUrlBlacklist + * @kind function + * + * @param {Array=} blacklist When provided, replaces the resourceUrlBlacklist with the value + * provided. This must be an array or null. A snapshot of this array is used so further + * changes to the array are ignored. + * + * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items + * allowed in this array. + * + * The typical usage for the blacklist is to **block + * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as + * these would otherwise be trusted but actually return content from the redirected domain. + * + * Finally, **the blacklist overrides the whitelist** and has the final say. + * + * @return {Array} the currently set blacklist array. + * + * The **default value** when no whitelist has been explicitly set is the empty array (i.e. there + * is no blacklist.) + * + * @description + * Sets/Gets the blacklist of trusted resource URLs. + */ + + this.resourceUrlBlacklist = function (value) { + if (arguments.length) { + resourceUrlBlacklist = adjustMatchers(value); + } + return resourceUrlBlacklist; + }; + + this.$get = ['$injector', function($injector) { + + var htmlSanitizer = function htmlSanitizer(html) { + throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); + }; + + if ($injector.has('$sanitize')) { + htmlSanitizer = $injector.get('$sanitize'); + } + + + function matchUrl(matcher, parsedUrl) { + if (matcher === 'self') { + return urlIsSameOrigin(parsedUrl); + } else { + // definitely a regex. See adjustMatchers() + return !!matcher.exec(parsedUrl.href); + } + } + + function isResourceUrlAllowedByPolicy(url) { + var parsedUrl = urlResolve(url.toString()); + var i, n, allowed = false; + // Ensure that at least one item from the whitelist allows this url. + for (i = 0, n = resourceUrlWhitelist.length; i < n; i++) { + if (matchUrl(resourceUrlWhitelist[i], parsedUrl)) { + allowed = true; + break; + } + } + if (allowed) { + // Ensure that no item from the blacklist blocked this url. + for (i = 0, n = resourceUrlBlacklist.length; i < n; i++) { + if (matchUrl(resourceUrlBlacklist[i], parsedUrl)) { + allowed = false; + break; + } + } + } + return allowed; + } + + function generateHolderType(Base) { + var holderType = function TrustedValueHolderType(trustedValue) { + this.$$unwrapTrustedValue = function() { + return trustedValue; + }; + }; + if (Base) { + holderType.prototype = new Base(); + } + holderType.prototype.valueOf = function sceValueOf() { + return this.$$unwrapTrustedValue(); + }; + holderType.prototype.toString = function sceToString() { + return this.$$unwrapTrustedValue().toString(); + }; + return holderType; + } + + var trustedValueHolderBase = generateHolderType(), + byType = {}; + + byType[SCE_CONTEXTS.HTML] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.CSS] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.URL] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.JS] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.RESOURCE_URL] = generateHolderType(byType[SCE_CONTEXTS.URL]); + + /** + * @ngdoc method + * @name $sceDelegate#trustAs * - * @param {object} config Object describing the request to be made and how it should be - * processed. The object has following properties: + * @description + * Returns an object that is trusted by angular for use in specified strict + * contextual escaping contexts (such as ng-bind-html, ng-include, any src + * attribute interpolation, any dom event binding attribute interpolation + * such as for onclick, etc.) that uses the provided value. + * See {@link ng.$sce $sce} for enabling strict contextual escaping. * - * - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) - * - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. - * - **params** – `{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.}` – - * 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.}` – - * 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 ng.$cacheFactory $cacheFactory}, this cache will be used for - * caching. - * - **timeout** – `{number}` – timeout in milliseconds. - * - **withCredentials** - `{boolean}` - whether to to set the `withCredentials` flag on the - * XHR object. See {@link https://developer.mozilla.org/en/http_access_control#section_5 - * requests with credentials} for more information. + * @param {string} type The kind of context in which this value is safe for use. e.g. url, + * resourceUrl, html, js and css. + * @param {*} value The value that that should be considered trusted/safe. + * @returns {*} A value that can be used to stand in for the provided `value` in places + * where Angular expects a $sce.trustAs() return value. + */ + function trustAs(type, trustedValue) { + var Constructor = (byType.hasOwnProperty(type) ? byType[type] : null); + if (!Constructor) { + throw $sceMinErr('icontext', + 'Attempted to trust a value in invalid context. Context: {0}; Value: {1}', + type, trustedValue); + } + if (trustedValue === null || trustedValue === undefined || trustedValue === '') { + return trustedValue; + } + // All the current contexts in SCE_CONTEXTS happen to be strings. In order to avoid trusting + // mutable objects, we ensure here that the value passed in is actually a string. + if (typeof trustedValue !== 'string') { + throw $sceMinErr('itype', + 'Attempted to trust a non-string value in a content requiring a string: Context: {0}', + type); + } + return new Constructor(trustedValue); + } + + /** + * @ngdoc method + * @name $sceDelegate#valueOf * - * @returns {HttpPromise} Returns a {@link 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: + * @description + * If the passed parameter had been returned by a prior call to {@link ng.$sceDelegate#trustAs + * `$sceDelegate.trustAs`}, returns the value that had been passed to {@link + * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. * - * - **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. + * If the passed parameter is not a value that had been returned by {@link + * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}, returns it as-is. * - * @property {Array.} pendingRequests Array of config objects for currently pending - * requests. This is primarily meant to be used for debugging purposes. + * @param {*} value The result of a prior {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} + * call or anything else. + * @returns {*} The `value` that was originally provided to {@link ng.$sceDelegate#trustAs + * `$sceDelegate.trustAs`} if `value` is the result of such a call. Otherwise, returns + * `value` unchanged. + */ + function valueOf(maybeTrusted) { + if (maybeTrusted instanceof trustedValueHolderBase) { + return maybeTrusted.$$unwrapTrustedValue(); + } else { + return maybeTrusted; + } + } + + /** + * @ngdoc method + * @name $sceDelegate#getTrusted * + * @description + * Takes the result of a {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} call and + * returns the originally supplied value if the queried context type is a supertype of the + * created type. If this condition isn't satisfied, throws an exception. * - * @example - - -
- - -
- - - -
http status code: {{status}}
-
http response data: {{data}}
-
-
- - function FetchCtrl($scope, $http, $templateCache) { - $scope.method = 'GET'; - $scope.url = 'http-hello.html'; - - $scope.fetch = function() { - $scope.code = null; - $scope.response = null; - - $http({method: $scope.method, url: $scope.url, cache: $templateCache}). - success(function(data, status) { - $scope.status = status; - $scope.data = data; - }). - error(function(data, status) { - $scope.data = data || "Request failed"; - $scope.status = status; - }); - }; + * @param {string} type The kind of context in which this value is to be used. + * @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs + * `$sceDelegate.trustAs`} call. + * @returns {*} The value the was originally provided to {@link ng.$sceDelegate#trustAs + * `$sceDelegate.trustAs`} if valid in this context. Otherwise, throws an exception. + */ + function getTrusted(type, maybeTrusted) { + if (maybeTrusted === null || maybeTrusted === undefined || maybeTrusted === '') { + return maybeTrusted; + } + var constructor = (byType.hasOwnProperty(type) ? byType[type] : null); + if (constructor && maybeTrusted instanceof constructor) { + return maybeTrusted.$$unwrapTrustedValue(); + } + // If we get here, then we may only take one of two actions. + // 1. sanitize the value for the requested type, or + // 2. throw an exception. + if (type === SCE_CONTEXTS.RESOURCE_URL) { + if (isResourceUrlAllowedByPolicy(maybeTrusted)) { + return maybeTrusted; + } else { + throw $sceMinErr('insecurl', + 'Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}', + maybeTrusted.toString()); + } + } else if (type === SCE_CONTEXTS.HTML) { + return htmlSanitizer(maybeTrusted); + } + throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); + } - $scope.updateModel = function(method, url) { - $scope.method = method; - $scope.url = url; - }; - } - - - Hello, $http! - - - 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')).toMatch(/Hello, \$http!/); - }); + return { trustAs: trustAs, + getTrusted: getTrusted, + valueOf: valueOf }; + }]; +} - 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'); - }); - -
- */ - function $http(config) { - config.method = uppercase(config.method); +/** + * @ngdoc provider + * @name $sceProvider + * @description + * + * The $sceProvider provider allows developers to configure the {@link ng.$sce $sce} service. + * - enable/disable Strict Contextual Escaping (SCE) in a module + * - override the default implementation with a custom delegate + * + * Read more about {@link ng.$sce Strict Contextual Escaping (SCE)}. + */ - var reqTransformFn = config.transformRequest || $config.transformRequest, - respTransformFn = config.transformResponse || $config.transformResponse, - reqHeaders = extend({}, config.headers), - defHeaders = extend( - {'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN']}, - $config.headers.common, - $config.headers[lowercase(config.method)] - ), - reqData, - defHeaderName, lowercaseDefHeaderName, headerName, - promise; - - // using for-in instead of forEach to avoid unecessary iteration after header has been found - defaultHeadersIteration: - for(defHeaderName in defHeaders) { - lowercaseDefHeaderName = lowercase(defHeaderName); - for(headerName in config.headers) { - if (lowercase(headerName) === lowercaseDefHeaderName) { - continue defaultHeadersIteration; - } - } - reqHeaders[defHeaderName] = defHeaders[defHeaderName]; - } +/* jshint maxlen: false*/ - // strip content-type if data is undefined - if (isUndefined(config.data)) { - for(var header in reqHeaders) { - if (lowercase(header) === 'content-type') { - delete reqHeaders[header]; - break; - } - } - } +/** + * @ngdoc service + * @name $sce + * @kind function + * + * @description + * + * `$sce` is a service that provides Strict Contextual Escaping services to AngularJS. + * + * # Strict Contextual Escaping + * + * Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain + * contexts to result in a value that is marked as safe to use for that context. One example of + * such a context is binding arbitrary html controlled by the user via `ng-bind-html`. We refer + * to these contexts as privileged or SCE contexts. + * + * As of version 1.2, Angular ships with SCE enabled by default. + * + * Note: When enabled (the default), IE8 in quirks mode is not supported. In this mode, IE8 allows + * one to execute arbitrary javascript by the use of the expression() syntax. Refer + * to learn more about them. + * You can ensure your document is in standards mode and not quirks mode by adding `` + * to the top of your HTML document. + * + * SCE assists in writing code in way that (a) is secure by default and (b) makes auditing for + * security vulnerabilities such as XSS, clickjacking, etc. a lot easier. + * + * Here's an example of a binding in a privileged context: + * + *
+ *     
+ *     
+ *
+ * + * Notice that `ng-bind-html` is bound to `userHtml` controlled by the user. With SCE + * disabled, this application allows the user to render arbitrary HTML into the DIV. + * In a more realistic example, one may be rendering user comments, blog articles, etc. via + * bindings. (HTML is just one example of a context where rendering user controlled input creates + * security vulnerabilities.) + * + * For the case of HTML, you might use a library, either on the client side, or on the server side, + * to sanitize unsafe HTML before binding to the value and rendering it in the document. + * + * How would you ensure that every place that used these types of bindings was bound to a value that + * was sanitized by your library (or returned as safe for rendering by your server?) How can you + * ensure that you didn't accidentally delete the line that sanitized the value, or renamed some + * properties/fields and forgot to update the binding to the sanitized value? + * + * To be secure by default, you want to ensure that any such bindings are disallowed unless you can + * determine that something explicitly says it's safe to use a value for binding in that + * context. You can then audit your code (a simple grep would do) to ensure that this is only done + * for those values that you can easily tell are safe - because they were received from your server, + * sanitized by your library, etc. You can organize your codebase to help with this - perhaps + * allowing only the files in a specific directory to do this. Ensuring that the internal API + * exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task. + * + * In the case of AngularJS' SCE service, one uses {@link ng.$sce#trustAs $sce.trustAs} + * (and shorthand methods such as {@link ng.$sce#trustAsHtml $sce.trustAsHtml}, etc.) to + * obtain values that will be accepted by SCE / privileged contexts. + * + * + * ## How does it work? + * + * In privileged contexts, directives and code will bind to the result of {@link ng.$sce#getTrusted + * $sce.getTrusted(context, value)} rather than to the value directly. Directives use {@link + * ng.$sce#parse $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the + * {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals. + * + * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link + * ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly + * simplified): + * + *
+ *   var ngBindHtmlDirective = ['$sce', function($sce) {
+ *     return function(scope, element, attr) {
+ *       scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) {
+ *         element.html(value || '');
+ *       });
+ *     };
+ *   }];
+ * 
+ * + * ## Impact on loading templates + * + * This applies both to the {@link ng.directive:ngInclude `ng-include`} directive as well as + * `templateUrl`'s specified by {@link guide/directive directives}. + * + * By default, Angular only loads templates from the same domain and protocol as the application + * document. This is done by calling {@link ng.$sce#getTrustedResourceUrl + * $sce.getTrustedResourceUrl} on the template URL. To load templates from other domains and/or + * protocols, you may either either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist + * them} or {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value. + * + * *Please note*: + * The browser's + * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest) + * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) + * policy apply in addition to this and may further restrict whether the template is successfully + * loaded. This means that without the right CORS policy, loading templates from a different domain + * won't work on all browsers. Also, loading templates from `file://` URL does not work on some + * browsers. + * + * ## This feels like too much overhead for the developer? + * + * It's important to remember that SCE only applies to interpolation expressions. + * + * If your expressions are constant literals, they're automatically trusted and you don't need to + * call `$sce.trustAs` on them (remember to include the `ngSanitize` module) (e.g. + * `
`) just works. + * + * Additionally, `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them + * through {@link ng.$sce#getTrusted $sce.getTrusted}. SCE doesn't play a role here. + * + * The included {@link ng.$sceDelegate $sceDelegate} comes with sane defaults to allow you to load + * templates in `ng-include` from your application's domain without having to even know about SCE. + * It blocks loading templates from other domains or loading templates over http from an https + * served document. You can change these by setting your own custom {@link + * ng.$sceDelegateProvider#resourceUrlWhitelist whitelists} and {@link + * ng.$sceDelegateProvider#resourceUrlBlacklist blacklists} for matching such URLs. + * + * This significantly reduces the overhead. It is far easier to pay the small overhead and have an + * application that's secure and can be audited to verify that with much more ease than bolting + * security onto an application later. + * + * + * ## What trusted context types are supported? + * + * | Context | Notes | + * |---------------------|----------------| + * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered and the {@link ngSanitize $sanitize} module is present this will sanitize the value instead of throwing an error. | + * | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. | + * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`
Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | + * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. | + * + * ## Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist}
+ * + * Each element in these arrays must be one of the following: + * + * - **'self'** + * - The special **string**, `'self'`, can be used to match against all URLs of the **same + * domain** as the application document using the **same protocol**. + * - **String** (except the special value `'self'`) + * - The string is matched against the full *normalized / absolute URL* of the resource + * being tested (substring matches are not good enough.) + * - There are exactly **two wildcard sequences** - `*` and `**`. All other characters + * match themselves. + * - `*`: matches zero or more occurrences of any character other than one of the following 6 + * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and ';'. It's a useful wildcard for use + * in a whitelist. + * - `**`: matches zero or more occurrences of *any* character. As such, it's not + * not appropriate to use in for a scheme, domain, etc. as it would match too much. (e.g. + * http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might + * not have been the intention.) Its usage at the very end of the path is ok. (e.g. + * http://foo.example.com/templates/**). + * - **RegExp** (*see caveat below*) + * - *Caveat*: While regular expressions are powerful and offer great flexibility, their syntax + * (and all the inevitable escaping) makes them *harder to maintain*. It's easy to + * accidentally introduce a bug when one updates a complex expression (imho, all regexes should + * have good test coverage.). For instance, the use of `.` in the regex is correct only in a + * small number of cases. A `.` character in the regex used when matching the scheme or a + * subdomain could be matched against a `:` or literal `.` that was likely not intended. It + * is highly recommended to use the string patterns and only fall back to regular expressions + * if they as a last resort. + * - The regular expression must be an instance of RegExp (i.e. not a string.) It is + * matched against the **entire** *normalized / absolute URL* of the resource being tested + * (even when the RegExp did not have the `^` and `$` codes.) In addition, any flags + * present on the RegExp (such as multiline, global, ignoreCase) are ignored. + * - If you are generating your JavaScript from some other templating engine (not + * recommended, e.g. in issue [#4006](https://github.com/angular/angular.js/issues/4006)), + * remember to escape your regular expression (and be aware that you might need more than + * one level of escaping depending on your templating engine and the way you interpolated + * the value.) Do make use of your platform's escaping mechanism as it might be good + * enough before coding your own. e.g. Ruby has + * [Regexp.escape(str)](http://www.ruby-doc.org/core-2.0.0/Regexp.html#method-c-escape) + * and Python has [re.escape](http://docs.python.org/library/re.html#re.escape). + * Javascript lacks a similar built in function for escaping. Take a look at Google + * Closure library's [goog.string.regExpEscape(s)]( + * http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962). + * + * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} for an example. + * + * ## Show me an example using SCE. + * + * @example + + +
+

+ User comments
+ By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when + $sanitize is available. If $sanitize isn't available, this results in an error instead of an + exploit. +
+
+ {{userComment.name}}: + +
+
+
+
+
+ + + var mySceApp = angular.module('mySceApp', ['ngSanitize']); + + mySceApp.controller("myAppController", function myAppController($http, $templateCache, $sce) { + var self = this; + $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { + self.userComments = userComments; + }); + self.explicitlyTrustedHtml = $sce.trustAsHtml( + 'Hover over this text.'); + }); + + + +[ + { "name": "Alice", + "htmlComment": + "Is anyone reading this?" + }, + { "name": "Bob", + "htmlComment": "Yes! Am I the only other one?" + } +] + + + + describe('SCE doc demo', function() { + it('should sanitize untrusted values', function() { + expect(element(by.css('.htmlComment')).getInnerHtml()) + .toBe('Is anyone reading this?'); + }); - reqData = transformData(config.data, headersGetter(reqHeaders), reqTransformFn); + it('should NOT sanitize explicitly trusted values', function() { + expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe( + 'Hover over this text.'); + }); + }); + +
+ * + * + * + * ## Can I disable SCE completely? + * + * Yes, you can. However, this is strongly discouraged. SCE gives you a lot of security benefits + * for little coding overhead. It will be much harder to take an SCE disabled application and + * either secure it on your own or enable SCE at a later stage. It might make sense to disable SCE + * for cases where you have a lot of existing code that was written before SCE was introduced and + * you're migrating them a module at a time. + * + * That said, here's how you can completely disable SCE: + * + *
+ *   angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) {
+ *     // Completely disable SCE.  For demonstration purposes only!
+ *     // Do not use in new projects.
+ *     $sceProvider.enabled(false);
+ *   });
+ * 
+ * + */ +/* jshint maxlen: 100 */ - // send request - promise = sendReq(config, reqData, reqHeaders); +function $SceProvider() { + var enabled = true; + /** + * @ngdoc method + * @name $sceProvider#enabled + * @kind function + * + * @param {boolean=} value If provided, then enables/disables SCE. + * @return {boolean} true if SCE is enabled, false otherwise. + * + * @description + * Enables/disables SCE and returns the current value. + */ + this.enabled = function (value) { + if (arguments.length) { + enabled = !!value; + } + return enabled; + }; - // transform future response - promise = promise.then(transformResponse, transformResponse); - // apply interceptors - forEach(responseInterceptors, function(interceptor) { - promise = interceptor(promise); - }); + /* Design notes on the default implementation for SCE. + * + * The API contract for the SCE delegate + * ------------------------------------- + * The SCE delegate object must provide the following 3 methods: + * + * - trustAs(contextEnum, value) + * This method is used to tell the SCE service that the provided value is OK to use in the + * contexts specified by contextEnum. It must return an object that will be accepted by + * getTrusted() for a compatible contextEnum and return this value. + * + * - valueOf(value) + * For values that were not produced by trustAs(), return them as is. For values that were + * produced by trustAs(), return the corresponding input value to trustAs. Basically, if + * trustAs is wrapping the given values into some type, this operation unwraps it when given + * such a value. + * + * - getTrusted(contextEnum, value) + * This function should return the a value that is safe to use in the context specified by + * contextEnum or throw and exception otherwise. + * + * NOTE: This contract deliberately does NOT state that values returned by trustAs() must be + * opaque or wrapped in some holder object. That happens to be an implementation detail. For + * instance, an implementation could maintain a registry of all trusted objects by context. In + * such a case, trustAs() would return the same object that was passed in. getTrusted() would + * return the same object passed in if it was found in the registry under a compatible context or + * throw an exception otherwise. An implementation might only wrap values some of the time based + * on some criteria. getTrusted() might return a value and not throw an exception for special + * constants or objects even if not wrapped. All such implementations fulfill this contract. + * + * + * A note on the inheritance model for SCE contexts + * ------------------------------------------------ + * I've used inheritance and made RESOURCE_URL wrapped types a subtype of URL wrapped types. This + * is purely an implementation details. + * + * The contract is simply this: + * + * getTrusted($sce.RESOURCE_URL, value) succeeding implies that getTrusted($sce.URL, value) + * will also succeed. + * + * Inheritance happens to capture this in a natural way. In some future, we + * may not use inheritance anymore. That is OK because no code outside of + * sce.js and sceSpecs.js would need to be aware of this detail. + */ - promise.success = function(fn) { - promise.then(function(response) { - fn(response.data, response.status, response.headers, config); - }); - return promise; - }; + this.$get = ['$parse', '$sniffer', '$sceDelegate', function( + $parse, $sniffer, $sceDelegate) { + // Prereq: Ensure that we're not running in IE8 quirks mode. In that mode, IE allows + // the "expression(javascript expression)" syntax which is insecure. + if (enabled && $sniffer.msie && $sniffer.msieDocumentMode < 8) { + throw $sceMinErr('iequirks', + 'Strict Contextual Escaping does not support Internet Explorer version < 9 in quirks ' + + 'mode. You can fix this by adding the text to the top of your HTML ' + + 'document. See http://docs.angularjs.org/api/ng.$sce for more information.'); + } - promise.error = function(fn) { - promise.then(null, function(response) { - fn(response.data, response.status, response.headers, config); - }); - return promise; - }; + var sce = shallowCopy(SCE_CONTEXTS); - return promise; + /** + * @ngdoc method + * @name $sce#isEnabled + * @kind function + * + * @return {Boolean} true if SCE is enabled, false otherwise. If you want to set the value, you + * have to do it at module config time on {@link ng.$sceProvider $sceProvider}. + * + * @description + * Returns a boolean indicating if SCE is enabled. + */ + sce.isEnabled = function () { + return enabled; + }; + sce.trustAs = $sceDelegate.trustAs; + sce.getTrusted = $sceDelegate.getTrusted; + sce.valueOf = $sceDelegate.valueOf; - 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); - } + if (!enabled) { + sce.trustAs = sce.getTrusted = function(type, value) { return value; }; + sce.valueOf = identity; } - $http.pendingRequests = []; - /** * @ngdoc method - * @name ng.$http#get - * @methodOf ng.$http + * @name $sce#parse * * @description - * Shortcut method to perform `GET` request. + * Converts Angular {@link guide/expression expression} into a function. This is like {@link + * ng.$parse $parse} and is identical when the expression is a literal constant. Otherwise, it + * wraps the expression in a call to {@link ng.$sce#getTrusted $sce.getTrusted(*type*, + * *result*)} * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object + * @param {string} type The kind of SCE context in which this result will be used. + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. */ + sce.parseAs = function sceParseAs(type, expr) { + var parsed = $parse(expr); + if (parsed.literal && parsed.constant) { + return parsed; + } else { + return function sceParseAsTrusted(self, locals) { + return sce.getTrusted(type, parsed(self, locals)); + }; + } + }; /** * @ngdoc method - * @name ng.$http#delete - * @methodOf ng.$http + * @name $sce#trustAs * * @description - * Shortcut method to perform `DELETE` request. + * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, + * returns an object that is trusted by angular for use in specified strict contextual + * escaping contexts (such as ng-bind-html, ng-include, any src attribute + * interpolation, any dom event binding attribute interpolation such as for onclick, etc.) + * that uses the provided value. See * {@link ng.$sce $sce} for enabling strict contextual + * escaping. * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object + * @param {string} type The kind of context in which this value is safe for use. e.g. url, + * resource_url, html, js and css. + * @param {*} value The value that that should be considered trusted/safe. + * @returns {*} A value that can be used to stand in for the provided `value` in places + * where Angular expects a $sce.trustAs() return value. */ /** * @ngdoc method - * @name ng.$http#head - * @methodOf ng.$http + * @name $sce#trustAsHtml * * @description - * Shortcut method to perform `HEAD` request. + * Shorthand method. `$sce.trustAsHtml(value)` → + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.HTML, value)`} * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object + * @param {*} value The value to trustAs. + * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedHtml + * $sce.getTrustedHtml(value)} to obtain the original value. (privileged directives + * only accept expressions that are either literal constants or are the + * return value of {@link ng.$sce#trustAs $sce.trustAs}.) */ /** * @ngdoc method - * @name ng.$http#jsonp - * @methodOf ng.$http + * @name $sce#trustAsUrl * * @description - * Shortcut method to perform `JSONP` request. + * Shorthand method. `$sce.trustAsUrl(value)` → + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.URL, value)`} * - * @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 {HttpPromise} Future object + * @param {*} value The value to trustAs. + * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedUrl + * $sce.getTrustedUrl(value)} to obtain the original value. (privileged directives + * only accept expressions that are either literal constants or are the + * return value of {@link ng.$sce#trustAs $sce.trustAs}.) */ - createShortMethods('get', 'delete', 'head', 'jsonp'); /** * @ngdoc method - * @name ng.$http#post - * @methodOf ng.$http + * @name $sce#trustAsResourceUrl * * @description - * Shortcut method to perform `POST` request. + * Shorthand method. `$sce.trustAsResourceUrl(value)` → + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`} * - * @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 + * @param {*} value The value to trustAs. + * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedResourceUrl + * $sce.getTrustedResourceUrl(value)} to obtain the original value. (privileged directives + * only accept expressions that are either literal constants or are the return + * value of {@link ng.$sce#trustAs $sce.trustAs}.) */ /** * @ngdoc method - * @name ng.$http#put - * @methodOf ng.$http + * @name $sce#trustAsJs * * @description - * Shortcut method to perform `PUT` request. + * Shorthand method. `$sce.trustAsJs(value)` → + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.JS, value)`} * - * @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 + * @param {*} value The value to trustAs. + * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedJs + * $sce.getTrustedJs(value)} to obtain the original value. (privileged directives + * only accept expressions that are either literal constants or are the + * return value of {@link ng.$sce#trustAs $sce.trustAs}.) */ - createShortMethodsWithData('post', 'put'); - - /** - * @ngdoc property - * @name ng.$http#defaults - * @propertyOf ng.$http - * - * @description - * Runtime equivalent of the `$httpProvider.defaults` property. Allows configuration of - * default headers as well as request and response transformations. - * - * See "Setting HTTP Headers" and "Transforming Requests and Responses" sections above. - */ - $http.defaults = $config; - - - 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. + * @ngdoc method + * @name $sce#getTrusted * - * !!! ACCESSES CLOSURE VARS: - * $httpBackend, $config, $log, $rootScope, defaultCache, $http.pendingRequests + * @description + * Delegates to {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted`}. As such, + * takes the result of a {@link ng.$sce#trustAs `$sce.trustAs`}() call and returns the + * originally supplied value if the queried context type is a supertype of the created type. + * If this condition isn't satisfied, throws an exception. + * + * @param {string} type The kind of context in which this value is to be used. + * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs `$sce.trustAs`} + * call. + * @returns {*} The value the was originally provided to + * {@link ng.$sce#trustAs `$sce.trustAs`} if valid in this context. + * Otherwise, throws an exception. */ - 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, - config.withCredentials); - } - 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); - } - } + /** + * @ngdoc method + * @name $sce#getTrustedHtml + * + * @description + * Shorthand method. `$sce.getTrustedHtml(value)` → + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`} + * + * @param {*} value The value to pass to `$sce.getTrusted`. + * @returns {*} The return value of `$sce.getTrusted($sce.HTML, value)` + */ - resolvePromise(response, status, headersString); - $rootScope.$apply(); - } + /** + * @ngdoc method + * @name $sce#getTrustedCss + * + * @description + * Shorthand method. `$sce.getTrustedCss(value)` → + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`} + * + * @param {*} value The value to pass to `$sce.getTrusted`. + * @returns {*} The return value of `$sce.getTrusted($sce.CSS, value)` + */ + /** + * @ngdoc method + * @name $sce#getTrustedUrl + * + * @description + * Shorthand method. `$sce.getTrustedUrl(value)` → + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.URL, value)`} + * + * @param {*} value The value to pass to `$sce.getTrusted`. + * @returns {*} The return value of `$sce.getTrusted($sce.URL, value)` + */ - /** - * Resolves the raw $http promise. - */ - function resolvePromise(response, status, headers) { - // normalize internal statuses to 0 - status = Math.max(status, 0); + /** + * @ngdoc method + * @name $sce#getTrustedResourceUrl + * + * @description + * Shorthand method. `$sce.getTrustedResourceUrl(value)` → + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`} + * + * @param {*} value The value to pass to `$sceDelegate.getTrusted`. + * @returns {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)` + */ - (isSuccess(status) ? deferred.resolve : deferred.reject)({ - data: response, - status: status, - headers: headersGetter(headers), - config: config - }); - } + /** + * @ngdoc method + * @name $sce#getTrustedJs + * + * @description + * Shorthand method. `$sce.getTrustedJs(value)` → + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.JS, value)`} + * + * @param {*} value The value to pass to `$sce.getTrusted`. + * @returns {*} The return value of `$sce.getTrusted($sce.JS, value)` + */ + /** + * @ngdoc method + * @name $sce#parseAsHtml + * + * @description + * Shorthand method. `$sce.parseAsHtml(expression string)` → + * {@link ng.$sce#parse `$sce.parseAs($sce.HTML, value)`} + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ - function removePendingReq() { - var idx = indexOf($http.pendingRequests, config); - if (idx !== -1) $http.pendingRequests.splice(idx, 1); - } - } + /** + * @ngdoc method + * @name $sce#parseAsCss + * + * @description + * Shorthand method. `$sce.parseAsCss(value)` → + * {@link ng.$sce#parse `$sce.parseAs($sce.CSS, value)`} + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ + /** + * @ngdoc method + * @name $sce#parseAsUrl + * + * @description + * Shorthand method. `$sce.parseAsUrl(value)` → + * {@link ng.$sce#parse `$sce.parseAs($sce.URL, value)`} + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ - 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('&'); - } + /** + * @ngdoc method + * @name $sce#parseAsResourceUrl + * + * @description + * Shorthand method. `$sce.parseAsResourceUrl(value)` → + * {@link ng.$sce#parse `$sce.parseAs($sce.RESOURCE_URL, value)`} + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ + /** + * @ngdoc method + * @name $sce#parseAsJs + * + * @description + * Shorthand method. `$sce.parseAsJs(value)` → + * {@link ng.$sce#parse `$sce.parseAs($sce.JS, value)`} + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ - }]; -} + // Shorthand delegations. + var parse = sce.parseAs, + getTrusted = sce.getTrusted, + trustAs = sce.trustAs; -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."); -}; + forEach(SCE_CONTEXTS, function (enumValue, name) { + var lName = lowercase(name); + sce[camelCase("parse_as_" + lName)] = function (expr) { + return parse(enumValue, expr); + }; + sce[camelCase("get_trusted_" + lName)] = function (value) { + return getTrusted(enumValue, value); + }; + sce[camelCase("trust_as_" + lName)] = function (value) { + return trustAs(enumValue, value); + }; + }); + return sce; + }]; +} /** - * @ngdoc object - * @name ng.$httpBackend - * @requires $browser + * !!! This is an undocumented "private" service !!! + * + * @name $sniffer * @requires $window * @requires $document * - * @description - * HTTP backend used by the {@link 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 ng.$http $http} or {@link ngResource.$resource $resource}. + * @property {boolean} history Does the browser support html5 history api ? + * @property {boolean} hashchange Does the browser support hashchange event ? + * @property {boolean} transitions Does the browser support CSS transition events ? + * @property {boolean} animations Does the browser support CSS animation events ? * - * During testing this implementation is swapped with {@link ngMock.$httpBackend mock - * $httpBackend} which can be trained with responses. + * @description + * This is very simple implementation of testing browser's features. */ -function $HttpBackendProvider() { - this.$get = ['$browser', '$window', '$document', function($browser, $window, $document) { - return createHttpBackend($browser, XHR, $browser.defer, $window.angular.callbacks, - $document[0], $window.location.protocol.replace(':', '')); - }]; -} - -function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument, locationProtocol) { - // TODO(vojta): fix the signature - return function(method, url, post, callback, headers, timeout, withCredentials) { - $browser.$$incOutstandingRequestCount(); - url = url || $browser.url(); - - if (lowercase(method) == 'jsonp') { - var callbackId = '_' + (callbacks.counter++).toString(36); - callbacks[callbackId] = function(data) { - callbacks[callbackId].data = data; - }; - - jsonpReq(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]; - }); - } 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) { - var responseHeaders = xhr.getAllResponseHeaders(); - - // TODO(vojta): remove once Firefox 21 gets released. - // begin: workaround to overcome Firefox CORS http response headers bug - // https://bugzilla.mozilla.org/show_bug.cgi?id=608735 - // Firefox already patched in nightly. Should land in Firefox 21. - - // CORS "simple response headers" http://www.w3.org/TR/cors/ - var value, - simpleHeaders = ["Cache-Control", "Content-Language", "Content-Type", - "Expires", "Last-Modified", "Pragma"]; - if (!responseHeaders) { - responseHeaders = ""; - forEach(simpleHeaders, function (header) { - var value = xhr.getResponseHeader(header); - if (value) { - responseHeaders += header + ": " + value + "\n"; - } - }); - } - // end of the workaround. - - completeRequest(callback, status || xhr.status, xhr.responseText, - responseHeaders); +function $SnifferProvider() { + this.$get = ['$window', '$document', function($window, $document) { + var eventSupport = {}, + android = + int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), + boxee = /Boxee/i.test(($window.navigator || {}).userAgent), + document = $document[0] || {}, + documentMode = document.documentMode, + vendorPrefix, + vendorRegex = /^(Moz|webkit|O|ms)(?=[A-Z])/, + bodyStyle = document.body && document.body.style, + transitions = false, + animations = false, + match; + + if (bodyStyle) { + for(var prop in bodyStyle) { + if(match = vendorRegex.exec(prop)) { + vendorPrefix = match[0]; + vendorPrefix = vendorPrefix.substr(0, 1).toUpperCase() + vendorPrefix.substr(1); + break; } - }; - - if (withCredentials) { - xhr.withCredentials = true; } - xhr.send(post || ''); - - if (timeout > 0) { - $browserDefer(function() { - status = -1; - xhr.abort(); - }, timeout); + if(!vendorPrefix) { + vendorPrefix = ('WebkitOpacity' in bodyStyle) && 'webkit'; } - } - - - 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); - } - }; - - function jsonpReq(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'), - doneWrapper = function() { - rawDocument.body.removeChild(script); - if (done) done(); - }; - script.type = 'text/javascript'; - script.src = url; + transitions = !!(('transition' in bodyStyle) || (vendorPrefix + 'Transition' in bodyStyle)); + animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle)); - if (msie) { - script.onreadystatechange = function() { - if (/loaded|complete/.test(script.readyState)) doneWrapper(); - }; - } else { - script.onload = script.onerror = doneWrapper; + if (android && (!transitions||!animations)) { + transitions = isString(document.body.style.webkitTransition); + animations = isString(document.body.style.webkitAnimation); + } } - rawDocument.body.appendChild(script); - } -} -/** - * @ngdoc object - * @name 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: '$' - }, + // Android has history.pushState, but it does not update location correctly + // so let's not use the history API at all. + // http://code.google.com/p/android/issues/detail?id=17471 + // https://github.com/angular/angular.js/issues/904 - 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' - }, + // older webkit browser (533.9) on Boxee box has exactly the same problem as Android has + // so let's not use the history API also + // We are purposefully using `!(android < 4)` to cover the case when `android` is undefined + // jshint -W018 + history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee), + // jshint +W018 + hashchange: 'onhashchange' in $window && + // IE8 compatible mode lies + (!documentMode || documentMode > 7), + hasEvent: function(event) { + // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have + // it. In particular the event is not fired when backspace or delete key are pressed or + // when cut operation is performed. + if (event == 'input' && msie == 9) return false; - pluralCat: function(num) { - if (num === 1) { - return 'one'; + if (isUndefined(eventSupport[event])) { + var divElm = document.createElement('div'); + eventSupport[event] = 'on' + event in divElm; } - return 'other'; - } + + return eventSupport[event]; + }, + csp: csp(), + vendorPrefix: vendorPrefix, + transitions : transitions, + animations : animations, + android: android, + msie : msie, + msieDocumentMode: documentMode }; - }; + }]; } function $TimeoutProvider() { @@ -9625,9 +14085,8 @@ function $TimeoutProvider() { /** - * @ngdoc function - * @name ng.$timeout - * @requires $browser + * @ngdoc service + * @name $timeout * * @description * Angular's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch @@ -9648,12 +14107,13 @@ function $TimeoutProvider() { * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. * @returns {Promise} Promise that will be resolved when the timeout is reached. The value this * promise will be resolved with is the return value of the `fn` function. + * */ function timeout(fn, delay, invokeApply) { var deferred = $q.defer(), promise = deferred.promise, skipApply = (isDefined(invokeApply) && !invokeApply), - timeoutId, cleanup; + timeoutId; timeoutId = $browser.defer(function() { try { @@ -9677,9 +14137,8 @@ function $TimeoutProvider() { /** - * @ngdoc function - * @name ng.$timeout#cancel - * @methodOf ng.$timeout + * @ngdoc method + * @name $timeout#cancel * * @description * Cancels a task associated with the `promise`. As a result of this, the promise will be @@ -9702,16 +14161,163 @@ function $TimeoutProvider() { }]; } +// NOTE: The usage of window and document instead of $window and $document here is +// deliberate. This service depends on the specific behavior of anchor nodes created by the +// browser (resolving and parsing URLs) that is unlikely to be provided by mock objects and +// cause us to break tests. In addition, when the browser resolves a URL for XHR, it +// doesn't know about mocked locations and resolves URLs to the real document - which is +// exactly the behavior needed here. There is little value is mocking these out for this +// service. +var urlParsingNode = document.createElement("a"); +var originUrl = urlResolve(window.location.href, true); + + +/** + * + * Implementation Notes for non-IE browsers + * ---------------------------------------- + * Assigning a URL to the href property of an anchor DOM node, even one attached to the DOM, + * results both in the normalizing and parsing of the URL. Normalizing means that a relative + * URL will be resolved into an absolute URL in the context of the application document. + * Parsing means that the anchor node's host, hostname, protocol, port, pathname and related + * properties are all populated to reflect the normalized URL. This approach has wide + * compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See + * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html + * + * Implementation Notes for IE + * --------------------------- + * IE >= 8 and <= 10 normalizes the URL when assigned to the anchor node similar to the other + * browsers. However, the parsed components will not be set if the URL assigned did not specify + * them. (e.g. if you assign a.href = "foo", then a.protocol, a.host, etc. will be empty.) We + * work around that by performing the parsing in a 2nd step by taking a previously normalized + * URL (e.g. by assigning to a.href) and assigning it a.href again. This correctly populates the + * properties such as protocol, hostname, port, etc. + * + * IE7 does not normalize the URL when assigned to an anchor node. (Apparently, it does, if one + * uses the inner HTML approach to assign the URL as part of an HTML snippet - + * http://stackoverflow.com/a/472729) However, setting img[src] does normalize the URL. + * Unfortunately, setting img[src] to something like "javascript:foo" on IE throws an exception. + * Since the primary usage for normalizing URLs is to sanitize such URLs, we can't use that + * method and IE < 8 is unsupported. + * + * References: + * http://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement + * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html + * http://url.spec.whatwg.org/#urlutils + * https://github.com/angular/angular.js/pull/2902 + * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/ + * + * @kind function + * @param {string} url The URL to be parsed. + * @description Normalizes and parses a URL. + * @returns {object} Returns the normalized URL as a dictionary. + * + * | member name | Description | + * |---------------|----------------| + * | href | A normalized version of the provided URL if it was not an absolute URL | + * | protocol | The protocol including the trailing colon | + * | host | The host and port (if the port is non-default) of the normalizedUrl | + * | search | The search params, minus the question mark | + * | hash | The hash string, minus the hash symbol + * | hostname | The hostname + * | port | The port, without ":" + * | pathname | The pathname, beginning with "/" + * + */ +function urlResolve(url, base) { + var href = url; + + if (msie) { + // Normalize before parse. Refer Implementation Notes on why this is + // done in two steps on IE. + urlParsingNode.setAttribute("href", href); + href = urlParsingNode.href; + } + + urlParsingNode.setAttribute('href', href); + + // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils + return { + href: urlParsingNode.href, + protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '', + host: urlParsingNode.host, + search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '', + hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '', + hostname: urlParsingNode.hostname, + port: urlParsingNode.port, + pathname: (urlParsingNode.pathname.charAt(0) === '/') + ? urlParsingNode.pathname + : '/' + urlParsingNode.pathname + }; +} + /** - * @ngdoc object - * @name ng.$filterProvider + * Parse a request URL and determine whether this is a same-origin request as the application document. + * + * @param {string|object} requestUrl The url of the request as a string that will be resolved + * or a parsed URL object. + * @returns {boolean} Whether the request is for the same origin as the application document. + */ +function urlIsSameOrigin(requestUrl) { + var parsed = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl; + return (parsed.protocol === originUrl.protocol && + parsed.host === originUrl.host); +} + +/** + * @ngdoc service + * @name $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 overridden, removed or mocked for testing. * - * 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 filter function. + * Expressions, like the one defined for the `ngClick` directive in the example + * below, are evaluated with respect to the current scope. Therefore, there is + * no risk of inadvertently coding in a dependency on a global value in such an + * expression. * - *
+ * @example
+   
+     
+       
+       
+ + +
+
+ + it('should display the greeting in the input box', function() { + element(by.model('greeting')).sendKeys('Hello, E2E Tests'); + // If we click the button it will block the test runner + // element(':button').click(); + }); + +
+ */ +function $WindowProvider(){ + this.$get = valueFn(window); +} + +/** + * @ngdoc provider + * @name $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 filter function. + * + * ```js * // Filter registration * function MyModule($provide, $filterProvider) { * // create a service to demonstrate injection (not always needed) @@ -9730,10 +14336,12 @@ function $TimeoutProvider() { * }; * }); * } - *
+ * ``` * - * The filter function is registered with the `$injector` under the filter name suffixe with `Filter`. - *
+ * The filter function is registered with the `$injector` under the filter name suffix with
+ * `Filter`.
+ *
+ * ```js
  *   it('should be the same instance', inject(
  *     function($filterProvider) {
  *       $filterProvider.register('reverse', function(){
@@ -9743,29 +14351,27 @@ function $TimeoutProvider() {
  *     function($filter, reverseFilter) {
  *       expect($filter('reverse')).toBe(reverseFilter);
  *     });
- * 
+ * ``` * * * 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. + * {@link guide/filter Filters} in the Angular Developer Guide. */ /** * @ngdoc method - * @name ng.$filterProvider#register - * @methodOf ng.$filterProvider + * @name $filterProvider#register * @description * Register filter factory function. * * @param {String} name Name of the filter. - * @param {function} fn The filter factory function which is injectable. + * @param {Function} fn The filter factory function which is injectable. */ /** - * @ngdoc function - * @name ng.$filter - * @function + * @ngdoc service + * @name $filter + * @kind function * @description * Filters are used for formatting data displayed to the user. * @@ -9775,24 +14381,69 @@ function $TimeoutProvider() { * * @param {String} name Name of the filter function to retrieve * @return {Function} the filter function - */ + * @example + + +
+

{{ originalText }}

+

{{ filteredText }}

+
+
+ + + angular.module('filterExample', []) + .controller('MainCtrl', function($scope, $filter) { + $scope.originalText = 'hello'; + $scope.filteredText = $filter('uppercase')($scope.originalText); + }); + +
+ */ $FilterProvider.$inject = ['$provide']; function $FilterProvider($provide) { var suffix = 'Filter'; + /** + * @ngdoc method + * @name $controllerProvider#register + * @param {string|Object} name Name of the filter function, or an object map of filters where + * the keys are the filter names and the values are the filter factories. + * @returns {Object} Registered filter instance, or if a map of filters was provided then a map + * of the registered filter instances. + */ function register(name, factory) { - return $provide.factory(name + suffix, factory); + if(isObject(name)) { + var filters = {}; + forEach(name, function(filter, key) { + filters[key] = register(key, filter); + }); + return filters; + } else { + return $provide.factory(name + suffix, factory); + } } this.register = register; this.$get = ['$injector', function($injector) { return function(name) { return $injector.get(name + suffix); - } + }; }]; //////////////////////////////////////// + /* global + currencyFilter: false, + dateFilter: false, + filterFilter: false, + jsonFilter: false, + limitToFilter: false, + lowercaseFilter: false, + numberFilter: false, + orderByFilter: false, + uppercaseFilter: false, + */ + register('currency', currencyFilter); register('date', dateFilter); register('filter', filterFilter); @@ -9806,23 +14457,20 @@ function $FilterProvider($provide) { /** * @ngdoc filter - * @name ng.filter:filter - * @function + * @name filter + * @kind 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 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 + * - `string`: The string is evaluated as an expression and the resulting value is used for substring match against + * the contents of the `array`. 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 @@ -9832,18 +14480,35 @@ function $FilterProvider($provide) { * 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 + * - `function(value)`: 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. * + * @param {function(actual, expected)|true|undefined} comparator Comparator which is used in + * determining if the expected value (from the filter expression) and actual value (from + * the object in the array) should be considered a match. + * + * Can be one of: + * + * - `function(actual, expected)`: + * The function will be given the object value and the predicate value to compare and + * should return true if the item should be included in filtered result. + * + * - `true`: A shorthand for `function(actual, expected) { return angular.equals(expected, actual)}`. + * this is essentially strict comparison of expected and actual. + * + * - `false|undefined`: A short hand for a function which will look for a substring match in case + * insensitive way. + * * @example - - + +
+ {name:'Julie', phone:'555-8765'}, + {name:'Juliette', phone:'555-5678'}]"> Search: @@ -9857,37 +14522,59 @@ function $FilterProvider($provide) { Any:
Name only
Phone only
+ Equality
- - - + + +
NamePhone
{{friend.name}}{{friend.phone}}
{{friendObj.name}}{{friendObj.phone}}
-
- - 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']); + + + var expectFriendNames = function(expectedNames, key) { + element.all(by.repeater(key + ' in friends').column(key + '.name')).then(function(arr) { + arr.forEach(function(wd, i) { + expect(wd.getText()).toMatch(expectedNames[i]); + }); + }); + }; - input('searchText').enter('76'); - expect(repeater('#searchTextResults tr', 'friend in friends').column('friend.name')). - toEqual(['John', 'Julie']); + it('should search across all fields when filtering with a string', function() { + var searchText = element(by.model('searchText')); + searchText.clear(); + searchText.sendKeys('m'); + expectFriendNames(['Mary', 'Mike', 'Adam'], 'friend'); + + searchText.clear(); + searchText.sendKeys('76'); + expectFriendNames(['John', 'Julie'], 'friend'); }); 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']); + var searchAny = element(by.model('search.$')); + searchAny.clear(); + searchAny.sendKeys('i'); + expectFriendNames(['Mary', 'Mike', 'Julie', 'Juliette'], 'friendObj'); }); - -
+ it('should use a equal comparison when comparator is true', function() { + var searchName = element(by.model('search.name')); + var strict = element(by.model('strict')); + searchName.clear(); + searchName.sendKeys('Julie'); + strict.click(); + expectFriendNames(['Julie'], 'friendObj'); + }); + + */ function filterFilter() { - return function(array, expression) { + return function(array, expression, comparator) { if (!isArray(array)) return array; - var predicates = []; + + var comparatorType = typeof(comparator), + predicates = []; + predicates.check = function(value) { for (var j = 0; j < predicates.length; j++) { if(!predicates[j](value)) { @@ -9896,20 +14583,49 @@ function filterFilter() { } return true; }; + + if (comparatorType !== 'function') { + if (comparatorType === 'boolean' && comparator) { + comparator = function(obj, text) { + return angular.equals(obj, text); + }; + } else { + comparator = function(obj, text) { + if (obj && text && typeof obj === 'object' && typeof text === 'object') { + for (var objKey in obj) { + if (objKey.charAt(0) !== '$' && hasOwnProperty.call(obj, objKey) && + comparator(obj[objKey], text[objKey])) { + return true; + } + } + return false; + } + text = (''+text).toLowerCase(); + return (''+obj).toLowerCase().indexOf(text) > -1; + }; + } + } + var search = function(obj, text){ - if (text.charAt(0) === '!') { + if (typeof text == 'string' && text.charAt(0) === '!') { return !search(obj, text.substr(1)); } switch (typeof obj) { case "boolean": case "number": case "string": - return ('' + obj).toLowerCase().indexOf(text) > -1; + return comparator(obj, text); case "object": - for ( var objKey in obj) { - if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) { - return true; - } + switch (typeof text) { + case "object": + return comparator(obj, text); + default: + for ( var objKey in obj) { + if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) { + return true; + } + } + break; } return false; case "array": @@ -9927,27 +14643,18 @@ function filterFilter() { case "boolean": case "number": case "string": + // Set up expression object and fall through expression = {$:expression}; + // jshint -W086 case "object": + // jshint +W086 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); - }); - })(); - } + (function(path) { + if (typeof expression[path] == 'undefined') return; + predicates.push(function(value) { + return search(path == '$' ? value : (value && value[path]), expression[path]); + }); + })(key); } break; case 'function': @@ -9964,13 +14671,13 @@ function filterFilter() { } } return filtered; - } + }; } /** * @ngdoc filter - * @name ng.filter:currency - * @function + * @name currency + * @kind function * * @description * Formats a number as a currency (ie $1,234.56). When no currency symbol is provided, default @@ -9982,8 +14689,8 @@ function filterFilter() { * * * @example - - + +

- default currency symbol ($): {{amount | currency}}
- custom currency identifier (USD$): {{amount | currency:"USD$"}} + default currency symbol ($): {{amount | currency}}
+ custom currency identifier (USD$): {{amount | currency:"USD$"}}
-
- + + 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'); + expect(element(by.id('currency-default')).getText()).toBe('$1,234.56'); + expect(element(by.binding('amount | currency:"USD$"')).getText()).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)'); + if (browser.params.browser == 'safari') { + // Safari does not understand the minus key. See + // https://github.com/angular/protractor/issues/481 + return; + } + element(by.model('amount')).clear(); + element(by.model('amount')).sendKeys('-1234'); + expect(element(by.id('currency-default')).getText()).toBe('($1,234.00)'); + expect(element(by.binding('amount | currency:"USD$"')).getText()).toBe('(USD$1,234.00)'); }); - -
+ + */ currencyFilter.$inject = ['$locale']; function currencyFilter($locale) { @@ -10020,8 +14733,8 @@ function currencyFilter($locale) { /** * @ngdoc filter - * @name ng.filter:number - * @function + * @name number + * @kind function * * @description * Formats a number as text. @@ -10035,8 +14748,8 @@ function currencyFilter($locale) { * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit. * * @example - - + +
Enter number:
- Default formatting: {{val | number}}
- No fractions: {{val | number:0}}
- Negative number: {{-val | number:4}} + Default formatting: {{val | number}}
+ No fractions: {{val | number:0}}
+ Negative number: {{-val | number:4}}
-
- + + 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'); + expect(element(by.id('number-default')).getText()).toBe('1,234.568'); + expect(element(by.binding('val | number:0')).getText()).toBe('1,235'); + expect(element(by.binding('-val | number:4')).getText()).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'); - }); - -
+ element(by.model('val')).clear(); + element(by.model('val')).sendKeys('3374.333'); + expect(element(by.id('number-default')).getText()).toBe('3,374.333'); + expect(element(by.binding('val | number:0')).getText()).toBe('3,374'); + expect(element(by.binding('-val | number:4')).getText()).toBe('-3,374.3330'); + }); + + */ @@ -10078,7 +14792,7 @@ function numberFilter($locale) { var DECIMAL_SEP = '.'; function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { - if (isNaN(number) || !isFinite(number)) return ''; + if (number == null || !isFinite(number) || isObject(number)) return ''; var isNegative = number < 0; number = Math.abs(number); @@ -10105,19 +14819,19 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac); } - var pow = Math.pow(10, fractionSize); - number = Math.round(number * pow) / pow; + var pow = Math.pow(10, fractionSize + 1); + number = Math.floor(number * pow + 5) / pow; var fraction = ('' + number).split(DECIMAL_SEP); var whole = fraction[0]; fraction = fraction[1] || ''; - var pos = 0, + var i, pos = 0, lgroup = pattern.lgSize, group = pattern.gSize; if (whole.length >= (lgroup + group)) { pos = whole.length - lgroup; - for (var i = 0; i < pos; i++) { + for (i = 0; i < pos; i++) { if ((pos - i)%group === 0 && i !== 0) { formatedText += groupSep; } @@ -10217,6 +14931,9 @@ var DATE_FORMATS = { m: dateGetter('Minutes', 1), ss: dateGetter('Seconds', 2), s: dateGetter('Seconds', 1), + // while ISO 8601 requires fractions to be prefixed with `.` or `,` + // we can be just safely rely on using `sss` since we currently don't support single or two digit fractions + sss: dateGetter('Milliseconds', 3), EEEE: dateStrGetter('Day'), EEE: dateStrGetter('Day', true), a: ampmGetter, @@ -10224,12 +14941,12 @@ var DATE_FORMATS = { }; var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/, - NUMBER_STRING = /^\d+$/; + NUMBER_STRING = /^\-?\d+$/; /** * @ngdoc filter - * @name ng.filter:date - * @function + * @name date + * @kind function * * @description * Formats `date` to a string based on the requested `format`. @@ -10255,6 +14972,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+ * * `'m'`: Minute in hour (0-59) * * `'ss'`: Second in minute, padded (00-59) * * `'s'`: Second in minute (0-59) + * * `'.sss' or ',sss'`: Millisecond in second, padded (000-999) * * `'a'`: am/pm marker * * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-+1200) * @@ -10285,44 +15003,52 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+ * @returns {string} Formatted string or the input if input is not recognized as date/millis. * * @example - - + + {{1288323623006 | date:'medium'}}: - {{1288323623006 | date:'medium'}}
+ {{1288323623006 | date:'medium'}}
{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}: - {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
+ {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
{{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}: - {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
-
- + {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
+ + it('should format date', function() { - expect(binding("1288323623006 | date:'medium'")). + expect(element(by.binding("1288323623006 | date:'medium'")).getText()). 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'")). + expect(element(by.binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).getText()). toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/); - expect(binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")). + expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()). toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); }); -
-
+ + */ dateFilter.$inject = ['$locale']; function dateFilter($locale) { var R_ISO8601_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/; - function jsonStringToDate(string){ + // 1 2 3 4 5 6 7 8 9 10 11 + function jsonStringToDate(string) { var match; if (match = string.match(R_ISO8601_STR)) { var date = new Date(0), tzHour = 0, - tzMin = 0; + tzMin = 0, + dateSetter = match[8] ? date.setUTCFullYear : date.setFullYear, + timeSetter = match[8] ? date.setUTCHours : date.setHours; + if (match[9]) { tzHour = int(match[9] + match[10]); tzMin = int(match[9] + match[11]); } - date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3])); - date.setUTCHours(int(match[4]||0) - tzHour, int(match[5]||0) - tzMin, int(match[6]||0), int(match[7]||0)); + dateSetter.call(date, int(match[1]), int(match[2]) - 1, int(match[3])); + var h = int(match[4]||0) - tzHour; + var m = int(match[5]||0) - tzMin; + var s = int(match[6]||0); + var ms = Math.round(parseFloat('0.' + (match[7]||0)) * 1000); + timeSetter.call(date, h, m, s, ms); return date; } return string; @@ -10376,8 +15102,8 @@ function dateFilter($locale) { /** * @ngdoc filter - * @name ng.filter:json - * @function + * @name json + * @kind function * * @description * Allows you to convert a JavaScript object into JSON string. @@ -10389,17 +15115,17 @@ function dateFilter($locale) { * @returns {string} JSON string. * * - * @example: - - + * @example + +
{{ {'name':'value'} | json }}
-
- + + it('should jsonify filtered objects', function() { - expect(binding("{'name':'value'}")).toMatch(/\{\n "name": ?"value"\n}/); + expect(element(by.binding("{'name':'value'}")).getText()).toMatch(/\{\n "name": ?"value"\n}/); }); - -
+ + * */ function jsonFilter() { @@ -10411,8 +15137,8 @@ function jsonFilter() { /** * @ngdoc filter - * @name ng.filter:lowercase - * @function + * @name lowercase + * @kind function * @description * Converts string to lowercase. * @see angular.lowercase @@ -10422,8 +15148,8 @@ var lowercaseFilter = valueFn(lowercase); /** * @ngdoc filter - * @name ng.filter:uppercase - * @function + * @name uppercase + * @kind function * @description * Converts string to uppercase. * @see angular.uppercase @@ -10431,101 +15157,127 @@ var lowercaseFilter = valueFn(lowercase); var uppercaseFilter = valueFn(uppercase); /** - * @ngdoc function - * @name ng.filter:limitTo - * @function + * @ngdoc filter + * @name limitTo + * @kind 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 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. + * Creates a new array or string containing only a specified number of elements. The elements + * are taken from either the beginning or the end of the source array or string, as specified by + * the value and sign (positive or negative) of `limit`. + * + * @param {Array|string} input Source array or string to be limited. + * @param {string|number} limit The length of the returned array or string. If the `limit` number + * is positive, `limit` number of items from the beginning of the source array/string are copied. + * If the number is negative, `limit` number of items from the end of the source array/string + * are copied. The `limit` will be trimmed if it exceeds `array.length` + * @returns {Array|string} A new sub-array or substring of length `limit` or less if input array + * had less than `limit` elements. * * @example - - + +
- Limit {{numbers}} to: -

Output: {{ numbers | limitTo:limit }}

+ Limit {{numbers}} to: +

Output numbers: {{ numbers | limitTo:numLimit }}

+ Limit {{letters}} to: +

Output letters: {{ letters | limitTo:letterLimit }}

-
- - 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')).toEqual('[1,2,3]'); + + + var numLimitInput = element(by.model('numLimit')); + var letterLimitInput = element(by.model('letterLimit')); + var limitedNumbers = element(by.binding('numbers | limitTo:numLimit')); + var limitedLetters = element(by.binding('letters | limitTo:letterLimit')); + + it('should limit the number array to first three items', function() { + expect(numLimitInput.getAttribute('value')).toBe('3'); + expect(letterLimitInput.getAttribute('value')).toBe('3'); + expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3]'); + expect(limitedLetters.getText()).toEqual('Output letters: abc'); }); it('should update the output when -3 is entered', function() { - input('limit').enter(-3); - expect(binding('numbers | limitTo:limit')).toEqual('[7,8,9]'); + numLimitInput.clear(); + numLimitInput.sendKeys('-3'); + letterLimitInput.clear(); + letterLimitInput.sendKeys('-3'); + expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]'); + expect(limitedLetters.getText()).toEqual('Output letters: ghi'); }); it('should not exceed the maximum size of input array', function() { - input('limit').enter(100); - expect(binding('numbers | limitTo:limit')).toEqual('[1,2,3,4,5,6,7,8,9]'); + numLimitInput.clear(); + numLimitInput.sendKeys('100'); + letterLimitInput.clear(); + letterLimitInput.sendKeys('100'); + expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3,4,5,6,7,8,9]'); + expect(limitedLetters.getText()).toEqual('Output letters: abcdefghi'); }); - -
+ + */ function limitToFilter(){ - return function(array, limit) { - if (!(array instanceof Array)) return array; - limit = int(limit); + return function(input, limit) { + if (!isArray(input) && !isString(input)) return input; + + if (Math.abs(Number(limit)) === Infinity) { + limit = Number(limit); + } else { + limit = int(limit); + } + + if (isString(input)) { + //NaN check on limit + if (limit) { + return limit >= 0 ? input.slice(0, limit) : input.slice(limit, input.length); + } else { + return ""; + } + } + 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 > input.length) + limit = input.length; + else if (limit < -input.length) + limit = -input.length; if (limit > 0) { i = 0; n = limit; } else { - i = array.length + limit; - n = array.length; + i = input.length + limit; + n = input.length; } for (; i} expression A predicate to be @@ -10541,12 +15293,12 @@ function limitToFilter(){ * - `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. + * @param {boolean=} reverse Reverse the order of the array. * @returns {Array} Sorted copy of the source array. * * @example - - + + -
+ + userType: Required!
userType = {{userType}}
@@ -11278,26 +16228,36 @@ function FormController(element, attrs) { myForm.$valid = {{myForm.$valid}}
myForm.$error.required = {{!!myForm.$error.required}}
-
- + + it('should initialize to model', function() { - expect(binding('userType')).toEqual('guest'); - expect(binding('myForm.input.$valid')).toEqual('true'); + var userType = element(by.binding('userType')); + var valid = element(by.binding('myForm.input.$valid')); + + expect(userType.getText()).toContain('guest'); + expect(valid.getText()).toContain('true'); }); it('should be invalid if empty', function() { - input('userType').enter(''); - expect(binding('userType')).toEqual(''); - expect(binding('myForm.input.$valid')).toEqual('false'); + var userType = element(by.binding('userType')); + var valid = element(by.binding('myForm.input.$valid')); + var userInput = element(by.model('userType')); + + userInput.clear(); + userInput.sendKeys(''); + + expect(userType.getText()).toEqual('userType ='); + expect(valid.getText()).toContain('false'); }); - -
+ + + * */ var formDirectiveFactory = function(isNgForm) { return ['$timeout', function($timeout) { var formDirective = { name: 'form', - restrict: 'E', + restrict: isNgForm ? 'EAC' : 'E', controller: FormController, compile: function() { return { @@ -11319,7 +16279,7 @@ var formDirectiveFactory = function(isNgForm) { // unregister the preventDefault listener so that we don't not leak memory but in a // way that will achieve the prevention of the default action. - formElement.bind('$destroy', function() { + formElement.on('$destroy', function() { $timeout(function() { removeEventListenerFn(formElement[0], 'submit', preventDefaultListener); }, 0, false); @@ -11330,13 +16290,13 @@ var formDirectiveFactory = function(isNgForm) { alias = attr.name || attr.ngForm; if (alias) { - scope[alias] = controller; + setter(scope, alias, controller, alias); } if (parentFormCtrl) { - formElement.bind('$destroy', function() { + formElement.on('$destroy', function() { parentFormCtrl.$removeControl(controller); if (alias) { - scope[alias] = undefined; + setter(scope, alias, undefined, alias); } extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards }); @@ -11346,22 +16306,30 @@ var formDirectiveFactory = function(isNgForm) { } }; - return isNgForm ? extend(copy(formDirective), {restrict: 'EAC'}) : formDirective; + return formDirective; }]; }; var formDirective = formDirectiveFactory(); var ngFormDirective = formDirectiveFactory(true); +/* global + + -VALID_CLASS, + -INVALID_CLASS, + -PRISTINE_CLASS, + -DIRTY_CLASS +*/ + 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,6}$/; +var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(\.[a-z0-9-]+)*$/i; var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/; var inputType = { /** - * @ngdoc inputType - * @name ng.directive:input.text + * @ngdoc input + * @name input[text] * * @description * Standard HTML text input with angular data binding. @@ -11381,19 +16349,20 @@ var inputType = { * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. + * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. * * @example - - + +
Single word: + ng-pattern="word" required ng-trim="false"> Required! @@ -11405,32 +16374,40 @@ var inputType = { myForm.$valid = {{myForm.$valid}}
myForm.$error.required = {{!!myForm.$error.required}}
-
- + + + var text = element(by.binding('text')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('text')); + it('should initialize to model', function() { - expect(binding('text')).toEqual('guest'); - expect(binding('myForm.input.$valid')).toEqual('true'); + expect(text.getText()).toContain('guest'); + expect(valid.getText()).toContain('true'); }); it('should be invalid if empty', function() { - input('text').enter(''); - expect(binding('text')).toEqual(''); - expect(binding('myForm.input.$valid')).toEqual('false'); + input.clear(); + input.sendKeys(''); + + expect(text.getText()).toEqual('text ='); + expect(valid.getText()).toContain('false'); }); it('should be invalid if multi word', function() { - input('text').enter('hello world'); - expect(binding('myForm.input.$valid')).toEqual('false'); + input.clear(); + input.sendKeys('hello world'); + + expect(valid.getText()).toContain('false'); }); - -
+ + */ 'text': textInputType, /** - * @ngdoc inputType - * @name ng.directive:input.number + * @ngdoc input + * @name input[number] * * @description * Text input with number validation and transformation. Sets the `number` validation @@ -11455,8 +16432,8 @@ var inputType = { * interaction with the input element. * * @example - - + +
Red
- Green
+ Green
Blue
- color = {{color}}
+ color = {{color | json}}
-
- + Note that `ng-value="specialValue"` sets radio item's value to be the value of `$scope.specialValue`. + + it('should change state', function() { - expect(binding('color')).toEqual('blue'); + var color = element(by.binding('color')); + + expect(color.getText()).toContain('blue'); + + element.all(by.model('color')).get(0).click(); - input('color').select('red'); - expect(binding('color')).toEqual('red'); + expect(color.getText()).toContain('red'); }); - -
+ + */ 'radio': radioInputType, /** - * @ngdoc inputType - * @name ng.directive:input.checkbox + * @ngdoc input + * @name input[checkbox] * * @description * HTML checkbox. @@ -11691,8 +16699,8 @@ var inputType = { * interaction with the input element. * * @example - - + + + + Update input to see transitions when valid/invalid. + Integer is a valid value. +
+ +
+
+ *
*/ var ngModelDirective = function() { return { @@ -12471,7 +17652,7 @@ var ngModelDirective = function() { formCtrl.$addControl(modelCtrl); - element.bind('$destroy', function() { + scope.$on('$destroy', function() { formCtrl.$removeControl(modelCtrl); }); } @@ -12481,20 +17662,24 @@ var ngModelDirective = function() { /** * @ngdoc directive - * @name ng.directive:ngChange - * @restrict E + * @name ngChange * * @description - * Evaluate given expression when user changes the input. + * Evaluate the given expression when the user changes the input. + * The expression is evaluated immediately, unlike the JavaScript onchange event + * which only triggers at the end of a change (usually, when the user leaves the + * form element or presses the return key). * The expression is not evaluated when the value change is coming from the model. * * Note, this directive requires `ngModel` to be present. * * @element input + * @param {expression} ngChange {@link guide/expression Expression} to evaluate upon change + * in input value. * * @example - * - * + * + * * +
+

Which is your favorite?

+ +
You chose {{my.favorite}}
+
+
+ + var favorite = element(by.binding('my.favorite')); + it('should initialize to model', function() { + expect(favorite.getText()).toContain('unicorns'); + }); + it('should bind the values to the inputs', function() { + element.all(by.model('my.favorite')).get(0).click(); + expect(favorite.getText()).toContain('pizza'); + }); + +
+ */ var ngValueDirective = function() { return { priority: 100, compile: function(tpl, tplAttr) { if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) { - return function(scope, elm, attr) { + return function ngValueConstantLink(scope, elm, attr) { attr.$set('value', scope.$eval(attr.ngValue)); }; } else { - return function(scope, elm, attr) { + return function ngValueLink(scope, elm, attr) { scope.$watch(attr.ngValue, function valueWatchAction(value) { attr.$set('value', value); }); @@ -12667,7 +17923,8 @@ var ngValueDirective = function() { /** * @ngdoc directive - * @name ng.directive:ngBind + * @name ngBind + * @restrict AC * * @description * The `ngBind` attribute tells Angular to replace the text content of the specified HTML element @@ -12677,8 +17934,8 @@ var ngValueDirective = function() { * Typically, you don't use `ngBind` directly, but instead you use the double curly markup like * `{{ expression }}` which is similar but less verbose. * - * It is preferrable to use `ngBind` instead of `{{ expression }}` when a template is momentarily - * displayed by the browser in its raw state before Angular compiles it. Since `ngBind` is an + * It is preferable to use `ngBind` instead of `{{ expression }}` when a template is momentarily + * displayed by the browser in its raw state before Angular compiles it. Since `ngBind` is an * element attribute, it makes the bindings invisible to the user while the page is loading. * * An alternative solution to this problem would be using the @@ -12690,8 +17947,8 @@ var ngValueDirective = function() { * * @example * Enter a name in the Live Preview text box; the greeting below the text box changes instantly. - - + + -
- Name: - [ greet ]
- Contact: -
    -
  • - - - [ clear - | X ] -
  • -
  • [ add ]
  • -
-
-
- - 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'); - }); - -
*/ var ngControllerDirective = [function() { return { scope: true, - controller: '@' + controller: '@', + priority: 500 }; }]; /** * @ngdoc directive - * @name ng.directive:ngCsp - * @priority 1000 + * @name ngCsp * * @element html * @description * Enables [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) support. - * + * * This is necessary when developing things like Google Chrome Extensions. - * + * * CSP forbids apps to use `eval` or `Function(string)` generated functions (among other things). * For us to be compatible, we just need to implement the "getterFn" in $parse without violating * any of these restrictions. - * - * AngularJS uses `Function(string)` generated functions as a speed optimization. By applying `ngCsp` - * it is be possible to opt into the CSP compatible mode. When this mode is on AngularJS will + * + * AngularJS uses `Function(string)` generated functions as a speed optimization. Applying the `ngCsp` + * directive will cause Angular to use CSP compatibility mode. When this mode is on AngularJS will * evaluate all expressions up to 30% slower than in non-CSP mode, but no security violations will * be raised. - * - * In order to use this feature put `ngCsp` directive on the root element of the application. - * + * + * CSP forbids JavaScript to inline stylesheet rules. In non CSP mode Angular automatically + * includes some CSS rules (e.g. {@link ng.directive:ngCloak ngCloak}). + * To make those directives work in CSP mode, include the `angular-csp.css` manually. + * + * In order to use this feature put the `ngCsp` directive on the root element of the application. + * + * *Note: This directive is only available in the `ng-csp` and `data-ng-csp` attribute form.* + * * @example * This example shows how to apply the `ngCsp` directive to the `html` tag. -
+   ```html
      
      
      ...
      ...
      
-   
+ ``` */ -var ngCspDirective = ['$sniffer', function($sniffer) { - return { - priority: 1000, - compile: function() { - $sniffer.csp = true; - } - }; -}]; +// ngCsp is not implemented as a proper directive any more, because we need it be processed while we bootstrap +// the system (before $parse is instantiated), for this reason we just have a csp() fn that looks for ng-csp attribute +// anywhere in the current doc /** * @ngdoc directive - * @name ng.directive:ngClick + * @name ngClick * * @description - * The ngClick allows you to specify custom behavior when - * element is clicked. + * The ngClick directive allows you to specify custom behavior when + * an element is clicked. * * @element ANY + * @priority 0 * @param {expression} ngClick {@link guide/expression Expression} to evaluate upon - * click. (Event object is available as `$event`) + * click. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - - + + count: {{count}} - - + + it('should check ng-click', function() { - expect(binding('count')).toBe('0'); - element('.doc-example-live :button').click(); - expect(binding('count')).toBe('1'); + expect(element(by.binding('count')).getText()).toMatch('0'); + element(by.css('button')).click(); + expect(element(by.binding('count')).getText()).toMatch('1'); }); - - + + */ /* * A directive that allows creation of custom onclick handlers that are defined as angular @@ -13245,17 +18825,21 @@ var ngCspDirective = ['$sniffer', function($sniffer) { */ var ngEventDirectives = {}; forEach( - 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave submit'.split(' '), + 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.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}); - }); - }); + return { + compile: function($element, attr) { + var fn = $parse(attr[directiveName]); + return function(scope, element, attr) { + element.on(lowercase(name), function(event) { + scope.$apply(function() { + fn(scope, {$event:event}); + }); + }); + }; + } }; }]; } @@ -13263,232 +18847,548 @@ forEach( /** * @ngdoc directive - * @name ng.directive:ngDblclick + * @name ngDblclick * * @description - * The `ngDblclick` directive allows you to specify custom behavior on dblclick event. + * The `ngDblclick` directive allows you to specify custom behavior on a dblclick event. * * @element ANY + * @priority 0 * @param {expression} ngDblclick {@link guide/expression Expression} to evaluate upon - * dblclick. (Event object is available as `$event`) + * a dblclick. (The Event object is available as `$event`) * * @example - * See {@link ng.directive:ngClick ngClick} + + + + count: {{count}} + + */ /** * @ngdoc directive - * @name ng.directive:ngMousedown + * @name ngMousedown * * @description * The ngMousedown directive allows you to specify custom behavior on mousedown event. * * @element ANY + * @priority 0 * @param {expression} ngMousedown {@link guide/expression Expression} to evaluate upon - * mousedown. (Event object is available as `$event`) + * mousedown. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - * See {@link ng.directive:ngClick ngClick} + + + + count: {{count}} + + */ /** * @ngdoc directive - * @name ng.directive:ngMouseup + * @name ngMouseup * * @description * Specify custom behavior on mouseup event. * * @element ANY + * @priority 0 * @param {expression} ngMouseup {@link guide/expression Expression} to evaluate upon - * mouseup. (Event object is available as `$event`) + * mouseup. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - * See {@link ng.directive:ngClick ngClick} + + + + count: {{count}} + + */ /** * @ngdoc directive - * @name ng.directive:ngMouseover + * @name ngMouseover * * @description * Specify custom behavior on mouseover event. * * @element ANY + * @priority 0 * @param {expression} ngMouseover {@link guide/expression Expression} to evaluate upon - * mouseover. (Event object is available as `$event`) + * mouseover. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - * See {@link ng.directive:ngClick ngClick} + + + + count: {{count}} + + */ /** * @ngdoc directive - * @name ng.directive:ngMouseenter + * @name ngMouseenter * * @description * Specify custom behavior on mouseenter event. * * @element ANY + * @priority 0 * @param {expression} ngMouseenter {@link guide/expression Expression} to evaluate upon - * mouseenter. (Event object is available as `$event`) + * mouseenter. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - * See {@link ng.directive:ngClick ngClick} + + + + count: {{count}} + + */ /** * @ngdoc directive - * @name ng.directive:ngMouseleave + * @name ngMouseleave * * @description * Specify custom behavior on mouseleave event. * * @element ANY + * @priority 0 * @param {expression} ngMouseleave {@link guide/expression Expression} to evaluate upon - * mouseleave. (Event object is available as `$event`) + * mouseleave. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - * See {@link ng.directive:ngClick ngClick} + + + + count: {{count}} + + */ /** * @ngdoc directive - * @name ng.directive:ngMousemove + * @name ngMousemove * * @description * Specify custom behavior on mousemove event. * * @element ANY + * @priority 0 * @param {expression} ngMousemove {@link guide/expression Expression} to evaluate upon - * mousemove. (Event object is available as `$event`) + * mousemove. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - * See {@link ng.directive:ngClick ngClick} + + + + count: {{count}} + + */ /** * @ngdoc directive - * @name ng.directive:ngKeydown + * @name ngKeydown * * @description * Specify custom behavior on keydown event. * - * @element ANY - * @param {expression} ngKeydown {@link guide/expression Expression} to evaluate upon - * keydown. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.) + * @element ANY + * @priority 0 + * @param {expression} ngKeydown {@link guide/expression Expression} to evaluate upon + * keydown. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.) + * + * @example + + + + key down count: {{count}} + + + */ + + +/** + * @ngdoc directive + * @name ngKeyup + * + * @description + * Specify custom behavior on keyup event. + * + * @element ANY + * @priority 0 + * @param {expression} ngKeyup {@link guide/expression Expression} to evaluate upon + * keyup. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.) + * + * @example + + +

Typing in the input box below updates the key count

+ key up count: {{count}} + +

Typing in the input box below updates the keycode

+ +

event keyCode: {{ event.keyCode }}

+

event altKey: {{ event.altKey }}

+
+
+ */ + + +/** + * @ngdoc directive + * @name ngKeypress + * + * @description + * Specify custom behavior on keypress event. + * + * @element ANY + * @param {expression} ngKeypress {@link guide/expression Expression} to evaluate upon + * keypress. ({@link guide/expression#-event- Event object is available as `$event`} + * and can be interrogated for keyCode, altKey, etc.) + * + * @example + + + + key press count: {{count}} + + + */ + + +/** + * @ngdoc directive + * @name ngSubmit + * + * @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), but only if the form does not contain `action`, + * `data-action`, or `x-action` attributes. + * + * @element form + * @priority 0 + * @param {expression} ngSubmit {@link guide/expression Expression} to eval. + * ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + + + +
+ Enter text and hit enter: + + +
list={{list}}
+
+
+ + it('should check ng-submit', function() { + expect(element(by.binding('list')).getText()).toBe('list=[]'); + element(by.css('#submit')).click(); + expect(element(by.binding('list')).getText()).toContain('hello'); + expect(element(by.input('text')).getAttribute('value')).toBe(''); + }); + it('should ignore empty strings', function() { + expect(element(by.binding('list')).getText()).toBe('list=[]'); + element(by.css('#submit')).click(); + element(by.css('#submit')).click(); + expect(element(by.binding('list')).getText()).toContain('hello'); + }); + +
+ */ + +/** + * @ngdoc directive + * @name ngFocus + * + * @description + * Specify custom behavior on focus event. + * + * @element window, input, select, textarea, a + * @priority 0 + * @param {expression} ngFocus {@link guide/expression Expression} to evaluate upon + * focus. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + * See {@link ng.directive:ngClick ngClick} + */ + +/** + * @ngdoc directive + * @name ngBlur + * + * @description + * Specify custom behavior on blur event. + * + * @element window, input, select, textarea, a + * @priority 0 + * @param {expression} ngBlur {@link guide/expression Expression} to evaluate upon + * blur. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + * See {@link ng.directive:ngClick ngClick} + */ + +/** + * @ngdoc directive + * @name ngCopy + * + * @description + * Specify custom behavior on copy event. + * + * @element window, input, select, textarea, a + * @priority 0 + * @param {expression} ngCopy {@link guide/expression Expression} to evaluate upon + * copy. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - * See {@link ng.directive:ngClick ngClick} + + + + copied: {{copied}} + + */ - /** * @ngdoc directive - * @name ng.directive:ngKeyup + * @name ngCut * * @description - * Specify custom behavior on keyup event. + * Specify custom behavior on cut event. * - * @element ANY - * @param {expression} ngKeyup {@link guide/expression Expression} to evaluate upon - * keyup. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.) + * @element window, input, select, textarea, a + * @priority 0 + * @param {expression} ngCut {@link guide/expression Expression} to evaluate upon + * cut. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - * See {@link ng.directive:ngClick ngClick} + + + + cut: {{cut}} + + */ - /** * @ngdoc directive - * @name ng.directive:ngKeypress + * @name ngPaste * * @description - * Specify custom behavior on keypress event. + * Specify custom behavior on paste event. * - * @element ANY - * @param {expression} ngKeypress {@link guide/expression Expression} to evaluate upon - * keypress. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.) + * @element window, input, select, textarea, a + * @priority 0 + * @param {expression} ngPaste {@link guide/expression Expression} to evaluate upon + * paste. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - * See {@link ng.directive:ngClick ngClick} + + + + pasted: {{paste}} + + */ - /** * @ngdoc directive - * @name ng.directive:ngSubmit + * @name ngIf + * @restrict A * * @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) **but only if the form does not contain an `action` - * attribute**. + * The `ngIf` directive removes or recreates a portion of the DOM tree based on an + * {expression}. If the expression assigned to `ngIf` evaluates to a false + * value then the element is removed from the DOM, otherwise a clone of the + * element is reinserted into the DOM. + * + * `ngIf` differs from `ngShow` and `ngHide` in that `ngIf` completely removes and recreates the + * element in the DOM rather than changing its visibility via the `display` css property. A common + * case when this difference is significant is when using css selectors that rely on an element's + * position within the DOM, such as the `:first-child` or `:last-child` pseudo-classes. + * + * Note that when an element is removed using `ngIf` its scope is destroyed and a new scope + * is created when the element is restored. The scope created within `ngIf` inherits from + * its parent scope using + * [prototypal inheritance](https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance). + * An important implication of this is if `ngModel` is used within `ngIf` to bind to + * a javascript primitive defined in the parent scope. In this case any modifications made to the + * variable within the child scope will override (hide) the value in the parent scope. + * + * Also, `ngIf` recreates elements using their compiled state. An example of this behavior + * is if an element's class attribute is directly modified after it's compiled, using something like + * jQuery's `.addClass()` method, and the element is later removed. When `ngIf` recreates the element + * the added class will be lost because the original compiled state is used to regenerate the element. + * + * Additionally, you can provide animations via the `ngAnimate` module to animate the `enter` + * and `leave` effects. + * + * @animations + * enter - happens just after the ngIf contents change and a new DOM element is created and injected into the ngIf container + * leave - happens just before the ngIf contents are removed from the DOM * - * @element form - * @param {expression} ngSubmit {@link guide/expression Expression} to eval. (Event object is available as `$event`) + * @element ANY + * @scope + * @priority 600 + * @param {expression} ngIf If the {@link guide/expression expression} is falsy then + * the element is removed from the DOM tree. If it is truthy a copy of the compiled + * element is added to the DOM tree. * * @example - - - -
- Enter text and hit enter: - - -
list={{list}}
-
-
- - 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"]'); - }); - -
+ + + Click me:
+ Show when checked: + + I'm removed when the checkbox is unchecked. + +
+ + .animate-if { + background:white; + border:1px solid black; + padding:10px; + } + + .animate-if.ng-enter, .animate-if.ng-leave { + -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + } + + .animate-if.ng-enter, + .animate-if.ng-leave.ng-leave-active { + opacity:0; + } + + .animate-if.ng-leave, + .animate-if.ng-enter.ng-enter-active { + opacity:1; + } + +
*/ +var ngIfDirective = ['$animate', function($animate) { + return { + transclude: 'element', + priority: 600, + terminal: true, + restrict: 'A', + $$tlb: true, + link: function ($scope, $element, $attr, ctrl, $transclude) { + var block, childScope, previousElements; + $scope.$watch($attr.ngIf, function ngIfWatchAction(value) { + + if (toBoolean(value)) { + if (!childScope) { + childScope = $scope.$new(); + $transclude(childScope, function (clone) { + clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' '); + // Note: We only need the first/last node of the cloned nodes. + // However, we need to keep the reference to the jqlite wrapper as it might be changed later + // by a directive with templateUrl when its template arrives. + block = { + clone: clone + }; + $animate.enter(clone, $element.parent(), $element); + }); + } + } else { + if(previousElements) { + previousElements.remove(); + previousElements = null; + } + if(childScope) { + childScope.$destroy(); + childScope = null; + } + if(block) { + previousElements = getBlockElements(block.clone); + $animate.leave(previousElements, function() { + previousElements = null; + }); + block = null; + } + } + }); + } + }; +}]; /** * @ngdoc directive - * @name ng.directive:ngInclude + * @name ngInclude * @restrict ECA * * @description * Fetches, compiles and includes an external HTML fragment. * - * Keep in mind that Same Origin Policy applies to included resources - * (e.g. ngInclude won't work for cross-domain requests on all browsers and for - * file:// access on some browsers). + * By default, the template URL is restricted to the same domain and protocol as the + * application document. This is done by calling {@link ng.$sce#getTrustedResourceUrl + * $sce.getTrustedResourceUrl} on it. To load templates from other domains or protocols + * you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist them} or + * [wrap them](ng.$sce#trustAsResourceUrl) as trusted values. Refer to Angular's {@link + * ng.$sce Strict Contextual Escaping}. + * + * In addition, the browser's + * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest) + * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) + * policy may further restrict whether the template is successfully loaded. + * For example, `ngInclude` won't work for cross-domain requests on all browsers and for `file://` + * access on some browsers. + * + * @animations + * enter - animation is used to bring new content into the browser. + * leave - animation is used to animate existing content away. + * + * The enter and leave animation occur concurrently. * * @scope + * @priority 400 * * @param {string} ngInclude|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'"`. + * make sure you wrap it in **single** quotes, e.g. `src="'myPartialTemplate.html'"`. * @param {string=} onload Expression to evaluate when a new partial is loaded. * * @param {string=} autoscroll Whether `ngInclude` should call {@link ng.$anchorScroll @@ -13499,7 +19399,7 @@ forEach( * - Otherwise enable scrolling only if the expression evaluates to truthy value. * * @example - +
url of the template: {{template.url}}
-
+
+
+
function Ctrl($scope) { $scope.templates = - [ { name: 'template1.html', url: 'template1.html'} - , { name: 'template2.html', url: 'template2.html'} ]; + [ { name: 'template1.html', url: 'template1.html'}, + { name: 'template2.html', url: 'template2.html'} ]; $scope.template = $scope.templates[0]; } @@ -13524,19 +19426,73 @@ forEach( Content of template2.html - + + .slide-animate-container { + position:relative; + background:white; + border:1px solid black; + height:40px; + overflow:hidden; + } + + .slide-animate { + padding:10px; + } + + .slide-animate.ng-enter, .slide-animate.ng-leave { + -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + + position:absolute; + top:0; + left:0; + right:0; + bottom:0; + display:block; + padding:10px; + } + + .slide-animate.ng-enter { + top:-50px; + } + .slide-animate.ng-enter.ng-enter-active { + top:0; + } + + .slide-animate.ng-leave { + top:0; + } + .slide-animate.ng-leave.ng-leave-active { + top:50px; + } + + + var templateSelect = element(by.model('template')); + var includeElem = element(by.css('[ng-include]')); + it('should load template1.html', function() { - expect(element('.doc-example-live [ng-include]').text()). - toMatch(/Content of template1.html/); + expect(includeElem.getText()).toMatch(/Content of template1.html/); }); + it('should load template2.html', function() { - select('template').option('1'); - expect(element('.doc-example-live [ng-include]').text()). - toMatch(/Content of template2.html/); + if (browser.params.browser == 'firefox') { + // Firefox can't handle using selects + // See https://github.com/angular/protractor/issues/480 + return; + } + templateSelect.click(); + templateSelect.element.all(by.css('option')).get(2).click(); + expect(includeElem.getText()).toMatch(/Content of template2.html/); }); + it('should change to blank', function() { - select('template').option(''); - expect(element('.doc-example-live [ng-include]').text()).toEqual(''); + if (browser.params.browser == 'firefox') { + // Firefox can't handle using selects + return; + } + templateSelect.click(); + templateSelect.element.all(by.css('option')).get(0).click(); + expect(includeElem.isPresent()).toBe(false); });
@@ -13545,152 +19501,238 @@ forEach( /** * @ngdoc event - * @name ng.directive:ngInclude#$includeContentLoaded - * @eventOf ng.directive:ngInclude + * @name ngInclude#$includeContentRequested + * @eventType emit on the scope ngInclude was declared in + * @description + * Emitted every time the ngInclude content is requested. + */ + + +/** + * @ngdoc event + * @name ngInclude#$includeContentLoaded * @eventType emit on the current ngInclude scope * @description * Emitted every time the ngInclude content is reloaded. */ -var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile', - function($http, $templateCache, $anchorScroll, $compile) { +var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate', '$sce', + function($http, $templateCache, $anchorScroll, $animate, $sce) { return { restrict: 'ECA', + priority: 400, terminal: true, + transclude: 'element', + controller: angular.noop, compile: function(element, attr) { var srcExp = attr.ngInclude || attr.src, onloadExp = attr.onload || '', autoScrollExp = attr.autoscroll; - return function(scope, element) { + return function(scope, $element, $attr, ctrl, $transclude) { var changeCounter = 0, - childScope; - - var clearContent = function() { - if (childScope) { - childScope.$destroy(); - childScope = null; + currentScope, + previousElement, + currentElement; + + var cleanupLastIncludeContent = function() { + if(previousElement) { + previousElement.remove(); + previousElement = null; + } + if(currentScope) { + currentScope.$destroy(); + currentScope = null; + } + if(currentElement) { + $animate.leave(currentElement, function() { + previousElement = null; + }); + previousElement = currentElement; + currentElement = null; } - - element.html(''); }; - scope.$watch(srcExp, function ngIncludeWatchAction(src) { + scope.$watch($sce.parseAsResourceUrl(srcExp), function ngIncludeWatchAction(src) { + var afterAnimation = function() { + if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) { + $anchorScroll(); + } + }; var thisChangeId = ++changeCounter; if (src) { $http.get(src, {cache: $templateCache}).success(function(response) { if (thisChangeId !== changeCounter) return; + var newScope = scope.$new(); + ctrl.template = response; + + // Note: This will also link all children of ng-include that were contained in the original + // html. If that content contains controllers, ... they could pollute/change the scope. + // However, using ng-include on an element with additional content does not make sense... + // Note: We can't remove them in the cloneAttchFn of $transclude as that + // function is called before linking the content, which would apply child + // directives to non existing elements. + var clone = $transclude(newScope, function(clone) { + cleanupLastIncludeContent(); + $animate.enter(clone, null, $element, afterAnimation); + }); - if (childScope) childScope.$destroy(); - childScope = scope.$new(); - - element.html(response); - $compile(element.contents())(childScope); - - if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) { - $anchorScroll(); - } + currentScope = newScope; + currentElement = clone; - childScope.$emit('$includeContentLoaded'); + currentScope.$emit('$includeContentLoaded'); scope.$eval(onloadExp); }).error(function() { - if (thisChangeId === changeCounter) clearContent(); + if (thisChangeId === changeCounter) cleanupLastIncludeContent(); }); - } else clearContent(); + scope.$emit('$includeContentRequested'); + } else { + cleanupLastIncludeContent(); + ctrl.template = null; + } }); }; } }; }]; +// This directive is called during the $transclude call of the first `ngInclude` directive. +// It will replace and compile the content of the element with the loaded template. +// We need this directive so that the element content is already filled when +// the link function of another directive on the same element as ngInclude +// is called. +var ngIncludeFillContentDirective = ['$compile', + function($compile) { + return { + restrict: 'ECA', + priority: -400, + require: 'ngInclude', + link: function(scope, $element, $attr, ctrl) { + $element.html(ctrl.template); + $compile($element.contents())(scope); + } + }; + }]; + /** * @ngdoc directive - * @name ng.directive:ngInit + * @name ngInit + * @restrict AC * * @description - * The `ngInit` directive specifies initialization tasks to be executed - * before the template enters execution mode during bootstrap. + * The `ngInit` directive allows you to evaluate an expression in the + * current scope. + * + *
+ * The only appropriate use of `ngInit` is for aliasing special properties of + * {@link ng.directive:ngRepeat `ngRepeat`}, as seen in the demo below. Besides this case, you + * should use {@link guide/controller controllers} rather than `ngInit` + * to initialize values on a scope. + *
+ *
+ * **Note**: If you have assignment in `ngInit` along with {@link ng.$filter `$filter`}, make + * sure you have parenthesis for correct precedence: + *
+ *   
+ *
+ *
+ * + * @priority 450 * * @element ANY * @param {expression} ngInit {@link guide/expression Expression} to eval. * * @example - - -
- {{greeting}} {{person}}! -
-
- - it('should check greeting', function() { - expect(binding('greeting')).toBe('Hello'); - expect(binding('person')).toBe('World'); + + + +
+
+
+ list[ {{outerIndex}} ][ {{innerIndex}} ] = {{value}}; +
+
+
+
+ + it('should alias index positions', function() { + var elements = element.all(by.css('.example-init')); + expect(elements.get(0).getText()).toBe('list[ 0 ][ 0 ] = a;'); + expect(elements.get(1).getText()).toBe('list[ 0 ][ 1 ] = b;'); + expect(elements.get(2).getText()).toBe('list[ 1 ][ 0 ] = c;'); + expect(elements.get(3).getText()).toBe('list[ 1 ][ 1 ] = d;'); }); -
-
+ +
*/ var ngInitDirective = ngDirective({ + priority: 450, compile: function() { return { pre: function(scope, element, attrs) { scope.$eval(attrs.ngInit); } - } + }; } }); /** * @ngdoc directive - * @name ng.directive:ngNonBindable + * @name ngNonBindable + * @restrict AC * @priority 1000 * * @description - * Sometimes it is necessary to write code which looks like bindings but which should be left alone - * by angular. Use `ngNonBindable` to make angular ignore a chunk of HTML. + * The `ngNonBindable` directive tells Angular not to compile or bind the contents of the current + * DOM element. This is useful if the element contains what appears to be Angular directives and + * bindings but which should be ignored by Angular. This could be the case if you have a site that + * displays snippets of code, for instance. * * @element ANY * * @example - * In this example there are two location where a simple binding (`{{}}`) is present, but the one - * wrapped in `ngNonBindable` is left alone. + * In this example there are two locations where a simple interpolation binding (`{{}}`) is present, + * but the one wrapped in `ngNonBindable` is left alone. * * @example - - + +
Normal: {{1 + 2}}
Ignored: {{1 + 2}}
-
- + + 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/); + expect(element(by.binding('1 + 2')).getText()).toContain('3'); + expect(element.all(by.css('div')).last().getText()).toMatch(/1 \+ 2/); }); - -
+ + */ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); /** * @ngdoc directive - * @name ng.directive:ngPluralize + * @name ngPluralize * @restrict EA * * @description - * # Overview * `ngPluralize` is a directive that displays messages according to en-US localization rules. * These rules are bundled with angular.js, but can be overridden * (see {@link guide/i18n Angular i18n} dev guide). You configure ngPluralize directive * 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](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html) + * 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". + * [plural categories](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html) + * 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 + * While a plural 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. There are examples of plural categories * and explicit number rules throughout the rest of this documentation. @@ -13707,13 +19749,13 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); * * The following example shows how to configure ngPluralize: * - *
+ * ```html
  * 
  * 
- *
+ *``` * * 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" @@ -13721,7 +19763,7 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); * 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 + * 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 * `{{personCount}}`. The closed braces `{}` is a placeholder * for {{numberExpression}}. @@ -13733,7 +19775,7 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); * The offset attribute allows you to offset a number by any desired value. * Let's take a look at an example: * - *
+ * ```html
  * 
  * 
- * 
+ * ``` * * Notice that we are still using two plural categories(one, other), but we added * three explicit number rules 0, 1 and 2. @@ -13756,13 +19798,13 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); * 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 {string|expression} count The variable to be bound to. + * @param {string} when The mapping between plural category to its corresponding strings. * @param {number=} offset Offset to deduct from the total number. * * @example - - + + -
- - selection={{selection}} -
-
-
Settings Div
- Home Span - default -
+ + +
+ + selection={{selection}} +
+
+
Settings Div
+
Home Span
+
default
- - - 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/); - }); - - - */ -var NG_SWITCH = 'ng-switch'; -var ngSwitchDirective = valueFn({ - restrict: 'EA', - require: 'ngSwitch', - // asks for $scope to fool the BC controller module - controller: ['$scope', function ngSwitchController() { - this.cases = {}; - }], - link: function(scope, element, attr, ctrl) { - var watchExpr = attr.ngSwitch || attr.on, - selectedTransclude, - selectedElement, - selectedScope; - - scope.$watch(watchExpr, function ngSwitchWatchAction(value) { - if (selectedElement) { - selectedScope.$destroy(); - selectedElement.remove(); - selectedElement = selectedScope = null; - } - if ((selectedTransclude = ctrl.cases['!' + value] || ctrl.cases['?'])) { - scope.$eval(attr.change); - selectedScope = scope.$new(); - selectedTransclude(selectedScope, function(caseElement) { - selectedElement = caseElement; - element.append(caseElement); - }); +
+
+ + function Ctrl($scope) { + $scope.items = ['settings', 'home', 'other']; + $scope.selection = $scope.items[0]; } - }); - } -}); + + + .animate-switch-container { + position:relative; + background:white; + border:1px solid black; + height:40px; + overflow:hidden; + } + + .animate-switch { + padding:10px; + } + + .animate-switch.ng-animate { + -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + + position:absolute; + top:0; + left:0; + right:0; + bottom:0; + } + + .animate-switch.ng-leave.ng-leave-active, + .animate-switch.ng-enter { + top:-50px; + } + .animate-switch.ng-leave, + .animate-switch.ng-enter.ng-enter-active { + top:0; + } + + + var switchElem = element(by.css('[ng-switch]')); + var select = element(by.model('selection')); + + it('should start in settings', function() { + expect(switchElem.getText()).toMatch(/Settings Div/); + }); + it('should change to home', function() { + select.element.all(by.css('option')).get(1).click(); + expect(switchElem.getText()).toMatch(/Home Span/); + }); + it('should select default', function() { + select.element.all(by.css('option')).get(2).click(); + expect(switchElem.getText()).toMatch(/default/); + }); + +
+ */ +var ngSwitchDirective = ['$animate', function($animate) { + return { + restrict: 'EA', + require: 'ngSwitch', + + // asks for $scope to fool the BC controller module + controller: ['$scope', function ngSwitchController() { + this.cases = {}; + }], + link: function(scope, element, attr, ngSwitchController) { + var watchExpr = attr.ngSwitch || attr.on, + selectedTranscludes = [], + selectedElements = [], + previousElements = [], + selectedScopes = []; + + scope.$watch(watchExpr, function ngSwitchWatchAction(value) { + var i, ii; + for (i = 0, ii = previousElements.length; i < ii; ++i) { + previousElements[i].remove(); + } + previousElements.length = 0; + + for (i = 0, ii = selectedScopes.length; i < ii; ++i) { + var selected = selectedElements[i]; + selectedScopes[i].$destroy(); + previousElements[i] = selected; + $animate.leave(selected, function() { + previousElements.splice(i, 1); + }); + } + + selectedElements.length = 0; + selectedScopes.length = 0; + + if ((selectedTranscludes = ngSwitchController.cases['!' + value] || ngSwitchController.cases['?'])) { + scope.$eval(attr.change); + forEach(selectedTranscludes, function(selectedTransclude) { + var selectedScope = scope.$new(); + selectedScopes.push(selectedScope); + selectedTransclude.transclude(selectedScope, function(caseElement) { + var anchor = selectedTransclude.element; + + selectedElements.push(caseElement); + $animate.enter(caseElement, anchor.parent(), anchor); + }); + }); + } + }); + } + }; +}]; var ngSwitchWhenDirective = ngDirective({ transclude: 'element', - priority: 500, + priority: 800, require: '^ngSwitch', - compile: function(element, attrs, transclude) { - return function(scope, element, attr, ctrl) { - ctrl.cases['!' + attrs.ngSwitchWhen] = transclude; - }; + link: function(scope, element, attrs, ctrl, $transclude) { + ctrl.cases['!' + attrs.ngSwitchWhen] = (ctrl.cases['!' + attrs.ngSwitchWhen] || []); + ctrl.cases['!' + attrs.ngSwitchWhen].push({ transclude: $transclude, element: element }); } }); var ngSwitchDefaultDirective = ngDirective({ transclude: 'element', - priority: 500, + priority: 800, require: '^ngSwitch', - compile: function(element, attrs, transclude) { - return function(scope, element, attr, ctrl) { - ctrl.cases['?'] = transclude; - }; - } + link: function(scope, element, attr, ctrl, $transclude) { + ctrl.cases['?'] = (ctrl.cases['?'] || []); + ctrl.cases['?'].push({ transclude: $transclude, element: element }); + } }); /** * @ngdoc directive - * @name ng.directive:ngTransclude + * @name ngTransclude + * @restrict AC * * @description - * Insert the transcluded DOM here. + * Directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion. + * + * Any existing content of the element that this directive is placed on will be removed before the transcluded content is inserted. * * @element ANY * * @example - - + + Load inlined template
-
- + + it('should load template defined inside script tag', function() { - element('#tpl-link').click(); - expect(element('#tpl-content').text()).toMatch(/Content of the template/); + element(by.css('#tpl-link')).click(); + expect(element(by.css('#tpl-content')).getText()).toMatch(/Content of the template/); }); - -
+ + */ var scriptDirective = ['$templateCache', function($templateCache) { return { @@ -14569,9 +21012,10 @@ var scriptDirective = ['$templateCache', function($templateCache) { }; }]; +var ngOptionsMinErr = minErr('ngOptions'); /** * @ngdoc directive - * @name ng.directive:select + * @name select * @restrict E * * @description @@ -14579,22 +21023,29 @@ var scriptDirective = ['$templateCache', function($templateCache) { * * # `ngOptions` * - * Optionally `ngOptions` attribute can be used to dynamically generate a list of `` * DOM element. + * * `trackexpr`: Used when working with an array of objects. The result of this expression will be + * used to identify the objects in the array. The `trackexpr` will most likely refer to the + * `value` variable (e.g. `value.propertyName`). * * @example - - + +
@@ -14656,45 +21110,49 @@ var scriptDirective = ['$templateCache', function($templateCache) {
Color (null not allowed): -
+
Color (null allowed): - +
Color grouped by shade: -
- Select bogus.
+ Select bogus.

- Currently selected: {{ {selected_color:color} }} + Currently selected: {{ {selected_color:myColor} }}
+ ng-style="{'background-color':myColor.name}">
-
- + + 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'); + expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('red'); + element.all(by.select('myColor')).first().click(); + element.all(by.css('select[ng-model="myColor"] option')).first().click(); + expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('black'); + element(by.css('.nullable select[ng-model="myColor"]')).click(); + element.all(by.css('.nullable select[ng-model="myColor"] option')).first().click(); + expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('null'); }); - -
+ + */ var ngOptionsDirective = valueFn({ terminal: true }); +// jshint maxlen: false var selectDirective = ['$compile', '$parse', function($compile, $parse) { - //0000111110000000000022220000000000000000000000333300000000000000444444444444444440000000005555555555555555500000006666666666666666600000000000000077770 - 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+(.*)$/, + //000011111111110000000000022222222220000000000000000000003333333333000000000000004444444444444440000000005555555555555550000000666666666666666000000000000000777777777700000000000000000008888888888 + var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/, nullModelCtrl = {$setViewValue: noop}; +// jshint maxlen: 100 return { restrict: 'E', @@ -14714,10 +21172,11 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { ngModelCtrl = ngModelCtrl_; nullOption = nullOption_; unknownOption = unknownOption_; - } + }; self.addOption = function(value) { + assertNotHasOwnProperty(value, '"option value"'); optionsMap[value] = true; if (ngModelCtrl.$viewValue == value) { @@ -14743,12 +21202,12 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { $element.prepend(unknownOption); $element.val(unknownVal); unknownOption.prop('selected', true); // needed for IE - } + }; self.hasOption = function(value) { return optionsMap.hasOwnProperty(value); - } + }; $scope.$on('$destroy', function() { // disable unknown option so that we don't do work when the whole select is being destroyed @@ -14774,7 +21233,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { // find "null" option for(var i = 0, children = element.children(), ii = children.length; i < ii; i++) { - if (children[i].value == '') { + if (children[i].value === '') { emptyOption = nullOption = children.eq(i); break; } @@ -14783,30 +21242,22 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { selectCtrl.init(ngModelCtrl, nullOption, unknownOption); // required validator - if (multiple && (attr.required || attr.ngRequired)) { - var requiredValidator = function(value) { - ngModelCtrl.$setValidity('required', !attr.required || (value && value.length)); - return value; + if (multiple) { + ngModelCtrl.$isEmpty = function(value) { + return !value || value.length === 0; }; - - ngModelCtrl.$parsers.push(requiredValidator); - ngModelCtrl.$formatters.unshift(requiredValidator); - - attr.$observe('required', function() { - requiredValidator(ngModelCtrl.$viewValue); - }); } - if (optionsExp) Options(scope, element, ngModelCtrl); - else if (multiple) Multiple(scope, element, ngModelCtrl); - else Single(scope, element, ngModelCtrl, selectCtrl); + if (optionsExp) setupAsOptions(scope, element, ngModelCtrl); + else if (multiple) setupAsMultiple(scope, element, ngModelCtrl); + else setupAsSingle(scope, element, ngModelCtrl, selectCtrl); //////////////////////////// - function Single(scope, selectElement, ngModelCtrl, selectCtrl) { + function setupAsSingle(scope, selectElement, ngModelCtrl, selectCtrl) { ngModelCtrl.$render = function() { var viewValue = ngModelCtrl.$viewValue; @@ -14823,7 +21274,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { } }; - selectElement.bind('change', function() { + selectElement.on('change', function() { scope.$apply(function() { if (unknownOption.parent()) unknownOption.remove(); ngModelCtrl.$setViewValue(selectElement.val()); @@ -14831,7 +21282,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { }); } - function Multiple(scope, selectElement, ctrl) { + function setupAsMultiple(scope, selectElement, ctrl) { var lastView; ctrl.$render = function() { var items = new HashMap(ctrl.$viewValue); @@ -14844,12 +21295,12 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { // we need to work of an array, so we need to see if anything was inserted/removed scope.$watch(function selectMultipleWatch() { if (!equals(lastView, ctrl.$viewValue)) { - lastView = copy(ctrl.$viewValue); + lastView = shallowCopy(ctrl.$viewValue); ctrl.$render(); } }); - selectElement.bind('change', function() { + selectElement.on('change', function() { scope.$apply(function() { var array = []; forEach(selectElement.find('option'), function(option) { @@ -14862,13 +21313,15 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { }); } - function Options(scope, selectElement, ctrl) { + function setupAsOptions(scope, selectElement, ctrl) { var match; - if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) { - throw Error( - "Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" + - " but got '" + optionsExp + "'."); + if (!(match = optionsExp.match(NG_OPTIONS_REGEXP))) { + throw ngOptionsMinErr('iexp', + "Expected expression in form of " + + "'_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" + + " but got '{0}'. Element: {1}", + optionsExp, startingTag(selectElement)); } var displayFn = $parse(match[2] || match[1]), @@ -14877,9 +21330,12 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { groupByFn = $parse(match[3] || ''), valueFn = $parse(match[2] ? match[1] : valueName), valuesFn = $parse(match[7]), - // 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 + track = match[8], + trackFn = track ? $parse(match[8]) : null, + // 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:''}]]; if (nullOption) { @@ -14890,20 +21346,20 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { // becomes the compilation root nullOption.removeClass('ng-scope'); - // we need to remove it before calling selectElement.html('') because otherwise IE will + // we need to remove it before calling selectElement.empty() because otherwise IE will // remove the label from the element. wtf? nullOption.remove(); } // clear contents, we'll add what's needed based on the model - selectElement.html(''); + selectElement.empty(); - selectElement.bind('change', function() { + selectElement.on('change', function() { scope.$apply(function() { var optionGroup, collection = valuesFn(scope) || [], locals = {}, - key, value, optionElement, index, groupIndex, length, groupLength; + key, value, optionElement, index, groupIndex, length, groupLength, trackIndex; if (multiple) { value = []; @@ -14917,7 +21373,14 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { if ((optionElement = optionGroup[index].element)[0].selected) { key = optionElement.val(); if (keyName) locals[keyName] = key; - locals[valueName] = collection[key]; + if (trackFn) { + for (trackIndex = 0; trackIndex < collection.length; trackIndex++) { + locals[valueName] = collection[trackIndex]; + if (trackFn(scope, locals) == key) break; + } + } else { + locals[valueName] = collection[key]; + } value.push(valueFn(scope, locals)); } } @@ -14926,12 +21389,28 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { key = selectElement.val(); if (key == '?') { value = undefined; - } else if (key == ''){ + } else if (key === ''){ value = null; } else { - locals[valueName] = collection[key]; - if (keyName) locals[keyName] = key; - value = valueFn(scope, locals); + if (trackFn) { + for (trackIndex = 0; trackIndex < collection.length; trackIndex++) { + locals[valueName] = collection[trackIndex]; + if (trackFn(scope, locals) == key) { + value = valueFn(scope, locals); + break; + } + } + } else { + locals[valueName] = collection[key]; + if (keyName) locals[keyName] = key; + value = valueFn(scope, locals); + } + } + // Update the null option's selected property here so $render cleans it up correctly + if (optionGroupsCache[0].length > 1) { + if (optionGroupsCache[0][1].id !== key) { + optionGroupsCache[0][1].selected = false; + } } } ctrl.$setViewValue(value); @@ -14944,7 +21423,8 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { scope.$watch(render); function render() { - var optionGroups = {'':[]}, // Temporary location for the option groups before we render them + // Temporary location for the option groups before we render them + var optionGroups = {'':[]}, optionGroupNames = [''], optionGroupName, optionGroup, @@ -14953,6 +21433,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { modelValue = ctrl.$modelValue, values = valuesFn(scope) || [], keys = keyName ? sortedKeys(values) : values, + key, groupLength, length, groupIndex, index, locals = {}, @@ -14963,27 +21444,55 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { label; if (multiple) { - selectedSet = new HashMap(modelValue); + if (trackFn && isArray(modelValue)) { + selectedSet = new HashMap([]); + for (var trackIndex = 0; trackIndex < modelValue.length; trackIndex++) { + locals[valueName] = modelValue[trackIndex]; + selectedSet.put(trackFn(scope, locals), modelValue[trackIndex]); + } + } else { + selectedSet = new HashMap(modelValue); + } } // 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) || ''; + + key = index; + if (keyName) { + key = keys[index]; + if ( key.charAt(0) === '$' ) continue; + locals[keyName] = key; + } + + locals[valueName] = values[key]; + + optionGroupName = groupByFn(scope, locals) || ''; if (!(optionGroup = optionGroups[optionGroupName])) { optionGroup = optionGroups[optionGroupName] = []; optionGroupNames.push(optionGroupName); } if (multiple) { - selected = selectedSet.remove(valueFn(scope, locals)) != undefined; + selected = isDefined( + selectedSet.remove(trackFn ? trackFn(scope, locals) : valueFn(scope, locals)) + ); } else { - selected = modelValue === valueFn(scope, locals); + if (trackFn) { + var modelCast = {}; + modelCast[valueName] = modelValue; + selected = trackFn(scope, modelCast) === trackFn(scope, locals); + } else { + selected = modelValue === valueFn(scope, locals); + } selectedSet = selectedSet || selected; // see if at least one item is selected } label = displayFn(scope, locals); // what will be seen by the user - label = label === undefined ? '' : label; // doing displayFn(scope, locals) || '' overwrites zero values + + // doing displayFn(scope, locals) || '' overwrites zero values + label = isDefined(label) ? label : ''; optionGroup.push({ - id: keyName ? keys[index] : index, // either the index into array or key from object + // either the index into array or key from object + id: trackFn ? trackFn(scope, locals) : (keyName ? keys[index] : index), label: label, selected: selected // determine if we should be selected }); @@ -15040,7 +21549,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { lastElement.val(existingOption.id = option.id); } // lastElement.prop('selected') provided by jQuery has side-effects - if (lastElement[0].selected !== option.selected) { + if (existingOption.selected !== option.selected) { lastElement.prop('selected', (existingOption.selected = option.selected)); } } else { @@ -15087,7 +21596,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { } } } - } + }; }]; var optionDirective = ['$interpolate', function($interpolate) { @@ -15131,12 +21640,12 @@ var optionDirective = ['$interpolate', function($interpolate) { selectCtrl.addOption(attr.value); } - element.bind('$destroy', function() { + element.on('$destroy', function() { selectCtrl.removeOption(attr.value); }); }; } - } + }; }]; var styleDirective = valueFn({ @@ -15144,6 +21653,12 @@ var styleDirective = valueFn({ terminal: true }); + if (window.angular.bootstrap) { + //AngularJS is already loaded, so we can return here... + console.log('WARNING: Tried to load angular more than once.'); + return; + } + //try to bind to jquery now so that one can write angular.element().read() //but we will rebind on bootstrap again. bindJQuery(); @@ -15155,4 +21670,5 @@ var styleDirective = valueFn({ }); })(window, document); -angular.element(document).find('head').append(''); \ No newline at end of file + +!window.angular.$$csp() && window.angular.element(document).find('head').prepend(''); \ No newline at end of file diff --git a/angular_app/js/app.js b/angular_app/js/app.js index 0441bbb..de52fd4 100644 --- a/angular_app/js/app.js +++ b/angular_app/js/app.js @@ -1,50 +1,73 @@ -var ticketApp = angular.module('ticketApp', []); - -ticketApp.factory('TicketList', function(){ - var TicketList = {}; - TicketList.tickets = [{ - name: "Write the presentation", - priority: 1, - }, - { name: "Write the demo app", - priority: 2 - }, - { name: "Give the presentation", - priority: 2 - }, - { name: "Understand what I'm talking about", - priority: 5 - } - ]; - return TicketList; -}) +var todoApp = angular.module('todoApp', ['ngResource']).config( + ['$httpProvider', function($httpProvider) { + var defaults = $httpProvider.defaults.headers; + defaults.patch = defaults.patch || {}; + defaults.patch['Content-Type'] = 'application/json'; + defaults.common['Accept'] = 'application/json'; +}]); -function TicketsController($scope, TicketList) { - $scope.tickets = TicketList.tickets; - $scope.total = function(list) { - return list.length; - } +todoApp.factory('TodoList', function($resource){ + + return $resource('http://localhost:3000/todos/:id', + {id: '@id'}, + { get: { + method:'GET', + transformResponse: function(data) { + return JSON.parse(data).todo; + } + }, + query: { + method:'GET', + isArray: true, + transformResponse: function(data) { + return JSON.parse(data).todos; + } + }, + save: { + method: 'POST', + transformRequest: function(data) { + return JSON.stringify({'todo': data}); + }, + transformResponse: function (data) { + return JSON.parse(data).todo; + } + } + }); +}); + +todoApp.controller('TodosController', function ($scope, TodoList) { + $scope.todos = []; - $scope.abbreviation = function(ticket) { - return "["+ ticket.name.slice(0,3) + "]"; + TodoList.query(function (todos) { + $scope.todos = todos; + }); + + $scope.total = $scope.todos.length; + + $scope.$watchCollection('todos',function(todos, oldTodos) { + $scope.total = todos.length; + }); + + $scope.abbreviation = function(todo) { + return "["+ todo.name.slice(0,3) + "]"; } $scope.add = function() { - obj = { + newTodo = new TodoList({ name: $scope.new_name, priority: $scope.new_priority - } - $scope.tickets.push(obj) + }); + newTodo.$save(function(todo) { + $scope.todos.push(todo) + }); } -} +}); -ticketApp.directive('total', function() { - return function(scope, element) { - element.html( - 'Total Tickets:' + scope.tickets.length - ); - } +todoApp.directive('total', function() { + return { + template: 'Total Todos: {{total}}' + }; }) diff --git a/ember_app/index.html b/ember_app/index.html index c9161ad..45c9591 100755 --- a/ember_app/index.html +++ b/ember_app/index.html @@ -7,19 +7,19 @@ - - - - - - - - + + + + diff --git a/ember_app/js/app.js b/ember_app/js/app.js index 50525c0..1582069 100755 --- a/ember_app/js/app.js +++ b/ember_app/js/app.js @@ -1,9 +1,13 @@ App = Ember.Application.create(); -App.Ticket = Ember.Object.extend({ +App.ApplicationAdapter = DS.RESTAdapter.extend({ + host: 'http://localhost:3000' +}); + +App.Todo = DS.Model.extend({ - name: null, - priority: null, + name: DS.attr(), + priority: DS.attr(), slug: function() { return this.get('name') @@ -21,69 +25,57 @@ App.Ticket = Ember.Object.extend({ App.Router.map(function() { // put your routes here - this.resource("tickets", function() { - this.route("edit", {path: "/:ticket_slug"}); + this.resource("todos", function() { + this.route("edit", {path: "/:id"}); }); }); -App.TicketsRoute = Ember.Route.extend({ +App.TodosRoute = Ember.Route.extend({ model: function() { - return [ - App.Ticket.create({ - name: "Write the presentation", - priority: 1, - }), - App.Ticket.create({ - name: "Write the demo app", - priority: 2 - }), - App.Ticket.create({ - name: "Give the presentation", - priority: 2 - }), - App.Ticket.create({ - name: "Understand what I'm talking about", - priority: 5 - }) - ]; + return this.store.find('todo'); } }); -App.TicketsIndexRoute = Ember.Route.extend({ - model: function() { - return this.modelFor('tickets'); - } -}); - - -App.TicketsIndexController = Ember.ArrayController.extend({ +App.TodosIndexController = Ember.ArrayController.extend({ actions: { addItem: function() { name = this.get("name") priority = this.get("priority") - ticket = App.Ticket.create({ + todo = this.store.createRecord('todo', { name: name, priority: priority }); - this.pushObject(ticket); + todo.save(); } } }); -App.TicketsEditRoute = Ember.Route.extend({ +App.TodosEditRoute = Ember.Route.extend({ model: function(params) { - return this.modelFor('tickets').find('slug', params.ticket_slug) + return this.store.find('todo', params.id) }, serialize: function(model) { // this will make the URL `/posts/foo-post` - return { ticket_slug: model.get('slug') }; + return { id: model.get('id') }; + } +}); + +App.TodosEditController = Ember.ObjectController.extend({ + + actions: { + updateItem: function(router, event) { + self = this; + this.get("model").save().then(function() { + self.transitionToRoute('todos'); + }); + } } }); App.IndexRoute = Ember.Route.extend({ redirect: function() { - this.transitionTo('tickets'); + this.transitionTo('todos'); } }); diff --git a/ember_app/js/libs/ember-1.1.1.js b/ember_app/js/libs/ember-1.5.1.js similarity index 73% rename from ember_app/js/libs/ember-1.1.1.js rename to ember_app/js/libs/ember-1.5.1.js index 3f2cea8..4ec7312 100755 --- a/ember_app/js/libs/ember-1.1.1.js +++ b/ember_app/js/libs/ember-1.5.1.js @@ -1,4 +1,13 @@ - // Version: 1.1.1 +/*! + * @overview Ember - JavaScript Application Framework + * @copyright Copyright 2011-2014 Tilde Inc. and contributors + * Portions Copyright 2006-2011 Strobe Inc. + * Portions Copyright 2008-2011 Apple Inc. All rights reserved. + * @license Licensed under MIT license + * See https://raw.github.com/emberjs/ember.js/master/LICENSE + * @version 1.5.1 + */ + (function() { /*global __fail__*/ @@ -22,7 +31,20 @@ if ('undefined' === typeof Ember) { } } -Ember.ENV = 'undefined' === typeof ENV ? {} : ENV; +// This needs to be kept in sync with the logic in +// `packages/ember-metal/lib/core.js`. +// +// This is duplicated here to ensure that `Ember.ENV` +// is setup even if `Ember` is not loaded yet. +if (Ember.ENV) { + // do nothing if Ember.ENV is already setup +} else if ('undefined' !== typeof EmberENV) { + Ember.ENV = EmberENV; +} else if('undefined' !== typeof ENV) { + Ember.ENV = ENV; +} else { + Ember.ENV = {}; +} if (!('MANDATORY_SETTER' in Ember.ENV)) { Ember.ENV.MANDATORY_SETTER = true; // default to true for debug dist @@ -48,11 +70,6 @@ if (!('MANDATORY_SETTER' in Ember.ENV)) { */ Ember.assert = function(desc, test) { if (!test) { - Ember.Logger.assert(test, desc); - } - - if (Ember.testing && !test) { - // when testing, ensure test failures when assertions fail throw new Ember.Error("Assertion Failed: " + desc); } }; @@ -100,9 +117,6 @@ Ember.debug = function(message) { will be displayed. */ Ember.deprecate = function(message, test) { - if (Ember.TESTING_DEPRECATION) { return; } - - if (arguments.length === 1) { test = false; } if (test) { return; } if (Ember.ENV.RAISE_ON_DEPRECATION) { throw new Ember.Error(message); } @@ -161,12 +175,44 @@ Ember.deprecateFunc = function(message, func) { }; +/** + Run a function meant for debugging. Ember build tools will remove any calls to + `Ember.runInDebug()` when doing a production build. + + ```javascript + Ember.runInDebug( function() { + Ember.Handlebars.EachView.reopen({ + didInsertElement: function() { + console.log("I'm happy"); + } + }); + }); + ``` + + @method runInDebug + @param {Function} func The function to be executed. +*/ +Ember.runInDebug = function(func) { + func() +}; + // Inform the developer about the Ember Inspector if not installed. if (!Ember.testing) { - if (typeof window !== 'undefined' && window.chrome && window.addEventListener) { + var isFirefox = typeof InstallTrigger !== 'undefined'; + var isChrome = !!window.chrome && !window.opera; + + if (typeof window !== 'undefined' && (isFirefox || isChrome) && window.addEventListener) { window.addEventListener("load", function() { - if (document.body && document.body.dataset && !document.body.dataset.emberExtension) { - Ember.debug('For more advanced debugging, install the Ember Inspector from https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi'); + if (document.documentElement && document.documentElement.dataset && !document.documentElement.dataset.emberExtension) { + var downloadURL; + + if(isChrome) { + downloadURL = 'https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi'; + } else if(isFirefox) { + downloadURL = 'https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/'; + } + + Ember.debug('For more advanced debugging, install the Ember Inspector from ' + downloadURL); } }, false); } @@ -174,10 +220,19 @@ if (!Ember.testing) { })(); - // Version: 1.1.1 +/*! + * @overview Ember - JavaScript Application Framework + * @copyright Copyright 2011-2014 Tilde Inc. and contributors + * Portions Copyright 2006-2011 Strobe Inc. + * Portions Copyright 2008-2011 Apple Inc. All rights reserved. + * @license Licensed under MIT license + * See https://raw.github.com/emberjs/ember.js/master/LICENSE + * @version 1.5.1 + */ + (function() { -var define, requireModule; +var define, requireModule, require, requirejs; (function() { var registry = {}, seen = {}; @@ -186,36 +241,52 @@ var define, requireModule; registry[name] = { deps: deps, callback: callback }; }; - requireModule = function(name) { + requirejs = require = requireModule = function(name) { + requirejs._eak_seen = registry; + if (seen[name]) { return seen[name]; } seen[name] = {}; - var mod, deps, callback, reified, exports; - - mod = registry[name]; - - if (!mod) { - throw new Error("Module '" + name + "' not found."); + if (!registry[name]) { + throw new Error("Could not find module " + name); } - deps = mod.deps; - callback = mod.callback; - reified = []; + var mod = registry[name], + deps = mod.deps, + callback = mod.callback, + reified = [], + exports; for (var i=0, l=deps.length; i= + * length, then append to the end of the array. + * @param {Number} amt Number of elements that should be removed from the array, + * starting at *idx* + * @param {Array} objects An array of zero or more objects that should be + * inserted into the array at *idx* + * + * @return {Array} The modified array. + */ replace: function(array, idx, amt, objects) { if (array.replace) { return array.replace(idx, amt, objects); @@ -1855,6 +2069,29 @@ var utils = Ember.EnumerableUtils = { } }, + /** + * Calculates the intersection of two arrays. This method returns a new array + * filled with the records that the two passed arrays share with each other. + * If there is no intersection, an empty array will be returned. + * + * ```javascript + * var array1 = [1, 2, 3, 4, 5]; + * var array2 = [1, 3, 5, 6, 7]; + * + * Ember.EnumerableUtils.intersection(array1, array2); // [1, 3, 5] + * + * var array1 = [1, 2, 3]; + * var array2 = [4, 5, 6]; + * + * Ember.EnumerableUtils.intersection(array1, array2); // [] + * ``` + * + * @method intersection + * @param {Array} array1 The first array + * @param {Array} array2 The second array + * + * @return {Array} The intersection of the two passed arrays. + */ intersection: function(array1, array2) { var intersection = []; @@ -1961,13 +2198,12 @@ if (Ember.config.overrideAccessors) { } /** - @private - Normalizes a target/path pair to reflect that actual target/path that should be observed, etc. This takes into account passing in global property paths (i.e. a path beginning with a captial letter not defined on the target) and * separators. + @private @method normalizeTuple @for Ember @param {Object} target The current target. May be `null`. @@ -1989,7 +2225,7 @@ var normalizeTuple = Ember.normalizeTuple = function(target, path) { } // must return some kind of path to be valid else other things will break. - if (!path || path.length===0) throw new Error('Invalid Path'); + if (!path || path.length===0) throw new Ember.Error('Path cannot be empty'); return [ target, path ]; }; @@ -2030,7 +2266,6 @@ Ember.getWithDefault = function(root, key, defaultValue) { Ember.get = get; -Ember.getPath = Ember.deprecateFunc('getPath is deprecated since get now supports paths', Ember.get); })(); @@ -2068,8 +2303,13 @@ var o_create = Ember.create, function indexOf(array, target, method) { var index = -1; - for (var i = 0, l = array.length; i < l; i += 3) { - if (target === array[i] && method === array[i+1]) { index = i; break; } + // hashes are added to the end of the event array + // so it makes sense to start searching at the end + // of the array and search in reverse + for (var i = array.length - 3 ; i >=0; i -= 3) { + if (target === array[i] && method === array[i + 1]) { + index = i; break; + } } return index; } @@ -2217,8 +2457,6 @@ function removeListener(obj, eventName, target, method) { } /** - @private - Suspend listener during callback. This should only be used by the target of the event listener @@ -2228,6 +2466,8 @@ function removeListener(obj, eventName, target, method) { @method suspendListener @for Ember + + @private @param obj @param {String} eventName @param {Object|Function} targetOrMethod A target object or a function @@ -2254,13 +2494,12 @@ function suspendListener(obj, eventName, target, method, callback) { } /** - @private - Suspends multiple listeners during a callback. - @method suspendListeners @for Ember + + @private @param obj @param {Array} eventName Array of event names @param {Object|Function} targetOrMethod A target object or a function @@ -2302,10 +2541,9 @@ function suspendListeners(obj, eventNames, target, method, callback) { } /** - @private - Return a list of currently watched events + @private @method watchedEvents @for Ember @param obj @@ -2326,7 +2564,7 @@ function watchedEvents(obj) { is skipped, and once listeners are removed. A listener without a target is executed on the passed object. If an array of actions is not passed, the actions stored on the passed object are invoked. - + @method sendEvent @for Ember @param obj @@ -2405,13 +2643,16 @@ function listenersFor(obj, eventName) { Define a property as a function that should be executed when a specified event or events are triggered. - var Job = Ember.Object.extend({ - logCompleted: Ember.on('completed', function(){ - console.log('Job completed!'); - }) - }); - var job = Job.create(); - Ember.sendEvent(job, 'completed'); // Logs "Job completed!" + + ``` javascript + var Job = Ember.Object.extend({ + logCompleted: Ember.on('completed', function(){ + console.log('Job completed!'); + }) + }); + var job = Job.create(); + Ember.sendEvent(job, 'completed'); // Logs "Job completed!" + ``` @method on @for Ember @@ -2510,7 +2751,7 @@ ObserverSet.prototype.clear = function() { (function() { -var metaFor = Ember.meta, +var META_KEY = Ember.META_KEY, guidFor = Ember.guidFor, tryFinally = Ember.tryFinally, sendEvent = Ember.sendEvent, @@ -2541,10 +2782,10 @@ var metaFor = Ember.meta, @return {void} */ function propertyWillChange(obj, keyName) { - var m = metaFor(obj, false), - watching = m.watching[keyName] > 0 || keyName === 'length', - proto = m.proto, - desc = m.descs[keyName]; + var m = obj[META_KEY], + watching = (m && m.watching[keyName] > 0) || keyName === 'length', + proto = m && m.proto, + desc = m && m.descs[keyName]; if (!watching) { return; } if (proto === obj) { return; } @@ -2571,10 +2812,10 @@ Ember.propertyWillChange = propertyWillChange; @return {void} */ function propertyDidChange(obj, keyName) { - var m = metaFor(obj, false), - watching = m.watching[keyName] > 0 || keyName === 'length', - proto = m.proto, - desc = m.descs[keyName]; + var m = obj[META_KEY], + watching = (m && m.watching[keyName] > 0) || keyName === 'length', + proto = m && m.proto, + desc = m && m.descs[keyName]; if (proto === obj) { return; } @@ -2647,7 +2888,7 @@ function chainsWillChange(obj, keyName, m) { } function chainsDidChange(obj, keyName, m, suppressEvents) { - if (!(m.hasOwnProperty('chainWatchers') && + if (!(m && m.hasOwnProperty('chainWatchers') && m.chainWatchers[keyName])) { return; } @@ -2676,6 +2917,7 @@ Ember.overrideChains = function(obj, keyName, m) { /** @method beginPropertyChanges @chainable + @private */ function beginPropertyChanges() { deferred++; @@ -2685,6 +2927,7 @@ Ember.beginPropertyChanges = beginPropertyChanges; /** @method endPropertyChanges + @private */ function endPropertyChanges() { deferred--; @@ -2761,16 +3004,6 @@ var META_KEY = Ember.META_KEY, property is not defined but the object implements the `setUnknownProperty` method then that will be invoked as well. - If you plan to run on IE8 and older browsers then you should use this - method anytime you want to set a property on an object that you don't - know for sure is private. (Properties beginning with an underscore '_' - are considered private.) - - On all newer browsers, you only need to use this method to set - properties if the property might not be defined on the object and you want - to respect the `setUnknownProperty` handler. Otherwise you can ignore this - method. - @method set @for Ember @param {Object} obj The object to modify. @@ -2817,7 +3050,7 @@ var set = function set(obj, keyName, value, tolerant) { if (value !== currentValue) { Ember.propertyWillChange(obj, keyName); if (MANDATORY_SETTER) { - if (currentValue === undefined && !(keyName in obj)) { + if ((currentValue === undefined && !(keyName in obj)) || !obj.propertyIsEnumerable(keyName)) { Ember.defineProperty(obj, keyName, null, value); // setup mandatory setter } else { meta.values[keyName] = value; @@ -2848,7 +3081,7 @@ function setPath(root, path, value, tolerant) { keyName = path.slice(path.lastIndexOf('.') + 1); // get the first part of the part - path = path.slice(0, path.length-(keyName.length+1)); + path = (path === keyName) ? keyName : path.slice(0, path.length-(keyName.length+1)); // unless the path is this, look up the first part to // get the root @@ -2857,19 +3090,18 @@ function setPath(root, path, value, tolerant) { } if (!keyName || keyName.length === 0) { - throw new Error('You passed an empty path'); + throw new Ember.Error('Property set failed: You passed an empty path'); } if (!root) { if (tolerant) { return; } - else { throw new Error('Object in path '+path+' could not be found or was destroyed.'); } + else { throw new Ember.Error('Property set failed: object in path "'+path+'" could not be found or was destroyed.'); } } return set(root, keyName, value); } Ember.set = set; -Ember.setPath = Ember.deprecateFunc('setPath is deprecated since set now supports paths', Ember.set); /** Error-tolerant form of `Ember.set`. Will not blow up if any part of the @@ -2887,7 +3119,6 @@ Ember.setPath = Ember.deprecateFunc('setPath is deprecated since set now support Ember.trySet = function(root, path, value) { return set(root, path, value, true); }; -Ember.trySetPath = Ember.deprecateFunc('trySetPath has been renamed to trySet', Ember.trySet); })(); @@ -3272,6 +3503,153 @@ MapWithDefault.prototype.copy = function() { +(function() { +function consoleMethod(name) { + var consoleObj, logToConsole; + if (Ember.imports.console) { + consoleObj = Ember.imports.console; + } else if (typeof console !== 'undefined') { + consoleObj = console; + } + + var method = typeof consoleObj === 'object' ? consoleObj[name] : null; + + if (method) { + // Older IE doesn't support apply, but Chrome needs it + if (typeof method.apply === 'function') { + logToConsole = function() { + method.apply(consoleObj, arguments); + }; + logToConsole.displayName = 'console.' + name; + return logToConsole; + } else { + return function() { + var message = Array.prototype.join.call(arguments, ', '); + method(message); + }; + } + } +} + +function assertPolyfill(test, message) { + if (!test) { + try { + // attempt to preserve the stack + throw new Ember.Error("assertion failed: " + message); + } catch(error) { + setTimeout(function() { + throw error; + }, 0); + } + } +} + +/** + Inside Ember-Metal, simply uses the methods from `imports.console`. + Override this to provide more robust logging functionality. + + @class Logger + @namespace Ember +*/ +Ember.Logger = { + /** + Logs the arguments to the console. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + var foo = 1; + Ember.Logger.log('log value of foo:', foo); + // "log value of foo: 1" will be printed to the console + ``` + + @method log + @for Ember.Logger + @param {*} arguments + */ + log: consoleMethod('log') || Ember.K, + + /** + Prints the arguments to the console with a warning icon. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + Ember.Logger.warn('Something happened!'); + // "Something happened!" will be printed to the console with a warning icon. + ``` + + @method warn + @for Ember.Logger + @param {*} arguments + */ + warn: consoleMethod('warn') || Ember.K, + + /** + Prints the arguments to the console with an error icon, red text and a stack trace. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + Ember.Logger.error('Danger! Danger!'); + // "Danger! Danger!" will be printed to the console in red text. + ``` + + @method error + @for Ember.Logger + @param {*} arguments + */ + error: consoleMethod('error') || Ember.K, + + /** + Logs the arguments to the console. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + var foo = 1; + Ember.Logger.info('log value of foo:', foo); + // "log value of foo: 1" will be printed to the console + ``` + + @method info + @for Ember.Logger + @param {*} arguments + */ + info: consoleMethod('info') || Ember.K, + + /** + Logs the arguments to the console in blue text. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + var foo = 1; + Ember.Logger.debug('log value of foo:', foo); + // "log value of foo: 1" will be printed to the console + ``` + + @method debug + @for Ember.Logger + @param {*} arguments + */ + debug: consoleMethod('debug') || consoleMethod('info') || Ember.K, + + /** + If the value passed into `Ember.Logger.assert` is not truthy it will throw an error with a stack trace. + + ```javascript + Ember.Logger.assert(true); // undefined + Ember.Logger.assert(true === false); // Throws an Assertion failed error. + ``` + + @method assert + @for Ember.Logger + @param {Boolean} bool Value to test + */ + assert: consoleMethod('assert') || assertPolyfill +}; + + +})(); + + + (function() { /** @module ember-metal @@ -3316,8 +3694,6 @@ var DEFAULT_GETTER_FUNCTION = Ember.DEFAULT_GETTER_FUNCTION = function(name) { }; /** - @private - NOTE: This is a low-level method used by other parts of the API. You almost never want to call this method directly. Instead you should use `Ember.mixin()` to define new properties. @@ -3351,6 +3727,7 @@ var DEFAULT_GETTER_FUNCTION = Ember.DEFAULT_GETTER_FUNCTION = function(name) { }).property('firstName', 'lastName')); ``` + @private @method defineProperty @for Ember @param {Object} obj the object to define this property on. This may be a prototype. @@ -3387,6 +3764,12 @@ Ember.defineProperty = function(obj, keyName, desc, data, meta) { } else { obj[keyName] = undefined; // make enumerable } + + if (Ember.FEATURES.isEnabled('composable-computed-properties')) { + if (desc.func && desc._dependentCPs) { + addImplicitCPs(obj, desc._dependentCPs, meta); + } + } } else { descs[keyName] = undefined; // shadow descriptor in proto if (desc == null) { @@ -3422,6 +3805,22 @@ Ember.defineProperty = function(obj, keyName, desc, data, meta) { return this; }; +if (Ember.FEATURES.isEnabled('composable-computed-properties')) { + var addImplicitCPs = function defineImplicitCPs(obj, implicitCPs, meta) { + var cp, key, length = implicitCPs.length; + + for (var i=0; i 1) { watching[keyName]--; @@ -3574,7 +3983,8 @@ var metaFor = Ember.meta, // utils.js warn = Ember.warn, watchKey = Ember.watchKey, unwatchKey = Ember.unwatchKey, - FIRST_KEY = /^([^\.\*]+)/; + FIRST_KEY = /^([^\.\*]+)/, + META_KEY = Ember.META_KEY; function firstKey(path) { return path.match(FIRST_KEY)[0]; @@ -3608,24 +4018,24 @@ function addChainWatcher(obj, keyName, node) { if (!nodes[keyName]) { nodes[keyName] = []; } nodes[keyName].push(node); - watchKey(obj, keyName); + watchKey(obj, keyName, m); } var removeChainWatcher = Ember.removeChainWatcher = function(obj, keyName, node) { if (!obj || 'object' !== typeof obj) { return; } // nothing to do - var m = metaFor(obj, false); - if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do + var m = obj[META_KEY]; + if (m && !m.hasOwnProperty('chainWatchers')) { return; } // nothing to do - var nodes = m.chainWatchers; + var nodes = m && m.chainWatchers; - if (nodes[keyName]) { + if (nodes && nodes[keyName]) { nodes = nodes[keyName]; for (var i = 0, l = nodes.length; i < l; i++) { if (nodes[i] === node) { nodes.splice(i, 1); } } } - unwatchKey(obj, keyName); + unwatchKey(obj, keyName, m); }; // A ChainNode watches a single key on an object. If you provide a starting @@ -3665,14 +4075,14 @@ var ChainNodePrototype = ChainNode.prototype; function lazyGet(obj, key) { if (!obj) return undefined; - var meta = metaFor(obj, false); + var meta = obj[META_KEY]; // check if object meant only to be a prototype - if (meta.proto === obj) return undefined; + if (meta && meta.proto === obj) return undefined; if (key === "@each") return get(obj, key); // if a CP only return cached value - var desc = meta.descs[key]; + var desc = meta && meta.descs[key]; if (desc && desc._cacheable) { if (key in meta.cache) { return meta.cache[key]; @@ -3884,12 +4294,64 @@ ChainNodePrototype.didChange = function(events) { }; Ember.finishChains = function(obj) { - var m = metaFor(obj, false), chains = m.chains; + // We only create meta if we really have to + var m = obj[META_KEY], chains = m && m.chains; if (chains) { if (chains.value() !== obj) { - m.chains = chains = chains.copy(obj); + metaFor(obj).chains = chains = chains.copy(obj); + } else { + chains.didChange(null); } - chains.didChange(null); + } +}; + +})(); + + + +(function() { +/** + @module ember-metal + */ + +var forEach = Ember.EnumerableUtils.forEach, +BRACE_EXPANSION = /^((?:[^\.]*\.)*)\{(.*)\}$/; + +/** + Expands `pattern`, invoking `callback` for each expansion. + + The only pattern supported is brace-expansion, anything else will be passed + once to `callback` directly. Brace expansion can only appear at the end of a + pattern, for example as the last item in a chain. + + Example + ```js + function echo(arg){ console.log(arg); } + + Ember.expandProperties('foo.bar', echo); //=> 'foo.bar' + Ember.expandProperties('{foo,bar}', echo); //=> 'foo', 'bar' + Ember.expandProperties('foo.{bar,baz}', echo); //=> 'foo.bar', 'foo.baz' + Ember.expandProperties('{foo,bar}.baz', echo); //=> '{foo,bar}.baz' + ``` + + @method + @private + @param {string} pattern The property pattern to expand. + @param {function} callback The callback to invoke. It is invoked once per + expansion, and is passed the expansion. + */ +Ember.expandProperties = function (pattern, callback) { + var match, prefix, list; + + if (match = BRACE_EXPANSION.exec(pattern)) { + prefix = match[1]; + list = match[2]; + + forEach(list.split(','), function (suffix) { + callback(prefix + suffix); + }); + } else { + callback(pattern); } }; @@ -3905,8 +4367,8 @@ var metaFor = Ember.meta, // utils.js // get the chains for the current object. If the current object has // chains inherited from the proto they will be cloned and reconfigured for // the current object. -function chainsFor(obj) { - var m = metaFor(obj), ret = m.chains; +function chainsFor(obj, meta) { + var m = meta || metaFor(obj), ret = m.chains; if (!ret) { ret = m.chains = new ChainNode(null, null, obj); } else if (ret.value() !== obj) { @@ -3915,26 +4377,26 @@ function chainsFor(obj) { return ret; } -Ember.watchPath = function(obj, keyPath) { +Ember.watchPath = function(obj, keyPath, meta) { // can't watch length on Array - it is special... if (keyPath === 'length' && typeOf(obj) === 'array') { return; } - var m = metaFor(obj), watching = m.watching; + var m = meta || metaFor(obj), watching = m.watching; if (!watching[keyPath]) { // activate watching first time watching[keyPath] = 1; - chainsFor(obj).add(keyPath); + chainsFor(obj, m).add(keyPath); } else { watching[keyPath] = (watching[keyPath] || 0) + 1; } }; -Ember.unwatchPath = function(obj, keyPath) { - var m = metaFor(obj), watching = m.watching; +Ember.unwatchPath = function(obj, keyPath, meta) { + var m = meta || metaFor(obj), watching = m.watching; if (watching[keyPath] === 1) { watching[keyPath] = 0; - chainsFor(obj).remove(keyPath); + chainsFor(obj, m).remove(keyPath); } else if (watching[keyPath] > 1) { watching[keyPath]--; } @@ -3966,27 +4428,26 @@ function isKeyName(path) { } /** - @private - Starts watching a property on an object. Whenever the property changes, invokes `Ember.propertyWillChange` and `Ember.propertyDidChange`. This is the primitive used by observers and dependent keys; usually you will never call this method directly but instead use higher level methods like `Ember.addObserver()` + @private @method watch @for Ember @param obj @param {String} keyName */ -Ember.watch = function(obj, keyPath) { +Ember.watch = function(obj, _keyPath, m) { // can't watch length on Array - it is special... - if (keyPath === 'length' && typeOf(obj) === 'array') { return; } + if (_keyPath === 'length' && typeOf(obj) === 'array') { return; } - if (isKeyName(keyPath)) { - watchKey(obj, keyPath); + if (isKeyName(_keyPath)) { + watchKey(obj, _keyPath, m); } else { - watchPath(obj, keyPath); + watchPath(obj, _keyPath, m); } }; @@ -3997,34 +4458,33 @@ Ember.isWatching = function isWatching(obj, key) { Ember.watch.flushPending = Ember.flushPendingChains; -Ember.unwatch = function(obj, keyPath) { +Ember.unwatch = function(obj, _keyPath, m) { // can't watch length on Array - it is special... - if (keyPath === 'length' && typeOf(obj) === 'array') { return; } + if (_keyPath === 'length' && typeOf(obj) === 'array') { return; } - if (isKeyName(keyPath)) { - unwatchKey(obj, keyPath); + if (isKeyName(_keyPath)) { + unwatchKey(obj, _keyPath, m); } else { - unwatchPath(obj, keyPath); + unwatchPath(obj, _keyPath, m); } }; /** - @private - Call on an object when you first beget it from another object. This will setup any chained watchers on the object instance as needed. This method is safe to call multiple times. + @private @method rewatch @for Ember @param obj */ Ember.rewatch = function(obj) { - var m = metaFor(obj, false), chains = m.chains; + var m = obj[META_KEY], chains = m && m.chains; // make sure the object has its own guid. if (GUID_KEY in obj && !obj.hasOwnProperty(GUID_KEY)) { - generateGuid(obj, 'ember'); + generateGuid(obj); } // make sure any chained watchers update. @@ -4097,6 +4557,9 @@ var get = Ember.get, watch = Ember.watch, unwatch = Ember.unwatch; +var expandProperties = Ember.expandProperties; + + // .......................................................... // DEPENDENT KEYS // @@ -4145,7 +4608,7 @@ function addDependentKeys(desc, obj, keyName, meta) { // Increment the number of times depKey depends on keyName. keys[keyName] = (keys[keyName] || 0) + 1; // Watch the depKey - watch(obj, depKey); + watch(obj, depKey, meta); } } @@ -4164,7 +4627,7 @@ function removeDependentKeys(desc, obj, keyName, meta) { // Increment the number of times depKey depends on keyName. keys[keyName] = (keys[keyName] || 0) - 1; // Watch the depKey - unwatch(obj, depKey); + unwatch(obj, depKey, meta); } } @@ -4255,16 +4718,36 @@ function removeDependentKeys(desc, obj, keyName, meta) { */ function ComputedProperty(func, opts) { this.func = func; + if (Ember.FEATURES.isEnabled('composable-computed-properties')) { + setDependentKeys(this, opts && opts.dependentKeys); + } else { + this._dependentKeys = opts && opts.dependentKeys; + } this._cacheable = (opts && opts.cacheable !== undefined) ? opts.cacheable : true; - this._dependentKeys = opts && opts.dependentKeys; this._readOnly = opts && (opts.readOnly !== undefined || !!opts.readOnly); } Ember.ComputedProperty = ComputedProperty; + ComputedProperty.prototype = new Ember.Descriptor(); var ComputedPropertyPrototype = ComputedProperty.prototype; +ComputedPropertyPrototype._dependentKeys = undefined; +ComputedPropertyPrototype._suspended = undefined; +ComputedPropertyPrototype._meta = undefined; + +if (Ember.FEATURES.isEnabled('composable-computed-properties')) { + ComputedPropertyPrototype._dependentCPs = undefined; + ComputedPropertyPrototype.implicitCPKey = undefined; + + ComputedPropertyPrototype.toString = function() { + if (this.implicitCPKey) { + return this.implicitCPKey; + } + return Ember.Descriptor.prototype.toString.apply(this, arguments); + }; +} /** Properties are cacheable by default. Computed property will automatically @@ -4291,11 +4774,11 @@ ComputedPropertyPrototype.cacheable = function(aFlag) { mode the computed property will not automatically cache the return value. ```javascript - MyApp.outsideService = Ember.Object.create({ + MyApp.outsideService = Ember.Object.extend({ value: function() { return OutsideService.getValue(); }.property().volatile() - }); + }).create(); ``` @method volatile @@ -4311,12 +4794,14 @@ ComputedPropertyPrototype.volatile = function() { mode the computed property will throw an error when set. ```javascript - MyApp.person = Ember.Object.create({ + MyApp.Person = Ember.Object.extend({ guid: function() { return 'guid-guid-guid'; }.property().readOnly() }); + MyApp.person = MyApp.Person.create(); + MyApp.person.set('guid', 'new-guid'); // will throw an exception ``` @@ -4334,7 +4819,7 @@ ComputedPropertyPrototype.readOnly = function(readOnly) { arguments containing key paths that this computed property depends on. ```javascript - MyApp.president = Ember.Object.create({ + MyApp.President = Ember.Object.extend({ fullName: Ember.computed(function() { return this.get('firstName') + ' ' + this.get('lastName'); @@ -4342,6 +4827,12 @@ ComputedPropertyPrototype.readOnly = function(readOnly) { // and lastName }).property('firstName', 'lastName') }); + + MyApp.president = MyApp.President.create({ + firstName: 'Barack', + lastName: 'Obama', + }); + MyApp.president.get('fullName'); // Barack Obama ``` @method property @@ -4350,11 +4841,23 @@ ComputedPropertyPrototype.readOnly = function(readOnly) { @chainable */ ComputedPropertyPrototype.property = function() { - var args = []; + var args; + + var addArg = function (property) { + args.push(property); + }; + + args = []; for (var i = 0, l = arguments.length; i < l; i++) { - args.push(arguments[i]); + expandProperties(arguments[i], addArg); + } + + if (Ember.FEATURES.isEnabled('composable-computed-properties')) { + setDependentKeys(this, args); + } else { + this._dependentKeys = args; } - this._dependentKeys = args; + return this; }; @@ -4481,7 +4984,7 @@ ComputedPropertyPrototype.set = function(obj, keyName, value) { funcArgLength, cachedValue, ret; if (this._readOnly) { - throw new Error('Cannot Set: ' + keyName + ' on: ' + obj.toString() ); + throw new Ember.Error('Cannot set read-only property "' + keyName + '" on object: ' + Ember.inspect(obj)); } this._suspended = obj; @@ -4571,7 +5074,7 @@ Ember.computed = function(func) { } if (typeof func !== "function") { - throw new Error("Computed Property declared without a property function"); + throw new Ember.Error("Computed Property declared without a property function"); } var cp = new ComputedProperty(func); @@ -4594,10 +5097,11 @@ Ember.computed = function(func) { @param {Object} obj the object whose property you want to check @param {String} key the name of the property whose cached value you want to return - @return {*} the cached value + @return {Object} the cached value */ Ember.cacheFor = function cacheFor(obj, key) { - var cache = metaFor(obj, false).cache; + var meta = obj[META_KEY], + cache = meta && meta.cache; if (cache && key in cache) { return cache[key]; @@ -4612,71 +5116,157 @@ function getProperties(self, propertyNames) { return ret; } -function registerComputed(name, macro) { - Ember.computed[name] = function(dependentKey) { - var args = a_slice.call(arguments); - return Ember.computed(dependentKey, function() { - return macro.apply(this, args); +var registerComputed, registerComputedWithProperties; + +if (Ember.FEATURES.isEnabled('composable-computed-properties')) { + var guidFor = Ember.guidFor, + map = Ember.EnumerableUtils.map, + filter = Ember.EnumerableUtils.filter, + typeOf = Ember.typeOf; + + var implicitKey = function (cp) { + return [guidFor(cp)].concat(cp._dependentKeys).join('_').replace(/\./g, '_DOT_'); + }; + + var normalizeDependentKey = function (key) { + if (key instanceof Ember.ComputedProperty) { + return implicitKey(key); + } else { + return key; + } + }; + + var normalizeDependentKeys = function (keys) { + return map(keys, function (key) { + return normalizeDependentKey(key); + }); + }; + + var selectDependentCPs = function (keys) { + return filter(keys, function (key) { + return key instanceof Ember.ComputedProperty; }); }; + + var setDependentKeys = function(cp, dependentKeys) { + if (dependentKeys) { + cp._dependentKeys = normalizeDependentKeys(dependentKeys); + cp._dependentCPs = selectDependentCPs(dependentKeys); + } else { + cp._dependentKeys = cp._dependentCPs = []; + } + cp.implicitCPKey = implicitKey(cp); + }; + // expose `normalizeDependentKey[s]` so user CP macros can easily support + // composition + Ember.computed.normalizeDependentKey = normalizeDependentKey; + Ember.computed.normalizeDependentKeys = normalizeDependentKeys; + + registerComputed = function (name, macro) { + Ember.computed[name] = function(dependentKey) { + var args = normalizeDependentKeys(a_slice.call(arguments)); + return Ember.computed(dependentKey, function() { + return macro.apply(this, args); + }); + }; + }; } -function registerComputedWithProperties(name, macro) { - Ember.computed[name] = function() { - var properties = a_slice.call(arguments); +if (Ember.FEATURES.isEnabled('composable-computed-properties')) { + registerComputedWithProperties = function(name, macro) { + Ember.computed[name] = function() { + var args = a_slice.call(arguments); + var properties = normalizeDependentKeys(args); - var computed = Ember.computed(function() { - return macro.apply(this, [getProperties(this, properties)]); - }); + var computed = Ember.computed(function() { + return macro.apply(this, [getProperties(this, properties)]); + }); + + return computed.property.apply(computed, args); + }; + }; +} else { + registerComputed = function (name, macro) { + Ember.computed[name] = function(dependentKey) { + var args = a_slice.call(arguments); + return Ember.computed(dependentKey, function() { + return macro.apply(this, args); + }); + }; + }; - return computed.property.apply(computed, properties); + registerComputedWithProperties = function(name, macro) { + Ember.computed[name] = function() { + var properties = a_slice.call(arguments); + + var computed = Ember.computed(function() { + return macro.apply(this, [getProperties(this, properties)]); + }); + + return computed.property.apply(computed, properties); + }; }; } -/** - A computed property that returns true if the value of the dependent - property is null, an empty string, empty array, or empty function. - Note: When using `Ember.computed.empty` to watch an array make sure to - use the `array.[]` syntax so the computed can subscribe to transitions - from empty to non-empty states. +if (Ember.FEATURES.isEnabled('composable-computed-properties')) { + Ember.computed.literal = function (value) { + return Ember.computed(function () { + return value; + }); + }; +} - Example - ```javascript - var ToDoList = Ember.Object.extend({ - done: Ember.computed.empty('todos.[]') // detect array changes + /** + A computed property that returns true if the value of the dependent + property is null, an empty string, empty array, or empty function. + + Note: When using `Ember.computed.empty` to watch an array make sure to + use the `array.[]` syntax so the computed can subscribe to transitions + from empty to non-empty states. + + Example + + ```javascript + var ToDoList = Ember.Object.extend({ + done: Ember.computed.empty('todos.[]') // detect array changes + }); + var todoList = ToDoList.create({todos: ['Unit Test', 'Documentation', 'Release']}); + todoList.get('done'); // false + todoList.get('todos').clear(); // [] + todoList.get('done'); // true + ``` + + @method computed.empty + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which negate + the original value for property + */ + registerComputed('empty', function(dependentKey) { + return Ember.isEmpty(get(this, dependentKey)); }); - var todoList = ToDoList.create({todos: ['Unit Test', 'Documentation', 'Release']}); - todoList.get('done'); // false - todoList.get('todos').clear(); // [] - todoList.get('done'); // true - ``` - @method computed.empty - @for Ember - @param {String} dependentKey - @return {Ember.ComputedProperty} computed property which negate - the original value for property -*/ -registerComputed('empty', function(dependentKey) { - return Ember.isEmpty(get(this, dependentKey)); -}); /** A computed property that returns true if the value of the dependent property is NOT null, an empty string, empty array, or empty function. + Note: When using `Ember.computed.notEmpty` to watch an array make sure to + use the `array.[]` syntax so the computed can subscribe to transitions + from empty to non-empty states. + Example ```javascript - var Hampster = Ember.Object.extend({ - hasStuff: Ember.computed.notEmpty('backpack') + var Hamster = Ember.Object.extend({ + hasStuff: Ember.computed.notEmpty('backpack.[]') }); - var hampster = Hampster.create({backpack: ['Food', 'Sleeping Bag', 'Tent']}); - hampster.get('hasStuff'); // true - hampster.get('backpack').clear(); // [] - hampster.get('hasStuff'); // false + var hamster = Hamster.create({backpack: ['Food', 'Sleeping Bag', 'Tent']}); + hamster.get('hasStuff'); // true + hamster.get('backpack').clear(); // [] + hamster.get('hasStuff'); // false ``` @method computed.notEmpty @@ -4697,15 +5287,15 @@ registerComputed('notEmpty', function(dependentKey) { Example ```javascript - var Hampster = Ember.Object.extend({ + var Hamster = Ember.Object.extend({ isHungry: Ember.computed.none('food') }); - var hampster = Hampster.create(); - hampster.get('isHungry'); // true - hampster.set('food', 'Banana'); - hampster.get('isHungry'); // false - hampster.set('food', null); - hampster.get('isHungry'); // true + var hamster = Hamster.create(); + hamster.get('isHungry'); // true + hamster.set('food', 'Banana'); + hamster.get('isHungry'); // false + hamster.set('food', null); + hamster.get('isHungry'); // true ``` @method computed.none @@ -4749,17 +5339,17 @@ registerComputed('not', function(dependentKey) { into a boolean value. ```javascript - var Hampster = Ember.Object.extend({ + var Hamster = Ember.Object.extend({ hasBananas: Ember.computed.bool('numBananas') }); - var hampster = Hampster.create(); - hampster.get('hasBananas'); // false - hampster.set('numBananas', 0); - hampster.get('hasBananas'); // false - hampster.set('numBananas', 1); - hampster.get('hasBananas'); // true - hampster.set('numBananas', null); - hampster.get('hasBananas'); // false + var hamster = Hamster.create(); + hamster.get('hasBananas'); // false + hamster.set('numBananas', 0); + hamster.get('hasBananas'); // false + hamster.set('numBananas', 1); + hamster.get('hasBananas'); // true + hamster.set('numBananas', null); + hamster.get('hasBananas'); // false ``` @method computed.bool @@ -4787,7 +5377,7 @@ registerComputed('bool', function(dependentKey) { user.get('hasValidEmail'); // false user.set('email', ''); user.get('hasValidEmail'); // false - user.set('email', 'ember_hampster@example.com'); + user.set('email', 'ember_hamster@example.com'); user.get('hasValidEmail'); // true ``` @@ -4800,7 +5390,7 @@ registerComputed('bool', function(dependentKey) { */ registerComputed('match', function(dependentKey, regexp) { var value = get(this, dependentKey); - return typeof value === 'string' ? !!value.match(regexp) : false; + return typeof value === 'string' ? regexp.test(value) : false; }); /** @@ -4810,15 +5400,15 @@ registerComputed('match', function(dependentKey, regexp) { Example ```javascript - var Hampster = Ember.Object.extend({ + var Hamster = Ember.Object.extend({ napTime: Ember.computed.equal('state', 'sleepy') }); - var hampster = Hampster.create(); - hampster.get('napTime'); // false - hampster.set('state', 'sleepy'); - hampster.get('napTime'); // true - hampster.set('state', 'hungry'); - hampster.get('napTime'); // false + var hamster = Hamster.create(); + hamster.get('napTime'); // false + hamster.set('state', 'sleepy'); + hamster.get('napTime'); // true + hamster.set('state', 'hungry'); + hamster.get('napTime'); // false ``` @method computed.equal @@ -4839,15 +5429,15 @@ registerComputed('equal', function(dependentKey, value) { Example ```javascript - var Hampster = Ember.Object.extend({ + var Hamster = Ember.Object.extend({ hasTooManyBananas: Ember.computed.gt('numBananas', 10) }); - var hampster = Hampster.create(); - hampster.get('hasTooManyBananas'); // false - hampster.set('numBananas', 3); - hampster.get('hasTooManyBananas'); // false - hampster.set('numBananas', 11); - hampster.get('hasTooManyBananas'); // true + var hamster = Hamster.create(); + hamster.get('hasTooManyBananas'); // false + hamster.set('numBananas', 3); + hamster.get('hasTooManyBananas'); // false + hamster.set('numBananas', 11); + hamster.get('hasTooManyBananas'); // true ``` @method computed.gt @@ -4868,15 +5458,15 @@ registerComputed('gt', function(dependentKey, value) { Example ```javascript - var Hampster = Ember.Object.extend({ + var Hamster = Ember.Object.extend({ hasTooManyBananas: Ember.computed.gte('numBananas', 10) }); - var hampster = Hampster.create(); - hampster.get('hasTooManyBananas'); // false - hampster.set('numBananas', 3); - hampster.get('hasTooManyBananas'); // false - hampster.set('numBananas', 10); - hampster.get('hasTooManyBananas'); // true + var hamster = Hamster.create(); + hamster.get('hasTooManyBananas'); // false + hamster.set('numBananas', 3); + hamster.get('hasTooManyBananas'); // false + hamster.set('numBananas', 10); + hamster.get('hasTooManyBananas'); // true ``` @method computed.gte @@ -4897,15 +5487,15 @@ registerComputed('gte', function(dependentKey, value) { Example ```javascript - var Hampster = Ember.Object.extend({ + var Hamster = Ember.Object.extend({ needsMoreBananas: Ember.computed.lt('numBananas', 3) }); - var hampster = Hampster.create(); - hampster.get('needsMoreBananas'); // true - hampster.set('numBananas', 3); - hampster.get('needsMoreBananas'); // false - hampster.set('numBananas', 2); - hampster.get('needsMoreBananas'); // true + var hamster = Hamster.create(); + hamster.get('needsMoreBananas'); // true + hamster.set('numBananas', 3); + hamster.get('needsMoreBananas'); // false + hamster.set('numBananas', 2); + hamster.get('needsMoreBananas'); // true ``` @method computed.lt @@ -4926,15 +5516,15 @@ registerComputed('lt', function(dependentKey, value) { Example ```javascript - var Hampster = Ember.Object.extend({ + var Hamster = Ember.Object.extend({ needsMoreBananas: Ember.computed.lte('numBananas', 3) }); - var hampster = Hampster.create(); - hampster.get('needsMoreBananas'); // true - hampster.set('numBananas', 5); - hampster.get('needsMoreBananas'); // false - hampster.set('numBananas', 3); - hampster.get('needsMoreBananas'); // true + var hamster = Hamster.create(); + hamster.get('needsMoreBananas'); // true + hamster.set('numBananas', 5); + hamster.get('needsMoreBananas'); // false + hamster.set('numBananas', 3); + hamster.get('needsMoreBananas'); // true ``` @method computed.lte @@ -4952,24 +5542,23 @@ registerComputed('lte', function(dependentKey, value) { A computed property that performs a logical `and` on the original values for the provided dependent properties. - Example ```javascript - var Hampster = Ember.Object.extend({ + var Hamster = Ember.Object.extend({ readyForCamp: Ember.computed.and('hasTent', 'hasBackpack') }); - var hampster = Hampster.create(); - hampster.get('readyForCamp'); // false - hampster.set('hasTent', true); - hampster.get('readyForCamp'); // false - hampster.set('hasBackpack', true); - hampster.get('readyForCamp'); // true + var hamster = Hamster.create(); + hamster.get('readyForCamp'); // false + hamster.set('hasTent', true); + hamster.get('readyForCamp'); // false + hamster.set('hasBackpack', true); + hamster.get('readyForCamp'); // true ``` @method computed.and @for Ember - @param {String} dependentKey, [dependentKey...] + @param {String} dependentKey* @return {Ember.ComputedProperty} computed property which performs a logical `and` on the values of all the original values for properties. */ @@ -4989,18 +5578,18 @@ registerComputedWithProperties('and', function(properties) { Example ```javascript - var Hampster = Ember.Object.extend({ + var Hamster = Ember.Object.extend({ readyForRain: Ember.computed.or('hasJacket', 'hasUmbrella') }); - var hampster = Hampster.create(); - hampster.get('readyForRain'); // false - hampster.set('hasJacket', true); - hampster.get('readyForRain'); // true + var hamster = Hamster.create(); + hamster.get('readyForRain'); // false + hamster.set('hasJacket', true); + hamster.get('readyForRain'); // true ``` @method computed.or @for Ember - @param {String} dependentKey, [dependentKey...] + @param {String} dependentKey* @return {Ember.ComputedProperty} computed property which performs a logical `or` on the values of all the original values for properties. */ @@ -5020,18 +5609,18 @@ registerComputedWithProperties('or', function(properties) { Example ```javascript - var Hampster = Ember.Object.extend({ + var Hamster = Ember.Object.extend({ hasClothes: Ember.computed.any('hat', 'shirt') }); - var hampster = Hampster.create(); - hampster.get('hasClothes'); // null - hampster.set('shirt', 'Hawaiian Shirt'); - hampster.get('hasClothes'); // 'Hawaiian Shirt' + var hamster = Hamster.create(); + hamster.get('hasClothes'); // null + hamster.set('shirt', 'Hawaiian Shirt'); + hamster.get('hasClothes'); // 'Hawaiian Shirt' ``` @method computed.any @for Ember - @param {String} dependentKey, [dependentKey...] + @param {String} dependentKey* @return {Ember.ComputedProperty} computed property which returns the first truthy value of given list of properties. */ @@ -5051,19 +5640,19 @@ registerComputedWithProperties('any', function(properties) { Example ```javascript - var Hampster = Ember.Object.extend({ - clothes: Ember.computed.map('hat', 'shirt') + var Hamster = Ember.Object.extend({ + clothes: Ember.computed.collect('hat', 'shirt') }); - var hampster = Hampster.create(); - hampster.get('clothes'); // [null, null] - hampster.set('hat', 'Camp Hat'); - hampster.set('shirt', 'Camp Shirt'); - hampster.get('clothes'); // ['Camp Hat', 'Camp Shirt'] + var hamster = Hamster.create(); + hamster.get('clothes'); // [null, null] + hamster.set('hat', 'Camp Hat'); + hamster.set('shirt', 'Camp Shirt'); + hamster.get('clothes'); // ['Camp Hat', 'Camp Shirt'] ``` - @method computed.map + @method computed.collect @for Ember - @param {String} dependentKey, [dependentKey...] + @param {String} dependentKey* @return {Ember.ComputedProperty} computed property which maps values of all passed properties in to an array. */ @@ -5160,6 +5749,51 @@ Ember.computed.oneWay = function(dependentKey) { }; + +/** + Where `computed.oneWay` provides oneWay bindings, `computed.readOnly` provides + a readOnly one way binding. Very often when using `computed.oneWay` one does + not also want changes to propogate back up, as they will replace the value. + + This prevents the reverse flow, and also throws an exception when it occurs. + + Example + + ```javascript + User = Ember.Object.extend({ + firstName: null, + lastName: null, + nickName: Ember.computed.readOnly('firstName') + }); + + user = User.create({ + firstName: 'Teddy', + lastName: 'Zeenny' + }); + + user.get('nickName'); + # 'Teddy' + + user.set('nickName', 'TeddyBear'); + # throws Exception + # throw new Ember.Error('Cannot Set: nickName on: ' );` + + user.get('firstName'); + # 'Teddy' + ``` + + @method computed.readOnly + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which creates a + one way computed property to the original value for property. +*/ +Ember.computed.readOnly = function(dependentKey) { + return Ember.computed(dependentKey, function() { + return get(this, dependentKey); + }).readOnly(); +}; + /** A computed property that acts like a standard getter and setter, but returns the value at the provided `defaultPath` if the @@ -5168,14 +5802,14 @@ Ember.computed.oneWay = function(dependentKey) { Example ```javascript - var Hampster = Ember.Object.extend({ + var Hamster = Ember.Object.extend({ wishList: Ember.computed.defaultTo('favoriteFood') }); - var hampster = Hampster.create({favoriteFood: 'Banana'}); - hampster.get('wishList'); // 'Banana' - hampster.set('wishList', 'More Unit Tests'); - hampster.get('wishList'); // 'More Unit Tests' - hampster.get('favoriteFood'); // 'Banana' + var hamster = Hamster.create({favoriteFood: 'Banana'}); + hamster.get('wishList'); // 'Banana' + hamster.set('wishList', 'More Unit Tests'); + hamster.get('wishList'); // 'More Unit Tests' + hamster.get('favoriteFood'); // 'Banana' ``` @method computed.defaultTo @@ -5194,7 +5828,6 @@ Ember.computed.defaultTo = function(defaultPath) { }; - })(); @@ -5205,8 +5838,8 @@ Ember.computed.defaultTo = function(defaultPath) { @module ember-metal */ -var AFTER_OBSERVERS = ':change'; -var BEFORE_OBSERVERS = ':before'; +var AFTER_OBSERVERS = ':change', + BEFORE_OBSERVERS = ':before'; function changeEvent(keyName) { return keyName+AFTER_OBSERVERS; @@ -5218,14 +5851,16 @@ function beforeEvent(keyName) { /** @method addObserver + @for Ember @param obj @param {String} path @param {Object|Function} targetOrMethod @param {Function|String} [method] */ -Ember.addObserver = function(obj, path, target, method) { - Ember.addListener(obj, changeEvent(path), target, method); - Ember.watch(obj, path); +Ember.addObserver = function(obj, _path, target, method) { + Ember.addListener(obj, changeEvent(_path), target, method); + Ember.watch(obj, _path); + return this; }; @@ -5235,27 +5870,31 @@ Ember.observersFor = function(obj, path) { /** @method removeObserver + @for Ember @param obj @param {String} path @param {Object|Function} targetOrMethod @param {Function|String} [method] */ -Ember.removeObserver = function(obj, path, target, method) { - Ember.unwatch(obj, path); - Ember.removeListener(obj, changeEvent(path), target, method); +Ember.removeObserver = function(obj, _path, target, method) { + Ember.unwatch(obj, _path); + Ember.removeListener(obj, changeEvent(_path), target, method); + return this; }; /** @method addBeforeObserver + @for Ember @param obj @param {String} path @param {Object|Function} targetOrMethod @param {Function|String} [method] */ -Ember.addBeforeObserver = function(obj, path, target, method) { - Ember.addListener(obj, beforeEvent(path), target, method); - Ember.watch(obj, path); +Ember.addBeforeObserver = function(obj, _path, target, method) { + Ember.addListener(obj, beforeEvent(_path), target, method); + Ember.watch(obj, _path); + return this; }; @@ -5289,22 +5928,25 @@ Ember.beforeObserversFor = function(obj, path) { /** @method removeBeforeObserver + @for Ember @param obj @param {String} path @param {Object|Function} targetOrMethod @param {Function|String} [method] */ -Ember.removeBeforeObserver = function(obj, path, target, method) { - Ember.unwatch(obj, path); - Ember.removeListener(obj, beforeEvent(path), target, method); +Ember.removeBeforeObserver = function(obj, _path, target, method) { + Ember.unwatch(obj, _path); + Ember.removeListener(obj, beforeEvent(_path), target, method); + return this; }; + })(); (function() { -define("backburner/queue", +define("backburner/queue", ["exports"], function(__exports__) { "use strict"; @@ -5411,11 +6053,10 @@ define("backburner/queue", } }; - __exports__.Queue = Queue; }); -define("backburner/deferred_action_queues", +define("backburner/deferred_action_queues", ["backburner/queue","exports"], function(__dependency1__, __exports__) { "use strict"; @@ -5514,11 +6155,10 @@ define("backburner/deferred_action_queues", return -1; } - __exports__.DeferredActionQueues = DeferredActionQueues; }); -define("backburner", +define("backburner", ["backburner/deferred_action_queues","exports"], function(__dependency1__, __exports__) { "use strict"; @@ -5627,7 +6267,7 @@ define("backburner", method = target[method]; } - var stack = this.DEBUG ? new Error().stack : undefined, + var stack = this.DEBUG ? new Error() : undefined, args = arguments.length > 3 ? slice.call(arguments, 3) : undefined; if (!this.currentInstance) { createAutorun(this); } return this.currentInstance.schedule(queueName, target, method, args, false, stack); @@ -5643,7 +6283,7 @@ define("backburner", method = target[method]; } - var stack = this.DEBUG ? new Error().stack : undefined, + var stack = this.DEBUG ? new Error() : undefined, args = arguments.length > 3 ? slice.call(arguments, 3) : undefined; if (!this.currentInstance) { createAutorun(this); } return this.currentInstance.schedule(queueName, target, method, args, true, stack); @@ -5714,50 +6354,49 @@ define("backburner", timers.splice(i, 0, executeAt, fn); - if (laterTimer && laterTimerExpiresAt < executeAt) { return fn; } - - if (laterTimer) { - clearTimeout(laterTimer); - laterTimer = null; - } - laterTimer = global.setTimeout(function() { - executeTimers(self); - laterTimer = null; - laterTimerExpiresAt = null; - }, wait); - laterTimerExpiresAt = executeAt; + updateLaterTimer(self, executeAt, wait); return fn; }, - throttle: function(target, method /* , args, wait */) { + throttle: function(target, method /* , args, wait, [immediate] */) { var self = this, args = arguments, - wait = pop.call(args), - throttler; + immediate = pop.call(args), + wait, + throttler, + index, + timer; - for (var i = 0, l = throttlers.length; i < l; i++) { - throttler = throttlers[i]; - if (throttler[0] === target && throttler[1] === method) { return; } // do nothing + if (typeof immediate === "number" || typeof immediate === "string") { + wait = immediate; + immediate = true; + } else { + wait = pop.call(args); } - var timer = global.setTimeout(function() { - self.run.apply(self, args); + wait = parseInt(wait, 10); - // remove throttler - var index = -1; - for (var i = 0, l = throttlers.length; i < l; i++) { - throttler = throttlers[i]; - if (throttler[0] === target && throttler[1] === method) { - index = i; - break; - } - } + index = findThrottler(target, method); + if (index > -1) { return throttlers[index]; } // throttled + timer = global.setTimeout(function() { + if (!immediate) { + self.run.apply(self, args); + } + var index = findThrottler(target, method); if (index > -1) { throttlers.splice(index, 1); } }, wait); - throttlers.push([target, method, timer]); + if (immediate) { + self.run.apply(self, args); + } + + throttler = [target, method, timer]; + + throttlers.push(throttler); + + return throttler; }, debounce: function(target, method /* , args, wait, [immediate] */) { @@ -5766,30 +6405,32 @@ define("backburner", immediate = pop.call(args), wait, index, - debouncee; + debouncee, + timer; - if (typeof immediate === "number") { + if (typeof immediate === "number" || typeof immediate === "string") { wait = immediate; immediate = false; } else { wait = pop.call(args); } + wait = parseInt(wait, 10); // Remove debouncee index = findDebouncee(target, method); - if (index !== -1) { + if (index > -1) { debouncee = debouncees[index]; debouncees.splice(index, 1); clearTimeout(debouncee[2]); } - var timer = global.setTimeout(function() { + timer = global.setTimeout(function() { if (!immediate) { self.run.apply(self, args); } - index = findDebouncee(target, method); - if (index) { + var index = findDebouncee(target, method); + if (index > -1) { debouncees.splice(index, 1); } }, wait); @@ -5798,7 +6439,11 @@ define("backburner", self.run.apply(self, args); } - debouncees.push([target, method, timer]); + debouncee = [target, method, timer]; + + debouncees.push(debouncee); + + return debouncee; }, cancelTimers: function() { @@ -5831,19 +6476,47 @@ define("backburner", }, cancel: function(timer) { - if (timer && typeof timer === 'object' && timer.queue && timer.method) { // we're cancelling a deferOnce + var timerType = typeof timer; + + if (timer && timerType === 'object' && timer.queue && timer.method) { // we're cancelling a deferOnce return timer.queue.cancel(timer); - } else if (typeof timer === 'function') { // we're cancelling a setTimeout + } else if (timerType === 'function') { // we're cancelling a setTimeout for (var i = 0, l = timers.length; i < l; i += 2) { if (timers[i + 1] === timer) { timers.splice(i, 2); // remove the two elements return true; } } + } else if (Object.prototype.toString.call(timer) === "[object Array]"){ // we're cancelling a throttle or debounce + return this._cancelItem(findThrottler, throttlers, timer) || + this._cancelItem(findDebouncee, debouncees, timer); } else { return; // timer was null or not a timer } + }, + + _cancelItem: function(findMethod, array, timer){ + var item, + index; + + if (timer.length < 3) { return false; } + + index = findMethod(timer[0], timer[1]); + + if(index > -1) { + + item = array[index]; + + if(item[2] === timer[2]){ + array.splice(index, 1); + clearTimeout(timer[2]); + return true; + } + } + + return false; } + }; Backburner.prototype.schedule = Backburner.prototype.defer; @@ -5858,6 +6531,20 @@ define("backburner", }); } + function updateLaterTimer(self, executeAt, wait) { + if (!laterTimer || executeAt < laterTimerExpiresAt) { + if (laterTimer) { + clearTimeout(laterTimer); + } + laterTimer = global.setTimeout(function() { + laterTimer = null; + laterTimerExpiresAt = null; + executeTimers(self); + }, wait); + laterTimerExpiresAt = executeAt; + } + } + function executeTimers(self) { var now = +new Date(), time, fns, i, l; @@ -5877,12 +6564,7 @@ define("backburner", }); if (timers.length) { - laterTimer = global.setTimeout(function() { - executeTimers(self); - laterTimer = null; - laterTimerExpiresAt = null; - }, timers[0] - now); - laterTimerExpiresAt = timers[0]; + updateLaterTimer(self, timers[0], timers[0] - now); } } @@ -5901,10 +6583,23 @@ define("backburner", return index; } + function findThrottler(target, method) { + var throttler, + index = -1; + + for (var i = 0, l = throttlers.length; i < l; i++) { + throttler = throttlers[i]; + if (throttler[0] === target && throttler[1] === method) { + index = i; + break; + } + } + + return index; + } __exports__.Backburner = Backburner; }); - })(); @@ -5928,7 +6623,8 @@ var Backburner = requireModule('backburner').Backburner, onBegin: onBegin, onEnd: onEnd }), - slice = [].slice; + slice = [].slice, + concat = [].concat; // .......................................................... // Ember.run - this is ideally the only public API the dev sees @@ -5961,24 +6657,22 @@ var Backburner = requireModule('backburner').Backburner, @param {Object} [args*] Any additional arguments you wish to pass to the method. @return {Object} return value from invoking the passed function. */ -Ember.run = function(target, method) { - var ret; - +Ember.run = function() { if (Ember.onerror) { - try { - ret = backburner.run.apply(backburner, arguments); - } catch (e) { - Ember.onerror(e); - } + return onerror(arguments); } else { - ret = backburner.run.apply(backburner, arguments); + return backburner.run.apply(backburner, arguments); } - - return ret; }; +function onerror(args) { + try { + return backburner.run.apply(backburner, args); + } catch(error) { + Ember.onerror(error); + } +} /** - If no run-loop is present, it creates a new one. If a run loop is present it will queue itself to run on the existing run-loops action queue. @@ -6012,10 +6706,10 @@ Ember.run = function(target, method) { May be a function or a string. If you pass a string then it will be looked up on the passed target. @param {Object} [args*] Any additional arguments you wish to pass to the method. - @return {Object} return value from invoking the passed function. Please note, + @return {Object} Return value from invoking the passed function. Please note, when called within an existing loop, no return value is possible. */ -Ember.run.join = function(target, method) { +Ember.run.join = function(target, method /* args */) { if (!Ember.run.currentRunLoop) { return Ember.run.apply(Ember.run, arguments); } @@ -6025,6 +6719,53 @@ Ember.run.join = function(target, method) { Ember.run.schedule.apply(Ember.run, args); }; +/** + Provides a useful utility for when integrating with non-Ember libraries + that provide asynchronous callbacks. + + Ember utilizes a run-loop to batch and coalesce changes. This works by + marking the start and end of Ember-related Javascript execution. + + When using events such as a View's click handler, Ember wraps the event + handler in a run-loop, but when integrating with non-Ember libraries this + can be tedious. + + For example, the following is rather verbose but is the correct way to combine + third-party events and Ember code. + + ```javascript + var that = this; + jQuery(window).on('resize', function(){ + Ember.run(function(){ + that.handleResize(); + }); + }); + ``` + + To reduce the boilerplate, the following can be used to construct a + run-loop-wrapped callback handler. + + ```javascript + jQuery(window).on('resize', Ember.run.bind(this, this.handleResize)); + ``` + + @method bind + @namespace Ember.run + @param {Object} [target] target of method to call + @param {Function|String} method Method to invoke. + May be a function or a string. If you pass a string + then it will be looked up on the passed target. + @param {Object} [args*] Any additional arguments you wish to pass to the method. + @return {Object} return value from invoking the passed function. Please note, + when called within an existing loop, no return value is possible. +*/ +Ember.run.bind = function(target, method /* args*/) { + var args = slice.call(arguments); + return function() { + return Ember.run.join.apply(Ember.run, args.concat(slice.call(arguments))); + }; +}; + Ember.run.backburner = backburner; var run = Ember.run; @@ -6101,7 +6842,8 @@ Ember.run.end = function() { console.log("scheduled on actions queue"); }); - // Note the functions will be run in order based on the run queues order. Output would be: + // Note the functions will be run in order based on the run queues order. + // Output would be: // scheduled on sync queue // scheduled on actions queue ``` @@ -6193,7 +6935,7 @@ Ember.run.later = function(target, method) { If you pass a string it will be resolved on the target at the time the method is invoked. @param {Object} [args*] Optional arguments to pass to the timeout. - @return {Object} timer + @return {Object} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.once = function(target, method) { checkAutoRun(); @@ -6244,7 +6986,7 @@ Ember.run.once = function(target, method) { If you pass a string it will be resolved on the target at the time the method is invoked. @param {Object} [args*] Optional arguments to pass to the timeout. - @return {Object} timer + @return {Object} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.scheduleOnce = function(queue, target, method) { checkAutoRun(); @@ -6258,7 +7000,8 @@ Ember.run.scheduleOnce = function(queue, target, method) { ```javascript Ember.run.next(myContext, function() { - // code to be executed in the next run loop, which will be scheduled after the current one + // code to be executed in the next run loop, + // which will be scheduled after the current one }); ``` @@ -6306,7 +7049,7 @@ Ember.run.scheduleOnce = function(queue, target, method) { If you pass a string it will be resolved on the target at the time the method is invoked. @param {Object} [args*] Optional arguments to pass to the timeout. - @return {Object} timer + @return {Object} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.next = function() { var args = slice.call(arguments); @@ -6316,7 +7059,8 @@ Ember.run.next = function() { /** Cancels a scheduled item. Must be a value returned by `Ember.run.later()`, - `Ember.run.once()`, or `Ember.run.next()`. + `Ember.run.once()`, `Ember.run.next()`, `Ember.run.debounce()`, or + `Ember.run.throttle()`. ```javascript var runNext = Ember.run.next(myContext, function() { @@ -6333,11 +7077,27 @@ Ember.run.next = function() { // will not be executed }); Ember.run.cancel(runOnce); + + var throttle = Ember.run.throttle(myContext, function() { + // will not be executed + }, 1, false); + Ember.run.cancel(throttle); + + var debounce = Ember.run.debounce(myContext, function() { + // will not be executed + }, 1); + Ember.run.cancel(debounce); + + var debounceImmediate = Ember.run.debounce(myContext, function() { + // will be executed since we passed in true (immediate) + }, 100, true); + // the 100ms delay until this method can be called again will be cancelled + Ember.run.cancel(debounceImmediate); ``` @method cancel @param {Object} timer Timer object to cancel - @return {void} + @return {Boolean} true if cancelled or false/undefined if it wasn't found */ Ember.run.cancel = function(timer) { return backburner.cancel(timer); @@ -6369,6 +7129,34 @@ Ember.run.cancel = function(timer) { // console logs 'debounce ran.' one time. ``` + Immediate allows you to run the function immediately, but debounce + other calls for this function until the wait time has elapsed. If + `debounce` is called again before the specified time has elapsed, + the timer is reset and the entire period must pass again before + the method can be called again. + + ```javascript + var myFunc = function() { console.log(this.name + ' ran.'); }; + var myContext = {name: 'debounce'}; + + Ember.run.debounce(myContext, myFunc, 150, true); + + // console logs 'debounce ran.' one time immediately. + // 100ms passes + + Ember.run.debounce(myContext, myFunc, 150, true); + + // 150ms passes and nothing else is logged to the console and + // the debouncee is no longer being watched + + Ember.run.debounce(myContext, myFunc, 150, true); + + // console logs 'debounce ran.' one time immediately. + // 150ms passes and nothing else is logged tot he console and + // the debouncee is no longer being watched + + ``` + @method debounce @param {Object} [target] target of method to invoke @param {Function|String} method The method to invoke. @@ -6376,8 +7164,9 @@ Ember.run.cancel = function(timer) { then it will be looked up on the passed target. @param {Object} [args*] Optional arguments to pass to the timeout. @param {Number} wait Number of milliseconds to wait. - @param {Boolean} immediate Trigger the function on the leading instead of the trailing edge of the wait interval. - @return {void} + @param {Boolean} immediate Trigger the function on the leading instead + of the trailing edge of the wait interval. Defaults to false. + @return {Array} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.debounce = function() { return backburner.debounce.apply(backburner, arguments); @@ -6392,9 +7181,7 @@ Ember.run.debounce = function() { var myContext = {name: 'throttle'}; Ember.run.throttle(myContext, myFunc, 150); - - // 50ms passes - Ember.run.throttle(myContext, myFunc, 150); + // myFunc is invoked with context myContext // 50ms passes Ember.run.throttle(myContext, myFunc, 150); @@ -6403,8 +7190,9 @@ Ember.run.debounce = function() { Ember.run.throttle(myContext, myFunc, 150); // 150ms passes + Ember.run.throttle(myContext, myFunc, 150); // myFunc is invoked with context myContext - // console logs 'throttle ran.' twice, 150ms apart. + // console logs 'throttle ran.' twice, 250ms apart. ``` @method throttle @@ -6414,7 +7202,7 @@ Ember.run.debounce = function() { then it will be looked up on the passed target. @param {Object} [args*] Optional arguments to pass to the timeout. @param {Number} spacing Number of milliseconds to space out requests. - @return {void} + @return {Array} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.throttle = function() { return backburner.throttle.apply(backburner, arguments); @@ -6922,10 +7710,24 @@ var Mixin, REQUIRED, Alias, a_slice = [].slice, o_create = Ember.create, defineProperty = Ember.defineProperty, - guidFor = Ember.guidFor; + guidFor = Ember.guidFor, + metaFor = Ember.meta, + META_KEY = Ember.META_KEY; + +var expandProperties = Ember.expandProperties; + +function superFunction(){ + var ret, func = this.__nextSuper; + if (func) { + this.__nextSuper = null; + ret = func.apply(this, arguments); + this.__nextSuper = func; + } + return ret; +} function mixinsMeta(obj) { - var m = Ember.meta(obj, true), ret = m.mixins; + var m = metaFor(obj, true), ret = m.mixins; if (!ret) { ret = m.mixins = {}; } else if (!m.hasOwnProperty('mixins')) { @@ -7040,9 +7842,7 @@ function applyConcatenatedProperties(obj, key, value, values) { return Ember.makeArray(baseValue).concat(value); } } else { - // Make sure this mixin has its own array so it is not - // accidentally mutated by another child's interactions - return Ember.makeArray(value).slice(); + return Ember.makeArray(value); } } @@ -7051,19 +7851,26 @@ function applyMergedProperties(obj, key, value, values) { if (!baseValue) { return value; } - var newBase = Ember.merge({}, baseValue); + var newBase = Ember.merge({}, baseValue), + hasFunction = false; + for (var prop in value) { if (!value.hasOwnProperty(prop)) { continue; } var propValue = value[prop]; if (isMethod(propValue)) { // TODO: support for Computed Properties, etc? + hasFunction = true; newBase[prop] = giveMethodSuper(obj, prop, propValue, baseValue, {}); } else { newBase[prop] = propValue; } } + if (hasFunction) { + newBase._super = superFunction; + } + return newBase; } @@ -7072,7 +7879,7 @@ function addNormalizedProperty(base, key, value, meta, descs, values, concats, m if (value === REQUIRED && descs[key]) { return CONTINUE; } // Wrap descriptor function to implement - // _super() if needed + // __nextSuper() if needed if (value.func) { value = giveDescriptorSuper(meta, key, value, values, descs); } @@ -7105,13 +7912,14 @@ function mergeMixins(mixins, m, descs, values, base, keys) { for(var i=0, l=mixins.length; i this.changingFrom ? 'green' : 'red'; // logic } - }, 'content.value'), + }), - friendsDidChange: Ember.observer(function(obj, keyName) { + friendsDidChange: Ember.observer('friends.@each.name', function(obj, keyName) { // some logic // obj.get(keyName) returns friends array - }, 'friends.@each.name') + }) }); ``` @@ -7654,12 +8460,35 @@ Ember.immediateObserver = function() { @method beforeObserver @for Ember - @param {Function} func @param {String} propertyNames* + @param {Function} func @return func */ -Ember.beforeObserver = function(func) { - var paths = a_slice.call(arguments, 1); +Ember.beforeObserver = function() { + var func = a_slice.call(arguments, -1)[0]; + var paths; + + var addWatchedProperty = function(path) { paths.push(path); }; + + var _paths = a_slice.call(arguments, 0, -1); + + if (typeof func !== "function") { + // revert to old, soft-deprecated argument ordering + + func = arguments[0]; + _paths = a_slice.call(arguments, 1); + } + + paths = []; + + for (var i=0; i<_paths.length; ++i) { + expandProperties(_paths[i], addWatchedProperty); + } + + if (typeof func !== "function") { + throw new Ember.Error("Ember.beforeObserver called without a function"); + } + func.__ember_observesBefore__ = paths; return func; }; @@ -7728,30 +8557,115 @@ Ember Metal })(); (function() { -define("rsvp/all", - ["rsvp/promise","exports"], +/** + @class RSVP + @module RSVP + */ +define("rsvp/all", + ["./promise","exports"], function(__dependency1__, __exports__) { "use strict"; - var Promise = __dependency1__.Promise; - /* global toString */ + var Promise = __dependency1__["default"]; + + /** + This is a convenient alias for `RSVP.Promise.all`. + + @method all + @for RSVP + @param {Array} array Array of promises. + @param {String} label An optional label. This is useful + for tooling. + @static + */ + __exports__["default"] = function all(array, label) { + return Promise.all(array, label); + }; + }); +define("rsvp/all_settled", + ["./promise","./utils","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; + var isArray = __dependency2__.isArray; + var isNonThenable = __dependency2__.isNonThenable; + + /** + `RSVP.allSettled` is similar to `RSVP.all`, but instead of implementing + a fail-fast method, it waits until all the promises have returned and + shows you all the results. This is useful if you want to handle multiple + promises' failure states together as a set. + Returns a promise that is fulfilled when all the given promises have been + settled. The return promise is fulfilled with an array of the states of + the promises passed into the `promises` array argument. - function all(promises) { - if (Object.prototype.toString.call(promises) !== "[object Array]") { - throw new TypeError('You must pass an array to all.'); - } + Each state object will either indicate fulfillment or rejection, and + provide the corresponding value or reason. The states will take one of + the following formats: + + ```javascript + { state: 'fulfilled', value: value } + or + { state: 'rejected', reason: reason } + ``` + + Example: + + ```javascript + var promise1 = RSVP.Promise.resolve(1); + var promise2 = RSVP.Promise.reject(new Error('2')); + var promise3 = RSVP.Promise.reject(new Error('3')); + var promises = [ promise1, promise2, promise3 ]; + + RSVP.allSettled(promises).then(function(array){ + // array == [ + // { state: 'fulfilled', value: 1 }, + // { state: 'rejected', reason: Error }, + // { state: 'rejected', reason: Error } + // ] + // Note that for the second item, reason.message will be "2", and for the + // third item, reason.message will be "3". + }, function(error) { + // Not run. (This block would only be called if allSettled had failed, + // for instance if passed an incorrect argument type.) + }); + ``` + + @method allSettled + @for RSVP + @param {Array} promises + @param {String} label - optional string that describes the promise. + Useful for tooling. + @return {Promise} promise that is fulfilled with an array of the settled + states of the constituent promises. + @static + */ + __exports__["default"] = function allSettled(entries, label) { return new Promise(function(resolve, reject) { - var results = [], remaining = promises.length, - promise; + if (!isArray(entries)) { + throw new TypeError('You must pass an array to allSettled.'); + } + + var remaining = entries.length; + var entry; if (remaining === 0) { resolve([]); + return; } - function resolver(index) { + var results = new Array(remaining); + + function fulfilledResolver(index) { return function(value) { - resolveAll(index, value); + resolveAll(index, fulfilled(value)); + }; + } + + function rejectedResolver(index) { + return function(reason) { + resolveAll(index, rejected(reason)); }; } @@ -7762,152 +8676,113 @@ define("rsvp/all", } } - for (var i = 0; i < promises.length; i++) { - promise = promises[i]; + for (var index = 0; index < entries.length; index++) { + entry = entries[index]; - if (promise && typeof promise.then === 'function') { - promise.then(resolver(i), reject); + if (isNonThenable(entry)) { + resolveAll(index, fulfilled(entry)); } else { - resolveAll(i, promise); + Promise.cast(entry).then(fulfilledResolver(index), rejectedResolver(index)); } } - }); - } + }, label); + }; + function fulfilled(value) { + return { state: 'fulfilled', value: value }; + } - __exports__.all = all; + function rejected(reason) { + return { state: 'rejected', reason: reason }; + } }); -define("rsvp/async", - ["exports"], - function(__exports__) { +define("rsvp/config", + ["./events","exports"], + function(__dependency1__, __exports__) { "use strict"; - var browserGlobal = (typeof window !== 'undefined') ? window : {}; - var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; - var async; - var local = (typeof global !== 'undefined') ? global : this; - - // old node - function useNextTick() { - return function(callback, arg) { - process.nextTick(function() { - callback(arg); - }); - }; - } + var EventTarget = __dependency1__["default"]; - // node >= 0.10.x - function useSetImmediate() { - return function(callback, arg) { - /* global setImmediate */ - setImmediate(function(){ - callback(arg); - }); - }; - } + var config = { + instrument: false + }; - function useMutationObserver() { - var queue = []; + EventTarget.mixin(config); - var observer = new BrowserMutationObserver(function() { - var toProcess = queue.slice(); - queue = []; + function configure(name, value) { + if (name === 'onerror') { + // handle for legacy users that expect the actual + // error to be passed to their function added via + // `RSVP.configure('onerror', someFunctionHere);` + config.on('error', value); + return; + } - toProcess.forEach(function(tuple) { - var callback = tuple[0], arg= tuple[1]; - callback(arg); - }); - }); + if (arguments.length === 2) { + config[name] = value; + } else { + return config[name]; + } + } - var element = document.createElement('div'); - observer.observe(element, { attributes: true }); + __exports__.config = config; + __exports__.configure = configure; + }); +define("rsvp/defer", + ["./promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; - // Chrome Memory Leak: https://bugs.webkit.org/show_bug.cgi?id=93661 - window.addEventListener('unload', function(){ - observer.disconnect(); - observer = null; - }, false); + /** + `RSVP.defer` returns an object similar to jQuery's `$.Deferred`. + `RSVP.defer` should be used when porting over code reliant on `$.Deferred`'s + interface. New code should use the `RSVP.Promise` constructor instead. - return function(callback, arg) { - queue.push([callback, arg]); - element.setAttribute('drainQueue', 'drainQueue'); - }; - } + The object returned from `RSVP.defer` is a plain object with three properties: - function useSetTimeout() { - return function(callback, arg) { - local.setTimeout(function() { - callback(arg); - }, 1); - }; - } + * promise - an `RSVP.Promise`. + * reject - a function that causes the `promise` property on this object to + become rejected + * resolve - a function that causes the `promise` property on this object to + become fulfilled. - if (typeof setImmediate === 'function') { - async = useSetImmediate(); - } else if (typeof process !== 'undefined' && {}.toString.call(process) === '[object process]') { - async = useNextTick(); - } else if (BrowserMutationObserver) { - async = useMutationObserver(); - } else { - async = useSetTimeout(); - } + Example: + ```javascript + var deferred = RSVP.defer(); - __exports__.async = async; - }); -define("rsvp/config", - ["rsvp/async","exports"], - function(__dependency1__, __exports__) { - "use strict"; - var async = __dependency1__.async; + deferred.resolve("Success!"); - var config = {}; - config.async = async; + defered.promise.then(function(value){ + // value here is "Success!" + }); + ``` + @method defer + @for RSVP + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Object} + */ - __exports__.config = config; - }); -define("rsvp/defer", - ["rsvp/promise","exports"], - function(__dependency1__, __exports__) { - "use strict"; - var Promise = __dependency1__.Promise; - - function defer() { - var deferred = { - // pre-allocate shape - resolve: undefined, - reject: undefined, - promise: undefined - }; + __exports__["default"] = function defer(label) { + var deferred = { }; deferred.promise = new Promise(function(resolve, reject) { deferred.resolve = resolve; deferred.reject = reject; - }); + }, label); return deferred; - } - - - __exports__.defer = defer; + }; }); -define("rsvp/events", +define("rsvp/events", ["exports"], function(__exports__) { "use strict"; - var Event = function(type, options) { - this.type = type; - - for (var option in options) { - if (!options.hasOwnProperty(option)) { continue; } - - this[option] = options[option]; - } - }; - var indexOf = function(callbacks, callback) { for (var i=0, l=callbacks.length; i 1; }; - var resolveAll = function(prop, value) { - results[prop] = value; - if (--remaining === 0) { - deferred.resolve(results); - } - }; + RSVP.filter(promises, filterFn).then(function(result){ + // result is [ 2, 3 ] + }); + ``` + + If any of the `promises` given to `RSVP.filter` are rejected, the first promise + that is rejected will be given as an argument to the returned promise's + rejection handler. For example: + + ```javascript + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.reject(new Error("2")); + var promise3 = RSVP.reject(new Error("3")); + var promises = [ promise1, promise2, promise3 ]; - var rejectAll = function(error) { - deferred.reject(error); + var filterFn = function(item){ + return item > 1; }; - for (var prop in promises) { - if (promises[prop] && typeof promises[prop].then === 'function') { - promises[prop].then(resolver(prop), rejectAll); - } else { - resolveAll(prop, promises[prop]); - } - } + RSVP.filter(promises, filterFn).then(function(array){ + // Code here never runs because there are rejected promises! + }, function(reason) { + // reason.message === "2" + }); + ``` - return deferred.promise; - } + `RSVP.filter` will also wait for any promises returned from `filterFn`. + For instance, you may want to fetch a list of users then return a subset + of those users based on some asynchronous operation: + ```javascript - __exports__.hash = hash; - }); -define("rsvp/node", - ["rsvp/promise","rsvp/all","exports"], - function(__dependency1__, __dependency2__, __exports__) { - "use strict"; - var Promise = __dependency1__.Promise; - var all = __dependency2__.all; + var alice = { name: 'alice' }; + var bob = { name: 'bob' }; + var users = [ alice, bob ]; - function makeNodeCallbackFor(resolve, reject) { - return function (error, value) { - if (error) { - reject(error); - } else if (arguments.length > 2) { - resolve(Array.prototype.slice.call(arguments, 1)); - } else { - resolve(value); - } + var promises = users.map(function(user){ + return RSVP.resolve(user); + }); + + var filterFn = function(user){ + // Here, Alice has permissions to create a blog post, but Bob does not. + return getPrivilegesForUser(user).then(function(privs){ + return privs.can_create_blog_post === true; + }); }; - } + RSVP.filter(promises, filterFn).then(function(users){ + // true, because the server told us only Alice can create a blog post. + users.length === 1; + // false, because Alice is the only user present in `users` + users[0] === bob; + }); + ``` - function denodeify(nodeFunc) { - return function() { - var nodeArgs = Array.prototype.slice.call(arguments), resolve, reject; - var thisArg = this; + @method filter + @for RSVP + @param {Array} promises + @param {Function} filterFn - function to be called on each resolved value to + filter the final results. + @param {String} label optional string describing the promise. Useful for + tooling. + @return {Promise} + */ + function filter(promises, filterFn, label) { + return all(promises, label).then(function(values){ + if (!isArray(promises)) { + throw new TypeError('You must pass an array to filter.'); + } - var promise = new Promise(function(nodeResolve, nodeReject) { - resolve = nodeResolve; - reject = nodeReject; - }); + if (!isFunction(filterFn)){ + throw new TypeError("You must pass a function to filter's second argument."); + } - all(nodeArgs).then(function(nodeArgs) { - nodeArgs.push(makeNodeCallbackFor(resolve, reject)); + return map(promises, filterFn, label).then(function(filterResults){ + var i, + valuesLen = values.length, + filtered = []; - try { - nodeFunc.apply(thisArg, nodeArgs); - } catch(e) { - reject(e); - } + for (i = 0; i < valuesLen; i++){ + if(filterResults[i]) filtered.push(values[i]); + } + return filtered; }); - - return promise; - }; + }); } - - __exports__.denodeify = denodeify; + __exports__["default"] = filter; }); -define("rsvp/promise", - ["rsvp/config","rsvp/events","exports"], +define("rsvp/hash", + ["./promise","./utils","exports"], function(__dependency1__, __dependency2__, __exports__) { "use strict"; - var config = __dependency1__.config; - var EventTarget = __dependency2__.EventTarget; - - function objectOrFunction(x) { - return isFunction(x) || (typeof x === "object" && x !== null); - } - - function isFunction(x){ - return typeof x === "function"; - } + var Promise = __dependency1__["default"]; + var isNonThenable = __dependency2__.isNonThenable; + var keysOf = __dependency2__.keysOf; - var Promise = function(resolver) { - var promise = this, - resolved = false; + /** + `RSVP.hash` is similar to `RSVP.all`, but takes an object instead of an array + for its `promises` argument. - if (typeof resolver !== 'function') { - throw new TypeError('You must pass a resolver function as the sole argument to the promise constructor'); - } + Returns a promise that is fulfilled when all the given promises have been + fulfilled, or rejected if any of them become rejected. The returned promise + is fulfilled with a hash that has the same key names as the `promises` object + argument. If any of the values in the object are not promises, they will + simply be copied over to the fulfilled object. - if (!(promise instanceof Promise)) { - return new Promise(resolver); - } + Example: - var resolvePromise = function(value) { - if (resolved) { return; } - resolved = true; - resolve(promise, value); + ```javascript + var promises = { + myPromise: RSVP.resolve(1), + yourPromise: RSVP.resolve(2), + theirPromise: RSVP.resolve(3), + notAPromise: 4 }; - var rejectPromise = function(value) { - if (resolved) { return; } - resolved = true; - reject(promise, value); - }; + RSVP.hash(promises).then(function(hash){ + // hash here is an object that looks like: + // { + // myPromise: 1, + // yourPromise: 2, + // theirPromise: 3, + // notAPromise: 4 + // } + }); + ```` - this.on('promise:resolved', function(event) { - this.trigger('success', { detail: event.detail }); - }, this); + If any of the `promises` given to `RSVP.hash` are rejected, the first promise + that is rejected will be given as the reason to the rejection handler. - this.on('promise:failed', function(event) { - this.trigger('error', { detail: event.detail }); - }, this); + Example: - this.on('error', onerror); + ```javascript + var promises = { + myPromise: RSVP.resolve(1), + rejectedPromise: RSVP.reject(new Error("rejectedPromise")), + anotherRejectedPromise: RSVP.reject(new Error("anotherRejectedPromise")), + }; - try { - resolver(resolvePromise, rejectPromise); - } catch(e) { - rejectPromise(e); - } - }; + RSVP.hash(promises).then(function(hash){ + // Code here never runs because there are rejected promises! + }, function(reason) { + // reason.message === "rejectedPromise" + }); + ``` - function onerror(event) { - if (config.onerror) { - config.onerror(event.detail); - } - } + An important note: `RSVP.hash` is intended for plain JavaScript objects that + are just a set of keys and values. `RSVP.hash` will NOT preserve prototype + chains. - var invokeCallback = function(type, promise, callback, event) { - var hasCallback = isFunction(callback), - value, error, succeeded, failed; + Example: - if (hasCallback) { - try { - value = callback(event.detail); - succeeded = true; - } catch(e) { - failed = true; - error = e; - } - } else { - value = event.detail; - succeeded = true; + ```javascript + function MyConstructor(){ + this.example = RSVP.resolve("Example"); } - if (handleThenable(promise, value)) { - return; - } else if (hasCallback && succeeded) { - resolve(promise, value); - } else if (failed) { - reject(promise, error); - } else if (type === 'resolve') { - resolve(promise, value); - } else if (type === 'reject') { - reject(promise, value); - } - }; + MyConstructor.prototype = { + protoProperty: RSVP.resolve("Proto Property") + }; - Promise.prototype = { - constructor: Promise, + var myObject = new MyConstructor(); + + RSVP.hash(myObject).then(function(hash){ + // protoProperty will not be present, instead you will just have an + // object that looks like: + // { + // example: "Example" + // } + // + // hash.hasOwnProperty('protoProperty'); // false + // 'undefined' === typeof hash.protoProperty + }); + ``` - isRejected: undefined, - isFulfilled: undefined, - rejectedReason: undefined, - fulfillmentValue: undefined, + @method hash + @for RSVP + @param {Object} promises + @param {String} label optional string that describes the promise. + Useful for tooling. + @return {Promise} promise that is fulfilled when all properties of `promises` + have been fulfilled, or rejected if any of them become rejected. + @static + */ + __exports__["default"] = function hash(object, label) { + return new Promise(function(resolve, reject){ + var results = {}; + var keys = keysOf(object); + var remaining = keys.length; + var entry, property; - then: function(done, fail) { - this.off('error', onerror); + if (remaining === 0) { + resolve(results); + return; + } - var thenPromise = new this.constructor(function() {}); + function fulfilledTo(property) { + return function(value) { + results[property] = value; + if (--remaining === 0) { + resolve(results); + } + }; + } - if (this.isFulfilled) { - config.async(function(promise) { - invokeCallback('resolve', thenPromise, done, { detail: promise.fulfillmentValue }); - }, this); + function onRejection(reason) { + remaining = 0; + reject(reason); } - if (this.isRejected) { - config.async(function(promise) { - invokeCallback('reject', thenPromise, fail, { detail: promise.rejectedReason }); - }, this); + for (var i = 0; i < keys.length; i++) { + property = keys[i]; + entry = object[property]; + + if (isNonThenable(entry)) { + results[property] = entry; + if (--remaining === 0) { + resolve(results); + } + } else { + Promise.cast(entry).then(fulfilledTo(property), onRejection); + } } + }); + }; + }); +define("rsvp/instrument", + ["./config","./utils","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var config = __dependency1__.config; + var now = __dependency2__.now; - this.on('promise:resolved', function(event) { - invokeCallback('resolve', thenPromise, done, event); + __exports__["default"] = function instrument(eventName, promise, child) { + // instrumentation should not disrupt normal usage. + try { + config.trigger(eventName, { + guid: promise._guidKey + promise._id, + eventName: eventName, + detail: promise._detail, + childGuid: child && promise._guidKey + child._id, + label: promise._label, + timeStamp: now(), + stack: new Error(promise._label).stack }); + } catch(error) { + setTimeout(function(){ + throw error; + }, 0); + } + }; + }); +define("rsvp/map", + ["./promise","./all","./utils","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var Promise = __dependency1__["default"]; + var all = __dependency2__["default"]; + var isArray = __dependency3__.isArray; + var isFunction = __dependency3__.isFunction; - this.on('promise:failed', function(event) { - invokeCallback('reject', thenPromise, fail, event); - }); + /** + `RSVP.map` is similar to JavaScript's native `map` method, except that it + waits for all promises to become fulfilled before running the `mapFn` on + each item in given to `promises`. `RSVP.map` returns a promise that will + become fulfilled with the result of running `mapFn` on the values the promises + become fulfilled with. - return thenPromise; - }, + For example: - fail: function(fail) { - return this.then(null, fail); - } - }; + ```javascript - EventTarget.mixin(Promise.prototype); + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.resolve(2); + var promise3 = RSVP.resolve(3); + var promises = [ promise1, promise2, promise3 ]; - function resolve(promise, value) { - if (promise === value) { - fulfill(promise, value); - } else if (!handleThenable(promise, value)) { - fulfill(promise, value); - } - } + var mapFn = function(item){ + return item + 1; + }; - function handleThenable(promise, value) { - var then = null, - resolved; + RSVP.map(promises, mapFn).then(function(result){ + // result is [ 2, 3, 4 ] + }); + ``` - try { - if (promise === value) { - throw new TypeError("A promises callback cannot return that same promise."); - } + If any of the `promises` given to `RSVP.map` are rejected, the first promise + that is rejected will be given as an argument to the returned promise's + rejection handler. For example: - if (objectOrFunction(value)) { - then = value.then; + ```javascript + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.reject(new Error("2")); + var promise3 = RSVP.reject(new Error("3")); + var promises = [ promise1, promise2, promise3 ]; - if (isFunction(then)) { - then.call(value, function(val) { - if (resolved) { return true; } - resolved = true; + var mapFn = function(item){ + return item + 1; + }; - if (value !== val) { - resolve(promise, val); - } else { - fulfill(promise, val); - } - }, function(val) { - if (resolved) { return true; } - resolved = true; + RSVP.map(promises, mapFn).then(function(array){ + // Code here never runs because there are rejected promises! + }, function(reason) { + // reason.message === "2" + }); + ``` - reject(promise, val); - }); + `RSVP.map` will also wait if a promise is returned from `mapFn`. For example, + say you want to get all comments from a set of blog posts, but you need + the blog posts first becuase they contain a url to those comments. - return true; - } - } - } catch (error) { - reject(promise, error); - return true; - } + ```javscript - return false; - } + var mapFn = function(blogPost){ + // getComments does some ajax and returns an RSVP.Promise that is fulfilled + // with some comments data + return getComments(blogPost.comments_url); + }; - function fulfill(promise, value) { - config.async(function() { - promise.trigger('promise:resolved', { detail: value }); - promise.isFulfilled = true; - promise.fulfillmentValue = value; + // getBlogPosts does some ajax and returns an RSVP.Promise that is fulfilled + // with some blog post data + RSVP.map(getBlogPosts(), mapFn).then(function(comments){ + // comments is the result of asking the server for the comments + // of all blog posts returned from getBlogPosts() }); - } + ``` - function reject(promise, value) { - config.async(function() { - promise.trigger('promise:failed', { detail: value }); - promise.isRejected = true; - promise.rejectedReason = value; - }); - } + @method map + @for RSVP + @param {Array} promises + @param {Function} mapFn function to be called on each fulfilled promise. + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} promise that is fulfilled with the result of calling + `mapFn` on each fulfilled promise or value when they become fulfilled. + The promise will be rejected if any of the given `promises` become rejected. + @static + */ + __exports__["default"] = function map(promises, mapFn, label) { + return all(promises, label).then(function(results){ + if (!isArray(promises)) { + throw new TypeError('You must pass an array to map.'); + } + if (!isFunction(mapFn)){ + throw new TypeError("You must pass a function to map's second argument."); + } - __exports__.Promise = Promise; - }); -define("rsvp/reject", - ["rsvp/promise","exports"], - function(__dependency1__, __exports__) { - "use strict"; - var Promise = __dependency1__.Promise; - function reject(reason) { - return new Promise(function (resolve, reject) { - reject(reason); - }); - } + var resultLen = results.length, + mappedResults = [], + i; + for (i = 0; i < resultLen; i++){ + mappedResults.push(mapFn(results[i])); + } - __exports__.reject = reject; + return all(mappedResults, label); + }); + }; }); -define("rsvp/resolve", - ["rsvp/promise","exports"], +define("rsvp/node", + ["./promise","exports"], function(__dependency1__, __exports__) { "use strict"; - var Promise = __dependency1__.Promise; + var Promise = __dependency1__["default"]; - function resolve(thenable) { - return new Promise(function(resolve, reject) { - resolve(thenable); - }); + var slice = Array.prototype.slice; + + function makeNodeCallbackFor(resolve, reject) { + return function (error, value) { + if (error) { + reject(error); + } else if (arguments.length > 2) { + resolve(slice.call(arguments, 1)); + } else { + resolve(value); + } + }; } + /** + `RSVP.denodeify` takes a "node-style" function and returns a function that + will return an `RSVP.Promise`. You can use `denodeify` in Node.js or the + browser when you'd prefer to use promises over using callbacks. For example, + `denodeify` transforms the following: - __exports__.resolve = resolve; - }); -define("rsvp/rethrow", - ["exports"], - function(__exports__) { - "use strict"; - var local = (typeof global === "undefined") ? this : global; + ```javascript + var fs = require('fs'); - function rethrow(reason) { - local.setTimeout(function() { - throw reason; + fs.readFile('myfile.txt', function(err, data){ + if (err) return handleError(err); + handleData(data); }); - throw reason; - } + ``` + into: - __exports__.rethrow = rethrow; - }); -define("rsvp", - ["rsvp/events","rsvp/promise","rsvp/node","rsvp/all","rsvp/hash","rsvp/rethrow","rsvp/defer","rsvp/config","rsvp/resolve","rsvp/reject","exports"], - function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __exports__) { - "use strict"; - var EventTarget = __dependency1__.EventTarget; - var Promise = __dependency2__.Promise; - var denodeify = __dependency3__.denodeify; - var all = __dependency4__.all; - var hash = __dependency5__.hash; - var rethrow = __dependency6__.rethrow; - var defer = __dependency7__.defer; - var config = __dependency8__.config; - var resolve = __dependency9__.resolve; - var reject = __dependency10__.reject; + ```javascript + var fs = require('fs'); - function configure(name, value) { - config[name] = value; - } + var readFile = RSVP.denodeify(fs.readFile); + readFile('myfile.txt').then(handleData, handleError); + ``` - __exports__.Promise = Promise; - __exports__.EventTarget = EventTarget; - __exports__.all = all; - __exports__.hash = hash; - __exports__.rethrow = rethrow; - __exports__.defer = defer; - __exports__.denodeify = denodeify; - __exports__.configure = configure; - __exports__.resolve = resolve; - __exports__.reject = reject; - }); -})(); + Using `denodeify` makes it easier to compose asynchronous operations instead + of using callbacks. For example, instead of: -(function() { -/** -@private -Public api for the container is still in flux. -The public api, specified on the application namespace should be considered the stable api. -// @module container -*/ - -/* - Flag to enable/disable model factory injections (disabled by default) - If model factory injections are enabled, models should not be - accessed globally (only through `container.lookupFactory('model:modelName'))`); -*/ -Ember.MODEL_FACTORY_INJECTIONS = false || !!Ember.ENV.MODEL_FACTORY_INJECTIONS; + ```javascript + var fs = require('fs'); + var log = require('some-async-logger'); + + fs.readFile('myfile.txt', function(err, data){ + if (err) return handleError(err); + fs.writeFile('myfile2.txt', data, function(err){ + if (err) throw err; + log('success', function(err) { + if (err) throw err; + }); + }); + }); + ``` -define("container", - [], - function() { + You can chain the operations together using `then` from the returned promise: - // A safe and simple inheriting object. - function InheritingDict(parent) { - this.parent = parent; - this.dict = {}; - } + ```javascript + var fs = require('fs'); + var denodeify = RSVP.denodeify; + var readFile = denodeify(fs.readFile); + var writeFile = denodeify(fs.writeFile); + var log = denodeify(require('some-async-logger')); + + readFile('myfile.txt').then(function(data){ + return writeFile('myfile2.txt', data); + }).then(function(){ + return log('SUCCESS'); + }).then(function(){ + // success handler + }, function(reason){ + // rejection handler + }); + ``` - InheritingDict.prototype = { + @method denodeify + @for RSVP + @param {Function} nodeFunc a "node-style" function that takes a callback as + its last argument. The callback expects an error to be passed as its first + argument (if an error occurred, otherwise null), and the value from the + operation as its second argument ("function(err, value){ }"). + @param {Any} binding optional argument for binding the "this" value when + calling the `nodeFunc` function. + @return {Function} a function that wraps `nodeFunc` to return an + `RSVP.Promise` + @static + */ + __exports__["default"] = function denodeify(nodeFunc, binding) { + return function() { + var nodeArgs = slice.call(arguments), resolve, reject; + var thisArg = this || binding; + + return new Promise(function(resolve, reject) { + Promise.all(nodeArgs).then(function(nodeArgs) { + try { + nodeArgs.push(makeNodeCallbackFor(resolve, reject)); + nodeFunc.apply(thisArg, nodeArgs); + } catch(e) { + reject(e); + } + }); + }); + }; + }; + }); +define("rsvp/promise", + ["./config","./events","./instrument","./utils","./promise/cast","./promise/all","./promise/race","./promise/resolve","./promise/reject","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __exports__) { + "use strict"; + var config = __dependency1__.config; + var EventTarget = __dependency2__["default"]; + var instrument = __dependency3__["default"]; + var objectOrFunction = __dependency4__.objectOrFunction; + var isFunction = __dependency4__.isFunction; + var now = __dependency4__.now; + var cast = __dependency5__["default"]; + var all = __dependency6__["default"]; + var race = __dependency7__["default"]; + var Resolve = __dependency8__["default"]; + var Reject = __dependency9__["default"]; - /** - @property parent - @type InheritingDict - @default null - */ + var guidKey = 'rsvp_' + now() + '-'; + var counter = 0; - parent: null, + function noop() {} - /** - Object used to store the current nodes data. + __exports__["default"] = Promise; - @property dict - @type Object - @default Object - */ - dict: null, - /** - Retrieve the value given a key, if the value is present at the current - level use it, otherwise walk up the parent hierarchy and try again. If - no matching key is found, return undefined. + /** + Promise objects represent the eventual result of an asynchronous operation. The + primary way of interacting with a promise is through its `then` method, which + registers callbacks to receive either a promise’s eventual value or the reason + why the promise cannot be fulfilled. - @method get - @return {any} - */ - get: function(key) { - var dict = this.dict; + Terminology + ----------- - if (dict.hasOwnProperty(key)) { - return dict[key]; - } + - `promise` is an object or function with a `then` method whose behavior conforms to this specification. + - `thenable` is an object or function that defines a `then` method. + - `value` is any legal JavaScript value (including undefined, a thenable, or a promise). + - `exception` is a value that is thrown using the throw statement. + - `reason` is a value that indicates why a promise was rejected. + - `settled` the final resting state of a promise, fulfilled or rejected. - if (this.parent) { - return this.parent.get(key); - } - }, + A promise can be in one of three states: pending, fulfilled, or rejected. - /** - Set the given value for the given key, at the current level. + Promises that are fulfilled have a fulfillment value and are in the fulfilled + state. Promises that are rejected have a rejection reason and are in the + rejected state. A fulfillment value is never a thenable. Similarly, a + rejection reason is never a thenable. - @method set - @param {String} key - @param {Any} value - */ - set: function(key, value) { - this.dict[key] = value; - }, + Promises can also be said to *resolve* a value. If this value is also a + promise, then the original promise's settled state will match the value's + settled state. So a promise that *resolves* a promise that rejects will + itself reject, and a promise that *resolves* a promise that fulfills will + itself fulfill. - /** - Delete the given key - @method remove - @param {String} key - */ - remove: function(key) { - delete this.dict[key]; - }, + Basic Usage: + ------------ - /** - Check for the existence of given a key, if the key is present at the current - level return true, otherwise walk up the parent hierarchy and try again. If - no matching key is found, return false. + ```js + var promise = new Promise(function(resolve, reject) { + // on success + resolve(value); - @method has - @param {String} key - @return {Boolean} - */ - has: function(key) { - var dict = this.dict; + // on failure + reject(reason); + }); - if (dict.hasOwnProperty(key)) { - return true; - } + promise.then(function(value) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` - if (this.parent) { - return this.parent.has(key); - } + Advanced Usage: + --------------- - return false; - }, + Promises shine when abstracting away asynchronous interactions such as + `XMLHttpRequest`s. - /** - Iterate and invoke a callback for each local key-value pair. + ```js + function getJSON(url) { + return new Promise(function(resolve, reject){ + var xhr = new XMLHttpRequest(); - @method eachLocal - @param {Function} callback - @param {Object} binding - */ - eachLocal: function(callback, binding) { - var dict = this.dict; + xhr.open('GET', url); + xhr.onreadystatechange = handler; + xhr.responseType = 'json'; + xhr.setRequestHeader('Accept', 'application/json'); + xhr.send(); - for (var prop in dict) { - if (dict.hasOwnProperty(prop)) { - callback.call(binding, prop, dict[prop]); - } - } + function handler() { + if (this.readyState === this.DONE) { + if (this.status === 200) { + resolve(this.response); + } else { + reject(new Error("getJSON: `" + url + "` failed with status: [" + this.status + "]"); + } + } + }; + }); } - }; + getJSON('/posts.json').then(function(json) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` - // A lightweight container that helps to assemble and decouple components. - // Public api for the container is still in flux. - // The public api, specified on the application namespace should be considered the stable api. - function Container(parent) { - this.parent = parent; - this.children = []; + Unlike callbacks, promises are great composable primitives. - this.resolver = parent && parent.resolver || function() {}; + ```js + Promise.all([ + getJSON('/posts'), + getJSON('/comments') + ]).then(function(values){ + values[0] // => postsJSON + values[1] // => commentsJSON - this.registry = new InheritingDict(parent && parent.registry); - this.cache = new InheritingDict(parent && parent.cache); - this.factoryCache = new InheritingDict(parent && parent.cache); - this.typeInjections = new InheritingDict(parent && parent.typeInjections); - this.injections = {}; + return values; + }); + ``` - this.factoryTypeInjections = new InheritingDict(parent && parent.factoryTypeInjections); - this.factoryInjections = {}; + @class RSVP.Promise + @param {function} + @param {String} label optional string for labeling the promise. + Useful for tooling. + @constructor + */ + function Promise(resolver, label) { + if (!isFunction(resolver)) { + throw new TypeError('You must pass a resolver function as the first argument to the promise constructor'); + } - this._options = new InheritingDict(parent && parent._options); - this._typeOptions = new InheritingDict(parent && parent._typeOptions); - } + if (!(this instanceof Promise)) { + throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function."); + } - Container.prototype = { + this._id = counter++; + this._label = label; + this._subscribers = []; - /** - @property parent - @type Container - @default null - */ - parent: null, + if (config.instrument) { + instrument('created', this); + } - /** - @property children - @type Array - @default [] - */ - children: null, + if (noop !== resolver) { + invokeResolver(resolver, this); + } + } - /** - @property resolver - @type function - */ - resolver: null, + function invokeResolver(resolver, promise) { + function resolvePromise(value) { + resolve(promise, value); + } - /** - @property registry - @type InheritingDict - */ - registry: null, + function rejectPromise(reason) { + reject(promise, reason); + } - /** - @property cache - @type InheritingDict - */ - cache: null, + try { + resolver(resolvePromise, rejectPromise); + } catch(e) { + rejectPromise(e); + } + } - /** - @property typeInjections - @type InheritingDict - */ - typeInjections: null, + Promise.cast = cast; + Promise.all = all; + Promise.race = race; + Promise.resolve = Resolve; + Promise.reject = Reject; - /** - @property injections - @type Object - @default {} - */ - injections: null, + var PENDING = void 0; + var SEALED = 0; + var FULFILLED = 1; + var REJECTED = 2; - /** - @private + function subscribe(parent, child, onFulfillment, onRejection) { + var subscribers = parent._subscribers; + var length = subscribers.length; - @property _options - @type InheritingDict - @default null - */ - _options: null, + subscribers[length] = child; + subscribers[length + FULFILLED] = onFulfillment; + subscribers[length + REJECTED] = onRejection; + } - /** - @private + function publish(promise, settled) { + var child, callback, subscribers = promise._subscribers, detail = promise._detail; - @property _typeOptions - @type InheritingDict - */ - _typeOptions: null, + if (config.instrument) { + instrument(settled === FULFILLED ? 'fulfilled' : 'rejected', promise); + } - /** - Returns a new child of the current container. These children are configured - to correctly inherit from the current container. + for (var i = 0; i < subscribers.length; i += 3) { + child = subscribers[i]; + callback = subscribers[i + settled]; - @method child - @return {Container} - */ - child: function() { - var container = new Container(this); - this.children.push(container); - return container; - }, + invokeCallback(settled, child, callback, detail); + } - /** - Sets a key-value pair on the current container. If a parent container, - has the same key, once set on a child, the parent and child will diverge - as expected. + promise._subscribers = null; + } - @method set - @param {Object} object - @param {String} key - @param {any} value - */ - set: function(object, key, value) { - object[key] = value; - }, + Promise.prototype = { + constructor: Promise, - /** - Registers a factory for later injection. + _id: undefined, + _guidKey: guidKey, + _label: undefined, - Example: + _state: undefined, + _detail: undefined, + _subscribers: undefined, - ```javascript - var container = new Container(); + _onerror: function (reason) { + config.trigger('error', reason); + }, - container.register('model:user', Person, {singleton: false }); - container.register('fruit:favorite', Orange); - container.register('communication:main', Email, {singleton: false}); - ``` + /** + The primary way of interacting with a promise is through its `then` method, + which registers callbacks to receive either a promise's eventual value or the + reason why the promise cannot be fulfilled. + + ```js + findUser().then(function(user){ + // user is available + }, function(reason){ + // user is unavailable, and you are given the reason why + }); + ``` - @method register - @param {String} fullName - @param {Function} factory - @param {Object} options - */ - register: function(fullName, factory, options) { - if (fullName.indexOf(':') === -1) { - throw new TypeError("malformed fullName, expected: `type:name` got: " + fullName + ""); - } + Chaining + -------- + + The return value of `then` is itself a promise. This second, "downstream" + promise is resolved with the return value of the first promise's fulfillment + or rejection handler, or rejected if the handler throws an exception. + + ```js + findUser().then(function (user) { + return user.name; + }, function (reason) { + return "default name"; + }).then(function (userName) { + // If `findUser` fulfilled, `userName` will be the user's name, otherwise it + // will be `"default name"` + }); - if (factory === undefined) { - throw new TypeError('Attempting to register an unknown factory: `' + fullName + '`'); - } + findUser().then(function (user) { + throw new Error("Found user, but still unhappy"); + }, function (reason) { + throw new Error("`findUser` rejected and we're unhappy"); + }).then(function (value) { + // never reached + }, function (reason) { + // if `findUser` fulfilled, `reason` will be "Found user, but still unhappy". + // If `findUser` rejected, `reason` will be "`findUser` rejected and we're unhappy". + }); + ``` + If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream. + + ```js + findUser().then(function (user) { + throw new PedagogicalException("Upstream error"); + }).then(function (value) { + // never reached + }).then(function (value) { + // never reached + }, function (reason) { + // The `PedgagocialException` is propagated all the way down to here + }); + ``` - var normalizedName = this.normalize(fullName); + Assimilation + ------------ - if (this.cache.has(normalizedName)) { - throw new Error('Cannot re-register: `' + fullName +'`, as it has already been looked up.'); - } + Sometimes the value you want to propagate to a downstream promise can only be + retrieved asynchronously. This can be achieved by returning a promise in the + fulfillment or rejection handler. The downstream promise will then be pending + until the returned promise is settled. This is called *assimilation*. - this.registry.set(normalizedName, factory); - this._options.set(normalizedName, options || {}); - }, + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // The user's comments are now available + }); + ``` - /** - Unregister a fullName + If the assimliated promise rejects, then the downstream promise will also reject. - ```javascript - var container = new Container(); - container.register('model:user', User); + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // If `findCommentsByAuthor` fulfills, we'll have the value here + }, function (reason) { + // If `findCommentsByAuthor` rejects, we'll have the reason here + }); + ``` - container.lookup('model:user') instanceof User //=> true + Simple Example + -------------- - container.unregister('model:user') - container.lookup('model:user') === undefined //=> true - ``` + Synchronous Example - @method unregister - @param {String} fullName - */ - unregister: function(fullName) { - var normalizedName = this.normalize(fullName); + ```javascript + var result; - this.registry.remove(normalizedName); - this.cache.remove(normalizedName); - this.factoryCache.remove(normalizedName); - this._options.remove(normalizedName); - }, + try { + result = findResult(); + // success + } catch(reason) { + // failure + } + ``` - /** - Given a fullName return the corresponding factory. + Errback Example - By default `resolve` will retrieve the factory from - its container's registry. + ```js + findResult(function(result, err){ + if (err) { + // failure + } else { + // success + } + }); + ``` - ```javascript - var container = new Container(); - container.register('api:twitter', Twitter); + Promise Example; - container.resolve('api:twitter') // => Twitter - ``` + ```javascript + findResult().then(function(result){ + // success + }, function(reason){ + // failure + }); + ``` - Optionally the container can be provided with a custom resolver. - If provided, `resolve` will first provide the custom resolver - the oppertunity to resolve the fullName, otherwise it will fallback - to the registry. + Advanced Example + -------------- - ```javascript - var container = new Container(); - container.resolver = function(fullName) { - // lookup via the module system of choice - }; + Synchronous Example - // the twitter factory is added to the module system - container.resolve('api:twitter') // => Twitter - ``` + ```javascript + var author, books; - @method resolve - @param {String} fullName - @return {Function} fullName's factory - */ - resolve: function(fullName) { - return this.resolver(fullName) || this.registry.get(fullName); - }, + try { + author = findAuthor(); + books = findBooksByAuthor(author); + // success + } catch(reason) { + // failure + } + ``` - /** - A hook that can be used to describe how the resolver will - attempt to find the factory. + Errback Example - For example, the default Ember `.describe` returns the full - class name (including namespace) where Ember's resolver expects - to find the `fullName`. + ```js - @method describe - */ - describe: function(fullName) { - return fullName; - }, + function foundBooks(books) { - /** - A hook to enable custom fullName normalization behaviour + } - @method normalize - @param {String} fullName - @return {string} normalized fullName - */ - normalize: function(fullName) { - return fullName; - }, + function failure(reason) { - /** - @method makeToString + } - @param {any} factory - @param {string} fullName - @return {function} toString function - */ - makeToString: function(factory, fullName) { - return factory.toString(); - }, + findAuthor(function(author, err){ + if (err) { + failure(err); + // failure + } else { + try { + findBoooksByAuthor(author, function(books, err) { + if (err) { + failure(err); + } else { + try { + foundBooks(books); + } catch(reason) { + failure(reason); + } + } + }); + } catch(error) { + failure(err); + } + // success + } + }); + ``` - /** - Given a fullName return a corresponding instance. + Promise Example; - The default behaviour is for lookup to return a singleton instance. - The singleton is scoped to the container, allowing multiple containers - to all have their own locally scoped singletons. + ```javascript + findAuthor(). + then(findBooksByAuthor). + then(function(books){ + // found books + }).catch(function(reason){ + // something went wrong + }); + ``` - ```javascript - var container = new Container(); - container.register('api:twitter', Twitter); + @method then + @param {Function} onFulfilled + @param {Function} onRejected + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} + */ + then: function(onFulfillment, onRejection, label) { + var promise = this; + this._onerror = null; - var twitter = container.lookup('api:twitter'); + var thenPromise = new this.constructor(noop, label); - twitter instanceof Twitter; // => true + if (this._state) { + var callbacks = arguments; + config.async(function invokePromiseCallback() { + invokeCallback(promise._state, thenPromise, callbacks[promise._state - 1], promise._detail); + }); + } else { + subscribe(this, thenPromise, onFulfillment, onRejection); + } - // by default the container will return singletons - twitter2 = container.lookup('api:twitter'); - twitter instanceof Twitter; // => true + if (config.instrument) { + instrument('chained', promise, thenPromise); + } - twitter === twitter2; //=> true - ``` + return thenPromise; + }, - If singletons are not wanted an optional flag can be provided at lookup. + /** + `catch` is simply sugar for `then(undefined, onRejection)` which makes it the same + as the catch block of a try/catch statement. - ```javascript - var container = new Container(); - container.register('api:twitter', Twitter); + ```js + function findAuthor(){ + throw new Error("couldn't find that author"); + } - var twitter = container.lookup('api:twitter', { singleton: false }); - var twitter2 = container.lookup('api:twitter', { singleton: false }); + // synchronous + try { + findAuthor(); + } catch(reason) { + // something went wrong + } - twitter === twitter2; //=> false - ``` + // async with promises + findAuthor().catch(function(reason){ + // something went wrong + }); + ``` - @method lookup - @param {String} fullName - @param {Object} options - @return {any} - */ - lookup: function(fullName, options) { - fullName = this.normalize(fullName); + @method catch + @param {Function} onRejection + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} + */ + 'catch': function(onRejection, label) { + return this.then(null, onRejection, label); + }, + + /** + `finally` will be invoked regardless of the promise's fate just as native + try/catch/finally behaves - options = options || {}; + Synchronous example: - if (this.cache.has(fullName) && options.singleton !== false) { - return this.cache.get(fullName); + ```js + findAuthor() { + if (Math.random() > 0.5) { + throw new Error(); } + return new Author(); + } - var value = instantiate(this, fullName); - - if (value === undefined) { return; } + try { + return findAuthor(); // succeed or fail + } catch(error) { + return findOtherAuther(); + } finally { + // always runs + // doesn't affect the return value + } + ``` - if (isSingleton(this, fullName) && options.singleton !== false) { - this.cache.set(fullName, value); - } + Asynchronous example: - return value; - }, + ```js + findAuthor().catch(function(reason){ + return findOtherAuther(); + }).finally(function(){ + // author was either found, or not + }); + ``` - /** - Given a fullName return the corresponding factory. + @method finally + @param {Function} callback + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} + */ + 'finally': function(callback, label) { + var constructor = this.constructor; - @method lookupFactory - @param {String} fullName - @return {any} - */ - lookupFactory: function(fullName) { - return factoryFor(this, fullName); - }, + return this.then(function(value) { + return constructor.cast(callback()).then(function(){ + return value; + }); + }, function(reason) { + return constructor.cast(callback()).then(function(){ + throw reason; + }); + }, label); + } + }; - /** - Given a fullName check if the container is aware of its factory - or singleton instance. + function invokeCallback(settled, promise, callback, detail) { + var hasCallback = isFunction(callback), + value, error, succeeded, failed; - @method has - @param {String} fullName - @return {Boolean} - */ - has: function(fullName) { - if (this.cache.has(fullName)) { - return true; + if (hasCallback) { + try { + value = callback(detail); + succeeded = true; + } catch(e) { + failed = true; + error = e; } + } else { + value = detail; + succeeded = true; + } - return !!factoryFor(this, fullName); - }, + if (handleThenable(promise, value)) { + return; + } else if (hasCallback && succeeded) { + resolve(promise, value); + } else if (failed) { + reject(promise, error); + } else if (settled === FULFILLED) { + resolve(promise, value); + } else if (settled === REJECTED) { + reject(promise, value); + } + } - /** - Allow registering options for all factories of a type. + function handleThenable(promise, value) { + var then = null, + resolved; - ```javascript - var container = new Container(); + try { + if (promise === value) { + throw new TypeError("A promises callback cannot return that same promise."); + } - // if all of type `connection` must not be singletons - container.optionsForType('connection', { singleton: false }); + if (objectOrFunction(value)) { + then = value.then; - container.register('connection:twitter', TwitterConnection); - container.register('connection:facebook', FacebookConnection); + if (isFunction(then)) { + then.call(value, function(val) { + if (resolved) { return true; } + resolved = true; - var twitter = container.lookup('connection:twitter'); - var twitter2 = container.lookup('connection:twitter'); + if (value !== val) { + resolve(promise, val); + } else { + fulfill(promise, val); + } + }, function(val) { + if (resolved) { return true; } + resolved = true; - twitter === twitter2; // => false + reject(promise, val); + }, 'derived from: ' + (promise._label || ' unknown promise')); - var facebook = container.lookup('connection:facebook'); - var facebook2 = container.lookup('connection:facebook'); - - facebook === facebook2; // => false - ``` - - @method optionsForType - @param {String} type - @param {Object} options - */ - optionsForType: function(type, options) { - if (this.parent) { illegalChildOperation('optionsForType'); } - - this._typeOptions.set(type, options); - }, - - /** - @method options - @param {String} type - @param {Object} options - */ - options: function(type, options) { - this.optionsForType(type, options); - }, - - /** - @private - - Used only via `injection`. - - Provides a specialized form of injection, specifically enabling - all objects of one type to be injected with a reference to another - object. - - For example, provided each object of type `controller` needed a `router`. - one would do the following: - - ```javascript - var container = new Container(); + return true; + } + } + } catch (error) { + if (resolved) { return true; } + reject(promise, error); + return true; + } - container.register('router:main', Router); - container.register('controller:user', UserController); - container.register('controller:post', PostController); + return false; + } - container.typeInjection('controller', 'router', 'router:main'); + function resolve(promise, value) { + if (promise === value) { + fulfill(promise, value); + } else if (!handleThenable(promise, value)) { + fulfill(promise, value); + } + } - var user = container.lookup('controller:user'); - var post = container.lookup('controller:post'); + function fulfill(promise, value) { + if (promise._state !== PENDING) { return; } + promise._state = SEALED; + promise._detail = value; - user.router instanceof Router; //=> true - post.router instanceof Router; //=> true + config.async(publishFulfillment, promise); + } - // both controllers share the same router - user.router === post.router; //=> true - ``` + function reject(promise, reason) { + if (promise._state !== PENDING) { return; } + promise._state = SEALED; + promise._detail = reason; - @method typeInjection - @param {String} type - @param {String} property - @param {String} fullName - */ - typeInjection: function(type, property, fullName) { - if (this.parent) { illegalChildOperation('typeInjection'); } + config.async(publishRejection, promise); + } - addTypeInjection(this.typeInjections, type, property, fullName); - }, + function publishFulfillment(promise) { + publish(promise, promise._state = FULFILLED); + } - /** - Defines injection rules. + function publishRejection(promise) { + if (promise._onerror) { + promise._onerror(promise._detail); + } - These rules are used to inject dependencies onto objects when they - are instantiated. + publish(promise, promise._state = REJECTED); + } + }); +define("rsvp/promise/all", + ["../utils","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var isArray = __dependency1__.isArray; + var isNonThenable = __dependency1__.isNonThenable; - Two forms of injections are possible: + /** + `RSVP.Promise.all` accepts an array of promises, and returns a new promise which + is fulfilled with an array of fulfillment values for the passed promises, or + rejected with the reason of the first passed promise to be rejected. It casts all + elements of the passed iterable to promises as it runs this algorithm. - * Injecting one fullName on another fullName - * Injecting one fullName on a type + Example: - Example: + ```javascript + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.resolve(2); + var promise3 = RSVP.resolve(3); + var promises = [ promise1, promise2, promise3 ]; - ```javascript - var container = new Container(); + RSVP.Promise.all(promises).then(function(array){ + // The array here would be [ 1, 2, 3 ]; + }); + ``` - container.register('source:main', Source); - container.register('model:user', User); - container.register('model:post', Post); + If any of the `promises` given to `RSVP.all` are rejected, the first promise + that is rejected will be given as an argument to the returned promises's + rejection handler. For example: - // injecting one fullName on another fullName - // eg. each user model gets a post model - container.injection('model:user', 'post', 'model:post'); + Example: - // injecting one fullName on another type - container.injection('model', 'source', 'source:main'); + ```javascript + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.reject(new Error("2")); + var promise3 = RSVP.reject(new Error("3")); + var promises = [ promise1, promise2, promise3 ]; + + RSVP.Promise.all(promises).then(function(array){ + // Code here never runs because there are rejected promises! + }, function(error) { + // error.message === "2" + }); + ``` - var user = container.lookup('model:user'); - var post = container.lookup('model:post'); + @method all + @for Ember.RSVP.Promise + @param {Array} entries array of promises + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} promise that is fulfilled when all `promises` have been + fulfilled, or rejected if any of them become rejected. + @static + */ + __exports__["default"] = function all(entries, label) { - user.source instanceof Source; //=> true - post.source instanceof Source; //=> true + /*jshint validthis:true */ + var Constructor = this; - user.post instanceof Post; //=> true + return new Constructor(function(resolve, reject) { + if (!isArray(entries)) { + throw new TypeError('You must pass an array to all.'); + } - // and both models share the same source - user.source === post.source; //=> true - ``` + var remaining = entries.length; + var results = new Array(remaining); + var entry, pending = true; - @method injection - @param {String} factoryName - @param {String} property - @param {String} injectionName - */ - injection: function(factoryName, property, injectionName) { - if (this.parent) { illegalChildOperation('injection'); } + if (remaining === 0) { + resolve(results); + return; + } - if (factoryName.indexOf(':') === -1) { - return this.typeInjection(factoryName, property, injectionName); + function fulfillmentAt(index) { + return function(value) { + results[index] = value; + if (--remaining === 0) { + resolve(results); + } + }; } - addInjection(this.injections, factoryName, property, injectionName); - }, + function onRejection(reason) { + remaining = 0; + reject(reason); + } + for (var index = 0; index < entries.length; index++) { + entry = entries[index]; + if (isNonThenable(entry)) { + results[index] = entry; + if (--remaining === 0) { + resolve(results); + } + } else { + Constructor.cast(entry).then(fulfillmentAt(index), onRejection); + } + } + }, label); + }; + }); +define("rsvp/promise/cast", + ["exports"], + function(__exports__) { + "use strict"; + /** + `RSVP.Promise.cast` coerces its argument to a promise, or returns the + argument if it is already a promise which shares a constructor with the caster. - /** - @private + Example: - Used only via `factoryInjection`. + ```javascript + var promise = RSVP.Promise.resolve(1); + var casted = RSVP.Promise.cast(promise); - Provides a specialized form of injection, specifically enabling - all factory of one type to be injected with a reference to another - object. + console.log(promise === casted); // true + ``` - For example, provided each factory of type `model` needed a `store`. - one would do the following: + In the case of a promise whose constructor does not match, it is assimilated. + The resulting promise will fulfill or reject based on the outcome of the + promise being casted. - ```javascript - var container = new Container(); + Example: - container.registerFactory('model:user', User); - container.register('store:main', SomeStore); + ```javascript + var thennable = $.getJSON('/api/foo'); + var casted = RSVP.Promise.cast(thennable); - container.factoryTypeInjection('model', 'store', 'store:main'); + console.log(thennable === casted); // false + console.log(casted instanceof RSVP.Promise) // true - var store = container.lookup('store:main'); - var UserFactory = container.lookupFactory('model:user'); + casted.then(function(data) { + // data is the value getJSON fulfills with + }); + ``` - UserFactory.store instanceof SomeStore; //=> true - ``` + In the case of a non-promise, a promise which will fulfill with that value is + returned. - @method factoryTypeInjection - @param {String} type - @param {String} property - @param {String} fullName - */ - factoryTypeInjection: function(type, property, fullName) { - if (this.parent) { illegalChildOperation('factoryTypeInjection'); } + Example: - addTypeInjection(this.factoryTypeInjections, type, property, fullName); - }, + ```javascript + var value = 1; // could be a number, boolean, string, undefined... + var casted = RSVP.Promise.cast(value); - /** - Defines factory injection rules. + console.log(value === casted); // false + console.log(casted instanceof RSVP.Promise) // true - Similar to regular injection rules, but are run against factories, via - `Container#lookupFactory`. + casted.then(function(val) { + val === value // => true + }); + ``` - These rules are used to inject objects onto factories when they - are looked up. + `RSVP.Promise.cast` is similar to `RSVP.Promise.resolve`, but `RSVP.Promise.cast` differs in the + following ways: + + * `RSVP.Promise.cast` serves as a memory-efficient way of getting a promise, when you + have something that could either be a promise or a value. RSVP.resolve + will have the same effect but will create a new promise wrapper if the + argument is a promise. + * `RSVP.Promise.cast` is a way of casting incoming thenables or promise subclasses to + promises of the exact class specified, so that the resulting object's `then` is + ensured to have the behavior of the constructor you are calling cast on (i.e., RSVP.Promise). + + @method cast + @param {Object} object to be casted + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} promise + @static + */ - Two forms of injections are possible: + __exports__["default"] = function cast(object, label) { + /*jshint validthis:true */ + var Constructor = this; - * Injecting one fullName on another fullName - * Injecting one fullName on a type + if (object && typeof object === 'object' && object.constructor === Constructor) { + return object; + } - Example: + return new Constructor(function(resolve) { + resolve(object); + }, label); + }; + }); +define("rsvp/promise/race", + ["../utils","exports"], + function(__dependency1__, __exports__) { + "use strict"; + /* global toString */ - ```javascript - var container = new Container(); + var isArray = __dependency1__.isArray; + var isFunction = __dependency1__.isFunction; + var isNonThenable = __dependency1__.isNonThenable; - container.register('store:main', Store); - container.register('store:secondary', OtherStore); - container.register('model:user', User); - container.register('model:post', Post); + /** + `RSVP.Promise.race` returns a new promise which is settled in the same way as the + first passed promise to settle. - // injecting one fullName on another type - container.factoryInjection('model', 'store', 'store:main'); + Example: - // injecting one fullName on another fullName - container.factoryInjection('model:post', 'secondaryStore', 'store:secondary'); + ```javascript + var promise1 = new RSVP.Promise(function(resolve, reject){ + setTimeout(function(){ + resolve("promise 1"); + }, 200); + }); - var UserFactory = container.lookupFactory('model:user'); - var PostFactory = container.lookupFactory('model:post'); - var store = container.lookup('store:main'); + var promise2 = new RSVP.Promise(function(resolve, reject){ + setTimeout(function(){ + resolve("promise 2"); + }, 100); + }); - UserFactory.store instanceof Store; //=> true - UserFactory.secondaryStore instanceof OtherStore; //=> false + RSVP.Promise.race([promise1, promise2]).then(function(result){ + // result === "promise 2" because it was resolved before promise1 + // was resolved. + }); + ``` - PostFactory.store instanceof Store; //=> true - PostFactory.secondaryStore instanceof OtherStore; //=> true + `RSVP.Promise.race` is deterministic in that only the state of the first + settled promise matters. For example, even if other promises given to the + `promises` array argument are resolved, but the first settled promise has + become rejected before the other promises became fulfilled, the returned + promise will become rejected: - // and both models share the same source instance - UserFactory.store === PostFactory.store; //=> true - ``` + ```javascript + var promise1 = new RSVP.Promise(function(resolve, reject){ + setTimeout(function(){ + resolve("promise 1"); + }, 200); + }); - @method factoryInjection - @param {String} factoryName - @param {String} property - @param {String} injectionName - */ - factoryInjection: function(factoryName, property, injectionName) { - if (this.parent) { illegalChildOperation('injection'); } + var promise2 = new RSVP.Promise(function(resolve, reject){ + setTimeout(function(){ + reject(new Error("promise 2")); + }, 100); + }); - if (factoryName.indexOf(':') === -1) { - return this.factoryTypeInjection(factoryName, property, injectionName); - } + RSVP.Promise.race([promise1, promise2]).then(function(result){ + // Code here never runs + }, function(reason){ + // reason.message === "promise2" because promise 2 became rejected before + // promise 1 became fulfilled + }); + ``` - addInjection(this.factoryInjections, factoryName, property, injectionName); - }, + An example real-world use case is implementing timeouts: - /** - A depth first traversal, destroying the container, its descendant containers and all - their managed objects. + ```javascript + RSVP.Promise.race([ajax('foo.json'), timeout(5000)]) + ``` - @method destroy - */ - destroy: function() { - this.isDestroyed = true; + @method race + @param {Array} promises array of promises to observe + @param {String} label optional string for describing the promise returned. + Useful for tooling. + @return {Promise} a promise which settles in the same way as the first passed + promise to settle. + @static + */ + __exports__["default"] = function race(entries, label) { + /*jshint validthis:true */ + var Constructor = this, entry; - for (var i=0, l=this.children.length; i w. -*/ -Ember.compare = function compare(v, w) { - if (v === w) { return 0; } - - var type1 = Ember.typeOf(v); - var type2 = Ember.typeOf(w); + Container.prototype = { - var Comparable = Ember.Comparable; - if (Comparable) { - if (type1==='instance' && Comparable.detect(v.constructor)) { - return v.constructor.compare(v, w); - } + /** + @property parent + @type Container + @default null + */ + parent: null, - if (type2 === 'instance' && Comparable.detect(w.constructor)) { - return 1-w.constructor.compare(w, v); - } - } + /** + @property children + @type Array + @default [] + */ + children: null, - // If we haven't yet generated a reverse-mapping of Ember.ORDER_DEFINITION, - // do so now. - var mapping = Ember.ORDER_DEFINITION_MAPPING; - if (!mapping) { - var order = Ember.ORDER_DEFINITION; - mapping = Ember.ORDER_DEFINITION_MAPPING = {}; - var idx, len; - for (idx = 0, len = order.length; idx < len; ++idx) { - mapping[order[idx]] = idx; - } + /** + @property resolver + @type function + */ + resolver: null, - // We no longer need Ember.ORDER_DEFINITION. - delete Ember.ORDER_DEFINITION; - } + /** + @property registry + @type InheritingDict + */ + registry: null, - var type1Index = mapping[type1]; - var type2Index = mapping[type2]; + /** + @property cache + @type InheritingDict + */ + cache: null, - if (type1Index < type2Index) { return -1; } - if (type1Index > type2Index) { return 1; } + /** + @property typeInjections + @type InheritingDict + */ + typeInjections: null, - // types are equal - so we have to check values now - switch (type1) { - case 'boolean': - case 'number': - if (v < w) { return -1; } - if (v > w) { return 1; } - return 0; + /** + @property injections + @type Object + @default {} + */ + injections: null, - case 'string': - var comp = v.localeCompare(w); - if (comp < 0) { return -1; } - if (comp > 0) { return 1; } - return 0; + /** + @private - case 'array': - var vLen = v.length; - var wLen = w.length; - var l = Math.min(vLen, wLen); - var r = 0; - var i = 0; - while (r === 0 && i < l) { - r = compare(v[i],w[i]); - i++; - } - if (r !== 0) { return r; } + @property _options + @type InheritingDict + @default null + */ + _options: null, - // all elements are equal now - // shorter array should be ordered first - if (vLen < wLen) { return -1; } - if (vLen > wLen) { return 1; } - // arrays are equal now - return 0; + /** + @private - case 'instance': - if (Ember.Comparable && Ember.Comparable.detect(v)) { - return v.compare(v, w); - } - return 0; + @property _typeOptions + @type InheritingDict + */ + _typeOptions: null, - case 'date': - var vNum = v.getTime(); - var wNum = w.getTime(); - if (vNum < wNum) { return -1; } - if (vNum > wNum) { return 1; } - return 0; + /** + Returns a new child of the current container. These children are configured + to correctly inherit from the current container. - default: - return 0; - } -}; + @method child + @return {Container} + */ + child: function() { + var container = new Container(this); + this.children.push(container); + return container; + }, -function _copy(obj, deep, seen, copies) { - var ret, loc, key; + /** + Sets a key-value pair on the current container. If a parent container, + has the same key, once set on a child, the parent and child will diverge + as expected. - // primitive data types are immutable, just return them. - if ('object' !== typeof obj || obj===null) return obj; + @method set + @param {Object} object + @param {String} key + @param {any} value + */ + set: function(object, key, value) { + object[key] = value; + }, - // avoid cyclical loops - if (deep && (loc=indexOf(seen, obj))>=0) return copies[loc]; + /** + Registers a factory for later injection. - Ember.assert('Cannot clone an Ember.Object that does not implement Ember.Copyable', !(obj instanceof Ember.Object) || (Ember.Copyable && Ember.Copyable.detect(obj))); + Example: - // IMPORTANT: this specific test will detect a native array only. Any other - // object will need to implement Copyable. - if (Ember.typeOf(obj) === 'array') { - ret = obj.slice(); - if (deep) { - loc = ret.length; - while(--loc>=0) ret[loc] = _copy(ret[loc], deep, seen, copies); - } - } else if (Ember.Copyable && Ember.Copyable.detect(obj)) { - ret = obj.copy(deep, seen, copies); - } else { - ret = {}; - for(key in obj) { - if (!obj.hasOwnProperty(key)) continue; + ```javascript + var container = new Container(); - // Prevents browsers that don't respect non-enumerability from - // copying internal Ember properties - if (key.substring(0,2) === '__') continue; + container.register('model:user', Person, {singleton: false }); + container.register('fruit:favorite', Orange); + container.register('communication:main', Email, {singleton: false}); + ``` - ret[key] = deep ? _copy(obj[key], deep, seen, copies) : obj[key]; - } - } + @method register + @param {String} fullName + @param {Function} factory + @param {Object} options + */ + register: function(fullName, factory, options) { + validateFullName(fullName); - if (deep) { - seen.push(obj); - copies.push(ret); - } + if (factory === undefined) { + throw new TypeError('Attempting to register an unknown factory: `' + fullName + '`'); + } - return ret; -} + var normalizedName = this.normalize(fullName); -/** - Creates a clone of the passed object. This function can take just about - any type of object and create a clone of it, including primitive values - (which are not actually cloned because they are immutable). + if (this.cache.has(normalizedName)) { + throw new Error('Cannot re-register: `' + fullName +'`, as it has already been looked up.'); + } - If the passed object implements the `clone()` method, then this function - will simply call that method and return the result. + this.registry.set(normalizedName, factory); + this._options.set(normalizedName, options || {}); + }, - @method copy - @for Ember - @param {Object} obj The object to clone - @param {Boolean} deep If true, a deep copy of the object is made - @return {Object} The cloned object -*/ -Ember.copy = function(obj, deep) { - // fast paths - if ('object' !== typeof obj || obj===null) return obj; // can't copy primitives - if (Ember.Copyable && Ember.Copyable.detect(obj)) return obj.copy(deep); - return _copy(obj, deep, deep ? [] : null, deep ? [] : null); -}; + /** + Unregister a fullName -/** - Convenience method to inspect an object. This method will attempt to - convert the object into a useful string description. + ```javascript + var container = new Container(); + container.register('model:user', User); - It is a pretty simple implementation. If you want something more robust, - use something like JSDump: https://github.com/NV/jsDump + container.lookup('model:user') instanceof User //=> true - @method inspect - @for Ember - @param {Object} obj The object you want to inspect. - @return {String} A description of the object -*/ -Ember.inspect = function(obj) { - var type = Ember.typeOf(obj); - if (type === 'array') { - return '[' + obj + ']'; - } - if (type !== 'object') { - return obj + ''; - } + container.unregister('model:user') + container.lookup('model:user') === undefined //=> true + ``` - var v, ret = []; - for(var key in obj) { - if (obj.hasOwnProperty(key)) { - v = obj[key]; - if (v === 'toString') { continue; } // ignore useless items - if (Ember.typeOf(v) === 'function') { v = "function() { ... }"; } - ret.push(key + ": " + v); - } - } - return "{" + ret.join(", ") + "}"; -}; + @method unregister + @param {String} fullName + */ + unregister: function(fullName) { + validateFullName(fullName); -/** - Compares two objects, returning true if they are logically equal. This is - a deeper comparison than a simple triple equal. For sets it will compare the - internal objects. For any other object that implements `isEqual()` it will - respect that method. + var normalizedName = this.normalize(fullName); - ```javascript - Ember.isEqual('hello', 'hello'); // true - Ember.isEqual(1, 2); // false - Ember.isEqual([4,2], [4,2]); // false - ``` + this.registry.remove(normalizedName); + this.cache.remove(normalizedName); + this.factoryCache.remove(normalizedName); + this.resolveCache.remove(normalizedName); + this._options.remove(normalizedName); + }, - @method isEqual - @for Ember - @param {Object} a first object to compare - @param {Object} b second object to compare - @return {Boolean} -*/ -Ember.isEqual = function(a, b) { - if (a && 'function'===typeof a.isEqual) return a.isEqual(b); - return a === b; -}; + /** + Given a fullName return the corresponding factory. -// Used by Ember.compare -Ember.ORDER_DEFINITION = Ember.ENV.ORDER_DEFINITION || [ - 'undefined', - 'null', - 'boolean', - 'number', - 'string', - 'array', - 'object', - 'instance', - 'function', - 'class', - 'date' -]; + By default `resolve` will retrieve the factory from + its container's registry. -/** - Returns all of the keys defined on an object or hash. This is useful - when inspecting objects for debugging. On browsers that support it, this - uses the native `Object.keys` implementation. + ```javascript + var container = new Container(); + container.register('api:twitter', Twitter); - @method keys - @for Ember - @param {Object} obj - @return {Array} Array containing keys of obj -*/ -Ember.keys = Object.keys; + container.resolve('api:twitter') // => Twitter + ``` -if (!Ember.keys || Ember.create.isSimulated) { - var prototypeProperties = [ - 'constructor', - 'hasOwnProperty', - 'isPrototypeOf', - 'propertyIsEnumerable', - 'valueOf', - 'toLocaleString', - 'toString' - ], - pushPropertyName = function(obj, array, key) { - // Prevents browsers that don't respect non-enumerability from - // copying internal Ember properties - if (key.substring(0,2) === '__') return; - if (key === '_super') return; - if (indexOf(array, key) >= 0) return; - if (!obj.hasOwnProperty(key)) return; + Optionally the container can be provided with a custom resolver. + If provided, `resolve` will first provide the custom resolver + the oppertunity to resolve the fullName, otherwise it will fallback + to the registry. - array.push(key); - }; + ```javascript + var container = new Container(); + container.resolver = function(fullName) { + // lookup via the module system of choice + }; - Ember.keys = function(obj) { - var ret = [], key; - for (key in obj) { - pushPropertyName(obj, ret, key); - } + // the twitter factory is added to the module system + container.resolve('api:twitter') // => Twitter + ``` - // IE8 doesn't enumerate property that named the same as prototype properties. - for (var i = 0, l = prototypeProperties.length; i < l; i++) { - key = prototypeProperties[i]; + @method resolve + @param {String} fullName + @return {Function} fullName's factory + */ + resolve: function(fullName) { + validateFullName(fullName); - pushPropertyName(obj, ret, key); - } + var normalizedName = this.normalize(fullName); + var cached = this.resolveCache.get(normalizedName); - return ret; - }; -} + if (cached) { return cached; } -// .......................................................... -// ERROR -// + var resolved = this.resolver(normalizedName) || this.registry.get(normalizedName); -var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; + this.resolveCache.set(normalizedName, resolved); -/** - A subclass of the JavaScript Error object for use in Ember. + return resolved; + }, - @class Error - @namespace Ember - @extends Error - @constructor -*/ -Ember.Error = function() { - var tmp = Error.apply(this, arguments); + /** + A hook that can be used to describe how the resolver will + attempt to find the factory. - // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. - for (var idx = 0; idx < errorProps.length; idx++) { - this[errorProps[idx]] = tmp[errorProps[idx]]; - } -}; + For example, the default Ember `.describe` returns the full + class name (including namespace) where Ember's resolver expects + to find the `fullName`. -Ember.Error.prototype = Ember.create(Error.prototype); + @method describe + @param {String} fullName + @return {string} described fullName + */ + describe: function(fullName) { + return fullName; + }, -})(); + /** + A hook to enable custom fullName normalization behaviour + @method normalize + @param {String} fullName + @return {string} normalized fullName + */ + normalize: function(fullName) { + return fullName; + }, + /** + @method makeToString -(function() { -/** -@module ember -@submodule ember-runtime -*/ + @param {any} factory + @param {string} fullName + @return {function} toString function + */ + makeToString: function(factory, fullName) { + return factory.toString(); + }, -// .......................................................... -// HELPERS -// + /** + Given a fullName return a corresponding instance. -var get = Ember.get, set = Ember.set; -var a_slice = Array.prototype.slice; -var a_indexOf = Ember.EnumerableUtils.indexOf; + The default behaviour is for lookup to return a singleton instance. + The singleton is scoped to the container, allowing multiple containers + to all have their own locally scoped singletons. -var contexts = []; + ```javascript + var container = new Container(); + container.register('api:twitter', Twitter); -function popCtx() { - return contexts.length===0 ? {} : contexts.pop(); -} + var twitter = container.lookup('api:twitter'); -function pushCtx(ctx) { - contexts.push(ctx); - return null; -} + twitter instanceof Twitter; // => true -function iter(key, value) { - var valueProvided = arguments.length === 2; + // by default the container will return singletons + var twitter2 = container.lookup('api:twitter'); + twitter instanceof Twitter; // => true - function i(item) { - var cur = get(item, key); - return valueProvided ? value===cur : !!cur; - } - return i ; -} + twitter === twitter2; //=> true + ``` -/** - This mixin defines the common interface implemented by enumerable objects - in Ember. Most of these methods follow the standard Array iteration - API defined up to JavaScript 1.8 (excluding language-specific features that - cannot be emulated in older versions of JavaScript). + If singletons are not wanted an optional flag can be provided at lookup. - This mixin is applied automatically to the Array class on page load, so you - can use any of these methods on simple arrays. If Array already implements - one of these methods, the mixin will not override them. + ```javascript + var container = new Container(); + container.register('api:twitter', Twitter); - ## Writing Your Own Enumerable + var twitter = container.lookup('api:twitter', { singleton: false }); + var twitter2 = container.lookup('api:twitter', { singleton: false }); - To make your own custom class enumerable, you need two items: + twitter === twitter2; //=> false + ``` - 1. You must have a length property. This property should change whenever - the number of items in your enumerable object changes. If you using this - with an `Ember.Object` subclass, you should be sure to change the length - property using `set().` + @method lookup + @param {String} fullName + @param {Object} options + @return {any} + */ + lookup: function(fullName, options) { + validateFullName(fullName); + return lookup(this, this.normalize(fullName), options); + }, - 2. If you must implement `nextObject().` See documentation. + /** + Given a fullName return the corresponding factory. - Once you have these two methods implement, apply the `Ember.Enumerable` mixin - to your class and you will be able to enumerate the contents of your object - like any other collection. + @method lookupFactory + @param {String} fullName + @return {any} + */ + lookupFactory: function(fullName) { + validateFullName(fullName); + return factoryFor(this, this.normalize(fullName)); + }, - ## Using Ember Enumeration with Other Libraries + /** + Given a fullName check if the container is aware of its factory + or singleton instance. - Many other libraries provide some kind of iterator or enumeration like - facility. This is often where the most common API conflicts occur. - Ember's API is designed to be as friendly as possible with other - libraries by implementing only methods that mostly correspond to the - JavaScript 1.8 API. + @method has + @param {String} fullName + @return {Boolean} + */ + has: function(fullName) { + validateFullName(fullName); + return has(this, this.normalize(fullName)); + }, - @class Enumerable - @namespace Ember - @since Ember 0.9 -*/ -Ember.Enumerable = Ember.Mixin.create({ + /** + Allow registering options for all factories of a type. - /** - Implement this method to make your class enumerable. + ```javascript + var container = new Container(); - This method will be call repeatedly during enumeration. The index value - will always begin with 0 and increment monotonically. You don't have to - rely on the index value to determine what object to return, but you should - always check the value and start from the beginning when you see the - requested index is 0. + // if all of type `connection` must not be singletons + container.optionsForType('connection', { singleton: false }); - The `previousObject` is the object that was returned from the last call - to `nextObject` for the current iteration. This is a useful way to - manage iteration if you are tracing a linked list, for example. + container.register('connection:twitter', TwitterConnection); + container.register('connection:facebook', FacebookConnection); - Finally the context parameter will always contain a hash you can use as - a "scratchpad" to maintain any other state you need in order to iterate - properly. The context object is reused and is not reset between - iterations so make sure you setup the context with a fresh state whenever - the index parameter is 0. + var twitter = container.lookup('connection:twitter'); + var twitter2 = container.lookup('connection:twitter'); - Generally iterators will continue to call `nextObject` until the index - reaches the your current length-1. If you run out of data before this - time for some reason, you should simply return undefined. + twitter === twitter2; // => false - The default implementation of this method simply looks up the index. - This works great on any Array-like objects. + var facebook = container.lookup('connection:facebook'); + var facebook2 = container.lookup('connection:facebook'); - @method nextObject - @param {Number} index the current index of the iteration - @param {Object} previousObject the value returned by the last call to - `nextObject`. - @param {Object} context a context object you can use to maintain state. - @return {Object} the next object in the iteration or undefined - */ - nextObject: Ember.required(Function), + facebook === facebook2; // => false + ``` - /** - Helper method returns the first object from a collection. This is usually - used by bindings and other parts of the framework to extract a single - object if the enumerable contains only one item. + @method optionsForType + @param {String} type + @param {Object} options + */ + optionsForType: function(type, options) { + if (this.parent) { illegalChildOperation('optionsForType'); } - If you override this method, you should implement it so that it will - always return the same value each time it is called. If your enumerable - contains only one object, this method should always return that object. - If your enumerable is empty, this method should return `undefined`. + this._typeOptions.set(type, options); + }, - ```javascript - var arr = ["a", "b", "c"]; - arr.get('firstObject'); // "a" + /** + @method options + @param {String} type + @param {Object} options + */ + options: function(type, options) { + this.optionsForType(type, options); + }, - var arr = []; - arr.get('firstObject'); // undefined - ``` + /** + Used only via `injection`. - @property firstObject - @return {Object} the object or undefined - */ - firstObject: Ember.computed(function() { - if (get(this, 'length')===0) return undefined ; + Provides a specialized form of injection, specifically enabling + all objects of one type to be injected with a reference to another + object. - // handle generic enumerables - var context = popCtx(), ret; - ret = this.nextObject(0, null, context); - pushCtx(context); - return ret ; - }).property('[]'), + For example, provided each object of type `controller` needed a `router`. + one would do the following: - /** - Helper method returns the last object from a collection. If your enumerable - contains only one object, this method should always return that object. - If your enumerable is empty, this method should return `undefined`. + ```javascript + var container = new Container(); - ```javascript - var arr = ["a", "b", "c"]; - arr.get('lastObject'); // "c" + container.register('router:main', Router); + container.register('controller:user', UserController); + container.register('controller:post', PostController); - var arr = []; - arr.get('lastObject'); // undefined - ``` + container.typeInjection('controller', 'router', 'router:main'); - @property lastObject - @return {Object} the last object or undefined - */ - lastObject: Ember.computed(function() { - var len = get(this, 'length'); - if (len===0) return undefined ; - var context = popCtx(), idx=0, cur, last = null; - do { - last = cur; - cur = this.nextObject(idx++, last, context); - } while (cur !== undefined); - pushCtx(context); - return last; - }).property('[]'), + var user = container.lookup('controller:user'); + var post = container.lookup('controller:post'); - /** - Returns `true` if the passed object can be found in the receiver. The - default version will iterate through the enumerable until the object - is found. You may want to override this with a more efficient version. + user.router instanceof Router; //=> true + post.router instanceof Router; //=> true - ```javascript - var arr = ["a", "b", "c"]; - arr.contains("a"); // true - arr.contains("z"); // false - ``` + // both controllers share the same router + user.router === post.router; //=> true + ``` - @method contains - @param {Object} obj The object to search for. - @return {Boolean} `true` if object is found in enumerable. - */ - contains: function(obj) { - return this.find(function(item) { return item===obj; }) !== undefined; - }, + @private + @method typeInjection + @param {String} type + @param {String} property + @param {String} fullName + */ + typeInjection: function(type, property, fullName) { + validateFullName(fullName); + if (this.parent) { illegalChildOperation('typeInjection'); } - /** - Iterates through the enumerable, calling the passed function on each - item. This method corresponds to the `forEach()` method defined in - JavaScript 1.6. + addTypeInjection(this.typeInjections, type, property, fullName); + }, - The callback method you provide should have the following signature (all - parameters are optional): + /** + Defines injection rules. - ```javascript - function(item, index, enumerable); - ``` + These rules are used to inject dependencies onto objects when they + are instantiated. - - `item` is the current item in the iteration. - - `index` is the current index in the iteration. - - `enumerable` is the enumerable object itself. + Two forms of injections are possible: - Note that in addition to a callback, you can also pass an optional target - object that will be set as `this` on the context. This is a good way - to give your iterator function access to the current object. + * Injecting one fullName on another fullName + * Injecting one fullName on a type - @method forEach - @param {Function} callback The callback to execute - @param {Object} [target] The target object to use - @return {Object} receiver - */ - forEach: function(callback, target) { - if (typeof callback !== "function") throw new TypeError() ; - var len = get(this, 'length'), last = null, context = popCtx(); + Example: - if (target === undefined) target = null; + ```javascript + var container = new Container(); - for(var idx=0;idx true + post.source instanceof Source; //=> true - /** - Maps all of the items in the enumeration to another value, returning - a new array. This method corresponds to `map()` defined in JavaScript 1.6. + user.post instanceof Post; //=> true - The callback method you provide should have the following signature (all - parameters are optional): + // and both models share the same source + user.source === post.source; //=> true + ``` - ```javascript - function(item, index, enumerable); - ``` + @method injection + @param {String} factoryName + @param {String} property + @param {String} injectionName + */ + injection: function(fullName, property, injectionName) { + if (this.parent) { illegalChildOperation('injection'); } - - `item` is the current item in the iteration. - - `index` is the current index in the iteration. - - `enumerable` is the enumerable object itself. + validateFullName(injectionName); + var normalizedInjectionName = this.normalize(injectionName); - It should return the mapped value. + if (fullName.indexOf(':') === -1) { + return this.typeInjection(fullName, property, normalizedInjectionName); + } - Note that in addition to a callback, you can also pass an optional target - object that will be set as `this` on the context. This is a good way - to give your iterator function access to the current object. + validateFullName(fullName); + var normalizedName = this.normalize(fullName); - @method map - @param {Function} callback The callback to execute - @param {Object} [target] The target object to use - @return {Array} The mapped array. - */ - map: function(callback, target) { - var ret = Ember.A(); - this.forEach(function(x, idx, i) { - ret[idx] = callback.call(target, x, idx,i); - }); - return ret ; - }, + addInjection(this.injections, normalizedName, property, normalizedInjectionName); + }, - /** - Similar to map, this specialized function returns the value of the named - property on all items in the enumeration. - @method mapBy - @param {String} key name of the property - @return {Array} The mapped array. - */ - mapBy: function(key) { - return this.map(function(next) { - return get(next, key); - }); - }, + /** + Used only via `factoryInjection`. - /** - Similar to map, this specialized function returns the value of the named - property on all items in the enumeration. + Provides a specialized form of injection, specifically enabling + all factory of one type to be injected with a reference to another + object. - @method mapProperty - @param {String} key name of the property - @return {Array} The mapped array. - @deprecated Use `mapBy` instead - */ + For example, provided each factory of type `model` needed a `store`. + one would do the following: - mapProperty: Ember.aliasMethod('mapBy'), + ```javascript + var container = new Container(); - /** - Returns an array with all of the items in the enumeration that the passed - function returns true for. This method corresponds to `filter()` defined in - JavaScript 1.6. + container.register('store:main', SomeStore); - The callback method you provide should have the following signature (all - parameters are optional): + container.factoryTypeInjection('model', 'store', 'store:main'); - ```javascript - function(item, index, enumerable); - ``` + var store = container.lookup('store:main'); + var UserFactory = container.lookupFactory('model:user'); - - `item` is the current item in the iteration. - - `index` is the current index in the iteration. - - `enumerable` is the enumerable object itself. + UserFactory.store instanceof SomeStore; //=> true + ``` - It should return the `true` to include the item in the results, `false` - otherwise. + @private + @method factoryTypeInjection + @param {String} type + @param {String} property + @param {String} fullName + */ + factoryTypeInjection: function(type, property, fullName) { + if (this.parent) { illegalChildOperation('factoryTypeInjection'); } - Note that in addition to a callback, you can also pass an optional target - object that will be set as `this` on the context. This is a good way - to give your iterator function access to the current object. + addTypeInjection(this.factoryTypeInjections, type, property, this.normalize(fullName)); + }, - @method filter - @param {Function} callback The callback to execute - @param {Object} [target] The target object to use - @return {Array} A filtered array. - */ - filter: function(callback, target) { - var ret = Ember.A(); - this.forEach(function(x, idx, i) { - if (callback.call(target, x, idx, i)) ret.push(x); - }); - return ret ; - }, + /** + Defines factory injection rules. - /** - Returns an array with all of the items in the enumeration where the passed - function returns false for. This method is the inverse of filter(). + Similar to regular injection rules, but are run against factories, via + `Container#lookupFactory`. - The callback method you provide should have the following signature (all - parameters are optional): + These rules are used to inject objects onto factories when they + are looked up. - function(item, index, enumerable); + Two forms of injections are possible: - - *item* is the current item in the iteration. - - *index* is the current index in the iteration - - *enumerable* is the enumerable object itself. + * Injecting one fullName on another fullName + * Injecting one fullName on a type - It should return the a falsey value to include the item in the results. + Example: - Note that in addition to a callback, you can also pass an optional target - object that will be set as "this" on the context. This is a good way - to give your iterator function access to the current object. + ```javascript + var container = new Container(); - @method reject - @param {Function} callback The callback to execute - @param {Object} [target] The target object to use - @return {Array} A rejected array. - */ - reject: function(callback, target) { - return this.filter(function() { - return !(callback.apply(target, arguments)); - }); - }, + container.register('store:main', Store); + container.register('store:secondary', OtherStore); + container.register('model:user', User); + container.register('model:post', Post); - /** - Returns an array with just the items with the matched property. You - can pass an optional second argument with the target value. Otherwise - this will match any property that evaluates to `true`. + // injecting one fullName on another type + container.factoryInjection('model', 'store', 'store:main'); - @method filterBy - @param {String} key the property to test - @param {String} [value] optional value to test against. - @return {Array} filtered array - */ - filterBy: function(key, value) { - return this.filter(iter.apply(this, arguments)); - }, + // injecting one fullName on another fullName + container.factoryInjection('model:post', 'secondaryStore', 'store:secondary'); - /** - Returns an array with just the items with the matched property. You - can pass an optional second argument with the target value. Otherwise - this will match any property that evaluates to `true`. + var UserFactory = container.lookupFactory('model:user'); + var PostFactory = container.lookupFactory('model:post'); + var store = container.lookup('store:main'); - @method filterProperty - @param {String} key the property to test - @param {String} [value] optional value to test against. - @return {Array} filtered array - @deprecated Use `filterBy` instead - */ - filterProperty: Ember.aliasMethod('filterBy'), + UserFactory.store instanceof Store; //=> true + UserFactory.secondaryStore instanceof OtherStore; //=> false - /** - Returns an array with the items that do not have truthy values for - key. You can pass an optional second argument with the target value. Otherwise - this will match any property that evaluates to false. + PostFactory.store instanceof Store; //=> true + PostFactory.secondaryStore instanceof OtherStore; //=> true - @method rejectBy - @param {String} key the property to test - @param {String} [value] optional value to test against. - @return {Array} rejected array - */ - rejectBy: function(key, value) { - var exactValue = function(item) { return get(item, key) === value; }, - hasValue = function(item) { return !!get(item, key); }, - use = (arguments.length === 2 ? exactValue : hasValue); + // and both models share the same source instance + UserFactory.store === PostFactory.store; //=> true + ``` - return this.reject(use); - }, + @method factoryInjection + @param {String} factoryName + @param {String} property + @param {String} injectionName + */ + factoryInjection: function(fullName, property, injectionName) { + if (this.parent) { illegalChildOperation('injection'); } - /** - Returns an array with the items that do not have truthy values for - key. You can pass an optional second argument with the target value. Otherwise - this will match any property that evaluates to false. + var normalizedName = this.normalize(fullName); + var normalizedInjectionName = this.normalize(injectionName); - @method rejectProperty - @param {String} key the property to test - @param {String} [value] optional value to test against. - @return {Array} rejected array - @deprecated Use `rejectBy` instead - */ - rejectProperty: Ember.aliasMethod('rejectBy'), + validateFullName(injectionName); - /** - Returns the first item in the array for which the callback returns true. - This method works similar to the `filter()` method defined in JavaScript 1.6 - except that it will stop working on the array once a match is found. + if (fullName.indexOf(':') === -1) { + return this.factoryTypeInjection(normalizedName, property, normalizedInjectionName); + } - The callback method you provide should have the following signature (all - parameters are optional): + validateFullName(fullName); - ```javascript - function(item, index, enumerable); - ``` + addInjection(this.factoryInjections, normalizedName, property, normalizedInjectionName); + }, - - `item` is the current item in the iteration. - - `index` is the current index in the iteration. - - `enumerable` is the enumerable object itself. + /** + A depth first traversal, destroying the container, its descendant containers and all + their managed objects. - It should return the `true` to include the item in the results, `false` - otherwise. + @method destroy + */ + destroy: function() { + for (var i=0, l=this.children.length; i1) args = a_slice.call(arguments, 1); + parent: null, - this.forEach(function(x, idx) { - var method = x && x[methodName]; - if ('function' === typeof method) { - ret[idx] = args ? method.apply(x, args) : method.call(x); - } - }, this); + /** + Object used to store the current nodes data. - return ret; - }, + @property dict + @type Object + @default Object + */ + dict: null, - /** - Simply converts the enumerable into a genuine array. The order is not - guaranteed. Corresponds to the method implemented by Prototype. + /** + Retrieve the value given a key, if the value is present at the current + level use it, otherwise walk up the parent hierarchy and try again. If + no matching key is found, return undefined. - @method toArray - @return {Array} the enumerable as an array. - */ - toArray: function() { - var ret = Ember.A(); - this.forEach(function(o, idx) { ret[idx] = o; }); - return ret ; - }, + @method get + @param {String} key + @return {any} + */ + get: function(key) { + var dict = this.dict; - /** - Returns a copy of the array with all null and undefined elements removed. + if (dict.hasOwnProperty(key)) { + return dict[key]; + } - ```javascript - var arr = ["a", null, "c", undefined]; - arr.compact(); // ["a", "c"] - ``` + if (this.parent) { + return this.parent.get(key); + } + }, - @method compact - @return {Array} the array without null and undefined elements. - */ - compact: function() { - return this.filter(function(value) { return value != null; }); - }, + /** + Set the given value for the given key, at the current level. - /** - Returns a new enumerable that excludes the passed value. The default - implementation returns an array regardless of the receiver type unless - the receiver does not contain the value. + @method set + @param {String} key + @param {Any} value + */ + set: function(key, value) { + this.dict[key] = value; + }, - ```javascript - var arr = ["a", "b", "a", "c"]; - arr.without("a"); // ["b", "c"] - ``` + /** + Delete the given key - @method without - @param {Object} value - @return {Ember.Enumerable} - */ - without: function(value) { - if (!this.contains(value)) return this; // nothing to do - var ret = Ember.A(); - this.forEach(function(k) { - if (k !== value) ret[ret.length] = k; - }) ; - return ret ; - }, + @method remove + @param {String} key + */ + remove: function(key) { + delete this.dict[key]; + }, - /** - Returns a new enumerable that contains only unique values. The default - implementation returns an array regardless of the receiver type. + /** + Check for the existence of given a key, if the key is present at the current + level return true, otherwise walk up the parent hierarchy and try again. If + no matching key is found, return false. - ```javascript - var arr = ["a", "a", "b", "b"]; - arr.uniq(); // ["a", "b"] - ``` + @method has + @param {String} key + @return {Boolean} + */ + has: function(key) { + var dict = this.dict; - @method uniq - @return {Ember.Enumerable} - */ - uniq: function() { - var ret = Ember.A(); - this.forEach(function(k) { - if (a_indexOf(ret, k)<0) ret.push(k); - }); - return ret; - }, + if (dict.hasOwnProperty(key)) { + return true; + } - /** - This property will trigger anytime the enumerable's content changes. - You can observe this property to be notified of changes to the enumerables - content. + if (this.parent) { + return this.parent.has(key); + } - For plain enumerables, this property is read only. `Ember.Array` overrides - this method. + return false; + }, - @property [] - @type Ember.Array - @return this - */ - '[]': Ember.computed(function(key, value) { - return this; - }), + /** + Iterate and invoke a callback for each local key-value pair. - // .......................................................... - // ENUMERABLE OBSERVERS - // + @method eachLocal + @param {Function} callback + @param {Object} binding + */ + eachLocal: function(callback, binding) { + var dict = this.dict; - /** - Registers an enumerable observer. Must implement `Ember.EnumerableObserver` - mixin. + for (var prop in dict) { + if (dict.hasOwnProperty(prop)) { + callback.call(binding, prop, dict[prop]); + } + } + } + }; - @method addEnumerableObserver - @param {Object} target - @param {Hash} [opts] - @return this - */ - addEnumerableObserver: function(target, opts) { - var willChange = (opts && opts.willChange) || 'enumerableWillChange', - didChange = (opts && opts.didChange) || 'enumerableDidChange'; + __exports__["default"] = InheritingDict; + }); +define("container", + ["container/container","exports"], + function(__dependency1__, __exports__) { + "use strict"; + /** + Public api for the container is still in flux. + The public api, specified on the application namespace should be considered the stable api. + // @module container + @private + */ - var hasObservers = get(this, 'hasEnumerableObservers'); - if (!hasObservers) Ember.propertyWillChange(this, 'hasEnumerableObservers'); - Ember.addListener(this, '@enumerable:before', target, willChange); - Ember.addListener(this, '@enumerable:change', target, didChange); - if (!hasObservers) Ember.propertyDidChange(this, 'hasEnumerableObservers'); - return this; - }, + /* + Flag to enable/disable model factory injections (disabled by default) + If model factory injections are enabled, models should not be + accessed globally (only through `container.lookupFactory('model:modelName'))`); + */ + Ember.MODEL_FACTORY_INJECTIONS = false || !!Ember.ENV.MODEL_FACTORY_INJECTIONS; - /** - Removes a registered enumerable observer. + var Container = __dependency1__["default"]; - @method removeEnumerableObserver - @param {Object} target - @param {Hash} [opts] - @return this - */ - removeEnumerableObserver: function(target, opts) { - var willChange = (opts && opts.willChange) || 'enumerableWillChange', - didChange = (opts && opts.didChange) || 'enumerableDidChange'; + __exports__["default"] = Container; + }); +})(); - var hasObservers = get(this, 'hasEnumerableObservers'); - if (hasObservers) Ember.propertyWillChange(this, 'hasEnumerableObservers'); - Ember.removeListener(this, '@enumerable:before', target, willChange); - Ember.removeListener(this, '@enumerable:change', target, didChange); - if (hasObservers) Ember.propertyDidChange(this, 'hasEnumerableObservers'); - return this; - }, +(function() { +/*globals ENV */ +/** +@module ember +@submodule ember-runtime +*/ - /** - Becomes true whenever the array currently has observers watching changes - on the array. +var indexOf = Ember.EnumerableUtils.indexOf; - @property hasEnumerableObservers - @type Boolean - */ - hasEnumerableObservers: Ember.computed(function() { - return Ember.hasListeners(this, '@enumerable:change') || Ember.hasListeners(this, '@enumerable:before'); - }), +/** + This will compare two javascript values of possibly different types. + It will tell you which one is greater than the other by returning: + - -1 if the first is smaller than the second, + - 0 if both are equal, + - 1 if the first is greater than the second. - /** - Invoke this method just before the contents of your enumerable will - change. You can either omit the parameters completely or pass the objects - to be removed or added if available or just a count. + The order is calculated based on `Ember.ORDER_DEFINITION`, if types are different. + In case they have the same type an appropriate comparison for this type is made. - @method enumerableContentWillChange - @param {Ember.Enumerable|Number} removing An enumerable of the objects to - be removed or the number of items to be removed. - @param {Ember.Enumerable|Number} adding An enumerable of the objects to be - added or the number of items to be added. - @chainable - */ - enumerableContentWillChange: function(removing, adding) { + ```javascript + Ember.compare('hello', 'hello'); // 0 + Ember.compare('abc', 'dfg'); // -1 + Ember.compare(2, 1); // 1 + ``` - var removeCnt, addCnt, hasDelta; + @method compare + @for Ember + @param {Object} v First value to compare + @param {Object} w Second value to compare + @return {Number} -1 if v < w, 0 if v = w and 1 if v > w. +*/ +Ember.compare = function compare(v, w) { + if (v === w) { return 0; } - if ('number' === typeof removing) removeCnt = removing; - else if (removing) removeCnt = get(removing, 'length'); - else removeCnt = removing = -1; + var type1 = Ember.typeOf(v); + var type2 = Ember.typeOf(w); - if ('number' === typeof adding) addCnt = adding; - else if (adding) addCnt = get(adding,'length'); - else addCnt = adding = -1; + var Comparable = Ember.Comparable; + if (Comparable) { + if (type1==='instance' && Comparable.detect(v.constructor)) { + return v.constructor.compare(v, w); + } - hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; + if (type2 === 'instance' && Comparable.detect(w.constructor)) { + return 1-w.constructor.compare(w, v); + } + } - if (removing === -1) removing = null; - if (adding === -1) adding = null; + // If we haven't yet generated a reverse-mapping of Ember.ORDER_DEFINITION, + // do so now. + var mapping = Ember.ORDER_DEFINITION_MAPPING; + if (!mapping) { + var order = Ember.ORDER_DEFINITION; + mapping = Ember.ORDER_DEFINITION_MAPPING = {}; + var idx, len; + for (idx = 0, len = order.length; idx < len; ++idx) { + mapping[order[idx]] = idx; + } - Ember.propertyWillChange(this, '[]'); - if (hasDelta) Ember.propertyWillChange(this, 'length'); - Ember.sendEvent(this, '@enumerable:before', [this, removing, adding]); + // We no longer need Ember.ORDER_DEFINITION. + delete Ember.ORDER_DEFINITION; + } - return this; - }, + var type1Index = mapping[type1]; + var type2Index = mapping[type2]; - /** - Invoke this method when the contents of your enumerable has changed. - This will notify any observers watching for content changes. If your are - implementing an ordered enumerable (such as an array), also pass the - start and end values where the content changed so that it can be used to - notify range observers. + if (type1Index < type2Index) { return -1; } + if (type1Index > type2Index) { return 1; } - @method enumerableContentDidChange - @param {Number} [start] optional start offset for the content change. - For unordered enumerables, you should always pass -1. - @param {Ember.Enumerable|Number} removing An enumerable of the objects to - be removed or the number of items to be removed. - @param {Ember.Enumerable|Number} adding An enumerable of the objects to - be added or the number of items to be added. - @chainable - */ - enumerableContentDidChange: function(removing, adding) { - var removeCnt, addCnt, hasDelta; + // types are equal - so we have to check values now + switch (type1) { + case 'boolean': + case 'number': + if (v < w) { return -1; } + if (v > w) { return 1; } + return 0; - if ('number' === typeof removing) removeCnt = removing; - else if (removing) removeCnt = get(removing, 'length'); - else removeCnt = removing = -1; + case 'string': + var comp = v.localeCompare(w); + if (comp < 0) { return -1; } + if (comp > 0) { return 1; } + return 0; - if ('number' === typeof adding) addCnt = adding; - else if (adding) addCnt = get(adding, 'length'); - else addCnt = adding = -1; + case 'array': + var vLen = v.length; + var wLen = w.length; + var l = Math.min(vLen, wLen); + var r = 0; + var i = 0; + while (r === 0 && i < l) { + r = compare(v[i],w[i]); + i++; + } + if (r !== 0) { return r; } - hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; + // all elements are equal now + // shorter array should be ordered first + if (vLen < wLen) { return -1; } + if (vLen > wLen) { return 1; } + // arrays are equal now + return 0; - if (removing === -1) removing = null; - if (adding === -1) adding = null; + case 'instance': + if (Ember.Comparable && Ember.Comparable.detect(v)) { + return v.compare(v, w); + } + return 0; - Ember.sendEvent(this, '@enumerable:change', [this, removing, adding]); - if (hasDelta) Ember.propertyDidChange(this, 'length'); - Ember.propertyDidChange(this, '[]'); + case 'date': + var vNum = v.getTime(); + var wNum = w.getTime(); + if (vNum < wNum) { return -1; } + if (vNum > wNum) { return 1; } + return 0; - return this ; + default: + return 0; } +}; -}) ; +function _copy(obj, deep, seen, copies) { + var ret, loc, key; -})(); + // primitive data types are immutable, just return them. + if ('object' !== typeof obj || obj===null) return obj; + // avoid cyclical loops + if (deep && (loc=indexOf(seen, obj))>=0) return copies[loc]; + Ember.assert('Cannot clone an Ember.Object that does not implement Ember.Copyable', !(obj instanceof Ember.Object) || (Ember.Copyable && Ember.Copyable.detect(obj))); + + // IMPORTANT: this specific test will detect a native array only. Any other + // object will need to implement Copyable. + if (Ember.typeOf(obj) === 'array') { + ret = obj.slice(); + if (deep) { + loc = ret.length; + while(--loc>=0) ret[loc] = _copy(ret[loc], deep, seen, copies); + } + } else if (Ember.Copyable && Ember.Copyable.detect(obj)) { + ret = obj.copy(deep, seen, copies); + } else if (obj instanceof Date) { + ret = new Date(obj.getTime()); + } else { + ret = {}; + for(key in obj) { + if (!obj.hasOwnProperty(key)) continue; + + // Prevents browsers that don't respect non-enumerability from + // copying internal Ember properties + if (key.substring(0,2) === '__') continue; + + ret[key] = deep ? _copy(obj[key], deep, seen, copies) : obj[key]; + } + } + + if (deep) { + seen.push(obj); + copies.push(ret); + } + + return ret; +} -(function() { /** -@module ember -@submodule ember-runtime + Creates a clone of the passed object. This function can take just about + any type of object and create a clone of it, including primitive values + (which are not actually cloned because they are immutable). + + If the passed object implements the `clone()` method, then this function + will simply call that method and return the result. + + @method copy + @for Ember + @param {Object} obj The object to clone + @param {Boolean} deep If true, a deep copy of the object is made + @return {Object} The cloned object */ +Ember.copy = function(obj, deep) { + // fast paths + if ('object' !== typeof obj || obj===null) return obj; // can't copy primitives + if (Ember.Copyable && Ember.Copyable.detect(obj)) return obj.copy(deep); + return _copy(obj, deep, deep ? [] : null, deep ? [] : null); +}; -// .......................................................... -// HELPERS -// +/** + Compares two objects, returning true if they are logically equal. This is + a deeper comparison than a simple triple equal. For sets it will compare the + internal objects. For any other object that implements `isEqual()` it will + respect that method. -var get = Ember.get, set = Ember.set, isNone = Ember.isNone, map = Ember.EnumerableUtils.map, cacheFor = Ember.cacheFor; + ```javascript + Ember.isEqual('hello', 'hello'); // true + Ember.isEqual(1, 2); // false + Ember.isEqual([4,2], [4,2]); // false + ``` + + @method isEqual + @for Ember + @param {Object} a first object to compare + @param {Object} b second object to compare + @return {Boolean} +*/ +Ember.isEqual = function(a, b) { + if (a && 'function'===typeof a.isEqual) return a.isEqual(b); + return a === b; +}; + +// Used by Ember.compare +Ember.ORDER_DEFINITION = Ember.ENV.ORDER_DEFINITION || [ + 'undefined', + 'null', + 'boolean', + 'number', + 'string', + 'array', + 'object', + 'instance', + 'function', + 'class', + 'date' +]; -// .......................................................... -// ARRAY -// /** - This module implements Observer-friendly Array-like behavior. This mixin is - picked up by the Array class as well as other controllers, etc. that want to - appear to be arrays. + Returns all of the keys defined on an object or hash. This is useful + when inspecting objects for debugging. On browsers that support it, this + uses the native `Object.keys` implementation. - Unlike `Ember.Enumerable,` this mixin defines methods specifically for - collections that provide index-ordered access to their contents. When you - are designing code that needs to accept any kind of Array-like object, you - should use these methods instead of Array primitives because these will - properly notify observers of changes to the array. + @method keys + @for Ember + @param {Object} obj + @return {Array} Array containing keys of obj +*/ +Ember.keys = Object.keys; - Although these methods are efficient, they do add a layer of indirection to - your application so it is a good idea to use them only when you need the - flexibility of using both true JavaScript arrays and "virtual" arrays such - as controllers and collections. +if (!Ember.keys || Ember.create.isSimulated) { + var prototypeProperties = [ + 'constructor', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'valueOf', + 'toLocaleString', + 'toString' + ], + pushPropertyName = function(obj, array, key) { + // Prevents browsers that don't respect non-enumerability from + // copying internal Ember properties + if (key.substring(0,2) === '__') return; + if (key === '_super') return; + if (indexOf(array, key) >= 0) return; + if (!obj.hasOwnProperty(key)) return; - You can use the methods defined in this module to access and modify array - contents in a KVO-friendly way. You can also be notified whenever the - membership of an array changes by changing the syntax of the property to - `.observes('*myProperty.[]')`. + array.push(key); + }; - To support `Ember.Array` in your own class, you must override two - primitives to use it: `replace()` and `objectAt()`. + Ember.keys = function(obj) { + var ret = [], key; + for (key in obj) { + pushPropertyName(obj, ret, key); + } - Note that the Ember.Array mixin also incorporates the `Ember.Enumerable` - mixin. All `Ember.Array`-like objects are also enumerable. + // IE8 doesn't enumerate property that named the same as prototype properties. + for (var i = 0, l = prototypeProperties.length; i < l; i++) { + key = prototypeProperties[i]; - @class Array - @namespace Ember - @uses Ember.Enumerable - @since Ember 0.9.0 + pushPropertyName(obj, ret, key); + } + + return ret; + }; +} + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime */ -Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.prototype */ { - /** - Your array must support the `length` property. Your replace methods should - set this property whenever it changes. +var STRING_DASHERIZE_REGEXP = (/[ _]/g); +var STRING_DASHERIZE_CACHE = {}; +var STRING_DECAMELIZE_REGEXP = (/([a-z\d])([A-Z])/g); +var STRING_CAMELIZE_REGEXP = (/(\-|_|\.|\s)+(.)?/g); +var STRING_UNDERSCORE_REGEXP_1 = (/([a-z\d])([A-Z]+)/g); +var STRING_UNDERSCORE_REGEXP_2 = (/\-|\s+/g); +var STRING_PARAMETERIZE_REGEXP_1 = (/[_|\/|\s]+/g); +var STRING_PARAMETERIZE_REGEXP_2 = (/[^a-z0-9\-]+/gi); +var STRING_PARAMETERIZE_REGEXP_3 = (/[\-]+/g); +var STRING_PARAMETERIZE_REGEXP_4 = (/^-+|-+$/g); - @property {Number} length - */ - length: Ember.required(), +/** + Defines the hash of localized strings for the current language. Used by + the `Ember.String.loc()` helper. To localize, add string values to this + hash. + + @property STRINGS + @for Ember + @type Hash +*/ +Ember.STRINGS = {}; + +/** + Defines string helper methods including string formatting and localization. + Unless `Ember.EXTEND_PROTOTYPES.String` is `false` these methods will also be + added to the `String.prototype` as well. + + @class String + @namespace Ember + @static +*/ +Ember.String = { /** - Returns the object at the given `index`. If the given `index` is negative - or is greater or equal than the array length, returns `undefined`. + Apply formatting options to the string. This will look for occurrences + of "%@" in your string and substitute them with the arguments you pass into + this method. If you want to control the specific order of replacement, + you can add a number after the key as well to indicate which argument + you want to insert. - This is one of the primitives you must implement to support `Ember.Array`. - If your object supports retrieving the value of an array item using `get()` - (i.e. `myArray.get(0)`), then you do not need to implement this method - yourself. + Ordered insertions are most useful when building loc strings where values + you need to insert may appear in different orders. ```javascript - var arr = ['a', 'b', 'c', 'd']; - arr.objectAt(0); // "a" - arr.objectAt(3); // "d" - arr.objectAt(-1); // undefined - arr.objectAt(4); // undefined - arr.objectAt(5); // undefined + "Hello %@ %@".fmt('John', 'Doe'); // "Hello John Doe" + "Hello %@2, %@1".fmt('John', 'Doe'); // "Hello Doe, John" ``` - @method objectAt - @param {Number} idx The index of the item to return. - @return {*} item at index or undefined + @method fmt + @param {String} str The string to format + @param {Array} formats An array of parameters to interpolate into string. + @return {String} formatted string */ - objectAt: function(idx) { - if ((idx < 0) || (idx>=get(this, 'length'))) return undefined ; - return get(this, idx); + fmt: function(str, formats) { + // first, replace any ORDERED replacements. + var idx = 0; // the current index for non-numerical replacements + return str.replace(/%@([0-9]+)?/g, function(s, argIndex) { + argIndex = (argIndex) ? parseInt(argIndex, 10) - 1 : idx++; + s = formats[argIndex]; + return (s === null) ? '(null)' : (s === undefined) ? '' : Ember.inspect(s); + }) ; }, /** - This returns the objects at the specified indexes, using `objectAt`. + Formats the passed string, but first looks up the string in the localized + strings hash. This is a convenient way to localize text. See + `Ember.String.fmt()` for more information on formatting. + + Note that it is traditional but not required to prefix localized string + keys with an underscore or other character so you can easily identify + localized strings. ```javascript - var arr = ['a', 'b', 'c', 'd']; - arr.objectsAt([0, 1, 2]); // ["a", "b", "c"] - arr.objectsAt([2, 3, 4]); // ["c", "d", undefined] - ``` + Ember.STRINGS = { + '_Hello World': 'Bonjour le monde', + '_Hello %@ %@': 'Bonjour %@ %@' + }; - @method objectsAt - @param {Array} indexes An array of indexes of items to return. - @return {Array} - */ - objectsAt: function(indexes) { - var self = this; - return map(indexes, function(idx) { return self.objectAt(idx); }); - }, + Ember.String.loc("_Hello World"); // 'Bonjour le monde'; + Ember.String.loc("_Hello %@ %@", ["John", "Smith"]); // "Bonjour John Smith"; + ``` - // overrides Ember.Enumerable version - nextObject: function(idx) { - return this.objectAt(idx); + @method loc + @param {String} str The string to format + @param {Array} formats Optional array of parameters to interpolate into string. + @return {String} formatted string + */ + loc: function(str, formats) { + str = Ember.STRINGS[str] || str; + return Ember.String.fmt(str, formats) ; }, /** - This is the handler for the special array content property. If you get - this property, it will return this. If you set this property it a new - array, it will replace the current content. + Splits a string into separate units separated by spaces, eliminating any + empty strings in the process. This is a convenience method for split that + is mostly useful when applied to the `String.prototype`. - This property overrides the default property defined in `Ember.Enumerable`. + ```javascript + Ember.String.w("alpha beta gamma").forEach(function(key) { + console.log(key); + }); - @property [] - @return this + // > alpha + // > beta + // > gamma + ``` + + @method w + @param {String} str The string to split + @return {String} split string */ - '[]': Ember.computed(function(key, value) { - if (value !== undefined) this.replace(0, get(this, 'length'), value) ; - return this ; - }), + w: function(str) { return str.split(/\s+/); }, - firstObject: Ember.computed(function() { - return this.objectAt(0); - }), + /** + Converts a camelized string into all lower case separated by underscores. - lastObject: Ember.computed(function() { - return this.objectAt(get(this, 'length')-1); - }), + ```javascript + 'innerHTML'.decamelize(); // 'inner_html' + 'action_name'.decamelize(); // 'action_name' + 'css-class-name'.decamelize(); // 'css-class-name' + 'my favorite items'.decamelize(); // 'my favorite items' + ``` - // optimized version from Enumerable - contains: function(obj) { - return this.indexOf(obj) >= 0; + @method decamelize + @param {String} str The string to decamelize. + @return {String} the decamelized string. + */ + decamelize: function(str) { + return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); }, - // Add any extra methods to Ember.Array that are native to the built-in Array. /** - Returns a new array that is a slice of the receiver. This implementation - uses the observable array methods to retrieve the objects for the new - slice. + Replaces underscores, spaces, or camelCase with dashes. ```javascript - var arr = ['red', 'green', 'blue']; - arr.slice(0); // ['red', 'green', 'blue'] - arr.slice(0, 2); // ['red', 'green'] - arr.slice(1, 100); // ['green', 'blue'] + 'innerHTML'.dasherize(); // 'inner-html' + 'action_name'.dasherize(); // 'action-name' + 'css-class-name'.dasherize(); // 'css-class-name' + 'my favorite items'.dasherize(); // 'my-favorite-items' ``` - @method slice - @param {Integer} beginIndex (Optional) index to begin slicing from. - @param {Integer} endIndex (Optional) index to end the slice at. - @return {Array} New array with specified slice + @method dasherize + @param {String} str The string to dasherize. + @return {String} the dasherized string. */ - slice: function(beginIndex, endIndex) { - var ret = Ember.A(); - var length = get(this, 'length') ; - if (isNone(beginIndex)) beginIndex = 0 ; - if (isNone(endIndex) || (endIndex > length)) endIndex = length ; - - if (beginIndex < 0) beginIndex = length + beginIndex; - if (endIndex < 0) endIndex = length + endIndex; + dasherize: function(str) { + var cache = STRING_DASHERIZE_CACHE, + hit = cache.hasOwnProperty(str), + ret; - while(beginIndex < endIndex) { - ret[ret.length] = this.objectAt(beginIndex++) ; + if (hit) { + return cache[str]; + } else { + ret = Ember.String.decamelize(str).replace(STRING_DASHERIZE_REGEXP,'-'); + cache[str] = ret; } - return ret ; + + return ret; }, /** - Returns the index of the given object's first occurrence. - If no `startAt` argument is given, the starting location to - search is 0. If it's negative, will count backward from - the end of the array. Returns -1 if no match is found. + Returns the lowerCamelCase form of a string. ```javascript - var arr = ["a", "b", "c", "d", "a"]; - arr.indexOf("a"); // 0 - arr.indexOf("z"); // -1 - arr.indexOf("a", 2); // 4 - arr.indexOf("a", -1); // 4 - arr.indexOf("b", 3); // -1 - arr.indexOf("a", 100); // -1 + 'innerHTML'.camelize(); // 'innerHTML' + 'action_name'.camelize(); // 'actionName' + 'css-class-name'.camelize(); // 'cssClassName' + 'my favorite items'.camelize(); // 'myFavoriteItems' + 'My Favorite Items'.camelize(); // 'myFavoriteItems' ``` - @method indexOf - @param {Object} object the item to search for - @param {Number} startAt optional starting location to search, default 0 - @return {Number} index or -1 if not found + @method camelize + @param {String} str The string to camelize. + @return {String} the camelized string. */ - indexOf: function(object, startAt) { - var idx, len = get(this, 'length'); - - if (startAt === undefined) startAt = 0; - if (startAt < 0) startAt += len; - - for(idx=startAt;idx= len) startAt = len-1; - if (startAt < 0) startAt += len; + classify: function(str) { + var parts = str.split("."), + out = []; - for(idx=startAt;idx>=0;idx--) { - if (this.objectAt(idx) === object) return idx ; + for (var i=0, l=parts.length; i=0 && removeAmt>=0 && get(this, 'hasEnumerableObservers')) { - removing = []; - lim = startIdx+removeAmt; - for(var idx=startIdx;idx=0 && addAmt>=0 && get(this, 'hasEnumerableObservers')) { - adding = []; - lim = startIdx+addAmt; - for(var idx=startIdx;idx Ember.TrackedArray instances. We use - // this to lazily recompute indexes for item property observers. - this.trackedArraysByGuid = {}; + ## Using `get()` and `set()` - // This is used to coalesce item changes from property observers. - this.changedItems = {}; -} + Because of Ember's support for bindings and observers, you will always + access properties using the get method, and set properties using the + set method. This allows the observing objects to be notified and + computed properties to be handled properly. -function ItemPropertyObserverContext (dependentArray, index, trackedArray) { - Ember.assert("Internal error: trackedArray is null or undefined", trackedArray); + More documentation about `get` and `set` are below. - this.dependentArray = dependentArray; - this.index = index; - this.item = dependentArray.objectAt(index); - this.trackedArray = trackedArray; - this.beforeObserver = null; - this.observer = null; + ## Observing Property Changes - this.destroyed = false; -} + You typically observe property changes simply by adding the `observes` + call to the end of your method declarations in classes that you write. + For example: -DependentArraysObserver.prototype = { - setValue: function (newValue) { - this.instanceMeta.setValue(newValue, true); - }, - getValue: function () { - return this.instanceMeta.getValue(); - }, - - setupObservers: function (dependentArray, dependentKey) { - Ember.assert("dependent array must be an `Ember.Array`", Ember.Array.detect(dependentArray)); + ```javascript + Ember.Object.extend({ + valueObserver: function() { + // Executes whenever the "value" property changes + }.observes('value') + }); + ``` - this.dependentKeysByGuid[guidFor(dependentArray)] = dependentKey; + Although this is the most common way to add an observer, this capability + is actually built into the `Ember.Object` class on top of two methods + defined in this mixin: `addObserver` and `removeObserver`. You can use + these two methods to add and remove observers yourself if you need to + do so at runtime. - dependentArray.addArrayObserver(this, { - willChange: 'dependentArrayWillChange', - didChange: 'dependentArrayDidChange' - }); + To add an observer for a property, call: - if (this.cp._itemPropertyKeys[dependentKey]) { - this.setupPropertyObservers(dependentKey, this.cp._itemPropertyKeys[dependentKey]); - } - }, + ```javascript + object.addObserver('propertyKey', targetObject, targetAction) + ``` - teardownObservers: function (dependentArray, dependentKey) { - var itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || []; + This will call the `targetAction` method on the `targetObject` whenever + the value of the `propertyKey` changes. - delete this.dependentKeysByGuid[guidFor(dependentArray)]; + Note that if `propertyKey` is a computed property, the observer will be + called when any of the property dependencies are changed, even if the + resulting value of the computed property is unchanged. This is necessary + because computed properties are not computed until `get` is called. - this.teardownPropertyObservers(dependentKey, itemPropertyKeys); + @class Observable + @namespace Ember +*/ +Ember.Observable = Ember.Mixin.create({ - dependentArray.removeArrayObserver(this, { - willChange: 'dependentArrayWillChange', - didChange: 'dependentArrayDidChange' - }); - }, + /** + Retrieves the value of a property from the object. - setupPropertyObservers: function (dependentKey, itemPropertyKeys) { - var dependentArray = get(this.instanceMeta.context, dependentKey), - length = get(dependentArray, 'length'), - observerContexts = new Array(length); + This method is usually similar to using `object[keyName]` or `object.keyName`, + however it supports both computed properties and the unknownProperty + handler. - this.resetTransformations(dependentKey, observerContexts); + Because `get` unifies the syntax for accessing all these kinds + of properties, it can make many refactorings easier, such as replacing a + simple property with a computed property, or vice versa. - forEach(dependentArray, function (item, index) { - var observerContext = this.createPropertyObserverContext(dependentArray, index, this.trackedArraysByGuid[dependentKey]); - observerContexts[index] = observerContext; + ### Computed Properties - forEach(itemPropertyKeys, function (propertyKey) { - addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); - addObserver(item, propertyKey, this, observerContext.observer); - }, this); - }, this); - }, + Computed properties are methods defined with the `property` modifier + declared at the end, such as: - teardownPropertyObservers: function (dependentKey, itemPropertyKeys) { - var dependentArrayObserver = this, - trackedArray = this.trackedArraysByGuid[dependentKey], - beforeObserver, - observer, - item; + ```javascript + fullName: function() { + return this.get('firstName') + ' ' + this.get('lastName'); + }.property('firstName', 'lastName') + ``` - if (!trackedArray) { return; } + When you call `get` on a computed property, the function will be + called and the return value will be returned instead of the function + itself. - trackedArray.apply(function (observerContexts, offset, operation) { - if (operation === Ember.TrackedArray.DELETE) { return; } + ### Unknown Properties - forEach(observerContexts, function (observerContext) { - observerContext.destroyed = true; - beforeObserver = observerContext.beforeObserver; - observer = observerContext.observer; - item = observerContext.item; + Likewise, if you try to call `get` on a property whose value is + `undefined`, the `unknownProperty()` method will be called on the object. + If this method returns any value other than `undefined`, it will be returned + instead. This allows you to implement "virtual" properties that are + not defined upfront. - forEach(itemPropertyKeys, function (propertyKey) { - removeBeforeObserver(item, propertyKey, dependentArrayObserver, beforeObserver); - removeObserver(item, propertyKey, dependentArrayObserver, observer); - }); - }); - }); + @method get + @param {String} keyName The property to retrieve + @return {Object} The property value or undefined. + */ + get: function(keyName) { + return get(this, keyName); }, - createPropertyObserverContext: function (dependentArray, index, trackedArray) { - var observerContext = new ItemPropertyObserverContext(dependentArray, index, trackedArray); - - this.createPropertyObserver(observerContext); + /** + To get multiple properties at once, call `getProperties` + with a list of strings or an array: - return observerContext; - }, + ```javascript + record.getProperties('firstName', 'lastName', 'zipCode'); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } + ``` - createPropertyObserver: function (observerContext) { - var dependentArrayObserver = this; + is equivalent to: - observerContext.beforeObserver = function (obj, keyName) { - return dependentArrayObserver.itemPropertyWillChange(obj, keyName, observerContext.dependentArray, observerContext); - }; - observerContext.observer = function (obj, keyName) { - return dependentArrayObserver.itemPropertyDidChange(obj, keyName, observerContext.dependentArray, observerContext); - }; - }, + ```javascript + record.getProperties(['firstName', 'lastName', 'zipCode']); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } + ``` - resetTransformations: function (dependentKey, observerContexts) { - this.trackedArraysByGuid[dependentKey] = new Ember.TrackedArray(observerContexts); + @method getProperties + @param {String...|Array} list of keys to get + @return {Hash} + */ + getProperties: function() { + return getProperties.apply(null, [this].concat(slice.call(arguments))); }, - trackAdd: function (dependentKey, index, newItems) { - var trackedArray = this.trackedArraysByGuid[dependentKey]; - if (trackedArray) { - trackedArray.addItems(index, newItems); - } - }, + /** + Sets the provided key or path to the value. - trackRemove: function (dependentKey, index, removedCount) { - var trackedArray = this.trackedArraysByGuid[dependentKey]; + This method is generally very similar to calling `object[key] = value` or + `object.key = value`, except that it provides support for computed + properties, the `setUnknownProperty()` method and property observers. - if (trackedArray) { - return trackedArray.removeItems(index, removedCount); - } + ### Computed Properties - return []; - }, + If you try to set a value on a key that has a computed property handler + defined (see the `get()` method for an example), then `set()` will call + that method, passing both the value and key instead of simply changing + the value itself. This is useful for those times when you need to + implement a property that is composed of one or more member + properties. - updateIndexes: function (trackedArray, array) { - var length = get(array, 'length'); - // OPTIMIZE: we could stop updating once we hit the object whose observer - // fired; ie partially apply the transformations - trackedArray.apply(function (observerContexts, offset, operation) { - // we don't even have observer contexts for removed items, even if we did, - // they no longer have any index in the array - if (operation === Ember.TrackedArray.DELETE) { return; } - if (operation === Ember.TrackedArray.RETAIN && observerContexts.length === length && offset === 0) { - // If we update many items we don't want to walk the array each time: we - // only need to update the indexes at most once per run loop. - return; - } + ### Unknown Properties - forEach(observerContexts, function (context, index) { - context.index = index + offset; - }); - }); - }, + If you try to set a value on a key that is undefined in the target + object, then the `setUnknownProperty()` handler will be called instead. This + gives you an opportunity to implement complex "virtual" properties that + are not predefined on the object. If `setUnknownProperty()` returns + undefined, then `set()` will simply set the value on the object. - dependentArrayWillChange: function (dependentArray, index, removedCount, addedCount) { - var removedItem = this.callbacks.removedItem, - changeMeta, - guid = guidFor(dependentArray), - dependentKey = this.dependentKeysByGuid[guid], - itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || [], - item, - itemIndex, - sliceIndex, - observerContexts; + ### Property Observers - observerContexts = this.trackRemove(dependentKey, index, removedCount); + In addition to changing the property, `set()` will also register a property + change with the object. Unless you have placed this call inside of a + `beginPropertyChanges()` and `endPropertyChanges(),` any "local" observers + (i.e. observer methods declared on the same object), will be called + immediately. Any "remote" observers (i.e. observer methods declared on + another object) will be placed in a queue and called at a later time in a + coalesced manner. - function removeObservers(propertyKey) { - observerContexts[sliceIndex].destroyed = true; - removeBeforeObserver(item, propertyKey, this, observerContexts[sliceIndex].beforeObserver); - removeObserver(item, propertyKey, this, observerContexts[sliceIndex].observer); - } + ### Chaining - for (sliceIndex = removedCount - 1; sliceIndex >= 0; --sliceIndex) { - itemIndex = index + sliceIndex; - item = dependentArray.objectAt(itemIndex); + In addition to property changes, `set()` returns the value of the object + itself so you can do chaining like this: - forEach(itemPropertyKeys, removeObservers, this); + ```javascript + record.set('firstName', 'Charles').set('lastName', 'Jolley'); + ``` - changeMeta = createChangeMeta(dependentArray, item, itemIndex, this.instanceMeta.propertyName, this.cp); - this.setValue( removedItem.call( - this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); - } + @method set + @param {String} keyName The property to set + @param {Object} value The value to set or `null`. + @return {Ember.Observable} + */ + set: function(keyName, value) { + set(this, keyName, value); + return this; }, - dependentArrayDidChange: function (dependentArray, index, removedCount, addedCount) { - var addedItem = this.callbacks.addedItem, - guid = guidFor(dependentArray), - dependentKey = this.dependentKeysByGuid[guid], - observerContexts = new Array(addedCount), - itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey], - changeMeta, - observerContext; - forEach(dependentArray.slice(index, index + addedCount), function (item, sliceIndex) { - if (itemPropertyKeys) { - observerContext = - observerContexts[sliceIndex] = - this.createPropertyObserverContext(dependentArray, index + sliceIndex, this.trackedArraysByGuid[dependentKey]); - forEach(itemPropertyKeys, function (propertyKey) { - addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); - addObserver(item, propertyKey, this, observerContext.observer); - }, this); - } + /** + Sets a list of properties at once. These properties are set inside + a single `beginPropertyChanges` and `endPropertyChanges` batch, so + observers will be buffered. - changeMeta = createChangeMeta(dependentArray, item, index + sliceIndex, this.instanceMeta.propertyName, this.cp); - this.setValue( addedItem.call( - this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); - }, this); + ```javascript + record.setProperties({ firstName: 'Charles', lastName: 'Jolley' }); + ``` - this.trackAdd(dependentKey, index, observerContexts); + @method setProperties + @param {Hash} hash the hash of keys and values to set + @return {Ember.Observable} + */ + setProperties: function(hash) { + return Ember.setProperties(this, hash); }, - itemPropertyWillChange: function (obj, keyName, array, observerContext) { - var guid = guidFor(obj); - - if (!this.changedItems[guid]) { - this.changedItems[guid] = { - array: array, - observerContext: observerContext, - obj: obj, - previousValues: {} - }; - } + /** + Begins a grouping of property changes. - this.changedItems[guid].previousValues[keyName] = get(obj, keyName); - }, + You can use this method to group property changes so that notifications + will not be sent until the changes are finished. If you plan to make a + large number of changes to an object at one time, you should call this + method at the beginning of the changes to begin deferring change + notifications. When you are done making changes, call + `endPropertyChanges()` to deliver the deferred change notifications and end + deferring. - itemPropertyDidChange: function(obj, keyName, array, observerContext) { - this.flushChanges(); + @method beginPropertyChanges + @return {Ember.Observable} + */ + beginPropertyChanges: function() { + Ember.beginPropertyChanges(); + return this; }, - flushChanges: function() { - var changedItems = this.changedItems, key, c, changeMeta; - - for (key in changedItems) { - c = changedItems[key]; - if (c.observerContext.destroyed) { continue; } - - this.updateIndexes(c.observerContext.trackedArray, c.observerContext.dependentArray); + /** + Ends a grouping of property changes. - changeMeta = createChangeMeta(c.array, c.obj, c.observerContext.index, this.instanceMeta.propertyName, this.cp, c.previousValues); - this.setValue( - this.callbacks.removedItem.call(this.instanceMeta.context, this.getValue(), c.obj, changeMeta, this.instanceMeta.sugarMeta)); - this.setValue( - this.callbacks.addedItem.call(this.instanceMeta.context, this.getValue(), c.obj, changeMeta, this.instanceMeta.sugarMeta)); - } - this.changedItems = {}; - } -}; + You can use this method to group property changes so that notifications + will not be sent until the changes are finished. If you plan to make a + large number of changes to an object at one time, you should call + `beginPropertyChanges()` at the beginning of the changes to defer change + notifications. When you are done making changes, call this method to + deliver the deferred change notifications and end deferring. -function createChangeMeta(dependentArray, item, index, propertyName, property, previousValues) { - var meta = { - arrayChanged: dependentArray, - index: index, - item: item, - propertyName: propertyName, - property: property - }; + @method endPropertyChanges + @return {Ember.Observable} + */ + endPropertyChanges: function() { + Ember.endPropertyChanges(); + return this; + }, - if (previousValues) { - // previous values only available for item property changes - meta.previousValues = previousValues; - } + /** + Notify the observer system that a property is about to change. - return meta; -} + Sometimes you need to change a value directly or indirectly without + actually calling `get()` or `set()` on it. In this case, you can use this + method and `propertyDidChange()` instead. Calling these two methods + together will notify all observers that the property has potentially + changed value. -function addItems (dependentArray, callbacks, cp, propertyName, meta) { - forEach(dependentArray, function (item, index) { - meta.setValue( callbacks.addedItem.call( - this, meta.getValue(), item, createChangeMeta(dependentArray, item, index, propertyName, cp), meta.sugarMeta)); - }, this); -} + Note that you must always call `propertyWillChange` and `propertyDidChange` + as a pair. If you do not, it may get the property change groups out of + order and cause notifications to be delivered more often than you would + like. -function reset(cp, propertyName) { - var callbacks = cp._callbacks(), - meta; + @method propertyWillChange + @param {String} keyName The property key that is about to change. + @return {Ember.Observable} + */ + propertyWillChange: function(keyName) { + Ember.propertyWillChange(this, keyName); + return this; + }, - if (cp._hasInstanceMeta(this, propertyName)) { - meta = cp._instanceMeta(this, propertyName); - meta.setValue(cp.resetValue(meta.getValue())); - } else { - meta = cp._instanceMeta(this, propertyName); - } + /** + Notify the observer system that a property has just changed. - if (cp.options.initialize) { - cp.options.initialize.call(this, meta.getValue(), { property: cp, propertyName: propertyName }, meta.sugarMeta); - } -} + Sometimes you need to change a value directly or indirectly without + actually calling `get()` or `set()` on it. In this case, you can use this + method and `propertyWillChange()` instead. Calling these two methods + together will notify all observers that the property has potentially + changed value. -function ReduceComputedPropertyInstanceMeta(context, propertyName, initialValue) { - this.context = context; - this.propertyName = propertyName; - this.cache = metaFor(context).cache; + Note that you must always call `propertyWillChange` and `propertyDidChange` + as a pair. If you do not, it may get the property change groups out of + order and cause notifications to be delivered more often than you would + like. - this.dependentArrays = {}; - this.sugarMeta = {}; + @method propertyDidChange + @param {String} keyName The property key that has just changed. + @return {Ember.Observable} + */ + propertyDidChange: function(keyName) { + Ember.propertyDidChange(this, keyName); + return this; + }, - this.initialValue = initialValue; -} + /** + Convenience method to call `propertyWillChange` and `propertyDidChange` in + succession. -ReduceComputedPropertyInstanceMeta.prototype = { - getValue: function () { - if (this.propertyName in this.cache) { - return this.cache[this.propertyName]; - } else { - return this.initialValue; - } + @method notifyPropertyChange + @param {String} keyName The property key to be notified about. + @return {Ember.Observable} + */ + notifyPropertyChange: function(keyName) { + this.propertyWillChange(keyName); + this.propertyDidChange(keyName); + return this; }, - setValue: function(newValue, triggerObservers) { - // This lets sugars force a recomputation, handy for very simple - // implementations of eg max. - if (newValue !== undefined) { - var fireObservers = triggerObservers && (newValue !== this.cache[this.propertyName]); + addBeforeObserver: function(key, target, method) { + Ember.addBeforeObserver(this, key, target, method); + }, - if (fireObservers) { - propertyWillChange(this.context, this.propertyName); - } + /** + Adds an observer on a property. - this.cache[this.propertyName] = newValue; + This is the core method used to register an observer for a property. - if (fireObservers) { - propertyDidChange(this.context, this.propertyName); - } - } else { - delete this.cache[this.propertyName]; - } - } -}; + Once you call this method, any time the key's value is set, your observer + will be notified. Note that the observers are triggered any time the + value is set, regardless of whether it has actually changed. Your + observer should be prepared to handle that. -/** - A computed property whose dependent keys are arrays and which is updated with - "one at a time" semantics. + You can also pass an optional context parameter to this method. The + context will be passed to your observer method whenever it is triggered. + Note that if you add the same target/method pair on a key multiple times + with different context parameters, your observer will only be called once + with the last context you passed. - @class ReduceComputedProperty - @namespace Ember - @extends Ember.ComputedProperty - @constructor -*/ -function ReduceComputedProperty(options) { - var cp = this; + ### Observer Methods - this.options = options; - this._instanceMetas = {}; + Observer methods you pass should generally have the following signature if + you do not pass a `context` parameter: - this._dependentKeys = null; - // A map of dependentKey -> [itemProperty, ...] that tracks what properties of - // items in the array we must track to update this property. - this._itemPropertyKeys = {}; - this._previousItemPropertyKeys = {}; + ```javascript + fooDidChange: function(sender, key, value, rev) { }; + ``` - this.readOnly(); - this.cacheable(); + The sender is the object that changed. The key is the property that + changes. The value property is currently reserved and unused. The rev + is the last property revision of the object when it changed, which you can + use to detect if the key value has really changed or not. - this.recomputeOnce = function(propertyName) { - // What we really want to do is coalesce by . - // We need a form of `scheduleOnce` that accepts an arbitrary token to - // coalesce by, in addition to the target and method. - Ember.run.once(this, recompute, propertyName); - }; - var recompute = function(propertyName) { - var dependentKeys = cp._dependentKeys, - meta = cp._instanceMeta(this, propertyName), - callbacks = cp._callbacks(); + If you pass a `context` parameter, the context will be passed before the + revision like so: - reset.call(this, cp, propertyName); + ```javascript + fooDidChange: function(sender, key, value, context, rev) { }; + ``` - forEach(cp._dependentKeys, function (dependentKey) { - var dependentArray = get(this, dependentKey), - previousDependentArray = meta.dependentArrays[dependentKey]; + Usually you will not need the value, context or revision parameters at + the end. In this case, it is common to write observer methods that take + only a sender and key value as parameters or, if you aren't interested in + any of these values, to write an observer that has no parameters at all. - if (dependentArray === previousDependentArray) { - // The array may be the same, but our item property keys may have - // changed, so we set them up again. We can't easily tell if they've - // changed: the array may be the same object, but with different - // contents. - if (cp._previousItemPropertyKeys[dependentKey]) { - delete cp._previousItemPropertyKeys[dependentKey]; - meta.dependentArraysObserver.setupPropertyObservers(dependentKey, cp._itemPropertyKeys[dependentKey]); - } - } else { - meta.dependentArrays[dependentKey] = dependentArray; + @method addObserver + @param {String} key The key to observer + @param {Object} target The target object to invoke + @param {String|Function} method The method to invoke. + @return {Ember.Object} self + */ + addObserver: function(key, target, method) { + Ember.addObserver(this, key, target, method); + }, - if (previousDependentArray) { - meta.dependentArraysObserver.teardownObservers(previousDependentArray, dependentKey); - } + /** + Remove an observer you have previously registered on this object. Pass + the same key, target, and method you passed to `addObserver()` and your + target will no longer receive notifications. - if (dependentArray) { - meta.dependentArraysObserver.setupObservers(dependentArray, dependentKey); - } - } - }, this); + @method removeObserver + @param {String} key The key to observer + @param {Object} target The target object to invoke + @param {String|Function} method The method to invoke. + @return {Ember.Observable} receiver + */ + removeObserver: function(key, target, method) { + Ember.removeObserver(this, key, target, method); + }, - forEach(cp._dependentKeys, function(dependentKey) { - var dependentArray = get(this, dependentKey); - if (dependentArray) { - addItems.call(this, dependentArray, callbacks, cp, propertyName, meta); - } - }, this); - }; + /** + Returns `true` if the object currently has observers registered for a + particular key. You can use this method to potentially defer performing + an expensive action until someone begins observing a particular property + on the object. - this.func = function (propertyName) { - Ember.assert("Computed reduce values require at least one dependent key", cp._dependentKeys); + @method hasObserverFor + @param {String} key Key to check + @return {Boolean} + */ + hasObserverFor: function(key) { + return Ember.hasListeners(this, key+':change'); + }, - recompute.call(this, propertyName); + /** + Retrieves the value of a property, or a default value in the case that the + property returns `undefined`. - return cp._instanceMeta(this, propertyName).getValue(); - }; -} + ```javascript + person.getWithDefault('lastName', 'Doe'); + ``` -Ember.ReduceComputedProperty = ReduceComputedProperty; -ReduceComputedProperty.prototype = o_create(ComputedProperty.prototype); + @method getWithDefault + @param {String} keyName The name of the property to retrieve + @param {Object} defaultValue The value to return if the property value is undefined + @return {Object} The property value or the defaultValue. + */ + getWithDefault: function(keyName, defaultValue) { + return Ember.getWithDefault(this, keyName, defaultValue); + }, -function defaultCallback(computedValue) { - return computedValue; -} + /** + Set the value of a property to the current value plus some amount. -ReduceComputedProperty.prototype._callbacks = function () { - if (!this.callbacks) { - var options = this.options; - this.callbacks = { - removedItem: options.removedItem || defaultCallback, - addedItem: options.addedItem || defaultCallback - }; - } - return this.callbacks; -}; + ```javascript + person.incrementProperty('age'); + team.incrementProperty('score', 2); + ``` -ReduceComputedProperty.prototype._hasInstanceMeta = function (context, propertyName) { - var guid = guidFor(context), - key = guid + ':' + propertyName; + @method incrementProperty + @param {String} keyName The name of the property to increment + @param {Number} increment The amount to increment by. Defaults to 1 + @return {Number} The new property value + */ + incrementProperty: function(keyName, increment) { + if (Ember.isNone(increment)) { increment = 1; } + Ember.assert("Must pass a numeric value to incrementProperty", (!isNaN(parseFloat(increment)) && isFinite(increment))); + set(this, keyName, (get(this, keyName) || 0) + increment); + return get(this, keyName); + }, - return !!this._instanceMetas[key]; -}; + /** + Set the value of a property to the current value minus some amount. -ReduceComputedProperty.prototype._instanceMeta = function (context, propertyName) { - var guid = guidFor(context), - key = guid + ':' + propertyName, - meta = this._instanceMetas[key]; + ```javascript + player.decrementProperty('lives'); + orc.decrementProperty('health', 5); + ``` - if (!meta) { - meta = this._instanceMetas[key] = new ReduceComputedPropertyInstanceMeta(context, propertyName, this.initialValue()); - meta.dependentArraysObserver = new DependentArraysObserver(this._callbacks(), this, meta, context, propertyName, meta.sugarMeta); - } + @method decrementProperty + @param {String} keyName The name of the property to decrement + @param {Number} decrement The amount to decrement by. Defaults to 1 + @return {Number} The new property value + */ + decrementProperty: function(keyName, decrement) { + if (Ember.isNone(decrement)) { decrement = 1; } + Ember.assert("Must pass a numeric value to decrementProperty", (!isNaN(parseFloat(decrement)) && isFinite(decrement))); + set(this, keyName, (get(this, keyName) || 0) - decrement); + return get(this, keyName); + }, - return meta; -}; + /** + Set the value of a boolean property to the opposite of it's + current value. -ReduceComputedProperty.prototype.initialValue = function () { - if (typeof this.options.initialValue === 'function') { - return this.options.initialValue(); - } - else { - return this.options.initialValue; - } -}; + ```javascript + starship.toggleProperty('warpDriveEngaged'); + ``` -ReduceComputedProperty.prototype.resetValue = function (value) { - return this.initialValue(); -}; + @method toggleProperty + @param {String} keyName The name of the property to toggle + @return {Object} The new property value + */ + toggleProperty: function(keyName) { + set(this, keyName, !get(this, keyName)); + return get(this, keyName); + }, -ReduceComputedProperty.prototype.itemPropertyKey = function (dependentArrayKey, itemPropertyKey) { - this._itemPropertyKeys[dependentArrayKey] = this._itemPropertyKeys[dependentArrayKey] || []; - this._itemPropertyKeys[dependentArrayKey].push(itemPropertyKey); -}; + /** + Returns the cached value of a computed property, if it exists. + This allows you to inspect the value of a computed property + without accidentally invoking it if it is intended to be + generated lazily. -ReduceComputedProperty.prototype.clearItemPropertyKeys = function (dependentArrayKey) { - if (this._itemPropertyKeys[dependentArrayKey]) { - this._previousItemPropertyKeys[dependentArrayKey] = this._itemPropertyKeys[dependentArrayKey]; - this._itemPropertyKeys[dependentArrayKey] = []; + @method cacheFor + @param {String} keyName + @return {Object} The cached value of the computed property, if any + */ + cacheFor: function(keyName) { + return Ember.cacheFor(this, keyName); + }, + + // intended for debugging purposes + observersForKey: function(keyName) { + return Ember.observersFor(this, keyName); } -}; +}); -ReduceComputedProperty.prototype.property = function () { - var cp = this, - args = a_slice.call(arguments), - propertyArgs = new Ember.Set(), - match, - dependentArrayKey, - itemPropertyKey; +})(); - forEach(a_slice.call(arguments), function (dependentKey) { - if (doubleEachPropertyPattern.test(dependentKey)) { - throw new Ember.Error("Nested @each properties not supported: " + dependentKey); - } else if (match = eachPropertyPattern.exec(dependentKey)) { - dependentArrayKey = match[1]; - itemPropertyKey = match[2]; - cp.itemPropertyKey(dependentArrayKey, itemPropertyKey); - propertyArgs.add(dependentArrayKey); - } else { - propertyArgs.add(dependentKey); - } - }); - return ComputedProperty.prototype.property.apply(this, propertyArgs.toArray()); -}; +(function() { /** - Creates a computed property which operates on dependent arrays and - is updated with "one at a time" semantics. When items are added or - removed from the dependent array(s) a reduce computed only operates - on the change instead of re-evaluating the entire array. + @module ember + @submodule ember-runtime +*/ - If there are more than one arguments the first arguments are - considered to be dependent property keys. The last argument is - required to be an options object. The options object can have the - following four properties: - `initialValue` - A value or function that will be used as the initial - value for the computed. If this property is a function the result of calling - the function will be used as the initial value. This property is required. +// NOTE: this object should never be included directly. Instead use `Ember.Object`. +// We only define this separately so that `Ember.Set` can depend on it. - `initialize` - An optional initialize function. Typically this will be used - to set up state on the instanceMeta object. - `removedItem` - A function that is called each time an element is removed - from the array. +var set = Ember.set, get = Ember.get, + o_create = Ember.create, + o_defineProperty = Ember.platform.defineProperty, + GUID_KEY = Ember.GUID_KEY, + guidFor = Ember.guidFor, + generateGuid = Ember.generateGuid, + meta = Ember.meta, + META_KEY = Ember.META_KEY, + rewatch = Ember.rewatch, + finishChains = Ember.finishChains, + sendEvent = Ember.sendEvent, + destroy = Ember.destroy, + schedule = Ember.run.schedule, + Mixin = Ember.Mixin, + applyMixin = Mixin._apply, + finishPartial = Mixin.finishPartial, + reopen = Mixin.prototype.reopen, + MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, + indexOf = Ember.EnumerableUtils.indexOf; - `addedItem` - A function that is called each time an element is added to - the array. +var undefinedDescriptor = { + configurable: true, + writable: true, + enumerable: false, + value: undefined +}; +var nullDescriptor = { + configurable: true, + writable: true, + enumerable: false, + value: null +}; - The `initialize` function has the following signature: +function makeCtor() { - ```javascript - function (initialValue, changeMeta, instanceMeta) - ``` + // Note: avoid accessing any properties on the object since it makes the + // method a lot faster. This is glue code so we want it to be as fast as + // possible. - `initialValue` - The value of the `initialValue` property from the - options object. + var wasApplied = false, initMixins, initProperties; - `changeMeta` - An object which contains meta information about the - computed. It contains the following properties: + var Class = function() { + if (!wasApplied) { + Class.proto(); // prepare prototype... + } + o_defineProperty(this, GUID_KEY, nullDescriptor); + o_defineProperty(this, '__nextSuper', undefinedDescriptor); + var m = meta(this), proto = m.proto; + m.proto = this; + if (initMixins) { + // capture locally so we can clear the closed over variable + var mixins = initMixins; + initMixins = null; + this.reopen.apply(this, mixins); + } + if (initProperties) { + // capture locally so we can clear the closed over variable + var props = initProperties; + initProperties = null; - - `property` the computed property - - `propertyName` the name of the property on the object + var concatenatedProperties = this.concatenatedProperties; - `instanceMeta` - An object that can be used to store meta - information needed for calculating your computed. For example a - unique computed might use this to store the number of times a given - element is found in the dependent array. + for (var i = 0, l = props.length; i < l; i++) { + var properties = props[i]; + Ember.assert("Ember.Object.create no longer supports mixing in other definitions, use createWithMixins instead.", !(properties instanceof Ember.Mixin)); - The `removedItem` and `addedItem` functions both have the following signature: + if (typeof properties !== 'object' && properties !== undefined) { + throw new Ember.Error("Ember.Object.create only accepts objects."); + } - ```javascript - function (accumulatedValue, item, changeMeta, instanceMeta) - ``` + if (!properties) { continue; } - `accumulatedValue` - The value returned from the last time - `removedItem` or `addedItem` was called or `initialValue`. + var keyNames = Ember.keys(properties); - `item` - the element added or removed from the array + for (var j = 0, ll = keyNames.length; j < ll; j++) { + var keyName = keyNames[j]; + if (!properties.hasOwnProperty(keyName)) { continue; } - `changeMeta` - An object which contains meta information about the - change. It contains the following properties: + var value = properties[keyName], + IS_BINDING = Ember.IS_BINDING; - - `property` the computed property - - `propertyName` the name of the property on the object - - `index` the index of the added or removed item - - `item` the added or removed item: this is exactly the same as - the second arg - - `arrayChanged` the array that triggered the change. Can be - useful when depending on multiple arrays. + if (IS_BINDING.test(keyName)) { + var bindings = m.bindings; + if (!bindings) { + bindings = m.bindings = {}; + } else if (!m.hasOwnProperty('bindings')) { + bindings = m.bindings = o_create(m.bindings); + } + bindings[keyName] = value; + } - For property changes triggered on an item property change (when - depKey is something like `someArray.@each.someProperty`), - `changeMeta` will also contain the following property: + var desc = m.descs[keyName]; - - `previousValues` an object whose keys are the properties that changed on - the item, and whose values are the item's previous values. + Ember.assert("Ember.Object.create no longer supports defining computed properties. Define computed properties using extend() or reopen() before calling create().", !(value instanceof Ember.ComputedProperty)); + Ember.assert("Ember.Object.create no longer supports defining methods that call _super.", !(typeof value === 'function' && value.toString().indexOf('._super') !== -1)); + Ember.assert("`actions` must be provided at extend time, not at create " + + "time, when Ember.ActionHandler is used (i.e. views, " + + "controllers & routes).", !((keyName === 'actions') && Ember.ActionHandler.detect(this))); - `previousValues` is important Ember coalesces item property changes via - Ember.run.once. This means that by the time removedItem gets called, item has - the new values, but you may need the previous value (eg for sorting & - filtering). + if (concatenatedProperties && indexOf(concatenatedProperties, keyName) >= 0) { + var baseValue = this[keyName]; - `instanceMeta` - An object that can be used to store meta - information needed for calculating your computed. For example a - unique computed might use this to store the number of times a given - element is found in the dependent array. + if (baseValue) { + if ('function' === typeof baseValue.concat) { + value = baseValue.concat(value); + } else { + value = Ember.makeArray(baseValue).concat(value); + } + } else { + value = Ember.makeArray(value); + } + } - The `removedItem` and `addedItem` functions should return the accumulated - value. It is acceptable to not return anything (ie return undefined) - to invalidate the computation. This is generally not a good idea for - arrayComputed but it's used in eg max and min. + if (desc) { + desc.set(this, keyName, value); + } else { + if (typeof this.setUnknownProperty === 'function' && !(keyName in this)) { + this.setUnknownProperty(keyName, value); + } else if (MANDATORY_SETTER) { + Ember.defineProperty(this, keyName, null, value); // setup mandatory setter + } else { + this[keyName] = value; + } + } + } + } + } + finishPartial(this, m); + this.init.apply(this, arguments); + m.proto = proto; + finishChains(this); + sendEvent(this, "init"); + }; - Note that observers will be fired if either of these functions return a value - that differs from the accumulated value. When returning an object that - mutates in response to array changes, for example an array that maps - everything from some other array (see `Ember.computed.map`), it is usually - important that the *same* array be returned to avoid accidentally triggering observers. + Class.toString = Mixin.prototype.toString; + Class.willReopen = function() { + if (wasApplied) { + Class.PrototypeMixin = Mixin.create(Class.PrototypeMixin); + } - Example + wasApplied = false; + }; + Class._initMixins = function(args) { initMixins = args; }; + Class._initProperties = function(args) { initProperties = args; }; - ```javascript - Ember.computed.max = function (dependentKey) { - return Ember.reduceComputed.call(null, dependentKey, { - initialValue: -Infinity, + Class.proto = function() { + var superclass = Class.superclass; + if (superclass) { superclass.proto(); } - addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { - return Math.max(accumulatedValue, item); - }, + if (!wasApplied) { + wasApplied = true; + Class.PrototypeMixin.applyPartial(Class.prototype); + rewatch(Class.prototype); + } - removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { - if (item < accumulatedValue) { - return accumulatedValue; - } - } - }); + return this.prototype; }; - ``` - @method reduceComputed - @for Ember - @param {String} [dependentKeys*] - @param {Object} options - @return {Ember.ComputedProperty} -*/ -Ember.reduceComputed = function (options) { - var args; + return Class; - if (arguments.length > 1) { - args = a_slice.call(arguments, 0, -1); - options = a_slice.call(arguments, -1)[0]; - } +} - if (typeof options !== "object") { - throw new Ember.Error("Reduce Computed Property declared without an options hash"); - } +/** + @class CoreObject + @namespace Ember +*/ +var CoreObject = makeCtor(); +CoreObject.toString = function() { return "Ember.CoreObject"; }; - if (!('initialValue' in options)) { - throw new Ember.Error("Reduce Computed Property declared without an initial value"); - } +CoreObject.PrototypeMixin = Mixin.create({ + reopen: function() { + applyMixin(this, arguments, true); + return this; + }, - var cp = new ReduceComputedProperty(options); + /** + An overridable method called when objects are instantiated. By default, + does nothing unless it is overridden during class definition. - if (args) { - cp.property.apply(cp, args); - } + Example: - return cp; -}; + ```javascript + App.Person = Ember.Object.extend({ + init: function() { + alert('Name is ' + this.get('name')); + } + }); -})(); + var steve = App.Person.create({ + name: "Steve" + }); + // alerts 'Name is Steve'. + ``` + NOTE: If you do override `init` for a framework class like `Ember.View` or + `Ember.ArrayController`, be sure to call `this._super()` in your + `init` declaration! If you don't, Ember may not have an opportunity to + do important setup work, and you'll see strange behavior in your + application. -(function() { -var ReduceComputedProperty = Ember.ReduceComputedProperty, - a_slice = [].slice, - o_create = Ember.create, - forEach = Ember.EnumerableUtils.forEach; + @method init + */ + init: function() {}, -function ArrayComputedProperty() { - var cp = this; + /** + Defines the properties that will be concatenated from the superclass + (instead of overridden). - ReduceComputedProperty.apply(this, arguments); + By default, when you extend an Ember class a property defined in + the subclass overrides a property with the same name that is defined + in the superclass. However, there are some cases where it is preferable + to build up a property's value by combining the superclass' property + value with the subclass' value. An example of this in use within Ember + is the `classNames` property of `Ember.View`. - this.func = (function(reduceFunc) { - return function (propertyName) { - if (!cp._hasInstanceMeta(this, propertyName)) { - // When we recompute an array computed property, we need already - // retrieved arrays to be updated; we can't simply empty the cache and - // hope the array is re-retrieved. - forEach(cp._dependentKeys, function(dependentKey) { - Ember.addObserver(this, dependentKey, function() { - cp.recomputeOnce.call(this, propertyName); - }); - }, this); - } + Here is some sample code showing the difference between a concatenated + property and a normal one: - return reduceFunc.apply(this, arguments); - }; - })(this.func); + ```javascript + App.BarView = Ember.View.extend({ + someNonConcatenatedProperty: ['bar'], + classNames: ['bar'] + }); - return this; -} -Ember.ArrayComputedProperty = ArrayComputedProperty; -ArrayComputedProperty.prototype = o_create(ReduceComputedProperty.prototype); -ArrayComputedProperty.prototype.initialValue = function () { - return Ember.A(); -}; -ArrayComputedProperty.prototype.resetValue = function (array) { - array.clear(); - return array; -}; + App.FooBarView = App.BarView.extend({ + someNonConcatenatedProperty: ['foo'], + classNames: ['foo'], + }); -/** - Creates a computed property which operates on dependent arrays and - is updated with "one at a time" semantics. When items are added or - removed from the dependent array(s) an array computed only operates - on the change instead of re-evaluating the entire array. This should - return an array, if you'd like to use "one at a time" semantics and - compute some value other then an array look at - `Ember.reduceComputed`. + var fooBarView = App.FooBarView.create(); + fooBarView.get('someNonConcatenatedProperty'); // ['foo'] + fooBarView.get('classNames'); // ['ember-view', 'bar', 'foo'] + ``` - If there are more than one arguments the first arguments are - considered to be dependent property keys. The last argument is - required to be an options object. The options object can have the - following three properties. + This behavior extends to object creation as well. Continuing the + above example: - `initialize` - An optional initialize function. Typically this will be used - to set up state on the instanceMeta object. + ```javascript + var view = App.FooBarView.create({ + someNonConcatenatedProperty: ['baz'], + classNames: ['baz'] + }) + view.get('someNonConcatenatedProperty'); // ['baz'] + view.get('classNames'); // ['ember-view', 'bar', 'foo', 'baz'] + ``` + Adding a single property that is not an array will just add it in the array: - `removedItem` - A function that is called each time an element is - removed from the array. + ```javascript + var view = App.FooBarView.create({ + classNames: 'baz' + }) + view.get('classNames'); // ['ember-view', 'bar', 'foo', 'baz'] + ``` - `addedItem` - A function that is called each time an element is - added to the array. + Using the `concatenatedProperties` property, we can tell to Ember that mix + the content of the properties. + In `Ember.View` the `classNameBindings` and `attributeBindings` properties + are also concatenated, in addition to `classNames`. - The `initialize` function has the following signature: + This feature is available for you to use throughout the Ember object model, + although typical app developers are likely to use it infrequently. Since + it changes expectations about behavior of properties, you should properly + document its usage in each individual concatenated property (to not + mislead your users to think they can override the property in a subclass). - ```javascript - function (array, changeMeta, instanceMeta) - ``` + @property concatenatedProperties + @type Array + @default null + */ + concatenatedProperties: null, - `array` - The initial value of the arrayComputed, an empty array. + /** + Destroyed object property flag. - `changeMeta` - An object which contains meta information about the - computed. It contains the following properties: + if this property is `true` the observers and bindings were already + removed by the effect of calling the `destroy()` method. - - `property` the computed property - - `propertyName` the name of the property on the object + @property isDestroyed + @default false + */ + isDestroyed: false, - `instanceMeta` - An object that can be used to store meta - information needed for calculating your computed. For example a - unique computed might use this to store the number of times a given - element is found in the dependent array. + /** + Destruction scheduled flag. The `destroy()` method has been called. + The object stays intact until the end of the run loop at which point + the `isDestroyed` flag is set. - The `removedItem` and `addedItem` functions both have the following signature: + @property isDestroying + @default false + */ + isDestroying: false, - ```javascript - function (accumulatedValue, item, changeMeta, instanceMeta) - ``` + /** + Destroys an object by setting the `isDestroyed` flag and removing its + metadata, which effectively destroys observers and bindings. - `accumulatedValue` - The value returned from the last time - `removedItem` or `addedItem` was called or an empty array. + If you try to set a property on a destroyed object, an exception will be + raised. - `item` - the element added or removed from the array + Note that destruction is scheduled for the end of the run loop and does not + happen immediately. It will set an isDestroying flag immediately. - `changeMeta` - An object which contains meta information about the - change. It contains the following properties: + @method destroy + @return {Ember.Object} receiver + */ + destroy: function() { + if (this.isDestroying) { return; } + this.isDestroying = true; - - `property` the computed property - - `propertyName` the name of the property on the object - - `index` the index of the added or removed item - - `item` the added or removed item: this is exactly the same as - the second arg - - `arrayChanged` the array that triggered the change. Can be - useful when depending on multiple arrays. + schedule('actions', this, this.willDestroy); + schedule('destroy', this, this._scheduledDestroy); + return this; + }, - For property changes triggered on an item property change (when - depKey is something like `someArray.@each.someProperty`), - `changeMeta` will also contain the following property: + /** + Override to implement teardown. - - `previousValues` an object whose keys are the properties that changed on - the item, and whose values are the item's previous values. + @method willDestroy + */ + willDestroy: Ember.K, - `previousValues` is important Ember coalesces item property changes via - Ember.run.once. This means that by the time removedItem gets called, item has - the new values, but you may need the previous value (eg for sorting & - filtering). + /** + Invoked by the run loop to actually destroy the object. This is + scheduled for execution by the `destroy` method. - `instanceMeta` - An object that can be used to store meta - information needed for calculating your computed. For example a - unique computed might use this to store the number of times a given - element is found in the dependent array. + @private + @method _scheduledDestroy + */ + _scheduledDestroy: function() { + if (this.isDestroyed) { return; } + destroy(this); + this.isDestroyed = true; + }, - The `removedItem` and `addedItem` functions should return the accumulated - value. It is acceptable to not return anything (ie return undefined) - to invalidate the computation. This is generally not a good idea for - arrayComputed but it's used in eg max and min. + bind: function(to, from) { + if (!(from instanceof Ember.Binding)) { from = Ember.Binding.from(from); } + from.to(to).connect(this); + return from; + }, - Example + /** + Returns a string representation which attempts to provide more information + than Javascript's `toString` typically does, in a generic way for all Ember + objects. - ```javascript - Ember.computed.map = function(dependentKey, callback) { - var options = { - addedItem: function(array, item, changeMeta, instanceMeta) { - var mapped = callback(item); - array.insertAt(changeMeta.index, mapped); - return array; - }, - removedItem: function(array, item, changeMeta, instanceMeta) { - array.removeAt(changeMeta.index, 1); - return array; - } - }; + ```javascript + App.Person = Em.Object.extend() + person = App.Person.create() + person.toString() //=> "" + ``` - return Ember.arrayComputed(dependentKey, options); - }; - ``` + If the object's class is not defined on an Ember namespace, it will + indicate it is a subclass of the registered superclass: - @method arrayComputed - @for Ember - @param {String} [dependentKeys*] - @param {Object} options - @return {Ember.ComputedProperty} -*/ -Ember.arrayComputed = function (options) { - var args; + ```javascript + Student = App.Person.extend() + student = Student.create() + student.toString() //=> "<(subclass of App.Person):ember1025>" + ``` - if (arguments.length > 1) { - args = a_slice.call(arguments, 0, -1); - options = a_slice.call(arguments, -1)[0]; - } + If the method `toStringExtension` is defined, its return value will be + included in the output. - if (typeof options !== "object") { - throw new Ember.Error("Array Computed Property declared without an options hash"); + ```javascript + App.Teacher = App.Person.extend({ + toStringExtension: function() { + return this.get('fullName'); + } + }); + teacher = App.Teacher.create() + teacher.toString(); //=> "" + ``` + + @method toString + @return {String} string representation + */ + toString: function toString() { + var hasToStringExtension = typeof this.toStringExtension === 'function', + extension = hasToStringExtension ? ":" + this.toStringExtension() : ''; + var ret = '<'+this.constructor.toString()+':'+guidFor(this)+extension+'>'; + this.toString = makeToString(ret); + return ret; } +}); - var cp = new ArrayComputedProperty(options); +CoreObject.PrototypeMixin.ownerConstructor = CoreObject; - if (args) { - cp.property.apply(cp, args); - } +function makeToString(ret) { + return function() { return ret; }; +} - return cp; -}; +if (Ember.config.overridePrototypeMixin) { + Ember.config.overridePrototypeMixin(CoreObject.PrototypeMixin); +} -})(); +CoreObject.__super__ = null; +var ClassMixin = Mixin.create({ + ClassMixin: Ember.required(), -(function() { -/** -@module ember -@submodule ember-runtime -*/ + PrototypeMixin: Ember.required(), -var get = Ember.get, - set = Ember.set, - guidFor = Ember.guidFor, - merge = Ember.merge, - a_slice = [].slice, - forEach = Ember.EnumerableUtils.forEach, - map = Ember.EnumerableUtils.map; + isClass: true, -/** - A computed property that calculates the maximum value in the - dependent array. This will return `-Infinity` when the dependent - array is empty. + isMethod: false, - Example + /** + Creates a new subclass. - ```javascript - App.Person = Ember.Object.extend({ - childAges: Ember.computed.mapBy('children', 'age'), - maxChildAge: Ember.computed.max('childAges') - }); + ```javascript + App.Person = Ember.Object.extend({ + say: function(thing) { + alert(thing); + } + }); + ``` - var lordByron = App.Person.create({children: []}); - lordByron.get('maxChildAge'); // -Infinity - lordByron.get('children').pushObject({name: 'Augusta Ada Byron', age: 7}); - lordByron.get('maxChildAge'); // 7 - lordByron.get('children').pushObjects([{name: 'Allegra Byron', age: 5}, {name: 'Elizabeth Medora Leigh', age: 8}]); - lordByron.get('maxChildAge'); // 8 - ``` + This defines a new subclass of Ember.Object: `App.Person`. It contains one method: `say()`. - @method computed.max - @for Ember - @param {String} dependentKey - @return {Ember.ComputedProperty} computes the largest value in the dependentKey's array -*/ -Ember.computed.max = function (dependentKey) { - return Ember.reduceComputed.call(null, dependentKey, { - initialValue: -Infinity, + You can also create a subclass from any existing class by calling its `extend()` method. For example, you might want to create a subclass of Ember's built-in `Ember.View` class: - addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { - return Math.max(accumulatedValue, item); - }, + ```javascript + App.PersonView = Ember.View.extend({ + tagName: 'li', + classNameBindings: ['isAdministrator'] + }); + ``` - removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { - if (item < accumulatedValue) { - return accumulatedValue; - } - } - }); -}; + When defining a subclass, you can override methods but still access the implementation of your parent class by calling the special `_super()` method: -/** - A computed property that calculates the minimum value in the - dependent array. This will return `Infinity` when the dependent - array is empty. + ```javascript + App.Person = Ember.Object.extend({ + say: function(thing) { + var name = this.get('name'); + alert(name + ' says: ' + thing); + } + }); - Example + App.Soldier = App.Person.extend({ + say: function(thing) { + this._super(thing + ", sir!"); + }, + march: function(numberOfHours) { + alert(this.get('name') + ' marches for ' + numberOfHours + ' hours.') + } + }); - ```javascript - App.Person = Ember.Object.extend({ - childAges: Ember.computed.mapBy('children', 'age'), - minChildAge: Ember.computed.min('childAges') - }); + var yehuda = App.Soldier.create({ + name: "Yehuda Katz" + }); - var lordByron = App.Person.create({children: []}); - lordByron.get('minChildAge'); // Infinity - lordByron.get('children').pushObject({name: 'Augusta Ada Byron', age: 7}); - lordByron.get('minChildAge'); // 7 - lordByron.get('children').pushObjects([{name: 'Allegra Byron', age: 5}, {name: 'Elizabeth Medora Leigh', age: 8}]); - lordByron.get('minChildAge'); // 5 - ``` + yehuda.say("Yes"); // alerts "Yehuda Katz says: Yes, sir!" + ``` - @method computed.min - @for Ember - @param {String} dependentKey - @return {Ember.ComputedProperty} computes the smallest value in the dependentKey's array -*/ -Ember.computed.min = function (dependentKey) { - return Ember.reduceComputed.call(null, dependentKey, { - initialValue: Infinity, + The `create()` on line #17 creates an *instance* of the `App.Soldier` class. The `extend()` on line #8 creates a *subclass* of `App.Person`. Any instance of the `App.Person` class will *not* have the `march()` method. - addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { - return Math.min(accumulatedValue, item); - }, + You can also pass `Ember.Mixin` classes to add additional properties to the subclass. - removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { - if (item > accumulatedValue) { - return accumulatedValue; + ```javascript + App.Person = Ember.Object.extend({ + say: function(thing) { + alert(this.get('name') + ' says: ' + thing); } - } - }); -}; + }); -/** - Returns an array mapped via the callback + App.SingingMixin = Ember.Mixin.create({ + sing: function(thing){ + alert(this.get('name') + ' sings: la la la ' + thing); + } + }); - The callback method you provide should have the following signature: + App.BroadwayStar = App.Person.extend(App.SingingMixin, { + dance: function() { + alert(this.get('name') + ' dances: tap tap tap tap '); + } + }); + ``` - ```javascript - function(item); - ``` + The `App.BroadwayStar` class contains three methods: `say()`, `sing()`, and `dance()`. - - `item` is the current item in the iteration. + @method extend + @static - Example + @param {Ember.Mixin} [mixins]* One or more Ember.Mixin classes + @param {Object} [arguments]* Object containing values to use within the new class + */ + extend: function() { + var Class = makeCtor(), proto; + Class.ClassMixin = Mixin.create(this.ClassMixin); + Class.PrototypeMixin = Mixin.create(this.PrototypeMixin); - ```javascript - App.Hampster = Ember.Object.extend({ - excitingChores: Ember.computed.map('chores', function(chore) { - return chore.toUpperCase() + '!'; - }) - }); + Class.ClassMixin.ownerConstructor = Class; + Class.PrototypeMixin.ownerConstructor = Class; - var hampster = App.Hampster.create({chores: ['cook', 'clean', 'write more unit tests']}); - hampster.get('excitingChores'); // ['COOK!', 'CLEAN!', 'WRITE MORE UNIT TESTS!'] - ``` + reopen.apply(Class.PrototypeMixin, arguments); - @method computed.map - @for Ember - @param {String} dependentKey - @param {Function} callback - @return {Ember.ComputedProperty} an array mapped via the callback -*/ -Ember.computed.map = function(dependentKey, callback) { - var options = { - addedItem: function(array, item, changeMeta, instanceMeta) { - var mapped = callback.call(this, item); - array.insertAt(changeMeta.index, mapped); - return array; - }, - removedItem: function(array, item, changeMeta, instanceMeta) { - array.removeAt(changeMeta.index, 1); - return array; - } - }; + Class.superclass = this; + Class.__super__ = this.prototype; - return Ember.arrayComputed(dependentKey, options); -}; + proto = Class.prototype = o_create(this.prototype); + proto.constructor = Class; + generateGuid(proto); + meta(proto).proto = proto; // this will disable observers on prototype -/** - Returns an array mapped to the specified key. + Class.ClassMixin.apply(Class); + return Class; + }, - Example + /** + Equivalent to doing `extend(arguments).create()`. + If possible use the normal `create` method instead. - ```javascript - App.Person = Ember.Object.extend({ - childAges: Ember.computed.mapBy('children', 'age') - }); + @method createWithMixins + @static + @param [arguments]* + */ + createWithMixins: function() { + var C = this; + if (arguments.length>0) { this._initMixins(arguments); } + return new C(); + }, - var lordByron = App.Person.create({children: []}); - lordByron.get('childAges'); // [] - lordByron.get('children').pushObject({name: 'Augusta Ada Byron', age: 7}); - lordByron.get('childAges'); // [7] - lordByron.get('children').pushObjects([{name: 'Allegra Byron', age: 5}, {name: 'Elizabeth Medora Leigh', age: 8}]); - lordByron.get('childAges'); // [7, 5, 8] - ``` + /** + Creates an instance of a class. Accepts either no arguments, or an object + containing values to initialize the newly instantiated object with. - @method computed.mapBy - @for Ember - @param {String} dependentKey - @param {String} propertyKey - @return {Ember.ComputedProperty} an array mapped to the specified key -*/ -Ember.computed.mapBy = function(dependentKey, propertyKey) { - var callback = function(item) { return get(item, propertyKey); }; - return Ember.computed.map(dependentKey + '.@each.' + propertyKey, callback); -}; + ```javascript + App.Person = Ember.Object.extend({ + helloWorld: function() { + alert("Hi, my name is " + this.get('name')); + } + }); -/** - @method computed.mapProperty - @for Ember - @deprecated Use `Ember.computed.mapBy` instead - @param dependentKey - @param propertyKey -*/ -Ember.computed.mapProperty = Ember.computed.mapBy; + var tom = App.Person.create({ + name: 'Tom Dale' + }); -/** - Filters the array by the callback. + tom.helloWorld(); // alerts "Hi, my name is Tom Dale". + ``` - The callback method you provide should have the following signature: + `create` will call the `init` function if defined during + `Ember.AnyObject.extend` - ```javascript - function(item); - ``` + If no arguments are passed to `create`, it will not set values to the new + instance during initialization: - - `item` is the current item in the iteration. + ```javascript + var noName = App.Person.create(); + noName.helloWorld(); // alerts undefined + ``` - Example + NOTE: For performance reasons, you cannot declare methods or computed + properties during `create`. You should instead declare methods and computed + properties when using `extend` or use the `createWithMixins` shorthand. - ```javascript - App.Hampster = Ember.Object.extend({ - remainingChores: Ember.computed.filter('chores', function(chore) { - return !chore.done; - }) - }); + @method create + @static + @param [arguments]* + */ + create: function() { + var C = this; + if (arguments.length>0) { this._initProperties(arguments); } + return new C(); + }, - var hampster = App.Hampster.create({chores: [ - {name: 'cook', done: true}, - {name: 'clean', done: true}, - {name: 'write more unit tests', done: false} - ]}); - hampster.get('remainingChores'); // [{name: 'write more unit tests', done: false}] - ``` + /** + Augments a constructor's prototype with additional + properties and functions: - @method computed.filter - @for Ember - @param {String} dependentKey - @param {Function} callback - @return {Ember.ComputedProperty} the filtered array -*/ -Ember.computed.filter = function(dependentKey, callback) { - var options = { - initialize: function (array, changeMeta, instanceMeta) { - instanceMeta.filteredArrayIndexes = new Ember.SubArray(); - }, + ```javascript + MyObject = Ember.Object.extend({ + name: 'an object' + }); - addedItem: function(array, item, changeMeta, instanceMeta) { - var match = !!callback.call(this, item), - filterIndex = instanceMeta.filteredArrayIndexes.addItem(changeMeta.index, match); + o = MyObject.create(); + o.get('name'); // 'an object' - if (match) { - array.insertAt(filterIndex, item); + MyObject.reopen({ + say: function(msg){ + console.log(msg); } + }) - return array; - }, + o2 = MyObject.create(); + o2.say("hello"); // logs "hello" - removedItem: function(array, item, changeMeta, instanceMeta) { - var filterIndex = instanceMeta.filteredArrayIndexes.removeItem(changeMeta.index); + o.say("goodbye"); // logs "goodbye" + ``` - if (filterIndex > -1) { - array.removeAt(filterIndex); + To add functions and properties to the constructor itself, + see `reopenClass` + + @method reopen + */ + reopen: function() { + this.willReopen(); + reopen.apply(this.PrototypeMixin, arguments); + return this; + }, + + /** + Augments a constructor's own properties and functions: + + ```javascript + MyObject = Ember.Object.extend({ + name: 'an object' + }); + + MyObject.reopenClass({ + canBuild: false + }); + + MyObject.canBuild; // false + o = MyObject.create(); + ``` + + In other words, this creates static properties and functions for the class. These are only available on the class + and not on any instance of that class. + + ```javascript + App.Person = Ember.Object.extend({ + name : "", + sayHello : function(){ + alert("Hello. My name is " + this.get('name')); } + }); - return array; - } - }; + App.Person.reopenClass({ + species : "Homo sapiens", + createPerson: function(newPersonsName){ + return App.Person.create({ + name:newPersonsName + }); + } + }); - return Ember.arrayComputed(dependentKey, options); -}; + var tom = App.Person.create({ + name : "Tom Dale" + }); + var yehuda = App.Person.createPerson("Yehuda Katz"); -/** - Filters the array by the property and value + tom.sayHello(); // "Hello. My name is Tom Dale" + yehuda.sayHello(); // "Hello. My name is Yehuda Katz" + alert(App.Person.species); // "Homo sapiens" + ``` - Example + Note that `species` and `createPerson` are *not* valid on the `tom` and `yehuda` + variables. They are only valid on `App.Person`. - ```javascript - App.Hampster = Ember.Object.extend({ - remainingChores: Ember.computed.filterBy('chores', 'done', false) - }); + To add functions and properties to instances of + a constructor by extending the constructor's prototype + see `reopen` - var hampster = App.Hampster.create({chores: [ - {name: 'cook', done: true}, - {name: 'clean', done: true}, - {name: 'write more unit tests', done: false} - ]}); - hampster.get('remainingChores'); // [{name: 'write more unit tests', done: false}] - ``` + @method reopenClass + */ + reopenClass: function() { + reopen.apply(this.ClassMixin, arguments); + applyMixin(this, arguments, false); + return this; + }, - @method computed.filterBy - @for Ember - @param {String} dependentKey - @param {String} propertyKey - @param {String} value - @return {Ember.ComputedProperty} the filtered array -*/ -Ember.computed.filterBy = function(dependentKey, propertyKey, value) { - var callback; + detect: function(obj) { + if ('function' !== typeof obj) { return false; } + while(obj) { + if (obj===this) { return true; } + obj = obj.superclass; + } + return false; + }, - if (arguments.length === 2) { - callback = function(item) { - return get(item, propertyKey); - }; - } else { - callback = function(item) { - return get(item, propertyKey) === value; - }; - } + detectInstance: function(obj) { + return obj instanceof this; + }, - return Ember.computed.filter(dependentKey + '.@each.' + propertyKey, callback); -}; + /** + In some cases, you may want to annotate computed properties with additional + metadata about how they function or what values they operate on. For + example, computed property functions may close over variables that are then + no longer available for introspection. -/** - @method computed.filterProperty - @for Ember - @param dependentKey - @param propertyKey - @param value - @deprecated Use `Ember.computed.filterBy` instead -*/ -Ember.computed.filterProperty = Ember.computed.filterBy; + You can pass a hash of these values to a computed property like this: -/** - A computed property which returns a new array with all the unique - elements from one or more dependent arrays. + ```javascript + person: function() { + var personId = this.get('personId'); + return App.Person.create({ id: personId }); + }.property().meta({ type: App.Person }) + ``` - Example + Once you've done this, you can retrieve the values saved to the computed + property from your class like this: - ```javascript - App.Hampster = Ember.Object.extend({ - uniqueFruits: Ember.computed.uniq('fruits') - }); + ```javascript + MyClass.metaForProperty('person'); + ``` - var hampster = App.Hampster.create({fruits: [ - 'banana', - 'grape', - 'kale', - 'banana' - ]}); - hampster.get('uniqueFruits'); // ['banana', 'grape', 'kale'] - ``` + This will return the original hash that was passed to `meta()`. - @method computed.uniq - @for Ember - @param {String} propertyKey* - @return {Ember.ComputedProperty} computes a new array with all the - unique elements from the dependent array -*/ -Ember.computed.uniq = function() { - var args = a_slice.call(arguments); - args.push({ - initialize: function(array, changeMeta, instanceMeta) { - instanceMeta.itemCounts = {}; - }, + @method metaForProperty + @param key {String} property name + */ + metaForProperty: function(key) { + var meta = this.proto()[META_KEY], + desc = meta && meta.descs[key]; - addedItem: function(array, item, changeMeta, instanceMeta) { - var guid = guidFor(item); + Ember.assert("metaForProperty() could not find a computed property with key '"+key+"'.", !!desc && desc instanceof Ember.ComputedProperty); + return desc._meta || {}; + }, - if (!instanceMeta.itemCounts[guid]) { - instanceMeta.itemCounts[guid] = 1; - } else { - ++instanceMeta.itemCounts[guid]; - } - array.addObject(item); - return array; - }, - removedItem: function(array, item, _, instanceMeta) { - var guid = guidFor(item), - itemCounts = instanceMeta.itemCounts; + /** + Iterate over each computed property for the class, passing its name + and any associated metadata (see `metaForProperty`) to the callback. - if (--itemCounts[guid] === 0) { - array.removeObject(item); + @method eachComputedProperty + @param {Function} callback + @param {Object} binding + */ + eachComputedProperty: function(callback, binding) { + var proto = this.proto(), + descs = meta(proto).descs, + empty = {}, + property; + + for (var name in descs) { + property = descs[name]; + + if (property instanceof Ember.ComputedProperty) { + callback.call(binding || this, name, property._meta || empty); } - return array; } - }); - return Ember.arrayComputed.apply(null, args); -}; + } -/** - Alias for [Ember.computed.uniq](/api/#method_computed_uniq). +}); - @method computed.union - @for Ember - @param {String} propertyKey* - @return {Ember.ComputedProperty} computes a new array with all the - unique elements from the dependent array -*/ -Ember.computed.union = Ember.computed.uniq; +ClassMixin.ownerConstructor = CoreObject; -/** - A computed property which returns a new array with all the duplicated - elements from two or more dependeny arrays. +if (Ember.config.overrideClassMixin) { + Ember.config.overrideClassMixin(ClassMixin); +} - Example +CoreObject.ClassMixin = ClassMixin; +ClassMixin.apply(CoreObject); - ```javascript - var obj = Ember.Object.createWithMixins({ - adaFriends: ['Charles Babbage', 'John Hobhouse', 'William King', 'Mary Somerville'], - charlesFriends: ['William King', 'Mary Somerville', 'Ada Lovelace', 'George Peacock'], - friendsInCommon: Ember.computed.intersect('adaFriends', 'charlesFriends') - }); +Ember.CoreObject = CoreObject; - obj.get('friendsInCommon'); // ['William King', 'Mary Somerville'] - ``` +})(); - @method computed.intersect - @for Ember - @param {String} propertyKey* - @return {Ember.ComputedProperty} computes a new array with all the - duplicated elements from the dependent arrays + + +(function() { +/** +@module ember +@submodule ember-runtime */ -Ember.computed.intersect = function () { - var getDependentKeyGuids = function (changeMeta) { - return map(changeMeta.property._dependentKeys, function (dependentKey) { - return guidFor(dependentKey); - }); - }; - var args = a_slice.call(arguments); - args.push({ - initialize: function (array, changeMeta, instanceMeta) { - instanceMeta.itemCounts = {}; - }, +/** + `Ember.Object` is the main base class for all Ember objects. It is a subclass + of `Ember.CoreObject` with the `Ember.Observable` mixin applied. For details, + see the documentation for each of these. - addedItem: function(array, item, changeMeta, instanceMeta) { - var itemGuid = guidFor(item), - dependentGuids = getDependentKeyGuids(changeMeta), - dependentGuid = guidFor(changeMeta.arrayChanged), - numberOfDependentArrays = changeMeta.property._dependentKeys.length, - itemCounts = instanceMeta.itemCounts; + @class Object + @namespace Ember + @extends Ember.CoreObject + @uses Ember.Observable +*/ +Ember.Object = Ember.CoreObject.extend(Ember.Observable); +Ember.Object.toString = function() { return "Ember.Object"; }; - if (!itemCounts[itemGuid]) { itemCounts[itemGuid] = {}; } - if (itemCounts[itemGuid][dependentGuid] === undefined) { itemCounts[itemGuid][dependentGuid] = 0; } +})(); - if (++itemCounts[itemGuid][dependentGuid] === 1 && - numberOfDependentArrays === Ember.keys(itemCounts[itemGuid]).length) { - array.addObject(item); - } - return array; - }, - removedItem: function(array, item, changeMeta, instanceMeta) { - var itemGuid = guidFor(item), - dependentGuids = getDependentKeyGuids(changeMeta), - dependentGuid = guidFor(changeMeta.arrayChanged), - numberOfDependentArrays = changeMeta.property._dependentKeys.length, - numberOfArraysItemAppearsIn, - itemCounts = instanceMeta.itemCounts; - if (itemCounts[itemGuid][dependentGuid] === undefined) { itemCounts[itemGuid][dependentGuid] = 0; } - if (--itemCounts[itemGuid][dependentGuid] === 0) { - delete itemCounts[itemGuid][dependentGuid]; - numberOfArraysItemAppearsIn = Ember.keys(itemCounts[itemGuid]).length; +(function() { +/** +@module ember +@submodule ember-runtime +*/ - if (numberOfArraysItemAppearsIn === 0) { - delete itemCounts[itemGuid]; - } - array.removeObject(item); - } - return array; - } - }); - return Ember.arrayComputed.apply(null, args); -}; +var get = Ember.get, indexOf = Ember.ArrayPolyfills.indexOf; /** - A computed property which returns a new array with all the - properties from the first dependent array that are not in the second - dependent array. + A Namespace is an object usually used to contain other objects or methods + such as an application or framework. Create a namespace anytime you want + to define one of these new containers. - Example + # Example Usage ```javascript - App.Hampster = Ember.Object.extend({ - likes: ['banana', 'grape', 'kale'], - wants: Ember.computed.setDiff('likes', 'fruits') + MyFramework = Ember.Namespace.create({ + VERSION: '1.0.0' }); - - var hampster = App.Hampster.create({fruits: [ - 'grape', - 'kale', - ]}); - hampster.get('wants'); // ['banana'] ``` - @method computed.setDiff - @for Ember - @param {String} setAProperty - @param {String} setBProperty - @return {Ember.ComputedProperty} computes a new array with all the - items from the first dependent array that are not in the second - dependent array + @class Namespace + @namespace Ember + @extends Ember.Object */ -Ember.computed.setDiff = function (setAProperty, setBProperty) { - if (arguments.length !== 2) { - throw new Ember.Error("setDiff requires exactly two dependent arrays."); - } - return Ember.arrayComputed.call(null, setAProperty, setBProperty, { - addedItem: function (array, item, changeMeta, instanceMeta) { - var setA = get(this, setAProperty), - setB = get(this, setBProperty); +var Namespace = Ember.Namespace = Ember.Object.extend({ + isNamespace: true, - if (changeMeta.arrayChanged === setA) { - if (!setB.contains(item)) { - array.addObject(item); - } - } else { - array.removeObject(item); - } - return array; - }, + init: function() { + Ember.Namespace.NAMESPACES.push(this); + Ember.Namespace.PROCESSED = false; + }, - removedItem: function (array, item, changeMeta, instanceMeta) { - var setA = get(this, setAProperty), - setB = get(this, setBProperty); + toString: function() { + var name = get(this, 'name'); + if (name) { return name; } - if (changeMeta.arrayChanged === setB) { - if (setA.contains(item)) { - array.addObject(item); - } - } else { - array.removeObject(item); - } - return array; - } - }); -}; + findNamespaces(); + return this[Ember.GUID_KEY+'_name']; + }, -function binarySearch(array, item, low, high) { - var mid, midItem, res, guidMid, guidItem; + nameClasses: function() { + processNamespace([this.toString()], this, {}); + }, - if (arguments.length < 4) { high = get(array, 'length'); } - if (arguments.length < 3) { low = 0; } + destroy: function() { + var namespaces = Ember.Namespace.NAMESPACES; - if (low === high) { - return low; + Ember.lookup[this.toString()] = undefined; + delete Ember.Namespace.NAMESPACES_BY_ID[this.toString()]; + namespaces.splice(indexOf.call(namespaces, this), 1); + this._super(); } +}); - mid = low + Math.floor((high - low) / 2); - midItem = array.objectAt(mid); - - guidMid = _guidFor(midItem); - guidItem = _guidFor(item); +Namespace.reopenClass({ + NAMESPACES: [Ember], + NAMESPACES_BY_ID: {}, + PROCESSED: false, + processAll: processAllNamespaces, + byName: function(name) { + if (!Ember.BOOTED) { + processAllNamespaces(); + } - if (guidMid === guidItem) { - return mid; + return NAMESPACES_BY_ID[name]; } +}); - res = this.order(midItem, item); - if (res === 0) { - res = guidMid < guidItem ? -1 : 1; - } +var NAMESPACES_BY_ID = Namespace.NAMESPACES_BY_ID; +var hasOwnProp = ({}).hasOwnProperty, + guidFor = Ember.guidFor; - if (res < 0) { - return this.binarySearch(array, item, mid+1, high); - } else if (res > 0) { - return this.binarySearch(array, item, low, mid); - } +function processNamespace(paths, root, seen) { + var idx = paths.length; - return mid; + NAMESPACES_BY_ID[paths.join('.')] = root; - function _guidFor(item) { - if (Ember.ObjectProxy.detectInstance(item)) { - return guidFor(get(item, 'content')); - } - return guidFor(item); - } -} + // Loop over all of the keys in the namespace, looking for classes + for(var key in root) { + if (!hasOwnProp.call(root, key)) { continue; } + var obj = root[key]; -/** - A computed property which returns a new array with all the - properties from the first dependent array sorted based on a property - or sort function. + // If we are processing the `Ember` namespace, for example, the + // `paths` will start with `["Ember"]`. Every iteration through + // the loop will update the **second** element of this list with + // the key, so processing `Ember.View` will make the Array + // `['Ember', 'View']`. + paths[idx] = key; - The callback method you provide should have the following signature: + // If we have found an unprocessed class + if (obj && obj.toString === classToString) { + // Replace the class' `toString` with the dot-separated path + // and set its `NAME_KEY` + obj.toString = makeToString(paths.join('.')); + obj[NAME_KEY] = paths.join('.'); - ```javascript - function(itemA, itemB); - ``` + // Support nested namespaces + } else if (obj && obj.isNamespace) { + // Skip aliased namespaces + if (seen[guidFor(obj)]) { continue; } + seen[guidFor(obj)] = true; - - `itemA` the first item to compare. - - `itemB` the second item to compare. + // Process the child namespace + processNamespace(paths, obj, seen); + } + } - This function should return `-1` when `itemA` should come before - `itemB`. It should return `1` when `itemA` should come after - `itemB`. If the `itemA` and `itemB` are equal this function should return `0`. + paths.length = idx; // cut out last item +} - Example +function findNamespaces() { + var Namespace = Ember.Namespace, lookup = Ember.lookup, obj, isNamespace; - ```javascript - var ToDoList = Ember.Object.extend({ - todosSorting: ['name'], - sortedTodos: Ember.computed.sort('todos', 'todosSorting'), - priorityTodos: Ember.computed.sort('todos', function(a, b){ - if (a.priority > b.priority) { - return 1; - } else if (a.priority < b.priority) { - return -1; - } - return 0; - }), - }); - var todoList = ToDoList.create({todos: [ - {name: 'Unit Test', priority: 2}, - {name: 'Documentation', priority: 3}, - {name: 'Release', priority: 1} - ]}); - - todoList.get('sortedTodos'); // [{name:'Documentation', priority:3}, {name:'Release', priority:1}, {name:'Unit Test', priority:2}] - todoList.get('priorityTodos'); // [{name:'Release', priority:1}, {name:'Unit Test', priority:2}, {name:'Documentation', priority:3}] - ``` - - @method computed.sort - @for Ember - @param {String} dependentKey - @param {String or Function} sortDefinition a dependent key to an - array of sort properties or a function to use when sorting - @return {Ember.ComputedProperty} computes a new sorted array based - on the sort property array or callback function -*/ -Ember.computed.sort = function (itemsKey, sortDefinition) { - Ember.assert("Ember.computed.sort requires two arguments: an array key to sort and either a sort properties key or sort function", arguments.length === 2); - - var initFn, sortPropertiesKey; - - if (typeof sortDefinition === 'function') { - initFn = function (array, changeMeta, instanceMeta) { - instanceMeta.order = sortDefinition; - instanceMeta.binarySearch = binarySearch; - }; - } else { - sortPropertiesKey = sortDefinition; - initFn = function (array, changeMeta, instanceMeta) { - function setupSortProperties() { - var sortPropertyDefinitions = get(this, sortPropertiesKey), - sortProperty, - sortProperties = instanceMeta.sortProperties = [], - sortPropertyAscending = instanceMeta.sortPropertyAscending = {}, - idx, - asc; - - Ember.assert("Cannot sort: '" + sortPropertiesKey + "' is not an array.", Ember.isArray(sortPropertyDefinitions)); - - changeMeta.property.clearItemPropertyKeys(itemsKey); - - forEach(sortPropertyDefinitions, function (sortPropertyDefinition) { - if ((idx = sortPropertyDefinition.indexOf(':')) !== -1) { - sortProperty = sortPropertyDefinition.substring(0, idx); - asc = sortPropertyDefinition.substring(idx+1).toLowerCase() !== 'desc'; - } else { - sortProperty = sortPropertyDefinition; - asc = true; - } - - sortProperties.push(sortProperty); - sortPropertyAscending[sortProperty] = asc; - changeMeta.property.itemPropertyKey(itemsKey, sortProperty); - }); - - sortPropertyDefinitions.addObserver('@each', this, updateSortPropertiesOnce); - } - - function updateSortPropertiesOnce() { - Ember.run.once(this, updateSortProperties, changeMeta.propertyName); - } - - function updateSortProperties(propertyName) { - setupSortProperties.call(this); - changeMeta.property.recomputeOnce.call(this, propertyName); - } + if (Namespace.PROCESSED) { return; } - Ember.addObserver(this, sortPropertiesKey, updateSortPropertiesOnce); + for (var prop in lookup) { + // These don't raise exceptions but can cause warnings + if (prop === "parent" || prop === "top" || prop === "frameElement" || prop === "webkitStorageInfo") { continue; } - setupSortProperties.call(this); + // get(window.globalStorage, 'isNamespace') would try to read the storage for domain isNamespace and cause exception in Firefox. + // globalStorage is a storage obsoleted by the WhatWG storage specification. See https://developer.mozilla.org/en/DOM/Storage#globalStorage + if (prop === "globalStorage" && lookup.StorageList && lookup.globalStorage instanceof lookup.StorageList) { continue; } + // Unfortunately, some versions of IE don't support window.hasOwnProperty + if (lookup.hasOwnProperty && !lookup.hasOwnProperty(prop)) { continue; } + // At times we are not allowed to access certain properties for security reasons. + // There are also times where even if we can access them, we are not allowed to access their properties. + try { + obj = Ember.lookup[prop]; + isNamespace = obj && obj.isNamespace; + } catch (e) { + continue; + } - instanceMeta.order = function (itemA, itemB) { - var sortProperty, result, asc; - for (var i = 0; i < this.sortProperties.length; ++i) { - sortProperty = this.sortProperties[i]; - result = Ember.compare(get(itemA, sortProperty), get(itemB, sortProperty)); + if (isNamespace) { + Ember.deprecate("Namespaces should not begin with lowercase.", /^[A-Z]/.test(prop)); + obj[NAME_KEY] = prop; + } + } +} - if (result !== 0) { - asc = this.sortPropertyAscending[sortProperty]; - return asc ? result : (-1 * result); - } - } +var NAME_KEY = Ember.NAME_KEY = Ember.GUID_KEY + '_name'; - return 0; - }; +function superClassString(mixin) { + var superclass = mixin.superclass; + if (superclass) { + if (superclass[NAME_KEY]) { return superclass[NAME_KEY]; } + else { return superClassString(superclass); } + } else { + return; + } +} - instanceMeta.binarySearch = binarySearch; - }; +function classToString() { + if (!Ember.BOOTED && !this[NAME_KEY]) { + processAllNamespaces(); } - return Ember.arrayComputed.call(null, itemsKey, { - initialize: initFn, + var ret; - addedItem: function (array, item, changeMeta, instanceMeta) { - var index = instanceMeta.binarySearch(array, item); - array.insertAt(index, item); - return array; - }, + if (this[NAME_KEY]) { + ret = this[NAME_KEY]; + } else if (this._toString) { + ret = this._toString; + } else { + var str = superClassString(this); + if (str) { + ret = "(subclass of " + str + ")"; + } else { + ret = "(unknown mixin)"; + } + this.toString = makeToString(ret); + } - removedItem: function (array, item, changeMeta, instanceMeta) { - var proxyProperties, index, searchItem; + return ret; +} - if (changeMeta.previousValues) { - proxyProperties = merge({ content: item }, changeMeta.previousValues); +function processAllNamespaces() { + var unprocessedNamespaces = !Namespace.PROCESSED, + unprocessedMixins = Ember.anyUnprocessedMixins; - searchItem = Ember.ObjectProxy.create(proxyProperties); - } else { - searchItem = item; - } + if (unprocessedNamespaces) { + findNamespaces(); + Namespace.PROCESSED = true; + } - index = instanceMeta.binarySearch(array, searchItem); - array.removeAt(index); - return array; + if (unprocessedNamespaces || unprocessedMixins) { + var namespaces = Namespace.NAMESPACES, namespace; + for (var i=0, l=namespaces.length; i alpha - // > beta - // > gamma - ``` + proxy.get('fullName'); // undefined + proxy.set('content', { + firstName: 'Tom', lastName: 'Dale' + }); // triggers property change for fullName on proxy - @method w - @param {String} str The string to split - @return {String} split string - */ - w: function(str) { return str.split(/\s+/); }, + proxy.get('fullName'); // 'Tom Dale' + ``` + @class ObjectProxy + @namespace Ember + @extends Ember.Object +*/ +Ember.ObjectProxy = Ember.Object.extend({ /** - Converts a camelized string into all lower case separated by underscores. - - ```javascript - 'innerHTML'.decamelize(); // 'inner_html' - 'action_name'.decamelize(); // 'action_name' - 'css-class-name'.decamelize(); // 'css-class-name' - 'my favorite items'.decamelize(); // 'my favorite items' - ``` + The object whose properties will be forwarded. - @method decamelize - @param {String} str The string to decamelize. - @return {String} the decamelized string. + @property content + @type Ember.Object + @default null */ - decamelize: function(str) { - return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); - }, - - /** - Replaces underscores, spaces, or camelCase with dashes. - - ```javascript - 'innerHTML'.dasherize(); // 'inner-html' - 'action_name'.dasherize(); // 'action-name' - 'css-class-name'.dasherize(); // 'css-class-name' - 'my favorite items'.dasherize(); // 'my-favorite-items' - ``` + content: null, + _contentDidChange: Ember.observer('content', function() { + Ember.assert("Can't set ObjectProxy's content to itself", this.get('content') !== this); + }), - @method dasherize - @param {String} str The string to dasherize. - @return {String} the dasherized string. - */ - dasherize: function(str) { - var cache = STRING_DASHERIZE_CACHE, - hit = cache.hasOwnProperty(str), - ret; + isTruthy: Ember.computed.bool('content'), - if (hit) { - return cache[str]; - } else { - ret = Ember.String.decamelize(str).replace(STRING_DASHERIZE_REGEXP,'-'); - cache[str] = ret; - } + _debugContainerKey: null, - return ret; + willWatchProperty: function (key) { + var contentKey = 'content.' + key; + addBeforeObserver(this, contentKey, null, contentPropertyWillChange); + addObserver(this, contentKey, null, contentPropertyDidChange); }, - /** - Returns the lowerCamelCase form of a string. - - ```javascript - 'innerHTML'.camelize(); // 'innerHTML' - 'action_name'.camelize(); // 'actionName' - 'css-class-name'.camelize(); // 'cssClassName' - 'my favorite items'.camelize(); // 'myFavoriteItems' - 'My Favorite Items'.camelize(); // 'myFavoriteItems' - ``` - - @method camelize - @param {String} str The string to camelize. - @return {String} the camelized string. - */ - camelize: function(str) { - return str.replace(STRING_CAMELIZE_REGEXP, function(match, separator, chr) { - return chr ? chr.toUpperCase() : ''; - }).replace(/^([A-Z])/, function(match, separator, chr) { - return match.toLowerCase(); - }); + didUnwatchProperty: function (key) { + var contentKey = 'content.' + key; + removeBeforeObserver(this, contentKey, null, contentPropertyWillChange); + removeObserver(this, contentKey, null, contentPropertyDidChange); }, - /** - Returns the UpperCamelCase form of a string. - - ```javascript - 'innerHTML'.classify(); // 'InnerHTML' - 'action_name'.classify(); // 'ActionName' - 'css-class-name'.classify(); // 'CssClassName' - 'my favorite items'.classify(); // 'MyFavoriteItems' - ``` - - @method classify - @param {String} str the string to classify - @return {String} the classified string - */ - classify: function(str) { - var parts = str.split("."), - out = []; - - for (var i=0, l=parts.length; i b` + @method reject + @param {Function} callback The callback to execute + @param {Object} [target] The target object to use + @return {Array} A rejected array. + */ + reject: function(callback, target) { + return this.filter(function() { + return !(callback.apply(target, arguments)); + }); + }, - Default implementation raises an exception. + /** + Returns an array with just the items with the matched property. You + can pass an optional second argument with the target value. Otherwise + this will match any property that evaluates to `true`. - @method compare - @param a {Object} the first object to compare - @param b {Object} the second object to compare - @return {Integer} the result of the comparison + @method filterBy + @param {String} key the property to test + @param {*} [value] optional value to test against. + @return {Array} filtered array */ - compare: Ember.required(Function) - -}); - - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ - - + filterBy: function(key, value) { + return this.filter(iter.apply(this, arguments)); + }, -var get = Ember.get, set = Ember.set; + /** + Returns an array with just the items with the matched property. You + can pass an optional second argument with the target value. Otherwise + this will match any property that evaluates to `true`. -/** - Implements some standard methods for copying an object. Add this mixin to - any object you create that can create a copy of itself. This mixin is - added automatically to the built-in array. + @method filterProperty + @param {String} key the property to test + @param {String} [value] optional value to test against. + @return {Array} filtered array + @deprecated Use `filterBy` instead + */ + filterProperty: Ember.aliasMethod('filterBy'), - You should generally implement the `copy()` method to return a copy of the - receiver. + /** + Returns an array with the items that do not have truthy values for + key. You can pass an optional second argument with the target value. Otherwise + this will match any property that evaluates to false. - Note that `frozenCopy()` will only work if you also implement - `Ember.Freezable`. + @method rejectBy + @param {String} key the property to test + @param {String} [value] optional value to test against. + @return {Array} rejected array + */ + rejectBy: function(key, value) { + var exactValue = function(item) { return get(item, key) === value; }, + hasValue = function(item) { return !!get(item, key); }, + use = (arguments.length === 2 ? exactValue : hasValue); - @class Copyable - @namespace Ember - @since Ember 0.9 -*/ -Ember.Copyable = Ember.Mixin.create(/** @scope Ember.Copyable.prototype */ { + return this.reject(use); + }, /** - Override to return a copy of the receiver. Default implementation raises - an exception. + Returns an array with the items that do not have truthy values for + key. You can pass an optional second argument with the target value. Otherwise + this will match any property that evaluates to false. - @method copy - @param {Boolean} deep if `true`, a deep copy of the object should be made - @return {Object} copy of receiver + @method rejectProperty + @param {String} key the property to test + @param {String} [value] optional value to test against. + @return {Array} rejected array + @deprecated Use `rejectBy` instead */ - copy: Ember.required(Function), + rejectProperty: Ember.aliasMethod('rejectBy'), /** - If the object implements `Ember.Freezable`, then this will return a new - copy if the object is not frozen and the receiver if the object is frozen. + Returns the first item in the array for which the callback returns true. + This method works similar to the `filter()` method defined in JavaScript 1.6 + except that it will stop working on the array once a match is found. - Raises an exception if you try to call this method on a object that does - not support freezing. + The callback method you provide should have the following signature (all + parameters are optional): - You should use this method whenever you want a copy of a freezable object - since a freezable object can simply return itself without actually - consuming more memory. + ```javascript + function(item, index, enumerable); + ``` - @method frozenCopy - @return {Object} copy of receiver or receiver - */ - frozenCopy: function() { - if (Ember.Freezable && Ember.Freezable.detect(this)) { - return get(this, 'isFrozen') ? this : this.copy().freeze(); - } else { - throw new Ember.Error(Ember.String.fmt("%@ does not support freezing", [this])); - } - } -}); + - `item` is the current item in the iteration. + - `index` is the current index in the iteration. + - `enumerable` is the enumerable object itself. -})(); + It should return the `true` to include the item in the results, `false` + otherwise. + Note that in addition to a callback, you can also pass an optional target + object that will be set as `this` on the context. This is a good way + to give your iterator function access to the current object. + @method find + @param {Function} callback The callback to execute + @param {Object} [target] The target object to use + @return {Object} Found item or `undefined`. + */ + find: function(callback, target) { + var len = get(this, 'length') ; + if (target === undefined) target = null; -(function() { -/** -@module ember -@submodule ember-runtime -*/ + var last = null, next, found = false, ret ; + var context = popCtx(); + for(var idx=0;idx= length, then append to the end of the array. - @param {Number} amt Number of elements that should be removed from - the array, starting at *idx*. - @param {Array} objects An array of zero or more objects that should be - inserted into the array at *idx* + @method invoke + @param {String} methodName the name of the method + @param {Object...} args optional arguments to pass as well. + @return {Array} return values from calling invoke. */ - replace: Ember.required(), + invoke: function(methodName) { + var args, ret = Ember.A(); + if (arguments.length>1) args = a_slice.call(arguments, 1); - /** - Remove all elements from self. This is useful if you - want to reuse an existing array without having to recreate it. + this.forEach(function(x, idx) { + var method = x && x[methodName]; + if ('function' === typeof method) { + ret[idx] = args ? method.apply(x, args) : x[methodName](); + } + }, this); - ```javascript - var colors = ["red", "green", "blue"]; - color.length(); // 3 - colors.clear(); // [] - colors.length(); // 0 - ``` + return ret; + }, - @method clear - @return {Ember.Array} An empty Array. + /** + Simply converts the enumerable into a genuine array. The order is not + guaranteed. Corresponds to the method implemented by Prototype. + + @method toArray + @return {Array} the enumerable as an array. */ - clear: function () { - var len = get(this, 'length'); - if (len === 0) return this; - this.replace(0, len, EMPTY); - return this; + toArray: function() { + var ret = Ember.A(); + this.forEach(function(o, idx) { ret[idx] = o; }); + return ret ; }, /** - This will use the primitive `replace()` method to insert an object at the - specified index. + Returns a copy of the array with all null and undefined elements removed. ```javascript - var colors = ["red", "green", "blue"]; - colors.insertAt(2, "yellow"); // ["red", "green", "yellow", "blue"] - colors.insertAt(5, "orange"); // Error: Index out of range + var arr = ["a", null, "c", undefined]; + arr.compact(); // ["a", "c"] ``` - @method insertAt - @param {Number} idx index of insert the object at. - @param {Object} object object to insert - @return this + @method compact + @return {Array} the array without null and undefined elements. */ - insertAt: function(idx, object) { - if (idx > get(this, 'length')) throw new Ember.Error(OUT_OF_RANGE_EXCEPTION) ; - this.replace(idx, 0, [object]) ; - return this ; + compact: function() { + return this.filter(function(value) { return value != null; }); }, /** - Remove an object at the specified index using the `replace()` primitive - method. You can pass either a single index, or a start and a length. - - If you pass a start and length that is beyond the - length this method will throw an `OUT_OF_RANGE_EXCEPTION` + Returns a new enumerable that excludes the passed value. The default + implementation returns an array regardless of the receiver type unless + the receiver does not contain the value. ```javascript - var colors = ["red", "green", "blue", "yellow", "orange"]; - colors.removeAt(0); // ["green", "blue", "yellow", "orange"] - colors.removeAt(2, 2); // ["green", "blue"] - colors.removeAt(4, 2); // Error: Index out of range + var arr = ["a", "b", "a", "c"]; + arr.without("a"); // ["b", "c"] ``` - @method removeAt - @param {Number} start index, start of range - @param {Number} len length of passing range - @return {Object} receiver + @method without + @param {Object} value + @return {Ember.Enumerable} */ - removeAt: function(start, len) { - if ('number' === typeof start) { - - if ((start < 0) || (start >= get(this, 'length'))) { - throw new Ember.Error(OUT_OF_RANGE_EXCEPTION); - } - - // fast case - if (len === undefined) len = 1; - this.replace(start, len, EMPTY); - } - - return this ; + without: function(value) { + if (!this.contains(value)) return this; // nothing to do + var ret = Ember.A(); + this.forEach(function(k) { + if (k !== value) ret[ret.length] = k; + }) ; + return ret ; }, /** - Push the object onto the end of the array. Works just like `push()` but it - is KVO-compliant. + Returns a new enumerable that contains only unique values. The default + implementation returns an array regardless of the receiver type. ```javascript - var colors = ["red", "green", "blue"]; - colors.pushObject("black"); // ["red", "green", "blue", "black"] - colors.pushObject(["yellow", "orange"]); // ["red", "green", "blue", "black", ["yellow", "orange"]] + var arr = ["a", "a", "b", "b"]; + arr.uniq(); // ["a", "b"] ``` - @method pushObject - @param {*} obj object to push - @return {*} the same obj passed as param + @method uniq + @return {Ember.Enumerable} */ - pushObject: function(obj) { - this.insertAt(get(this, 'length'), obj) ; - return obj ; + uniq: function() { + var ret = Ember.A(); + this.forEach(function(k) { + if (a_indexOf(ret, k)<0) ret.push(k); + }); + return ret; }, /** - Add the objects in the passed numerable to the end of the array. Defers - notifying observers of the change until all objects are added. + This property will trigger anytime the enumerable's content changes. + You can observe this property to be notified of changes to the enumerables + content. - ```javascript - var colors = ["red", "green", "blue"]; - colors.pushObjects(["black"]); // ["red", "green", "blue", "black"] - colors.pushObjects(["yellow", "orange"]); // ["red", "green", "blue", "black", "yellow", "orange"] - ``` + For plain enumerables, this property is read only. `Ember.Array` overrides + this method. - @method pushObjects - @param {Ember.Enumerable} objects the objects to add - @return {Ember.Array} receiver + @property [] + @type Ember.Array + @return this */ - pushObjects: function(objects) { - if (!(Ember.Enumerable.detect(objects) || Ember.isArray(objects))) { - throw new TypeError("Must pass Ember.Enumerable to Ember.MutableArray#pushObjects"); - } - this.replace(get(this, 'length'), 0, objects); + '[]': Ember.computed(function(key, value) { return this; - }, + }), - /** - Pop object from array or nil if none are left. Works just like `pop()` but - it is KVO-compliant. + // .......................................................... + // ENUMERABLE OBSERVERS + // - ```javascript - var colors = ["red", "green", "blue"]; - colors.popObject(); // "blue" - console.log(colors); // ["red", "green"] - ``` + /** + Registers an enumerable observer. Must implement `Ember.EnumerableObserver` + mixin. - @method popObject - @return object + @method addEnumerableObserver + @param {Object} target + @param {Hash} [opts] + @return this */ - popObject: function() { - var len = get(this, 'length') ; - if (len === 0) return null ; + addEnumerableObserver: function(target, opts) { + var willChange = (opts && opts.willChange) || 'enumerableWillChange', + didChange = (opts && opts.didChange) || 'enumerableDidChange'; - var ret = this.objectAt(len-1) ; - this.removeAt(len-1, 1) ; - return ret ; + var hasObservers = get(this, 'hasEnumerableObservers'); + if (!hasObservers) Ember.propertyWillChange(this, 'hasEnumerableObservers'); + Ember.addListener(this, '@enumerable:before', target, willChange); + Ember.addListener(this, '@enumerable:change', target, didChange); + if (!hasObservers) Ember.propertyDidChange(this, 'hasEnumerableObservers'); + return this; }, /** - Shift an object from start of array or nil if none are left. Works just - like `shift()` but it is KVO-compliant. - - ```javascript - var colors = ["red", "green", "blue"]; - colors.shiftObject(); // "red" - console.log(colors); // ["green", "blue"] - ``` + Removes a registered enumerable observer. - @method shiftObject - @return object + @method removeEnumerableObserver + @param {Object} target + @param {Hash} [opts] + @return this */ - shiftObject: function() { - if (get(this, 'length') === 0) return null ; - var ret = this.objectAt(0) ; - this.removeAt(0) ; - return ret ; + removeEnumerableObserver: function(target, opts) { + var willChange = (opts && opts.willChange) || 'enumerableWillChange', + didChange = (opts && opts.didChange) || 'enumerableDidChange'; + + var hasObservers = get(this, 'hasEnumerableObservers'); + if (hasObservers) Ember.propertyWillChange(this, 'hasEnumerableObservers'); + Ember.removeListener(this, '@enumerable:before', target, willChange); + Ember.removeListener(this, '@enumerable:change', target, didChange); + if (hasObservers) Ember.propertyDidChange(this, 'hasEnumerableObservers'); + return this; }, /** - Unshift an object to start of array. Works just like `unshift()` but it is - KVO-compliant. - - ```javascript - var colors = ["red", "green", "blue"]; - colors.unshiftObject("yellow"); // ["yellow", "red", "green", "blue"] - colors.unshiftObject(["black", "white"]); // [["black", "white"], "yellow", "red", "green", "blue"] - ``` + Becomes true whenever the array currently has observers watching changes + on the array. - @method unshiftObject - @param {*} obj object to unshift - @return {*} the same obj passed as param + @property hasEnumerableObservers + @type Boolean */ - unshiftObject: function(obj) { - this.insertAt(0, obj) ; - return obj ; - }, + hasEnumerableObservers: Ember.computed(function() { + return Ember.hasListeners(this, '@enumerable:change') || Ember.hasListeners(this, '@enumerable:before'); + }), - /** - Adds the named objects to the beginning of the array. Defers notifying - observers until all objects have been added. - ```javascript - var colors = ["red", "green", "blue"]; - colors.unshiftObjects(["black", "white"]); // ["black", "white", "red", "green", "blue"] - colors.unshiftObjects("yellow"); // Type Error: 'undefined' is not a function - ``` + /** + Invoke this method just before the contents of your enumerable will + change. You can either omit the parameters completely or pass the objects + to be removed or added if available or just a count. - @method unshiftObjects - @param {Ember.Enumerable} objects the objects to add - @return {Ember.Array} receiver + @method enumerableContentWillChange + @param {Ember.Enumerable|Number} removing An enumerable of the objects to + be removed or the number of items to be removed. + @param {Ember.Enumerable|Number} adding An enumerable of the objects to be + added or the number of items to be added. + @chainable */ - unshiftObjects: function(objects) { - this.replace(0, 0, objects); - return this; - }, + enumerableContentWillChange: function(removing, adding) { - /** - Reverse objects in the array. Works just like `reverse()` but it is - KVO-compliant. + var removeCnt, addCnt, hasDelta; + + if ('number' === typeof removing) removeCnt = removing; + else if (removing) removeCnt = get(removing, 'length'); + else removeCnt = removing = -1; + + if ('number' === typeof adding) addCnt = adding; + else if (adding) addCnt = get(adding,'length'); + else addCnt = adding = -1; + + hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; + + if (removing === -1) removing = null; + if (adding === -1) adding = null; + + Ember.propertyWillChange(this, '[]'); + if (hasDelta) Ember.propertyWillChange(this, 'length'); + Ember.sendEvent(this, '@enumerable:before', [this, removing, adding]); - @method reverseObjects - @return {Ember.Array} receiver - */ - reverseObjects: function() { - var len = get(this, 'length'); - if (len === 0) return this; - var objects = this.toArray().reverse(); - this.replace(0, len, objects); return this; }, /** - Replace all the the receiver's content with content of the argument. - If argument is an empty array receiver will be cleared. + Invoke this method when the contents of your enumerable has changed. + This will notify any observers watching for content changes. If your are + implementing an ordered enumerable (such as an array), also pass the + start and end values where the content changed so that it can be used to + notify range observers. - ```javascript - var colors = ["red", "green", "blue"]; - colors.setObjects(["black", "white"]); // ["black", "white"] - colors.setObjects([]); // [] - ``` + @method enumerableContentDidChange + @param {Ember.Enumerable|Number} removing An enumerable of the objects to + be removed or the number of items to be removed. + @param {Ember.Enumerable|Number} adding An enumerable of the objects to + be added or the number of items to be added. + @chainable + */ + enumerableContentDidChange: function(removing, adding) { + var removeCnt, addCnt, hasDelta; - @method setObjects - @param {Ember.Array} objects array whose content will be used for replacing - the content of the receiver - @return {Ember.Array} receiver with the new content - */ - setObjects: function(objects) { - if (objects.length === 0) return this.clear(); + if ('number' === typeof removing) removeCnt = removing; + else if (removing) removeCnt = get(removing, 'length'); + else removeCnt = removing = -1; - var len = get(this, 'length'); - this.replace(0, len, objects); - return this; - }, + if ('number' === typeof adding) addCnt = adding; + else if (adding) addCnt = get(adding, 'length'); + else addCnt = adding = -1; - // .......................................................... - // IMPLEMENT Ember.MutableEnumerable - // + hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; + + if (removing === -1) removing = null; + if (adding === -1) adding = null; + + Ember.sendEvent(this, '@enumerable:change', [this, removing, adding]); + if (hasDelta) Ember.propertyDidChange(this, 'length'); + Ember.propertyDidChange(this, '[]'); - removeObject: function(obj) { - var loc = get(this, 'length') || 0; - while(--loc >= 0) { - var curObject = this.objectAt(loc) ; - if (curObject === obj) this.removeAt(loc) ; - } return this ; }, - addObject: function(obj) { - if (!this.contains(obj)) this.pushObject(obj); - return this ; - } + /** + Converts the enumerable into an array and sorts by the keys + specified in the argument. + You may provide multiple arguments to sort by multiple properties. + + @method sortBy + @param {String} property name(s) to sort on + @return {Array} The sorted array. + */ + sortBy: function() { + var sortKeys = arguments; + return this.toArray().sort(function(a, b){ + for(var i = 0; i < sortKeys.length; i++) { + var key = sortKeys[i], + propA = get(a, key), + propB = get(b, key); + // return 1 or -1 else continue to the next sortKey + var compareValue = Ember.compare(propA, propB); + if (compareValue) { return compareValue; } + } + return 0; + }); + } }); })(); @@ -13816,2340 +14977,2488 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable,/** @submodule ember-runtime */ -var get = Ember.get, - set = Ember.set, - slice = Array.prototype.slice, - getProperties = Ember.getProperties; - -/** - ## Overview - - This mixin provides properties and property observing functionality, core - features of the Ember object model. - - Properties and observers allow one object to observe changes to a - property on another object. This is one of the fundamental ways that - models, controllers and views communicate with each other in an Ember - application. - - Any object that has this mixin applied can be used in observer - operations. That includes `Ember.Object` and most objects you will - interact with as you write your Ember application. - - Note that you will not generally apply this mixin to classes yourself, - but you will use the features provided by this module frequently, so it - is important to understand how to use it. - - ## Using `get()` and `set()` - - Because of Ember's support for bindings and observers, you will always - access properties using the get method, and set properties using the - set method. This allows the observing objects to be notified and - computed properties to be handled properly. - - More documentation about `get` and `set` are below. +// .......................................................... +// HELPERS +// - ## Observing Property Changes +var get = Ember.get, set = Ember.set, isNone = Ember.isNone, map = Ember.EnumerableUtils.map, cacheFor = Ember.cacheFor; - You typically observe property changes simply by adding the `observes` - call to the end of your method declarations in classes that you write. - For example: +// .......................................................... +// ARRAY +// +/** + This mixin implements Observer-friendly Array-like behavior. It is not a + concrete implementation, but it can be used up by other classes that want + to appear like arrays. - ```javascript - Ember.Object.extend({ - valueObserver: function() { - // Executes whenever the "value" property changes - }.observes('value') - }); - ``` + For example, ArrayProxy and ArrayController are both concrete classes that can + be instantiated to implement array-like behavior. Both of these classes use + the Array Mixin by way of the MutableArray mixin, which allows observable + changes to be made to the underlying array. - Although this is the most common way to add an observer, this capability - is actually built into the `Ember.Object` class on top of two methods - defined in this mixin: `addObserver` and `removeObserver`. You can use - these two methods to add and remove observers yourself if you need to - do so at runtime. + Unlike `Ember.Enumerable,` this mixin defines methods specifically for + collections that provide index-ordered access to their contents. When you + are designing code that needs to accept any kind of Array-like object, you + should use these methods instead of Array primitives because these will + properly notify observers of changes to the array. - To add an observer for a property, call: + Although these methods are efficient, they do add a layer of indirection to + your application so it is a good idea to use them only when you need the + flexibility of using both true JavaScript arrays and "virtual" arrays such + as controllers and collections. - ```javascript - object.addObserver('propertyKey', targetObject, targetAction) - ``` + You can use the methods defined in this module to access and modify array + contents in a KVO-friendly way. You can also be notified whenever the + membership of an array changes by changing the syntax of the property to + `.observes('*myProperty.[]')`. - This will call the `targetAction` method on the `targetObject` whenever - the value of the `propertyKey` changes. + To support `Ember.Array` in your own class, you must override two + primitives to use it: `replace()` and `objectAt()`. - Note that if `propertyKey` is a computed property, the observer will be - called when any of the property dependencies are changed, even if the - resulting value of the computed property is unchanged. This is necessary - because computed properties are not computed until `get` is called. + Note that the Ember.Array mixin also incorporates the `Ember.Enumerable` + mixin. All `Ember.Array`-like objects are also enumerable. - @class Observable + @class Array @namespace Ember + @uses Ember.Enumerable + @since Ember 0.9.0 */ -Ember.Observable = Ember.Mixin.create({ +Ember.Array = Ember.Mixin.create(Ember.Enumerable, { /** - Retrieves the value of a property from the object. - - This method is usually similar to using `object[keyName]` or `object.keyName`, - however it supports both computed properties and the unknownProperty - handler. + Your array must support the `length` property. Your replace methods should + set this property whenever it changes. - Because `get` unifies the syntax for accessing all these kinds - of properties, it can make many refactorings easier, such as replacing a - simple property with a computed property, or vice versa. + @property {Number} length + */ + length: Ember.required(), - ### Computed Properties + /** + Returns the object at the given `index`. If the given `index` is negative + or is greater or equal than the array length, returns `undefined`. - Computed properties are methods defined with the `property` modifier - declared at the end, such as: + This is one of the primitives you must implement to support `Ember.Array`. + If your object supports retrieving the value of an array item using `get()` + (i.e. `myArray.get(0)`), then you do not need to implement this method + yourself. ```javascript - fullName: function() { - return this.getEach('firstName', 'lastName').compact().join(' '); - }.property('firstName', 'lastName') + var arr = ['a', 'b', 'c', 'd']; + arr.objectAt(0); // "a" + arr.objectAt(3); // "d" + arr.objectAt(-1); // undefined + arr.objectAt(4); // undefined + arr.objectAt(5); // undefined ``` - When you call `get` on a computed property, the function will be - called and the return value will be returned instead of the function - itself. - - ### Unknown Properties - - Likewise, if you try to call `get` on a property whose value is - `undefined`, the `unknownProperty()` method will be called on the object. - If this method returns any value other than `undefined`, it will be returned - instead. This allows you to implement "virtual" properties that are - not defined upfront. - - @method get - @param {String} keyName The property to retrieve - @return {Object} The property value or undefined. + @method objectAt + @param {Number} idx The index of the item to return. + @return {*} item at index or undefined */ - get: function(keyName) { - return get(this, keyName); + objectAt: function(idx) { + if ((idx < 0) || (idx>=get(this, 'length'))) return undefined ; + return get(this, idx); }, /** - To get multiple properties at once, call `getProperties` - with a list of strings or an array: - - ```javascript - record.getProperties('firstName', 'lastName', 'zipCode'); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } - ``` - - is equivalent to: + This returns the objects at the specified indexes, using `objectAt`. ```javascript - record.getProperties(['firstName', 'lastName', 'zipCode']); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } + var arr = ['a', 'b', 'c', 'd']; + arr.objectsAt([0, 1, 2]); // ["a", "b", "c"] + arr.objectsAt([2, 3, 4]); // ["c", "d", undefined] ``` - @method getProperties - @param {String...|Array} list of keys to get - @return {Hash} - */ - getProperties: function() { - return getProperties.apply(null, [this].concat(slice.call(arguments))); + @method objectsAt + @param {Array} indexes An array of indexes of items to return. + @return {Array} + */ + objectsAt: function(indexes) { + var self = this; + return map(indexes, function(idx) { return self.objectAt(idx); }); + }, + + // overrides Ember.Enumerable version + nextObject: function(idx) { + return this.objectAt(idx); }, /** - Sets the provided key or path to the value. + This is the handler for the special array content property. If you get + this property, it will return this. If you set this property it a new + array, it will replace the current content. - This method is generally very similar to calling `object[key] = value` or - `object.key = value`, except that it provides support for computed - properties, the `setUnknownProperty()` method and property observers. + This property overrides the default property defined in `Ember.Enumerable`. - ### Computed Properties + @property [] + @return this + */ + '[]': Ember.computed(function(key, value) { + if (value !== undefined) this.replace(0, get(this, 'length'), value) ; + return this ; + }), - If you try to set a value on a key that has a computed property handler - defined (see the `get()` method for an example), then `set()` will call - that method, passing both the value and key instead of simply changing - the value itself. This is useful for those times when you need to - implement a property that is composed of one or more member - properties. + firstObject: Ember.computed(function() { + return this.objectAt(0); + }), - ### Unknown Properties + lastObject: Ember.computed(function() { + return this.objectAt(get(this, 'length')-1); + }), - If you try to set a value on a key that is undefined in the target - object, then the `setUnknownProperty()` handler will be called instead. This - gives you an opportunity to implement complex "virtual" properties that - are not predefined on the object. If `setUnknownProperty()` returns - undefined, then `set()` will simply set the value on the object. + // optimized version from Enumerable + contains: function(obj) { + return this.indexOf(obj) >= 0; + }, - ### Property Observers + // Add any extra methods to Ember.Array that are native to the built-in Array. + /** + Returns a new array that is a slice of the receiver. This implementation + uses the observable array methods to retrieve the objects for the new + slice. - In addition to changing the property, `set()` will also register a property - change with the object. Unless you have placed this call inside of a - `beginPropertyChanges()` and `endPropertyChanges(),` any "local" observers - (i.e. observer methods declared on the same object), will be called - immediately. Any "remote" observers (i.e. observer methods declared on - another object) will be placed in a queue and called at a later time in a - coalesced manner. + ```javascript + var arr = ['red', 'green', 'blue']; + arr.slice(0); // ['red', 'green', 'blue'] + arr.slice(0, 2); // ['red', 'green'] + arr.slice(1, 100); // ['green', 'blue'] + ``` - ### Chaining + @method slice + @param {Integer} beginIndex (Optional) index to begin slicing from. + @param {Integer} endIndex (Optional) index to end the slice at (but not included). + @return {Array} New array with specified slice + */ + slice: function(beginIndex, endIndex) { + var ret = Ember.A(); + var length = get(this, 'length') ; + if (isNone(beginIndex)) beginIndex = 0 ; + if (isNone(endIndex) || (endIndex > length)) endIndex = length ; - In addition to property changes, `set()` returns the value of the object - itself so you can do chaining like this: + if (beginIndex < 0) beginIndex = length + beginIndex; + if (endIndex < 0) endIndex = length + endIndex; + + while(beginIndex < endIndex) { + ret[ret.length] = this.objectAt(beginIndex++) ; + } + return ret ; + }, + + /** + Returns the index of the given object's first occurrence. + If no `startAt` argument is given, the starting location to + search is 0. If it's negative, will count backward from + the end of the array. Returns -1 if no match is found. ```javascript - record.set('firstName', 'Charles').set('lastName', 'Jolley'); + var arr = ["a", "b", "c", "d", "a"]; + arr.indexOf("a"); // 0 + arr.indexOf("z"); // -1 + arr.indexOf("a", 2); // 4 + arr.indexOf("a", -1); // 4 + arr.indexOf("b", 3); // -1 + arr.indexOf("a", 100); // -1 ``` - @method set - @param {String} keyName The property to set - @param {Object} value The value to set or `null`. - @return {Ember.Observable} + @method indexOf + @param {Object} object the item to search for + @param {Number} startAt optional starting location to search, default 0 + @return {Number} index or -1 if not found */ - set: function(keyName, value) { - set(this, keyName, value); - return this; + indexOf: function(object, startAt) { + var idx, len = get(this, 'length'); + + if (startAt === undefined) startAt = 0; + if (startAt < 0) startAt += len; + + for(idx=startAt;idx= len) startAt = len-1; + if (startAt < 0) startAt += len; + + for(idx=startAt;idx>=0;idx--) { + if (this.objectAt(idx) === object) return idx ; + } + return -1; }, + // .......................................................... + // ARRAY OBSERVERS + // + /** - Begins a grouping of property changes. + Adds an array observer to the receiving array. The array observer object + normally must implement two methods: - You can use this method to group property changes so that notifications - will not be sent until the changes are finished. If you plan to make a - large number of changes to an object at one time, you should call this - method at the beginning of the changes to begin deferring change - notifications. When you are done making changes, call - `endPropertyChanges()` to deliver the deferred change notifications and end - deferring. + * `arrayWillChange(observedObj, start, removeCount, addCount)` - This method will be + called just before the array is modified. + * `arrayDidChange(observedObj, start, removeCount, addCount)` - This method will be + called just after the array is modified. - @method beginPropertyChanges - @return {Ember.Observable} + Both callbacks will be passed the observed object, starting index of the + change as well a a count of the items to be removed and added. You can use + these callbacks to optionally inspect the array during the change, clear + caches, or do any other bookkeeping necessary. + + In addition to passing a target, you can also include an options hash + which you can use to override the method names that will be invoked on the + target. + + @method addArrayObserver + @param {Object} target The observer object. + @param {Hash} opts Optional hash of configuration options including + `willChange` and `didChange` option. + @return {Ember.Array} receiver */ - beginPropertyChanges: function() { - Ember.beginPropertyChanges(); + addArrayObserver: function(target, opts) { + var willChange = (opts && opts.willChange) || 'arrayWillChange', + didChange = (opts && opts.didChange) || 'arrayDidChange'; + + var hasObservers = get(this, 'hasArrayObservers'); + if (!hasObservers) Ember.propertyWillChange(this, 'hasArrayObservers'); + Ember.addListener(this, '@array:before', target, willChange); + Ember.addListener(this, '@array:change', target, didChange); + if (!hasObservers) Ember.propertyDidChange(this, 'hasArrayObservers'); return this; }, /** - Ends a grouping of property changes. - - You can use this method to group property changes so that notifications - will not be sent until the changes are finished. If you plan to make a - large number of changes to an object at one time, you should call - `beginPropertyChanges()` at the beginning of the changes to defer change - notifications. When you are done making changes, call this method to - deliver the deferred change notifications and end deferring. + Removes an array observer from the object if the observer is current + registered. Calling this method multiple times with the same object will + have no effect. - @method endPropertyChanges - @return {Ember.Observable} + @method removeArrayObserver + @param {Object} target The object observing the array. + @param {Hash} opts Optional hash of configuration options including + `willChange` and `didChange` option. + @return {Ember.Array} receiver */ - endPropertyChanges: function() { - Ember.endPropertyChanges(); + removeArrayObserver: function(target, opts) { + var willChange = (opts && opts.willChange) || 'arrayWillChange', + didChange = (opts && opts.didChange) || 'arrayDidChange'; + + var hasObservers = get(this, 'hasArrayObservers'); + if (hasObservers) Ember.propertyWillChange(this, 'hasArrayObservers'); + Ember.removeListener(this, '@array:before', target, willChange); + Ember.removeListener(this, '@array:change', target, didChange); + if (hasObservers) Ember.propertyDidChange(this, 'hasArrayObservers'); return this; }, /** - Notify the observer system that a property is about to change. + Becomes true whenever the array currently has observers watching changes + on the array. - Sometimes you need to change a value directly or indirectly without - actually calling `get()` or `set()` on it. In this case, you can use this - method and `propertyDidChange()` instead. Calling these two methods - together will notify all observers that the property has potentially - changed value. + @property {Boolean} hasArrayObservers + */ + hasArrayObservers: Ember.computed(function() { + return Ember.hasListeners(this, '@array:change') || Ember.hasListeners(this, '@array:before'); + }), - Note that you must always call `propertyWillChange` and `propertyDidChange` - as a pair. If you do not, it may get the property change groups out of - order and cause notifications to be delivered more often than you would - like. + /** + If you are implementing an object that supports `Ember.Array`, call this + method just before the array content changes to notify any observers and + invalidate any related properties. Pass the starting index of the change + as well as a delta of the amounts to change. - @method propertyWillChange - @param {String} keyName The property key that is about to change. - @return {Ember.Observable} + @method arrayContentWillChange + @param {Number} startIdx The starting index in the array that will change. + @param {Number} removeAmt The number of items that will be removed. If you + pass `null` assumes 0 + @param {Number} addAmt The number of items that will be added. If you + pass `null` assumes 0. + @return {Ember.Array} receiver */ - propertyWillChange: function(keyName) { - Ember.propertyWillChange(this, keyName); - return this; - }, + arrayContentWillChange: function(startIdx, removeAmt, addAmt) { - /** - Notify the observer system that a property has just changed. + // if no args are passed assume everything changes + if (startIdx===undefined) { + startIdx = 0; + removeAmt = addAmt = -1; + } else { + if (removeAmt === undefined) removeAmt=-1; + if (addAmt === undefined) addAmt=-1; + } - Sometimes you need to change a value directly or indirectly without - actually calling `get()` or `set()` on it. In this case, you can use this - method and `propertyWillChange()` instead. Calling these two methods - together will notify all observers that the property has potentially - changed value. + // Make sure the @each proxy is set up if anyone is observing @each + if (Ember.isWatching(this, '@each')) { get(this, '@each'); } - Note that you must always call `propertyWillChange` and `propertyDidChange` - as a pair. If you do not, it may get the property change groups out of - order and cause notifications to be delivered more often than you would - like. + Ember.sendEvent(this, '@array:before', [this, startIdx, removeAmt, addAmt]); + + var removing, lim; + if (startIdx>=0 && removeAmt>=0 && get(this, 'hasEnumerableObservers')) { + removing = []; + lim = startIdx+removeAmt; + for(var idx=startIdx;idx=0 && addAmt>=0 && get(this, 'hasEnumerableObservers')) { + adding = []; + lim = startIdx+addAmt; + for(var idx=startIdx;idx Ember.TrackedArray instances. We use + // this to lazily recompute indexes for item property observers. + this.trackedArraysByGuid = {}; - /** - @deprecated - @method setPath - @param {String} path The path to the property that will be set - @param {Object} value The value to set or `null`. - @return {Ember.Observable} - */ - setPath: function(path, value) { - Ember.deprecate("setPath is deprecated since set now supports paths"); - return this.set(path, value); - }, + // We suspend observers to ignore replacements from `reset` when totally + // recomputing. Unfortunately we cannot properly suspend the observers + // because we only have the key; instead we make the observers no-ops + this.suspended = false; - /** - Retrieves the value of a property, or a default value in the case that the - property returns `undefined`. + // This is used to coalesce item changes from property observers. + this.changedItems = {}; +} - ```javascript - person.getWithDefault('lastName', 'Doe'); - ``` +function ItemPropertyObserverContext (dependentArray, index, trackedArray) { + Ember.assert("Internal error: trackedArray is null or undefined", trackedArray); - @method getWithDefault - @param {String} keyName The name of the property to retrieve - @param {Object} defaultValue The value to return if the property value is undefined - @return {Object} The property value or the defaultValue. - */ - getWithDefault: function(keyName, defaultValue) { - return Ember.getWithDefault(this, keyName, defaultValue); + this.dependentArray = dependentArray; + this.index = index; + this.item = dependentArray.objectAt(index); + this.trackedArray = trackedArray; + this.beforeObserver = null; + this.observer = null; + + this.destroyed = false; +} + +DependentArraysObserver.prototype = { + setValue: function (newValue) { + this.instanceMeta.setValue(newValue, true); + }, + getValue: function () { + return this.instanceMeta.getValue(); }, - /** - Set the value of a property to the current value plus some amount. + setupObservers: function (dependentArray, dependentKey) { + this.dependentKeysByGuid[guidFor(dependentArray)] = dependentKey; - ```javascript - person.incrementProperty('age'); - team.incrementProperty('score', 2); - ``` + dependentArray.addArrayObserver(this, { + willChange: 'dependentArrayWillChange', + didChange: 'dependentArrayDidChange' + }); - @method incrementProperty - @param {String} keyName The name of the property to increment - @param {Number} increment The amount to increment by. Defaults to 1 - @return {Number} The new property value - */ - incrementProperty: function(keyName, increment) { - if (Ember.isNone(increment)) { increment = 1; } - Ember.assert("Must pass a numeric value to incrementProperty", (!isNaN(parseFloat(increment)) && isFinite(increment))); - set(this, keyName, (get(this, keyName) || 0) + increment); - return get(this, keyName); + if (this.cp._itemPropertyKeys[dependentKey]) { + this.setupPropertyObservers(dependentKey, this.cp._itemPropertyKeys[dependentKey]); + } }, - /** - Set the value of a property to the current value minus some amount. + teardownObservers: function (dependentArray, dependentKey) { + var itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || []; - ```javascript - player.decrementProperty('lives'); - orc.decrementProperty('health', 5); - ``` + delete this.dependentKeysByGuid[guidFor(dependentArray)]; - @method decrementProperty - @param {String} keyName The name of the property to decrement - @param {Number} decrement The amount to decrement by. Defaults to 1 - @return {Number} The new property value - */ - decrementProperty: function(keyName, decrement) { - if (Ember.isNone(decrement)) { decrement = 1; } - Ember.assert("Must pass a numeric value to decrementProperty", (!isNaN(parseFloat(decrement)) && isFinite(decrement))); - set(this, keyName, (get(this, keyName) || 0) - decrement); - return get(this, keyName); + this.teardownPropertyObservers(dependentKey, itemPropertyKeys); + + dependentArray.removeArrayObserver(this, { + willChange: 'dependentArrayWillChange', + didChange: 'dependentArrayDidChange' + }); }, - /** - Set the value of a boolean property to the opposite of it's - current value. + suspendArrayObservers: function (callback, binding) { + var oldSuspended = this.suspended; + this.suspended = true; + callback.call(binding); + this.suspended = oldSuspended; + }, - ```javascript - starship.toggleProperty('warpDriveEngaged'); - ``` + setupPropertyObservers: function (dependentKey, itemPropertyKeys) { + var dependentArray = get(this.instanceMeta.context, dependentKey), + length = get(dependentArray, 'length'), + observerContexts = new Array(length); - @method toggleProperty - @param {String} keyName The name of the property to toggle - @return {Object} The new property value - */ - toggleProperty: function(keyName) { - set(this, keyName, !get(this, keyName)); - return get(this, keyName); - }, + this.resetTransformations(dependentKey, observerContexts); - /** - Returns the cached value of a computed property, if it exists. - This allows you to inspect the value of a computed property - without accidentally invoking it if it is intended to be - generated lazily. + forEach(dependentArray, function (item, index) { + var observerContext = this.createPropertyObserverContext(dependentArray, index, this.trackedArraysByGuid[dependentKey]); + observerContexts[index] = observerContext; - @method cacheFor - @param {String} keyName - @return {Object} The cached value of the computed property, if any - */ - cacheFor: function(keyName) { - return Ember.cacheFor(this, keyName); + forEach(itemPropertyKeys, function (propertyKey) { + addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); + addObserver(item, propertyKey, this, observerContext.observer); + }, this); + }, this); }, - // intended for debugging purposes - observersForKey: function(keyName) { - return Ember.observersFor(this, keyName); - } -}); + teardownPropertyObservers: function (dependentKey, itemPropertyKeys) { + var dependentArrayObserver = this, + trackedArray = this.trackedArraysByGuid[dependentKey], + beforeObserver, + observer, + item; -})(); + if (!trackedArray) { return; } + trackedArray.apply(function (observerContexts, offset, operation) { + if (operation === Ember.TrackedArray.DELETE) { return; } + + forEach(observerContexts, function (observerContext) { + observerContext.destroyed = true; + beforeObserver = observerContext.beforeObserver; + observer = observerContext.observer; + item = observerContext.item; + forEach(itemPropertyKeys, function (propertyKey) { + removeBeforeObserver(item, propertyKey, dependentArrayObserver, beforeObserver); + removeObserver(item, propertyKey, dependentArrayObserver, observer); + }); + }); + }); + }, -(function() { -/** -@module ember -@submodule ember-runtime -*/ + createPropertyObserverContext: function (dependentArray, index, trackedArray) { + var observerContext = new ItemPropertyObserverContext(dependentArray, index, trackedArray); -var get = Ember.get, set = Ember.set; + this.createPropertyObserver(observerContext); -/** -`Ember.TargetActionSupport` is a mixin that can be included in a class -to add a `triggerAction` method with semantics similar to the Handlebars -`{{action}}` helper. In normal Ember usage, the `{{action}}` helper is -usually the best choice. This mixin is most often useful when you are -doing more complex event handling in View objects. + return observerContext; + }, -See also `Ember.ViewTargetActionSupport`, which has -view-aware defaults for target and actionContext. + createPropertyObserver: function (observerContext) { + var dependentArrayObserver = this; -@class TargetActionSupport -@namespace Ember -@extends Ember.Mixin -*/ -Ember.TargetActionSupport = Ember.Mixin.create({ - target: null, - action: null, - actionContext: null, + observerContext.beforeObserver = function (obj, keyName) { + return dependentArrayObserver.itemPropertyWillChange(obj, keyName, observerContext.dependentArray, observerContext); + }; + observerContext.observer = function (obj, keyName) { + return dependentArrayObserver.itemPropertyDidChange(obj, keyName, observerContext.dependentArray, observerContext); + }; + }, - targetObject: Ember.computed(function() { - var target = get(this, 'target'); + resetTransformations: function (dependentKey, observerContexts) { + this.trackedArraysByGuid[dependentKey] = new Ember.TrackedArray(observerContexts); + }, - if (Ember.typeOf(target) === "string") { - var value = get(this, target); - if (value === undefined) { value = get(Ember.lookup, target); } - return value; - } else { - return target; + trackAdd: function (dependentKey, index, newItems) { + var trackedArray = this.trackedArraysByGuid[dependentKey]; + if (trackedArray) { + trackedArray.addItems(index, newItems); } - }).property('target'), + }, - actionContextObject: Ember.computed(function() { - var actionContext = get(this, 'actionContext'); + trackRemove: function (dependentKey, index, removedCount) { + var trackedArray = this.trackedArraysByGuid[dependentKey]; - if (Ember.typeOf(actionContext) === "string") { - var value = get(this, actionContext); - if (value === undefined) { value = get(Ember.lookup, actionContext); } - return value; - } else { - return actionContext; + if (trackedArray) { + return trackedArray.removeItems(index, removedCount); } - }).property('actionContext'), - /** - Send an "action" with an "actionContext" to a "target". The action, actionContext - and target will be retrieved from properties of the object. For example: + return []; + }, - ```javascript - App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { - target: Ember.computed.alias('controller'), - action: 'save', - actionContext: Ember.computed.alias('context'), - click: function() { - this.triggerAction(); // Sends the `save` action, along with the current context - // to the current controller - } - }); - ``` + updateIndexes: function (trackedArray, array) { + var length = get(array, 'length'); + // OPTIMIZE: we could stop updating once we hit the object whose observer + // fired; ie partially apply the transformations + trackedArray.apply(function (observerContexts, offset, operation) { + // we don't even have observer contexts for removed items, even if we did, + // they no longer have any index in the array + if (operation === Ember.TrackedArray.DELETE) { return; } + if (operation === Ember.TrackedArray.RETAIN && observerContexts.length === length && offset === 0) { + // If we update many items we don't want to walk the array each time: we + // only need to update the indexes at most once per run loop. + return; + } - The `target`, `action`, and `actionContext` can be provided as properties of - an optional object argument to `triggerAction` as well. + forEach(observerContexts, function (context, index) { + context.index = index + offset; + }); + }); + }, - ```javascript - App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { - click: function() { - this.triggerAction({ - action: 'save', - target: this.get('controller'), - actionContext: this.get('context'), - }); // Sends the `save` action, along with the current context - // to the current controller - } - }); - ``` + dependentArrayWillChange: function (dependentArray, index, removedCount, addedCount) { + if (this.suspended) { return; } - The `actionContext` defaults to the object you mixing `TargetActionSupport` into. - But `target` and `action` must be specified either as properties or with the argument - to `triggerAction`, or a combination: + var removedItem = this.callbacks.removedItem, + changeMeta, + guid = guidFor(dependentArray), + dependentKey = this.dependentKeysByGuid[guid], + itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || [], + length = get(dependentArray, 'length'), + normalizedIndex = normalizeIndex(index, length, 0), + normalizedRemoveCount = normalizeRemoveCount(normalizedIndex, length, removedCount), + item, + itemIndex, + sliceIndex, + observerContexts; - ```javascript - App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { - target: Ember.computed.alias('controller'), - click: function() { - this.triggerAction({ - action: 'save' - }); // Sends the `save` action, along with a reference to `this`, - // to the current controller + observerContexts = this.trackRemove(dependentKey, normalizedIndex, normalizedRemoveCount); + + function removeObservers(propertyKey) { + observerContexts[sliceIndex].destroyed = true; + removeBeforeObserver(item, propertyKey, this, observerContexts[sliceIndex].beforeObserver); + removeObserver(item, propertyKey, this, observerContexts[sliceIndex].observer); } - }); - ``` - @method triggerAction - @param opts {Hash} (optional, with the optional keys action, target and/or actionContext) - @return {Boolean} true if the action was sent successfully and did not return false - */ - triggerAction: function(opts) { - opts = opts || {}; - var action = opts.action || get(this, 'action'), - target = opts.target || get(this, 'targetObject'), - actionContext = opts.actionContext; + for (sliceIndex = normalizedRemoveCount - 1; sliceIndex >= 0; --sliceIndex) { + itemIndex = normalizedIndex + sliceIndex; + if (itemIndex >= length) { break; } - function args(options, actionName) { - var ret = []; - if (actionName) { ret.push(actionName); } + item = dependentArray.objectAt(itemIndex); - return ret.concat(options); - } + forEach(itemPropertyKeys, removeObservers, this); - if (typeof actionContext === 'undefined') { - actionContext = get(this, 'actionContextObject') || this; + changeMeta = createChangeMeta(dependentArray, item, itemIndex, this.instanceMeta.propertyName, this.cp); + this.setValue( removedItem.call( + this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); } + }, - if (target && action) { - var ret; + dependentArrayDidChange: function (dependentArray, index, removedCount, addedCount) { + if (this.suspended) { return; } - if (target.send) { - ret = target.send.apply(target, args(actionContext, action)); - } else { - Ember.assert("The action '" + action + "' did not exist on " + target, typeof target[action] === 'function'); - ret = target[action].apply(target, args(actionContext)); + var addedItem = this.callbacks.addedItem, + guid = guidFor(dependentArray), + dependentKey = this.dependentKeysByGuid[guid], + observerContexts = new Array(addedCount), + itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey], + length = get(dependentArray, 'length'), + normalizedIndex = normalizeIndex(index, length, addedCount), + changeMeta, + observerContext; + + forEach(dependentArray.slice(normalizedIndex, normalizedIndex + addedCount), function (item, sliceIndex) { + if (itemPropertyKeys) { + observerContext = + observerContexts[sliceIndex] = + this.createPropertyObserverContext(dependentArray, normalizedIndex + sliceIndex, this.trackedArraysByGuid[dependentKey]); + forEach(itemPropertyKeys, function (propertyKey) { + addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); + addObserver(item, propertyKey, this, observerContext.observer); + }, this); } - if (ret !== false) ret = true; + changeMeta = createChangeMeta(dependentArray, item, normalizedIndex + sliceIndex, this.instanceMeta.propertyName, this.cp); + this.setValue( addedItem.call( + this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); + }, this); - return ret; - } else { - return false; + this.trackAdd(dependentKey, normalizedIndex, observerContexts); + }, + + itemPropertyWillChange: function (obj, keyName, array, observerContext) { + var guid = guidFor(obj); + + if (!this.changedItems[guid]) { + this.changedItems[guid] = { + array: array, + observerContext: observerContext, + obj: obj, + previousValues: {} + }; } - } -}); -})(); + this.changedItems[guid].previousValues[keyName] = get(obj, keyName); + }, + itemPropertyDidChange: function(obj, keyName, array, observerContext) { + this.flushChanges(); + }, + flushChanges: function() { + var changedItems = this.changedItems, key, c, changeMeta; -(function() { -/** -@module ember -@submodule ember-runtime -*/ + for (key in changedItems) { + c = changedItems[key]; + if (c.observerContext.destroyed) { continue; } -/** - This mixin allows for Ember objects to subscribe to and emit events. + this.updateIndexes(c.observerContext.trackedArray, c.observerContext.dependentArray); - ```javascript - App.Person = Ember.Object.extend(Ember.Evented, { - greet: function() { - // ... - this.trigger('greet'); + changeMeta = createChangeMeta(c.array, c.obj, c.observerContext.index, this.instanceMeta.propertyName, this.cp, c.previousValues); + this.setValue( + this.callbacks.removedItem.call(this.instanceMeta.context, this.getValue(), c.obj, changeMeta, this.instanceMeta.sugarMeta)); + this.setValue( + this.callbacks.addedItem.call(this.instanceMeta.context, this.getValue(), c.obj, changeMeta, this.instanceMeta.sugarMeta)); } - }); + this.changedItems = {}; + } +}; - var person = App.Person.create(); +function normalizeIndex(index, length, newItemsOffset) { + if (index < 0) { + return Math.max(0, length + index); + } else if (index < length) { + return index; + } else /* index > length */ { + return Math.min(length - newItemsOffset, index); + } +} - person.on('greet', function() { - console.log('Our person has greeted'); - }); +function normalizeRemoveCount(index, length, removedCount) { + return Math.min(removedCount, length - index); +} - person.greet(); +function createChangeMeta(dependentArray, item, index, propertyName, property, previousValues) { + var meta = { + arrayChanged: dependentArray, + index: index, + item: item, + propertyName: propertyName, + property: property + }; - // outputs: 'Our person has greeted' - ``` + if (previousValues) { + // previous values only available for item property changes + meta.previousValues = previousValues; + } - You can also chain multiple event subscriptions: + return meta; +} - ```javascript - person.on('greet', function() { - console.log('Our person has greeted'); - }).one('greet', function() { - console.log('Offer one-time special'); - }).off('event', this, forgetThis); - ``` +function addItems (dependentArray, callbacks, cp, propertyName, meta) { + forEach(dependentArray, function (item, index) { + meta.setValue( callbacks.addedItem.call( + this, meta.getValue(), item, createChangeMeta(dependentArray, item, index, propertyName, cp), meta.sugarMeta)); + }, this); +} - @class Evented - @namespace Ember - */ -Ember.Evented = Ember.Mixin.create({ +function reset(cp, propertyName) { + var callbacks = cp._callbacks(), + meta; - /** - Subscribes to a named event with given function. + if (cp._hasInstanceMeta(this, propertyName)) { + meta = cp._instanceMeta(this, propertyName); + meta.setValue(cp.resetValue(meta.getValue())); + } else { + meta = cp._instanceMeta(this, propertyName); + } - ```javascript - person.on('didLoad', function() { - // fired once the person has loaded - }); - ``` + if (cp.options.initialize) { + cp.options.initialize.call(this, meta.getValue(), { property: cp, propertyName: propertyName }, meta.sugarMeta); + } +} - An optional target can be passed in as the 2nd argument that will - be set as the "this" for the callback. This is a good way to give your - function access to the object triggering the event. When the target - parameter is used the callback becomes the third argument. +function partiallyRecomputeFor(obj, dependentKey) { + if (arrayBracketPattern.test(dependentKey)) { + return false; + } - @method on - @param {String} name The name of the event - @param {Object} [target] The "this" binding for the callback - @param {Function} method The callback to execute - @return this - */ - on: function(name, target, method) { - Ember.addListener(this, name, target, method); - return this; - }, + var value = get(obj, dependentKey); + return Ember.Array.detect(value); +} - /** - Subscribes a function to a named event and then cancels the subscription - after the first time the event is triggered. It is good to use ``one`` when - you only care about the first time an event has taken place. +function ReduceComputedPropertyInstanceMeta(context, propertyName, initialValue) { + this.context = context; + this.propertyName = propertyName; + this.cache = metaFor(context).cache; - This function takes an optional 2nd argument that will become the "this" - value for the callback. If this argument is passed then the 3rd argument - becomes the function. + this.dependentArrays = {}; + this.sugarMeta = {}; - @method one - @param {String} name The name of the event - @param {Object} [target] The "this" binding for the callback - @param {Function} method The callback to execute - @return this - */ - one: function(name, target, method) { - if (!method) { - method = target; - target = null; - } + this.initialValue = initialValue; +} - Ember.addListener(this, name, target, method, true); - return this; +ReduceComputedPropertyInstanceMeta.prototype = { + getValue: function () { + if (this.propertyName in this.cache) { + return this.cache[this.propertyName]; + } else { + return this.initialValue; + } }, - /** - Triggers a named event for the object. Any additional arguments - will be passed as parameters to the functions that are subscribed to the - event. - - ```javascript - person.on('didEat', function(food) { - console.log('person ate some ' + food); - }); + setValue: function(newValue, triggerObservers) { + // This lets sugars force a recomputation, handy for very simple + // implementations of eg max. + if (newValue === this.cache[this.propertyName]) { + return; + } - person.trigger('didEat', 'broccoli'); + if (triggerObservers) { + propertyWillChange(this.context, this.propertyName); + } - // outputs: person ate some broccoli - ``` - @method trigger - @param {String} name The name of the event - @param {Object...} args Optional arguments to pass on - */ - trigger: function(name) { - var args = [], i, l; - for (i = 1, l = arguments.length; i < l; i++) { - args.push(arguments[i]); + if (newValue === undefined) { + delete this.cache[this.propertyName]; + } else { + this.cache[this.propertyName] = newValue; } - Ember.sendEvent(this, name, args); - }, - /** - Cancels subscription for given name, target, and method. + if (triggerObservers) { + propertyDidChange(this.context, this.propertyName); + } + } +}; - @method off - @param {String} name The name of the event - @param {Object} target The target of the subscription - @param {Function} method The function of the subscription - @return this - */ - off: function(name, target, method) { - Ember.removeListener(this, name, target, method); - return this; - }, +/** + A computed property whose dependent keys are arrays and which is updated with + "one at a time" semantics. - /** - Checks to see if object has any subscriptions for named event. + @class ReduceComputedProperty + @namespace Ember + @extends Ember.ComputedProperty + @constructor +*/ +function ReduceComputedProperty(options) { + var cp = this; - @method has - @param {String} name The name of the event - @return {Boolean} does the object have a subscription for event - */ - has: function(name) { - return Ember.hasListeners(this, name); - } -}); + this.options = options; -})(); + this._dependentKeys = null; + // A map of dependentKey -> [itemProperty, ...] that tracks what properties of + // items in the array we must track to update this property. + this._itemPropertyKeys = {}; + this._previousItemPropertyKeys = {}; + this.readOnly(); + this.cacheable(); + this.recomputeOnce = function(propertyName) { + // What we really want to do is coalesce by . + // We need a form of `scheduleOnce` that accepts an arbitrary token to + // coalesce by, in addition to the target and method. + Ember.run.once(this, recompute, propertyName); + }; + var recompute = function(propertyName) { + var dependentKeys = cp._dependentKeys, + meta = cp._instanceMeta(this, propertyName), + callbacks = cp._callbacks(); -(function() { -var RSVP = requireModule("rsvp"); + reset.call(this, cp, propertyName); -RSVP.configure('async', function(callback, promise) { - Ember.run.schedule('actions', promise, callback, promise); -}); + meta.dependentArraysObserver.suspendArrayObservers(function () { + forEach(cp._dependentKeys, function (dependentKey) { + Ember.assert( + "dependent array " + dependentKey + " must be an `Ember.Array`. " + + "If you are not extending arrays, you will need to wrap native arrays with `Ember.A`", + !(Ember.isArray(get(this, dependentKey)) && !Ember.Array.detect(get(this, dependentKey)))); + + if (!partiallyRecomputeFor(this, dependentKey)) { return; } + + var dependentArray = get(this, dependentKey), + previousDependentArray = meta.dependentArrays[dependentKey]; + + if (dependentArray === previousDependentArray) { + // The array may be the same, but our item property keys may have + // changed, so we set them up again. We can't easily tell if they've + // changed: the array may be the same object, but with different + // contents. + if (cp._previousItemPropertyKeys[dependentKey]) { + delete cp._previousItemPropertyKeys[dependentKey]; + meta.dependentArraysObserver.setupPropertyObservers(dependentKey, cp._itemPropertyKeys[dependentKey]); + } + } else { + meta.dependentArrays[dependentKey] = dependentArray; -/** -@module ember -@submodule ember-runtime -*/ + if (previousDependentArray) { + meta.dependentArraysObserver.teardownObservers(previousDependentArray, dependentKey); + } -var get = Ember.get; + if (dependentArray) { + meta.dependentArraysObserver.setupObservers(dependentArray, dependentKey); + } + } + }, this); + }, this); -/** - @class Deferred - @namespace Ember - */ -Ember.DeferredMixin = Ember.Mixin.create({ - /** - Add handlers to be called when the Deferred object is resolved or rejected. + forEach(cp._dependentKeys, function(dependentKey) { + if (!partiallyRecomputeFor(this, dependentKey)) { return; } - @method then - @param {Function} resolve a callback function to be called when done - @param {Function} reject a callback function to be called when failed - */ - then: function(resolve, reject) { - var deferred, promise, entity; + var dependentArray = get(this, dependentKey); + if (dependentArray) { + addItems.call(this, dependentArray, callbacks, cp, propertyName, meta); + } + }, this); + }; - entity = this; - deferred = get(this, '_deferred'); - promise = deferred.promise; - function fulfillmentHandler(fulfillment) { - if (fulfillment === promise) { - return resolve(entity); - } else { - return resolve(fulfillment); - } - } + this.func = function (propertyName) { + Ember.assert("Computed reduce values require at least one dependent key", cp._dependentKeys); - return promise.then(resolve && fulfillmentHandler, reject); - }, + recompute.call(this, propertyName); - /** - Resolve a Deferred object and call any `doneCallbacks` with the given args. + return cp._instanceMeta(this, propertyName).getValue(); + }; +} - @method resolve - */ - resolve: function(value) { - var deferred, promise; +Ember.ReduceComputedProperty = ReduceComputedProperty; +ReduceComputedProperty.prototype = o_create(ComputedProperty.prototype); - deferred = get(this, '_deferred'); - promise = deferred.promise; +function defaultCallback(computedValue) { + return computedValue; +} - if (value === this) { - deferred.resolve(promise); +ReduceComputedProperty.prototype._callbacks = function () { + if (!this.callbacks) { + var options = this.options; + this.callbacks = { + removedItem: options.removedItem || defaultCallback, + addedItem: options.addedItem || defaultCallback + }; + } + return this.callbacks; +}; + +ReduceComputedProperty.prototype._hasInstanceMeta = function (context, propertyName) { + return !!metaFor(context).cacheMeta[propertyName]; +}; + +ReduceComputedProperty.prototype._instanceMeta = function (context, propertyName) { + var cacheMeta = metaFor(context).cacheMeta, + meta = cacheMeta[propertyName]; + + if (!meta) { + meta = cacheMeta[propertyName] = new ReduceComputedPropertyInstanceMeta(context, propertyName, this.initialValue()); + meta.dependentArraysObserver = new DependentArraysObserver(this._callbacks(), this, meta, context, propertyName, meta.sugarMeta); + } + + return meta; +}; + +ReduceComputedProperty.prototype.initialValue = function () { + if (typeof this.options.initialValue === 'function') { + return this.options.initialValue(); + } + else { + return this.options.initialValue; + } +}; + +ReduceComputedProperty.prototype.resetValue = function (value) { + return this.initialValue(); +}; + +ReduceComputedProperty.prototype.itemPropertyKey = function (dependentArrayKey, itemPropertyKey) { + this._itemPropertyKeys[dependentArrayKey] = this._itemPropertyKeys[dependentArrayKey] || []; + this._itemPropertyKeys[dependentArrayKey].push(itemPropertyKey); +}; + +ReduceComputedProperty.prototype.clearItemPropertyKeys = function (dependentArrayKey) { + if (this._itemPropertyKeys[dependentArrayKey]) { + this._previousItemPropertyKeys[dependentArrayKey] = this._itemPropertyKeys[dependentArrayKey]; + this._itemPropertyKeys[dependentArrayKey] = []; + } +}; + +ReduceComputedProperty.prototype.property = function () { + var cp = this, + args = a_slice.call(arguments), + propertyArgs = new Ember.Set(), + match, + dependentArrayKey, + itemPropertyKey; + + forEach(args, function (dependentKey) { + if (doubleEachPropertyPattern.test(dependentKey)) { + throw new Ember.Error("Nested @each properties not supported: " + dependentKey); + } else if (match = eachPropertyPattern.exec(dependentKey)) { + dependentArrayKey = match[1]; + + var itemPropertyKeyPattern = match[2], + addItemPropertyKey = function (itemPropertyKey) { + cp.itemPropertyKey(dependentArrayKey, itemPropertyKey); + }; + + expandProperties(itemPropertyKeyPattern, addItemPropertyKey); + propertyArgs.add(dependentArrayKey); } else { - deferred.resolve(value); + propertyArgs.add(dependentKey); } - }, + }); - /** - Reject a Deferred object and call any `failCallbacks` with the given args. + return ComputedProperty.prototype.property.apply(this, propertyArgs.toArray()); - @method reject - */ - reject: function(value) { - get(this, '_deferred').reject(value); - }, +}; - _deferred: Ember.computed(function() { - return RSVP.defer(); - }) -}); +/** + Creates a computed property which operates on dependent arrays and + is updated with "one at a time" semantics. When items are added or + removed from the dependent array(s) a reduce computed only operates + on the change instead of re-evaluating the entire array. + If there are more than one arguments the first arguments are + considered to be dependent property keys. The last argument is + required to be an options object. The options object can have the + following four properties: -})(); + `initialValue` - A value or function that will be used as the initial + value for the computed. If this property is a function the result of calling + the function will be used as the initial value. This property is required. + `initialize` - An optional initialize function. Typically this will be used + to set up state on the instanceMeta object. + `removedItem` - A function that is called each time an element is removed + from the array. -(function() { -/** -@module ember -@submodule ember-runtime -*/ + `addedItem` - A function that is called each time an element is added to + the array. -var get = Ember.get; -/** - The `Ember.ActionHandler` mixin implements support for moving an `actions` - property to an `_actions` property at extend time, and adding `_actions` - to the object's mergedProperties list. + The `initialize` function has the following signature: - `Ember.ActionHandler` is used internally by Ember in `Ember.View`, - `Ember.Controller`, and `Ember.Route`. + ```javascript + function (initialValue, changeMeta, instanceMeta) + ``` - @class ActionHandler - @namespace Ember -*/ -Ember.ActionHandler = Ember.Mixin.create({ - mergedProperties: ['_actions'], + `initialValue` - The value of the `initialValue` property from the + options object. - /** - @private + `changeMeta` - An object which contains meta information about the + computed. It contains the following properties: - Moves `actions` to `_actions` at extend time. Note that this currently - modifies the mixin themselves, which is technically dubious but - is practically of little consequence. This may change in the future. + - `property` the computed property + - `propertyName` the name of the property on the object - @method willMergeMixin - */ - willMergeMixin: function(props) { - if (props.actions && !props._actions) { - props._actions = Ember.merge(props._actions || {}, props.actions); - delete props.actions; - } - }, + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. - send: function(actionName) { - var args = [].slice.call(arguments, 1), target; - if (this._actions && this._actions[actionName]) { - if (this._actions[actionName].apply(this, args) === true) { - // handler returned true, so this action will bubble - } else { - return; - } - } else if (this.deprecatedSend && this.deprecatedSendHandles && this.deprecatedSendHandles(actionName)) { - if (this.deprecatedSend.apply(this, [].slice.call(arguments)) === true) { - // handler return true, so this action will bubble - } else { - return; - } - } + The `removedItem` and `addedItem` functions both have the following signature: - if (target = get(this, 'target')) { - Ember.assert("The `target` for " + this + " (" + target + ") does not have a `send` method", typeof target.send === 'function'); - target.send.apply(target, arguments); - } - } + ```javascript + function (accumulatedValue, item, changeMeta, instanceMeta) + ``` -}); + `accumulatedValue` - The value returned from the last time + `removedItem` or `addedItem` was called or `initialValue`. -})(); + `item` - the element added or removed from the array + `changeMeta` - An object which contains meta information about the + change. It contains the following properties: + - `property` the computed property + - `propertyName` the name of the property on the object + - `index` the index of the added or removed item + - `item` the added or removed item: this is exactly the same as + the second arg + - `arrayChanged` the array that triggered the change. Can be + useful when depending on multiple arrays. -(function() { -var set = Ember.set, get = Ember.get, - resolve = Ember.RSVP.resolve, - rethrow = Ember.RSVP.rethrow, - not = Ember.computed.not, - or = Ember.computed.or; + For property changes triggered on an item property change (when + depKey is something like `someArray.@each.someProperty`), + `changeMeta` will also contain the following property: -/** - @module ember - @submodule ember-runtime - */ + - `previousValues` an object whose keys are the properties that changed on + the item, and whose values are the item's previous values. -function installPromise(proxy, promise) { - promise.then(function(value) { - set(proxy, 'isFulfilled', true); - set(proxy, 'content', value); + `previousValues` is important Ember coalesces item property changes via + Ember.run.once. This means that by the time removedItem gets called, item has + the new values, but you may need the previous value (eg for sorting & + filtering). - return value; - }, function(reason) { - set(proxy, 'isRejected', true); - set(proxy, 'reason', reason); - }).fail(rethrow); -} + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. -/** - A low level mixin making ObjectProxy, ObjectController or ArrayController's promise aware. + The `removedItem` and `addedItem` functions should return the accumulated + value. It is acceptable to not return anything (ie return undefined) + to invalidate the computation. This is generally not a good idea for + arrayComputed but it's used in eg max and min. + + Note that observers will be fired if either of these functions return a value + that differs from the accumulated value. When returning an object that + mutates in response to array changes, for example an array that maps + everything from some other array (see `Ember.computed.map`), it is usually + important that the *same* array be returned to avoid accidentally triggering observers. + + Example ```javascript - var ObjectPromiseController = Ember.ObjectController.extend(Ember.PromiseProxyMixin); + Ember.computed.max = function (dependentKey) { + return Ember.reduceComputed(dependentKey, { + initialValue: -Infinity, - var controller = ObjectPromiseController.create({ - promise: $.getJSON('/some/remote/data.json') - }); + addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + return Math.max(accumulatedValue, item); + }, - controller.then(function(json){ - // the json - }, function(reason) { - // the reason why you have no json - }); + removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + if (item < accumulatedValue) { + return accumulatedValue; + } + } + }); + }; ``` - the controller has bindable attributes which - track the promises life cycle + Dependent keys may refer to `@this` to observe changes to the object itself, + which must be array-like, rather than a property of the object. This is + mostly useful for array proxies, to ensure objects are retrieved via + `objectAtContent`. This is how you could sort items by properties defined on an item controller. + + Example ```javascript - controller.get('isPending') //=> true - controller.get('isSettled') //=> false - controller.get('isRejected') //=> false - controller.get('isFulfilled') //=> false - ``` + App.PeopleController = Ember.ArrayController.extend({ + itemController: 'person', + + sortedPeople: Ember.computed.sort('@this.@each.reversedName', function(personA, personB) { + // `reversedName` isn't defined on Person, but we have access to it via + // the item controller App.PersonController. If we'd used + // `content.@each.reversedName` above, we would be getting the objects + // directly and not have access to `reversedName`. + // + var reversedNameA = get(personA, 'reversedName'), + reversedNameB = get(personB, 'reversedName'); - When the the $.getJSON completes, and the promise is fulfilled - with json, the life cycle attributes will update accordingly. + return Ember.compare(reversedNameA, reversedNameB); + }) + }); - ```javascript - controller.get('isPending') //=> false - controller.get('isSettled') //=> true - controller.get('isRejected') //=> false - controller.get('isFulfilled') //=> true + App.PersonController = Ember.ObjectController.extend({ + reversedName: function () { + return reverse(get(this, 'name')); + }.property('name') + }) ``` - As the controller is an ObjectController, and the json now its content, - all the json properties will be available directly from the controller. + Dependent keys whose values are not arrays are treated as regular + dependencies: when they change, the computed property is completely + recalculated. It is sometimes useful to have dependent arrays with similar + semantics. Dependent keys which end in `.[]` do not use "one at a time" + semantics. When an item is added or removed from such a dependency, the + computed property is completely recomputed. + + Example ```javascript - // Assuming the following json: - { - firstName: 'Stefan', - lastName: 'Penner' - } + Ember.Object.extend({ + // When `string` is changed, `computed` is completely recomputed. + string: 'a string', - // both properties will accessible on the controller - controller.get('firstName') //=> 'Stefan' - controller.get('lastName') //=> 'Penner' - ``` + // When an item is added to `array`, `addedItem` is called. + array: [], - If the controller is backing a template, the attributes are - bindable from within that template + // When an item is added to `anotherArray`, `computed` is completely + // recomputed. + anotherArray: [], - ```handlebars - {{#if isPending}} - loading... - {{else}} - firstName: {{firstName}} - lastName: {{lastName}} - {{/if}} + computed: Ember.reduceComputed('string', 'array', 'anotherArray.[]', { + addedItem: addedItemCallback, + removedItem: removedItemCallback + }) + }); ``` - @class Ember.PromiseProxyMixin -*/ -Ember.PromiseProxyMixin = Ember.Mixin.create({ - reason: null, - isPending: not('isSettled').readOnly(), - isSettled: or('isRejected', 'isFulfilled').readOnly(), - isRejected: false, - isFulfilled: false, - promise: Ember.computed(function(key, promise) { - if (arguments.length === 2) { - promise = resolve(promise); - installPromise(this, promise); - return promise; - } else { - throw new Ember.Error("PromiseProxy's promise must be set"); - } - }), + @method reduceComputed + @for Ember + @param {String} [dependentKeys*] + @param {Object} options + @return {Ember.ComputedProperty} +*/ +Ember.reduceComputed = function (options) { + var args; - then: function(fulfill, reject) { - return get(this, 'promise').then(fulfill, reject); + if (arguments.length > 1) { + args = a_slice.call(arguments, 0, -1); + options = a_slice.call(arguments, -1)[0]; } -}); + if (typeof options !== "object") { + throw new Ember.Error("Reduce Computed Property declared without an options hash"); + } -})(); + if (!('initialValue' in options)) { + throw new Ember.Error("Reduce Computed Property declared without an initial value"); + } + var cp = new ReduceComputedProperty(options); + if (args) { + cp.property.apply(cp, args); + } -(function() { + return cp; +}; })(); (function() { -var get = Ember.get, - forEach = Ember.EnumerableUtils.forEach, - RETAIN = 'r', - INSERT = 'i', - DELETE = 'd'; +var ReduceComputedProperty = Ember.ReduceComputedProperty, + a_slice = [].slice, + o_create = Ember.create, + forEach = Ember.EnumerableUtils.forEach; -/** - An `Ember.TrackedArray` tracks array operations. It's useful when you want to - lazily compute the indexes of items in an array after they've been shifted by - subsequent operations. +function ArrayComputedProperty() { + var cp = this; - @class TrackedArray - @namespace Ember - @param {array} [items=[]] The array to be tracked. This is used just to get - the initial items for the starting state of retain:n. -*/ -Ember.TrackedArray = function (items) { - if (arguments.length < 1) { items = []; } + ReduceComputedProperty.apply(this, arguments); - var length = get(items, 'length'); + this.func = (function(reduceFunc) { + return function (propertyName) { + if (!cp._hasInstanceMeta(this, propertyName)) { + // When we recompute an array computed property, we need already + // retrieved arrays to be updated; we can't simply empty the cache and + // hope the array is re-retrieved. + forEach(cp._dependentKeys, function(dependentKey) { + Ember.addObserver(this, dependentKey, function() { + cp.recomputeOnce.call(this, propertyName); + }); + }, this); + } - if (length) { - this._operations = [new ArrayOperation(RETAIN, length, items)]; - } else { - this._operations = []; - } + return reduceFunc.apply(this, arguments); + }; + })(this.func); + + return this; +} +Ember.ArrayComputedProperty = ArrayComputedProperty; +ArrayComputedProperty.prototype = o_create(ReduceComputedProperty.prototype); +ArrayComputedProperty.prototype.initialValue = function () { + return Ember.A(); +}; +ArrayComputedProperty.prototype.resetValue = function (array) { + array.clear(); + return array; }; -Ember.TrackedArray.RETAIN = RETAIN; -Ember.TrackedArray.INSERT = INSERT; -Ember.TrackedArray.DELETE = DELETE; - -Ember.TrackedArray.prototype = { - - /** - Track that `newItems` were added to the tracked array at `index`. - - @method addItems - @param index - @param newItems - */ - addItems: function (index, newItems) { - var count = get(newItems, 'length'); - if (count < 1) { return; } - - var match = this._findArrayOperation(index), - arrayOperation = match.operation, - arrayOperationIndex = match.index, - arrayOperationRangeStart = match.rangeStart, - composeIndex, - splitIndex, - splitItems, - splitArrayOperation, - newArrayOperation; - - newArrayOperation = new ArrayOperation(INSERT, count, newItems); - - if (arrayOperation) { - if (!match.split) { - // insert left of arrayOperation - this._operations.splice(arrayOperationIndex, 0, newArrayOperation); - composeIndex = arrayOperationIndex; - } else { - this._split(arrayOperationIndex, index - arrayOperationRangeStart, newArrayOperation); - composeIndex = arrayOperationIndex + 1; - } - } else { - // insert at end - this._operations.push(newArrayOperation); - composeIndex = arrayOperationIndex; - } +// This is a stopgap to keep the reference counts correct with lazy CPs. +ArrayComputedProperty.prototype.didChange = function (obj, keyName) { + return; +}; - this._composeInsert(composeIndex); - }, +/** + Creates a computed property which operates on dependent arrays and + is updated with "one at a time" semantics. When items are added or + removed from the dependent array(s) an array computed only operates + on the change instead of re-evaluating the entire array. This should + return an array, if you'd like to use "one at a time" semantics and + compute some value other then an array look at + `Ember.reduceComputed`. - /** - Track that `count` items were removed at `index`. + If there are more than one arguments the first arguments are + considered to be dependent property keys. The last argument is + required to be an options object. The options object can have the + following three properties. - @method removeItems - @param index - @param count - */ - removeItems: function (index, count) { - if (count < 1) { return; } + `initialize` - An optional initialize function. Typically this will be used + to set up state on the instanceMeta object. - var match = this._findArrayOperation(index), - arrayOperation = match.operation, - arrayOperationIndex = match.index, - arrayOperationRangeStart = match.rangeStart, - newArrayOperation, - composeIndex; + `removedItem` - A function that is called each time an element is + removed from the array. - newArrayOperation = new ArrayOperation(DELETE, count); - if (!match.split) { - // insert left of arrayOperation - this._operations.splice(arrayOperationIndex, 0, newArrayOperation); - composeIndex = arrayOperationIndex; - } else { - this._split(arrayOperationIndex, index - arrayOperationRangeStart, newArrayOperation); - composeIndex = arrayOperationIndex + 1; - } + `addedItem` - A function that is called each time an element is + added to the array. - return this._composeDelete(composeIndex); - }, - /** - Apply all operations, reducing them to retain:n, for `n`, the number of - items in the array. + The `initialize` function has the following signature: - `callback` will be called for each operation and will be passed the following arguments: - * {array} items The items for the given operation - * {number} offset The computed offset of the items, ie the index in the - array of the first item for this operation. - * {string} operation The type of the operation. One of - `Ember.TrackedArray.{RETAIN, DELETE, INSERT}` + ```javascript + function (array, changeMeta, instanceMeta) + ``` - @method apply - @param {function} callback - */ - apply: function (callback) { - var items = [], - offset = 0; + `array` - The initial value of the arrayComputed, an empty array. - forEach(this._operations, function (arrayOperation) { - callback(arrayOperation.items, offset, arrayOperation.type); + `changeMeta` - An object which contains meta information about the + computed. It contains the following properties: - if (arrayOperation.type !== DELETE) { - offset += arrayOperation.count; - items = items.concat(arrayOperation.items); - } - }); + - `property` the computed property + - `propertyName` the name of the property on the object - this._operations = [new ArrayOperation(RETAIN, items.length, items)]; - }, + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. - /** - Return an ArrayOperationMatch for the operation that contains the item at `index`. - @method _findArrayOperation + The `removedItem` and `addedItem` functions both have the following signature: - @param {number} index the index of the item whose operation information - should be returned. - @private - */ - _findArrayOperation: function (index) { - var arrayOperationIndex, - len, - split = false, - arrayOperation, - arrayOperationRangeStart, - arrayOperationRangeEnd; + ```javascript + function (accumulatedValue, item, changeMeta, instanceMeta) + ``` - // OPTIMIZE: we could search these faster if we kept a balanced tree. - // find leftmost arrayOperation to the right of `index` - for (arrayOperationIndex = arrayOperationRangeStart = 0, len = this._operations.length; arrayOperationIndex < len; ++arrayOperationIndex) { - arrayOperation = this._operations[arrayOperationIndex]; + `accumulatedValue` - The value returned from the last time + `removedItem` or `addedItem` was called or an empty array. - if (arrayOperation.type === DELETE) { continue; } + `item` - the element added or removed from the array - arrayOperationRangeEnd = arrayOperationRangeStart + arrayOperation.count - 1; + `changeMeta` - An object which contains meta information about the + change. It contains the following properties: - if (index === arrayOperationRangeStart) { - break; - } else if (index > arrayOperationRangeStart && index <= arrayOperationRangeEnd) { - split = true; - break; - } else { - arrayOperationRangeStart = arrayOperationRangeEnd + 1; - } - } + - `property` the computed property + - `propertyName` the name of the property on the object + - `index` the index of the added or removed item + - `item` the added or removed item: this is exactly the same as + the second arg + - `arrayChanged` the array that triggered the change. Can be + useful when depending on multiple arrays. - return new ArrayOperationMatch(arrayOperation, arrayOperationIndex, split, arrayOperationRangeStart); - }, + For property changes triggered on an item property change (when + depKey is something like `someArray.@each.someProperty`), + `changeMeta` will also contain the following property: - _split: function (arrayOperationIndex, splitIndex, newArrayOperation) { - var arrayOperation = this._operations[arrayOperationIndex], - splitItems = arrayOperation.items.slice(splitIndex), - splitArrayOperation = new ArrayOperation(arrayOperation.type, splitItems.length, splitItems); + - `previousValues` an object whose keys are the properties that changed on + the item, and whose values are the item's previous values. - // truncate LHS - arrayOperation.count = splitIndex; - arrayOperation.items = arrayOperation.items.slice(0, splitIndex); + `previousValues` is important Ember coalesces item property changes via + Ember.run.once. This means that by the time removedItem gets called, item has + the new values, but you may need the previous value (eg for sorting & + filtering). - this._operations.splice(arrayOperationIndex + 1, 0, newArrayOperation, splitArrayOperation); - }, + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. - // see SubArray for a better implementation. - _composeInsert: function (index) { - var newArrayOperation = this._operations[index], - leftArrayOperation = this._operations[index-1], // may be undefined - rightArrayOperation = this._operations[index+1], // may be undefined - leftOp = leftArrayOperation && leftArrayOperation.type, - rightOp = rightArrayOperation && rightArrayOperation.type; + The `removedItem` and `addedItem` functions should return the accumulated + value. It is acceptable to not return anything (ie return undefined) + to invalidate the computation. This is generally not a good idea for + arrayComputed but it's used in eg max and min. - if (leftOp === INSERT) { - // merge left - leftArrayOperation.count += newArrayOperation.count; - leftArrayOperation.items = leftArrayOperation.items.concat(newArrayOperation.items); + Example - if (rightOp === INSERT) { - // also merge right (we have split an insert with an insert) - leftArrayOperation.count += rightArrayOperation.count; - leftArrayOperation.items = leftArrayOperation.items.concat(rightArrayOperation.items); - this._operations.splice(index, 2); - } else { - // only merge left - this._operations.splice(index, 1); + ```javascript + Ember.computed.map = function(dependentKey, callback) { + var options = { + addedItem: function(array, item, changeMeta, instanceMeta) { + var mapped = callback(item); + array.insertAt(changeMeta.index, mapped); + return array; + }, + removedItem: function(array, item, changeMeta, instanceMeta) { + array.removeAt(changeMeta.index, 1); + return array; } - } else if (rightOp === INSERT) { - // merge right - newArrayOperation.count += rightArrayOperation.count; - newArrayOperation.items = newArrayOperation.items.concat(rightArrayOperation.items); - this._operations.splice(index + 1, 1); - } - }, - - _composeDelete: function (index) { - var arrayOperation = this._operations[index], - deletesToGo = arrayOperation.count, - leftArrayOperation = this._operations[index-1], // may be undefined - leftOp = leftArrayOperation && leftArrayOperation.type, - nextArrayOperation, - nextOp, - nextCount, - removeNewAndNextOp = false, - removedItems = []; + }; - if (leftOp === DELETE) { - arrayOperation = leftArrayOperation; - index -= 1; - } + return Ember.arrayComputed(dependentKey, options); + }; + ``` - for (var i = index + 1; deletesToGo > 0; ++i) { - nextArrayOperation = this._operations[i]; - nextOp = nextArrayOperation.type; - nextCount = nextArrayOperation.count; + @method arrayComputed + @for Ember + @param {String} [dependentKeys*] + @param {Object} options + @return {Ember.ComputedProperty} +*/ +Ember.arrayComputed = function (options) { + var args; - if (nextOp === DELETE) { - arrayOperation.count += nextCount; - continue; - } + if (arguments.length > 1) { + args = a_slice.call(arguments, 0, -1); + options = a_slice.call(arguments, -1)[0]; + } - if (nextCount > deletesToGo) { - // d:2 {r,i}:5 we reduce the retain or insert, but it stays - removedItems = removedItems.concat(nextArrayOperation.items.splice(0, deletesToGo)); - nextArrayOperation.count -= deletesToGo; + if (typeof options !== "object") { + throw new Ember.Error("Array Computed Property declared without an options hash"); + } - // In the case where we truncate the last arrayOperation, we don't need to - // remove it; also the deletesToGo reduction is not the entirety of - // nextCount - i -= 1; - nextCount = deletesToGo; + var cp = new ArrayComputedProperty(options); - deletesToGo = 0; - } else { - if (nextCount === deletesToGo) { - // Handle edge case of d:2 i:2 in which case both operations go away - // during composition. - removeNewAndNextOp = true; - } - removedItems = removedItems.concat(nextArrayOperation.items); - deletesToGo -= nextCount; - } + if (args) { + cp.property.apply(cp, args); + } - if (nextOp === INSERT) { - // d:2 i:3 will result in delete going away - arrayOperation.count -= nextCount; - } - } + return cp; +}; - if (arrayOperation.count > 0) { - // compose our new delete with possibly several operations to the right of - // disparate types - this._operations.splice(index+1, i-1-index); - } else { - // The delete operation can go away; it has merely reduced some other - // operation, as in d:3 i:4; it may also have eliminated that operation, - // as in d:3 i:3. - this._operations.splice(index, removeNewAndNextOp ? 2 : 1); - } +})(); - return removedItems; - }, - toString: function () { - var str = ""; - forEach(this._operations, function (operation) { - str += " " + operation.type + ":" + operation.count; - }); - return str.substring(1); - } -}; +(function() { /** - Internal data structure to represent an array operation. - - @method ArrayOperation - @private - @property {string} type The type of the operation. One of - `Ember.TrackedArray.{RETAIN, INSERT, DELETE}` - @property {number} count The number of items in this operation. - @property {array} items The items of the operation, if included. RETAIN and - INSERT include their items, DELETE does not. +@module ember +@submodule ember-runtime */ -function ArrayOperation (operation, count, items) { - this.type = operation; // RETAIN | INSERT | DELETE - this.count = count; - this.items = items; -} + +var get = Ember.get, + set = Ember.set, + guidFor = Ember.guidFor, + merge = Ember.merge, + a_slice = [].slice, + forEach = Ember.EnumerableUtils.forEach, + map = Ember.EnumerableUtils.map, + SearchProxy; /** - Internal data structure used to include information when looking up operations - by item index. + A computed property that returns the sum of the value + in the dependent array. - @method ArrayOperationMatch - @private - @property {ArrayOperation} operation - @property {number} index The index of `operation` in the array of operations. - @property {boolean} split Whether or not the item index searched for would - require a split for a new operation type. - @property {number} rangeStart The index of the first item in the operation, - with respect to the tracked array. The index of the last item can be computed - from `rangeStart` and `operation.count`. + @method computed.sum + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computes the sum of all values in the dependentKey's array */ -function ArrayOperationMatch(operation, index, split, rangeStart) { - this.operation = operation; - this.index = index; - this.split = split; - this.rangeStart = rangeStart; -} -})(); +Ember.computed.sum = function(dependentKey){ + return Ember.reduceComputed(dependentKey, { + initialValue: 0, + addedItem: function(accumulatedValue, item, changeMeta, instanceMeta){ + return accumulatedValue + item; + }, + removedItem: function(accumulatedValue, item, changeMeta, instanceMeta){ + return accumulatedValue - item; + } + }); +}; -(function() { -var get = Ember.get, - forEach = Ember.EnumerableUtils.forEach, - RETAIN = 'r', - FILTER = 'f'; +/** + A computed property that calculates the maximum value in the + dependent array. This will return `-Infinity` when the dependent + array is empty. -function Operation (type, count) { - this.type = type; - this.count = count; -} + ```javascript + App.Person = Ember.Object.extend({ + childAges: Ember.computed.mapBy('children', 'age'), + maxChildAge: Ember.computed.max('childAges') + }); -/** - An `Ember.SubArray` tracks an array in a way similar to, but more specialized - than, `Ember.TrackedArray`. It is useful for keeping track of the indexes of - items within a filtered array. + var lordByron = App.Person.create({children: []}); + lordByron.get('maxChildAge'); // -Infinity + lordByron.get('children').pushObject({ + name: 'Augusta Ada Byron', age: 7 + }); + lordByron.get('maxChildAge'); // 7 + lordByron.get('children').pushObjects([{ + name: 'Allegra Byron', + age: 5 + }, { + name: 'Elizabeth Medora Leigh', + age: 8 + }]); + lordByron.get('maxChildAge'); // 8 + ``` - @class SubArray - @namespace Ember + @method computed.max + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computes the largest value in the dependentKey's array */ -Ember.SubArray = function (length) { - if (arguments.length < 1) { length = 0; } - - if (length > 0) { - this._operations = [new Operation(RETAIN, length)]; - } else { - this._operations = []; - } -}; +Ember.computed.max = function (dependentKey) { + return Ember.reduceComputed(dependentKey, { + initialValue: -Infinity, -Ember.SubArray.prototype = { - /** - Track that an item was added to the tracked array. + addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + return Math.max(accumulatedValue, item); + }, - @method addItem + removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + if (item < accumulatedValue) { + return accumulatedValue; + } + } + }); +}; - @param {number} index The index of the item in the tracked array. - @param {boolean} match `true` iff the item is included in the subarray. +/** + A computed property that calculates the minimum value in the + dependent array. This will return `Infinity` when the dependent + array is empty. - @return {number} The index of the item in the subarray. - */ - addItem: function(index, match) { - var returnValue = -1, - itemType = match ? RETAIN : FILTER, - self = this; + ```javascript + App.Person = Ember.Object.extend({ + childAges: Ember.computed.mapBy('children', 'age'), + minChildAge: Ember.computed.min('childAges') + }); - this._findOperation(index, function(operation, operationIndex, rangeStart, rangeEnd, seenInSubArray) { - var newOperation, splitOperation; + var lordByron = App.Person.create({children: []}); + lordByron.get('minChildAge'); // Infinity + lordByron.get('children').pushObject({ + name: 'Augusta Ada Byron', age: 7 + }); + lordByron.get('minChildAge'); // 7 + lordByron.get('children').pushObjects([{ + name: 'Allegra Byron', + age: 5 + }, { + name: 'Elizabeth Medora Leigh', + age: 8 + }]); + lordByron.get('minChildAge'); // 5 + ``` - if (itemType === operation.type) { - ++operation.count; - } else if (index === rangeStart) { - // insert to the left of `operation` - self._operations.splice(operationIndex, 0, new Operation(itemType, 1)); - } else { - newOperation = new Operation(itemType, 1); - splitOperation = new Operation(operation.type, rangeEnd - index + 1); - operation.count = index - rangeStart; + @method computed.min + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computes the smallest value in the dependentKey's array +*/ +Ember.computed.min = function (dependentKey) { + return Ember.reduceComputed(dependentKey, { + initialValue: Infinity, - self._operations.splice(operationIndex + 1, 0, newOperation, splitOperation); - } + addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + return Math.min(accumulatedValue, item); + }, - if (match) { - if (operation.type === RETAIN) { - returnValue = seenInSubArray + (index - rangeStart); - } else { - returnValue = seenInSubArray; - } + removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + if (item > accumulatedValue) { + return accumulatedValue; } + } + }); +}; - self._composeAt(operationIndex); - }, function(seenInSubArray) { - self._operations.push(new Operation(itemType, 1)); - - if (match) { - returnValue = seenInSubArray; - } +/** + Returns an array mapped via the callback - self._composeAt(self._operations.length-1); - }); + The callback method you provide should have the following signature. + `item` is the current item in the iteration. - return returnValue; - }, + ```javascript + function(item); + ``` - /** - Track that an item was removed from the tracked array. + Example - @method removeItem + ```javascript + App.Hamster = Ember.Object.extend({ + excitingChores: Ember.computed.map('chores', function(chore) { + return chore.toUpperCase() + '!'; + }) + }); - @param {number} index The index of the item in the tracked array. + var hamster = App.Hamster.create({ + chores: ['clean', 'write more unit tests'] + }); + hamster.get('excitingChores'); // ['CLEAN!', 'WRITE MORE UNIT TESTS!'] + ``` - @return {number} The index of the item in the subarray, or `-1` if the item - was not in the subarray. - */ - removeItem: function(index) { - var returnValue = -1, - self = this; + @method computed.map + @for Ember + @param {String} dependentKey + @param {Function} callback + @return {Ember.ComputedProperty} an array mapped via the callback +*/ +Ember.computed.map = function(dependentKey, callback) { + var options = { + addedItem: function(array, item, changeMeta, instanceMeta) { + var mapped = callback.call(this, item); + array.insertAt(changeMeta.index, mapped); + return array; + }, + removedItem: function(array, item, changeMeta, instanceMeta) { + array.removeAt(changeMeta.index, 1); + return array; + } + }; - this._findOperation(index, function (operation, operationIndex, rangeStart, rangeEnd, seenInSubArray) { - if (operation.type === RETAIN) { - returnValue = seenInSubArray + (index - rangeStart); - } + return Ember.arrayComputed(dependentKey, options); +}; - if (operation.count > 1) { - --operation.count; - } else { - self._operations.splice(operationIndex, 1); - self._composeAt(operationIndex); - } - }, function() { - throw new Ember.Error("Can't remove an item that has never been added."); - }); +/** + Returns an array mapped to the specified key. - return returnValue; - }, + ```javascript + App.Person = Ember.Object.extend({ + childAges: Ember.computed.mapBy('children', 'age') + }); + var lordByron = App.Person.create({children: []}); + lordByron.get('childAges'); // [] + lordByron.get('children').pushObject({name: 'Augusta Ada Byron', age: 7}); + lordByron.get('childAges'); // [7] + lordByron.get('children').pushObjects([{ + name: 'Allegra Byron', + age: 5 + }, { + name: 'Elizabeth Medora Leigh', + age: 8 + }]); + lordByron.get('childAges'); // [7, 5, 8] + ``` - _findOperation: function (index, foundCallback, notFoundCallback) { - var operationIndex, - len, - operation, - rangeStart, - rangeEnd, - seenInSubArray = 0; + @method computed.mapBy + @for Ember + @param {String} dependentKey + @param {String} propertyKey + @return {Ember.ComputedProperty} an array mapped to the specified key +*/ +Ember.computed.mapBy = function(dependentKey, propertyKey) { + var callback = function(item) { return get(item, propertyKey); }; + return Ember.computed.map(dependentKey + '.@each.' + propertyKey, callback); +}; - // OPTIMIZE: change to balanced tree - // find leftmost operation to the right of `index` - for (operationIndex = rangeStart = 0, len = this._operations.length; operationIndex < len; rangeStart = rangeEnd + 1, ++operationIndex) { - operation = this._operations[operationIndex]; - rangeEnd = rangeStart + operation.count - 1; +/** + @method computed.mapProperty + @for Ember + @deprecated Use `Ember.computed.mapBy` instead + @param dependentKey + @param propertyKey +*/ +Ember.computed.mapProperty = Ember.computed.mapBy; - if (index >= rangeStart && index <= rangeEnd) { - foundCallback(operation, operationIndex, rangeStart, rangeEnd, seenInSubArray); - return; - } else if (operation.type === RETAIN) { - seenInSubArray += operation.count; - } - } +/** + Filters the array by the callback. - notFoundCallback(seenInSubArray); - }, + The callback method you provide should have the following signature. + `item` is the current item in the iteration. - _composeAt: function(index) { - var op = this._operations[index], - otherOp; + ```javascript + function(item); + ``` - if (!op) { - // Composing out of bounds is a no-op, as when removing the last operation - // in the list. - return; - } + ```javascript + App.Hamster = Ember.Object.extend({ + remainingChores: Ember.computed.filter('chores', function(chore) { + return !chore.done; + }) + }); - if (index > 0) { - otherOp = this._operations[index-1]; - if (otherOp.type === op.type) { - op.count += otherOp.count; - this._operations.splice(index-1, 1); - --index; + var hamster = App.Hamster.create({chores: [ + {name: 'cook', done: true}, + {name: 'clean', done: true}, + {name: 'write more unit tests', done: false} + ]}); + hamster.get('remainingChores'); // [{name: 'write more unit tests', done: false}] + ``` + + @method computed.filter + @for Ember + @param {String} dependentKey + @param {Function} callback + @return {Ember.ComputedProperty} the filtered array +*/ +Ember.computed.filter = function(dependentKey, callback) { + var options = { + initialize: function (array, changeMeta, instanceMeta) { + instanceMeta.filteredArrayIndexes = new Ember.SubArray(); + }, + + addedItem: function(array, item, changeMeta, instanceMeta) { + var match = !!callback.call(this, item), + filterIndex = instanceMeta.filteredArrayIndexes.addItem(changeMeta.index, match); + + if (match) { + array.insertAt(filterIndex, item); } - } - if (index < this._operations.length-1) { - otherOp = this._operations[index+1]; - if (otherOp.type === op.type) { - op.count += otherOp.count; - this._operations.splice(index+1, 1); + return array; + }, + + removedItem: function(array, item, changeMeta, instanceMeta) { + var filterIndex = instanceMeta.filteredArrayIndexes.removeItem(changeMeta.index); + + if (filterIndex > -1) { + array.removeAt(filterIndex); } + + return array; } - } -}; + }; -})(); + return Ember.arrayComputed(dependentKey, options); +}; +/** + Filters the array by the property and value + ```javascript + App.Hamster = Ember.Object.extend({ + remainingChores: Ember.computed.filterBy('chores', 'done', false) + }); -(function() { -Ember.Container = requireModule('container'); -Ember.Container.set = Ember.set; + var hamster = App.Hamster.create({chores: [ + {name: 'cook', done: true}, + {name: 'clean', done: true}, + {name: 'write more unit tests', done: false} + ]}); + hamster.get('remainingChores'); // [{name: 'write more unit tests', done: false}] + ``` -})(); + @method computed.filterBy + @for Ember + @param {String} dependentKey + @param {String} propertyKey + @param {*} value + @return {Ember.ComputedProperty} the filtered array +*/ +Ember.computed.filterBy = function(dependentKey, propertyKey, value) { + var callback; + if (arguments.length === 2) { + callback = function(item) { + return get(item, propertyKey); + }; + } else { + callback = function(item) { + return get(item, propertyKey) === value; + }; + } + return Ember.computed.filter(dependentKey + '.@each.' + propertyKey, callback); +}; -(function() { /** -@module ember -@submodule ember-runtime + @method computed.filterProperty + @for Ember + @param dependentKey + @param propertyKey + @param value + @deprecated Use `Ember.computed.filterBy` instead */ +Ember.computed.filterProperty = Ember.computed.filterBy; +/** + A computed property which returns a new array with all the unique + elements from one or more dependent arrays. -// NOTE: this object should never be included directly. Instead use `Ember.Object`. -// We only define this separately so that `Ember.Set` can depend on it. - + Example -var set = Ember.set, get = Ember.get, - o_create = Ember.create, - o_defineProperty = Ember.platform.defineProperty, - GUID_KEY = Ember.GUID_KEY, - guidFor = Ember.guidFor, - generateGuid = Ember.generateGuid, - meta = Ember.meta, - rewatch = Ember.rewatch, - finishChains = Ember.finishChains, - sendEvent = Ember.sendEvent, - destroy = Ember.destroy, - schedule = Ember.run.schedule, - Mixin = Ember.Mixin, - applyMixin = Mixin._apply, - finishPartial = Mixin.finishPartial, - reopen = Mixin.prototype.reopen, - MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, - indexOf = Ember.EnumerableUtils.indexOf; + ```javascript + App.Hamster = Ember.Object.extend({ + uniqueFruits: Ember.computed.uniq('fruits') + }); -var undefinedDescriptor = { - configurable: true, - writable: true, - enumerable: false, - value: undefined -}; + var hamster = App.Hamster.create({fruits: [ + 'banana', + 'grape', + 'kale', + 'banana' + ]}); + hamster.get('uniqueFruits'); // ['banana', 'grape', 'kale'] + ``` -function makeCtor() { + @method computed.uniq + @for Ember + @param {String} propertyKey* + @return {Ember.ComputedProperty} computes a new array with all the + unique elements from the dependent array +*/ +Ember.computed.uniq = function() { + var args = a_slice.call(arguments); + args.push({ + initialize: function(array, changeMeta, instanceMeta) { + instanceMeta.itemCounts = {}; + }, - // Note: avoid accessing any properties on the object since it makes the - // method a lot faster. This is glue code so we want it to be as fast as - // possible. + addedItem: function(array, item, changeMeta, instanceMeta) { + var guid = guidFor(item); - var wasApplied = false, initMixins, initProperties; + if (!instanceMeta.itemCounts[guid]) { + instanceMeta.itemCounts[guid] = 1; + } else { + ++instanceMeta.itemCounts[guid]; + } + array.addObject(item); + return array; + }, + removedItem: function(array, item, _, instanceMeta) { + var guid = guidFor(item), + itemCounts = instanceMeta.itemCounts; - var Class = function() { - if (!wasApplied) { - Class.proto(); // prepare prototype... - } - o_defineProperty(this, GUID_KEY, undefinedDescriptor); - o_defineProperty(this, '_super', undefinedDescriptor); - var m = meta(this), proto = m.proto; - m.proto = this; - if (initMixins) { - // capture locally so we can clear the closed over variable - var mixins = initMixins; - initMixins = null; - this.reopen.apply(this, mixins); + if (--itemCounts[guid] === 0) { + array.removeObject(item); + } + return array; } - if (initProperties) { - // capture locally so we can clear the closed over variable - var props = initProperties; - initProperties = null; + }); + return Ember.arrayComputed.apply(null, args); +}; - var concatenatedProperties = this.concatenatedProperties; +/** + Alias for [Ember.computed.uniq](/api/#method_computed_uniq). - for (var i = 0, l = props.length; i < l; i++) { - var properties = props[i]; + @method computed.union + @for Ember + @param {String} propertyKey* + @return {Ember.ComputedProperty} computes a new array with all the + unique elements from the dependent array +*/ +Ember.computed.union = Ember.computed.uniq; - Ember.assert("Ember.Object.create no longer supports mixing in other definitions, use createWithMixins instead.", !(properties instanceof Ember.Mixin)); +/** + A computed property which returns a new array with all the duplicated + elements from two or more dependent arrays. - if (properties === null || typeof properties !== 'object') { - Ember.assert("Ember.Object.create only accepts objects."); - continue; - } + Example - var keyNames = Ember.keys(properties); - for (var j = 0, ll = keyNames.length; j < ll; j++) { - var keyName = keyNames[j]; - if (!properties.hasOwnProperty(keyName)) { continue; } + ```javascript + var obj = Ember.Object.createWithMixins({ + adaFriends: ['Charles Babbage', 'John Hobhouse', 'William King', 'Mary Somerville'], + charlesFriends: ['William King', 'Mary Somerville', 'Ada Lovelace', 'George Peacock'], + friendsInCommon: Ember.computed.intersect('adaFriends', 'charlesFriends') + }); - var value = properties[keyName], - IS_BINDING = Ember.IS_BINDING; + obj.get('friendsInCommon'); // ['William King', 'Mary Somerville'] + ``` - if (IS_BINDING.test(keyName)) { - var bindings = m.bindings; - if (!bindings) { - bindings = m.bindings = {}; - } else if (!m.hasOwnProperty('bindings')) { - bindings = m.bindings = o_create(m.bindings); - } - bindings[keyName] = value; - } + @method computed.intersect + @for Ember + @param {String} propertyKey* + @return {Ember.ComputedProperty} computes a new array with all the + duplicated elements from the dependent arrays +*/ +Ember.computed.intersect = function () { + var getDependentKeyGuids = function (changeMeta) { + return map(changeMeta.property._dependentKeys, function (dependentKey) { + return guidFor(dependentKey); + }); + }; - var desc = m.descs[keyName]; + var args = a_slice.call(arguments); + args.push({ + initialize: function (array, changeMeta, instanceMeta) { + instanceMeta.itemCounts = {}; + }, - Ember.assert("Ember.Object.create no longer supports defining computed properties.", !(value instanceof Ember.ComputedProperty)); - Ember.assert("Ember.Object.create no longer supports defining methods that call _super.", !(typeof value === 'function' && value.toString().indexOf('._super') !== -1)); - Ember.assert("`actions` must be provided at extend time, not at create time, when Ember.ActionHandler is used (i.e. views, controllers & routes).", !((keyName === 'actions') && Ember.ActionHandler.detect(this))); + addedItem: function(array, item, changeMeta, instanceMeta) { + var itemGuid = guidFor(item), + dependentGuids = getDependentKeyGuids(changeMeta), + dependentGuid = guidFor(changeMeta.arrayChanged), + numberOfDependentArrays = changeMeta.property._dependentKeys.length, + itemCounts = instanceMeta.itemCounts; - if (concatenatedProperties && indexOf(concatenatedProperties, keyName) >= 0) { - var baseValue = this[keyName]; + if (!itemCounts[itemGuid]) { itemCounts[itemGuid] = {}; } + if (itemCounts[itemGuid][dependentGuid] === undefined) { itemCounts[itemGuid][dependentGuid] = 0; } - if (baseValue) { - if ('function' === typeof baseValue.concat) { - value = baseValue.concat(value); - } else { - value = Ember.makeArray(baseValue).concat(value); - } - } else { - value = Ember.makeArray(value); - } - } + if (++itemCounts[itemGuid][dependentGuid] === 1 && + numberOfDependentArrays === Ember.keys(itemCounts[itemGuid]).length) { - if (desc) { - desc.set(this, keyName, value); - } else { - if (typeof this.setUnknownProperty === 'function' && !(keyName in this)) { - this.setUnknownProperty(keyName, value); - } else if (MANDATORY_SETTER) { - Ember.defineProperty(this, keyName, null, value); // setup mandatory setter - } else { - this[keyName] = value; - } - } + array.addObject(item); + } + return array; + }, + removedItem: function(array, item, changeMeta, instanceMeta) { + var itemGuid = guidFor(item), + dependentGuids = getDependentKeyGuids(changeMeta), + dependentGuid = guidFor(changeMeta.arrayChanged), + numberOfDependentArrays = changeMeta.property._dependentKeys.length, + numberOfArraysItemAppearsIn, + itemCounts = instanceMeta.itemCounts; + + if (itemCounts[itemGuid][dependentGuid] === undefined) { itemCounts[itemGuid][dependentGuid] = 0; } + if (--itemCounts[itemGuid][dependentGuid] === 0) { + delete itemCounts[itemGuid][dependentGuid]; + numberOfArraysItemAppearsIn = Ember.keys(itemCounts[itemGuid]).length; + + if (numberOfArraysItemAppearsIn === 0) { + delete itemCounts[itemGuid]; } + array.removeObject(item); } + return array; } - finishPartial(this, m); - this.init.apply(this, arguments); - m.proto = proto; - finishChains(this); - sendEvent(this, "init"); - }; + }); + return Ember.arrayComputed.apply(null, args); +}; - Class.toString = Mixin.prototype.toString; - Class.willReopen = function() { - if (wasApplied) { - Class.PrototypeMixin = Mixin.create(Class.PrototypeMixin); - } +/** + A computed property which returns a new array with all the + properties from the first dependent array that are not in the second + dependent array. - wasApplied = false; - }; - Class._initMixins = function(args) { initMixins = args; }; - Class._initProperties = function(args) { initProperties = args; }; + Example - Class.proto = function() { - var superclass = Class.superclass; - if (superclass) { superclass.proto(); } + ```javascript + App.Hamster = Ember.Object.extend({ + likes: ['banana', 'grape', 'kale'], + wants: Ember.computed.setDiff('likes', 'fruits') + }); - if (!wasApplied) { - wasApplied = true; - Class.PrototypeMixin.applyPartial(Class.prototype); - rewatch(Class.prototype); - } + var hamster = App.Hamster.create({fruits: [ + 'grape', + 'kale', + ]}); + hamster.get('wants'); // ['banana'] + ``` - return this.prototype; - }; + @method computed.setDiff + @for Ember + @param {String} setAProperty + @param {String} setBProperty + @return {Ember.ComputedProperty} computes a new array with all the + items from the first dependent array that are not in the second + dependent array +*/ +Ember.computed.setDiff = function (setAProperty, setBProperty) { + if (arguments.length !== 2) { + throw new Ember.Error("setDiff requires exactly two dependent arrays."); + } + return Ember.arrayComputed(setAProperty, setBProperty, { + addedItem: function (array, item, changeMeta, instanceMeta) { + var setA = get(this, setAProperty), + setB = get(this, setBProperty); - return Class; + if (changeMeta.arrayChanged === setA) { + if (!setB.contains(item)) { + array.addObject(item); + } + } else { + array.removeObject(item); + } + return array; + }, -} + removedItem: function (array, item, changeMeta, instanceMeta) { + var setA = get(this, setAProperty), + setB = get(this, setBProperty); -/** - @class CoreObject - @namespace Ember -*/ -var CoreObject = makeCtor(); -CoreObject.toString = function() { return "Ember.CoreObject"; }; + if (changeMeta.arrayChanged === setB) { + if (setA.contains(item)) { + array.addObject(item); + } + } else { + array.removeObject(item); + } + return array; + } + }); +}; -CoreObject.PrototypeMixin = Mixin.create({ - reopen: function() { - applyMixin(this, arguments, true); - return this; - }, +function binarySearch(array, item, low, high) { + var mid, midItem, res, guidMid, guidItem; - /** - An overridable method called when objects are instantiated. By default, - does nothing unless it is overridden during class definition. + if (arguments.length < 4) { high = get(array, 'length'); } + if (arguments.length < 3) { low = 0; } - Example: + if (low === high) { + return low; + } - ```javascript - App.Person = Ember.Object.extend({ - init: function() { - this._super(); - alert('Name is ' + this.get('name')); - } - }); + mid = low + Math.floor((high - low) / 2); + midItem = array.objectAt(mid); - var steve = App.Person.create({ - name: "Steve" - }); + guidMid = _guidFor(midItem); + guidItem = _guidFor(item); - // alerts 'Name is Steve'. - ``` + if (guidMid === guidItem) { + return mid; + } - NOTE: If you do override `init` for a framework class like `Ember.View` or - `Ember.ArrayController`, be sure to call `this._super()` in your - `init` declaration! If you don't, Ember may not have an opportunity to - do important setup work, and you'll see strange behavior in your - application. + res = this.order(midItem, item); + if (res === 0) { + res = guidMid < guidItem ? -1 : 1; + } - @method init - */ - init: function() {}, - /** - Defines the properties that will be concatenated from the superclass - (instead of overridden). + if (res < 0) { + return this.binarySearch(array, item, mid+1, high); + } else if (res > 0) { + return this.binarySearch(array, item, low, mid); + } - By default, when you extend an Ember class a property defined in - the subclass overrides a property with the same name that is defined - in the superclass. However, there are some cases where it is preferable - to build up a property's value by combining the superclass' property - value with the subclass' value. An example of this in use within Ember - is the `classNames` property of `Ember.View`. + return mid; - Here is some sample code showing the difference between a concatenated - property and a normal one: + function _guidFor(item) { + if (SearchProxy.detectInstance(item)) { + return guidFor(get(item, 'content')); + } + return guidFor(item); + } +} - ```javascript - App.BarView = Ember.View.extend({ - someNonConcatenatedProperty: ['bar'], - classNames: ['bar'] - }); - App.FooBarView = App.BarView.extend({ - someNonConcatenatedProperty: ['foo'], - classNames: ['foo'], - }); +SearchProxy = Ember.ObjectProxy.extend(); - var fooBarView = App.FooBarView.create(); - fooBarView.get('someNonConcatenatedProperty'); // ['foo'] - fooBarView.get('classNames'); // ['ember-view', 'bar', 'foo'] - ``` +/** + A computed property which returns a new array with all the + properties from the first dependent array sorted based on a property + or sort function. - This behavior extends to object creation as well. Continuing the - above example: + The callback method you provide should have the following signature: - ```javascript - var view = App.FooBarView.create({ - someNonConcatenatedProperty: ['baz'], - classNames: ['baz'] - }) - view.get('someNonConcatenatedProperty'); // ['baz'] - view.get('classNames'); // ['ember-view', 'bar', 'foo', 'baz'] - ``` - Adding a single property that is not an array will just add it in the array: + ```javascript + function(itemA, itemB); + ``` - ```javascript - var view = App.FooBarView.create({ - classNames: 'baz' - }) - view.get('classNames'); // ['ember-view', 'bar', 'foo', 'baz'] - ``` + - `itemA` the first item to compare. + - `itemB` the second item to compare. - Using the `concatenatedProperties` property, we can tell to Ember that mix - the content of the properties. + This function should return negative number (e.g. `-1`) when `itemA` should come before + `itemB`. It should return positive number (e.g. `1`) when `itemA` should come after + `itemB`. If the `itemA` and `itemB` are equal this function should return `0`. + + Therefore, if this function is comparing some numeric values, simple `itemA - itemB` or + `itemA.get( 'foo' ) - itemB.get( 'foo' )` can be used instead of series of `if`. - In `Ember.View` the `classNameBindings` and `attributeBindings` properties - are also concatenated, in addition to `classNames`. + Example - This feature is available for you to use throughout the Ember object model, - although typical app developers are likely to use it infrequently. Since - it changes expectations about behavior of properties, you should properly - document its usage in each individual concatenated property (to not - mislead your users to think they can override the property in a subclass). + ```javascript + var ToDoList = Ember.Object.extend({ + todosSorting: ['name'], + sortedTodos: Ember.computed.sort('todos', 'todosSorting'), + priorityTodos: Ember.computed.sort('todos', function(a, b){ + if (a.priority > b.priority) { + return 1; + } else if (a.priority < b.priority) { + return -1; + } + return 0; + }), + }); + var todoList = ToDoList.create({todos: [ + {name: 'Unit Test', priority: 2}, + {name: 'Documentation', priority: 3}, + {name: 'Release', priority: 1} + ]}); - @property concatenatedProperties - @type Array - @default null - */ - concatenatedProperties: null, + todoList.get('sortedTodos'); // [{name:'Documentation', priority:3}, {name:'Release', priority:1}, {name:'Unit Test', priority:2}] + todoList.get('priorityTodos'); // [{name:'Release', priority:1}, {name:'Unit Test', priority:2}, {name:'Documentation', priority:3}] + ``` - /** - Destroyed object property flag. + @method computed.sort + @for Ember + @param {String} dependentKey + @param {String or Function} sortDefinition a dependent key to an + array of sort properties or a function to use when sorting + @return {Ember.ComputedProperty} computes a new sorted array based + on the sort property array or callback function +*/ +Ember.computed.sort = function (itemsKey, sortDefinition) { + Ember.assert("Ember.computed.sort requires two arguments: an array key to sort and either a sort properties key or sort function", arguments.length === 2); - if this property is `true` the observers and bindings were already - removed by the effect of calling the `destroy()` method. + var initFn, sortPropertiesKey; - @property isDestroyed - @default false - */ - isDestroyed: false, + if (typeof sortDefinition === 'function') { + initFn = function (array, changeMeta, instanceMeta) { + instanceMeta.order = sortDefinition; + instanceMeta.binarySearch = binarySearch; + }; + } else { + sortPropertiesKey = sortDefinition; + initFn = function (array, changeMeta, instanceMeta) { + function setupSortProperties() { + var sortPropertyDefinitions = get(this, sortPropertiesKey), + sortProperty, + sortProperties = instanceMeta.sortProperties = [], + sortPropertyAscending = instanceMeta.sortPropertyAscending = {}, + idx, + asc; - /** - Destruction scheduled flag. The `destroy()` method has been called. + Ember.assert("Cannot sort: '" + sortPropertiesKey + "' is not an array.", Ember.isArray(sortPropertyDefinitions)); - The object stays intact until the end of the run loop at which point - the `isDestroyed` flag is set. + changeMeta.property.clearItemPropertyKeys(itemsKey); - @property isDestroying - @default false - */ - isDestroying: false, + forEach(sortPropertyDefinitions, function (sortPropertyDefinition) { + if ((idx = sortPropertyDefinition.indexOf(':')) !== -1) { + sortProperty = sortPropertyDefinition.substring(0, idx); + asc = sortPropertyDefinition.substring(idx+1).toLowerCase() !== 'desc'; + } else { + sortProperty = sortPropertyDefinition; + asc = true; + } - /** - Destroys an object by setting the `isDestroyed` flag and removing its - metadata, which effectively destroys observers and bindings. + sortProperties.push(sortProperty); + sortPropertyAscending[sortProperty] = asc; + changeMeta.property.itemPropertyKey(itemsKey, sortProperty); + }); - If you try to set a property on a destroyed object, an exception will be - raised. + sortPropertyDefinitions.addObserver('@each', this, updateSortPropertiesOnce); + } - Note that destruction is scheduled for the end of the run loop and does not - happen immediately. It will set an isDestroying flag immediately. + function updateSortPropertiesOnce() { + Ember.run.once(this, updateSortProperties, changeMeta.propertyName); + } - @method destroy - @return {Ember.Object} receiver - */ - destroy: function() { - if (this.isDestroying) { return; } - this.isDestroying = true; + function updateSortProperties(propertyName) { + setupSortProperties.call(this); + changeMeta.property.recomputeOnce.call(this, propertyName); + } - schedule('actions', this, this.willDestroy); - schedule('destroy', this, this._scheduledDestroy); - return this; - }, + Ember.addObserver(this, sortPropertiesKey, updateSortPropertiesOnce); - /** - Override to implement teardown. + setupSortProperties.call(this); - @method willDestroy - */ - willDestroy: Ember.K, - /** - @private + instanceMeta.order = function (itemA, itemB) { + var isProxy = itemB instanceof SearchProxy, + sortProperty, result, asc; - Invoked by the run loop to actually destroy the object. This is - scheduled for execution by the `destroy` method. + for (var i = 0; i < this.sortProperties.length; ++i) { + sortProperty = this.sortProperties[i]; + result = Ember.compare(get(itemA, sortProperty), isProxy ? itemB[sortProperty] : get(itemB, sortProperty)); - @method _scheduledDestroy - */ - _scheduledDestroy: function() { - if (this.isDestroyed) { return; } - destroy(this); - this.isDestroyed = true; - }, + if (result !== 0) { + asc = this.sortPropertyAscending[sortProperty]; + return asc ? result : (-1 * result); + } + } - bind: function(to, from) { - if (!(from instanceof Ember.Binding)) { from = Ember.Binding.from(from); } - from.to(to).connect(this); - return from; - }, + return 0; + }; - /** - Returns a string representation which attempts to provide more information - than Javascript's `toString` typically does, in a generic way for all Ember - objects. + instanceMeta.binarySearch = binarySearch; + }; + } - App.Person = Em.Object.extend() - person = App.Person.create() - person.toString() //=> "" + return Ember.arrayComputed(itemsKey, { + initialize: initFn, - If the object's class is not defined on an Ember namespace, it will - indicate it is a subclass of the registered superclass: + addedItem: function (array, item, changeMeta, instanceMeta) { + var index = instanceMeta.binarySearch(array, item); + array.insertAt(index, item); + return array; + }, - Student = App.Person.extend() - student = Student.create() - student.toString() //=> "<(subclass of App.Person):ember1025>" + removedItem: function (array, item, changeMeta, instanceMeta) { + var proxyProperties, index, searchItem; - If the method `toStringExtension` is defined, its return value will be - included in the output. + if (changeMeta.previousValues) { + proxyProperties = merge({ content: item }, changeMeta.previousValues); - App.Teacher = App.Person.extend({ - toStringExtension: function() { - return this.get('fullName'); - } - }); - teacher = App.Teacher.create() - teacher.toString(); //=> "" + searchItem = SearchProxy.create(proxyProperties); + } else { + searchItem = item; + } - @method toString - @return {String} string representation - */ - toString: function toString() { - var hasToStringExtension = typeof this.toStringExtension === 'function', - extension = hasToStringExtension ? ":" + this.toStringExtension() : ''; - var ret = '<'+this.constructor.toString()+':'+guidFor(this)+extension+'>'; - this.toString = makeToString(ret); - return ret; - } -}); + index = instanceMeta.binarySearch(array, searchItem); + array.removeAt(index); + return array; + } + }); +}; -CoreObject.PrototypeMixin.ownerConstructor = CoreObject; +})(); -function makeToString(ret) { - return function() { return ret; }; -} -if (Ember.config.overridePrototypeMixin) { - Ember.config.overridePrototypeMixin(CoreObject.PrototypeMixin); -} -CoreObject.__super__ = null; +(function() { +Ember.RSVP = requireModule('rsvp'); -var ClassMixin = Mixin.create({ +Ember.RSVP.onerrorDefault = function(error) { + if (error instanceof Error) { + if (Ember.testing) { + if (Ember.Test && Ember.Test.adapter) { + Ember.Test.adapter.exception(error); + } else { + throw error; + } + } else { + Ember.Logger.error(error.stack); + Ember.assert(error, false); + } + } +}; - ClassMixin: Ember.required(), +Ember.RSVP.on('error', Ember.RSVP.onerrorDefault); - PrototypeMixin: Ember.required(), +})(); - isClass: true, - isMethod: false, - /** - Creates a new subclass. +(function() { +/** +@module ember +@submodule ember-runtime +*/ - ```javascript - App.Person = Ember.Object.extend({ - say: function(thing) { - alert(thing); - } - }); - ``` +var a_slice = Array.prototype.slice; - This defines a new subclass of Ember.Object: `App.Person`. It contains one method: `say()`. +var expandProperties = Ember.expandProperties; - You can also create a subclass from any existing class by calling its `extend()` method. For example, you might want to create a subclass of Ember's built-in `Ember.View` class: +if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Function) { - ```javascript - App.PersonView = Ember.View.extend({ - tagName: 'li', - classNameBindings: ['isAdministrator'] - }); - ``` + /** + The `property` extension of Javascript's Function prototype is available + when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is + `true`, which is the default. - When defining a subclass, you can override methods but still access the implementation of your parent class by calling the special `_super()` method: + Computed properties allow you to treat a function like a property: ```javascript - App.Person = Ember.Object.extend({ - say: function(thing) { - var name = this.get('name'); - alert(name + ' says: ' + thing); - } - }); + MyApp.President = Ember.Object.extend({ + firstName: '', + lastName: '', - App.Soldier = App.Person.extend({ - say: function(thing) { - this._super(thing + ", sir!"); - }, - march: function(numberOfHours) { - alert(this.get('name') + ' marches for ' + numberOfHours + ' hours.') - } + fullName: function() { + return this.get('firstName') + ' ' + this.get('lastName'); + + // Call this flag to mark the function as a property + }.property() }); - var yehuda = App.Soldier.create({ - name: "Yehuda Katz" + var president = MyApp.President.create({ + firstName: "Barack", + lastName: "Obama" }); - yehuda.say("Yes"); // alerts "Yehuda Katz says: Yes, sir!" + president.get('fullName'); // "Barack Obama" ``` - The `create()` on line #17 creates an *instance* of the `App.Soldier` class. The `extend()` on line #8 creates a *subclass* of `App.Person`. Any instance of the `App.Person` class will *not* have the `march()` method. + Treating a function like a property is useful because they can work with + bindings, just like any other property. - You can also pass `Ember.Mixin` classes to add additional properties to the subclass. + Many computed properties have dependencies on other properties. For + example, in the above example, the `fullName` property depends on + `firstName` and `lastName` to determine its value. You can tell Ember + about these dependencies like this: ```javascript - App.Person = Ember.Object.extend({ - say: function(thing) { - alert(this.get('name') + ' says: ' + thing); - } - }); + MyApp.President = Ember.Object.extend({ + firstName: '', + lastName: '', - App.SingingMixin = Ember.Mixin.create({ - sing: function(thing){ - alert(this.get('name') + ' sings: la la la ' + thing); - } - }); + fullName: function() { + return this.get('firstName') + ' ' + this.get('lastName'); - App.BroadwayStar = App.Person.extend(App.SingingMixin, { - dance: function() { - alert(this.get('name') + ' dances: tap tap tap tap '); - } + // Tell Ember.js that this computed property depends on firstName + // and lastName + }.property('firstName', 'lastName') }); ``` - The `App.BroadwayStar` class contains three methods: `say()`, `sing()`, and `dance()`. + Make sure you list these dependencies so Ember knows when to update + bindings that connect to a computed property. Changing a dependency + will not immediately trigger an update of the computed property, but + will instead clear the cache so that it is updated when the next `get` + is called on the property. - @method extend - @static + See [Ember.ComputedProperty](/api/classes/Ember.ComputedProperty.html), [Ember.computed](/api/#method_computed). - @param {Ember.Mixin} [mixins]* One or more Ember.Mixin classes - @param {Object} [arguments]* Object containing values to use within the new class + @method property + @for Function */ - extend: function() { - var Class = makeCtor(), proto; - Class.ClassMixin = Mixin.create(this.ClassMixin); - Class.PrototypeMixin = Mixin.create(this.PrototypeMixin); - - Class.ClassMixin.ownerConstructor = Class; - Class.PrototypeMixin.ownerConstructor = Class; + Function.prototype.property = function() { + var ret = Ember.computed(this); + // ComputedProperty.prototype.property expands properties; no need for us to + // do so here. + return ret.property.apply(ret, arguments); + }; - reopen.apply(Class.PrototypeMixin, arguments); + /** + The `observes` extension of Javascript's Function prototype is available + when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is + true, which is the default. - Class.superclass = this; - Class.__super__ = this.prototype; + You can observe property changes simply by adding the `observes` + call to the end of your method declarations in classes that you write. + For example: - proto = Class.prototype = o_create(this.prototype); - proto.constructor = Class; - generateGuid(proto, 'ember'); - meta(proto).proto = proto; // this will disable observers on prototype + ```javascript + Ember.Object.extend({ + valueObserver: function() { + // Executes whenever the "value" property changes + }.observes('value') + }); + ``` - Class.ClassMixin.apply(Class); - return Class; - }, + In the future this method may become asynchronous. If you want to ensure + synchronous behavior, use `observesImmediately`. - /** - Equivalent to doing `extend(arguments).create()`. - If possible use the normal `create` method instead. + See `Ember.observer`. - @method createWithMixins - @static - @param [arguments]* + @method observes + @for Function */ - createWithMixins: function() { - var C = this; - if (arguments.length>0) { this._initMixins(arguments); } - return new C(); - }, - - /** - Creates an instance of a class. Accepts either no arguments, or an object - containing values to initialize the newly instantiated object with. + Function.prototype.observes = function() { + var addWatchedProperty = function (obs) { watched.push(obs); }; + var watched = []; - ```javascript - App.Person = Ember.Object.extend({ - helloWorld: function() { - alert("Hi, my name is " + this.get('name')); - } - }); + for (var i=0; i0) { this._initProperties(arguments); } - return new C(); - }, + Function.prototype.observesImmediately = function() { + for (var i=0, l=arguments.length; i b` -ClassMixin.ownerConstructor = CoreObject; + Default implementation raises an exception. -if (Ember.config.overrideClassMixin) { - Ember.config.overrideClassMixin(ClassMixin); -} + @method compare + @param a {Object} the first object to compare + @param b {Object} the second object to compare + @return {Integer} the result of the comparison + */ + compare: Ember.required(Function) -CoreObject.ClassMixin = ClassMixin; -ClassMixin.apply(CoreObject); +}); -Ember.CoreObject = CoreObject; })(); @@ -16161,232 +17470,156 @@ Ember.CoreObject = CoreObject; @submodule ember-runtime */ -/** - `Ember.Object` is the main base class for all Ember objects. It is a subclass - of `Ember.CoreObject` with the `Ember.Observable` mixin applied. For details, - see the documentation for each of these. - - @class Object - @namespace Ember - @extends Ember.CoreObject - @uses Ember.Observable -*/ -Ember.Object = Ember.CoreObject.extend(Ember.Observable); -Ember.Object.toString = function() { return "Ember.Object"; }; - -})(); - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ -var get = Ember.get, indexOf = Ember.ArrayPolyfills.indexOf; +var get = Ember.get, set = Ember.set; /** - A Namespace is an object usually used to contain other objects or methods - such as an application or framework. Create a namespace anytime you want - to define one of these new containers. + Implements some standard methods for copying an object. Add this mixin to + any object you create that can create a copy of itself. This mixin is + added automatically to the built-in array. - # Example Usage + You should generally implement the `copy()` method to return a copy of the + receiver. - ```javascript - MyFramework = Ember.Namespace.create({ - VERSION: '1.0.0' - }); - ``` + Note that `frozenCopy()` will only work if you also implement + `Ember.Freezable`. - @class Namespace + @class Copyable @namespace Ember - @extends Ember.Object + @since Ember 0.9 */ -var Namespace = Ember.Namespace = Ember.Object.extend({ - isNamespace: true, +Ember.Copyable = Ember.Mixin.create({ - init: function() { - Ember.Namespace.NAMESPACES.push(this); - Ember.Namespace.PROCESSED = false; - }, + /** + Override to return a copy of the receiver. Default implementation raises + an exception. - toString: function() { - var name = get(this, 'name'); - if (name) { return name; } + @method copy + @param {Boolean} deep if `true`, a deep copy of the object should be made + @return {Object} copy of receiver + */ + copy: Ember.required(Function), - findNamespaces(); - return this[Ember.GUID_KEY+'_name']; - }, + /** + If the object implements `Ember.Freezable`, then this will return a new + copy if the object is not frozen and the receiver if the object is frozen. - nameClasses: function() { - processNamespace([this.toString()], this, {}); - }, + Raises an exception if you try to call this method on a object that does + not support freezing. - destroy: function() { - var namespaces = Ember.Namespace.NAMESPACES; - Ember.lookup[this.toString()] = undefined; - namespaces.splice(indexOf.call(namespaces, this), 1); - this._super(); - } -}); + You should use this method whenever you want a copy of a freezable object + since a freezable object can simply return itself without actually + consuming more memory. -Namespace.reopenClass({ - NAMESPACES: [Ember], - NAMESPACES_BY_ID: {}, - PROCESSED: false, - processAll: processAllNamespaces, - byName: function(name) { - if (!Ember.BOOTED) { - processAllNamespaces(); + @method frozenCopy + @return {Object} copy of receiver or receiver + */ + frozenCopy: function() { + if (Ember.Freezable && Ember.Freezable.detect(this)) { + return get(this, 'isFrozen') ? this : this.copy().freeze(); + } else { + throw new Ember.Error(Ember.String.fmt("%@ does not support freezing", [this])); } - - return NAMESPACES_BY_ID[name]; } }); -var NAMESPACES_BY_ID = Namespace.NAMESPACES_BY_ID; - -var hasOwnProp = ({}).hasOwnProperty, - guidFor = Ember.guidFor; - -function processNamespace(paths, root, seen) { - var idx = paths.length; +})(); - NAMESPACES_BY_ID[paths.join('.')] = root; - // Loop over all of the keys in the namespace, looking for classes - for(var key in root) { - if (!hasOwnProp.call(root, key)) { continue; } - var obj = root[key]; - // If we are processing the `Ember` namespace, for example, the - // `paths` will start with `["Ember"]`. Every iteration through - // the loop will update the **second** element of this list with - // the key, so processing `Ember.View` will make the Array - // `['Ember', 'View']`. - paths[idx] = key; +(function() { +/** +@module ember +@submodule ember-runtime +*/ - // If we have found an unprocessed class - if (obj && obj.toString === classToString) { - // Replace the class' `toString` with the dot-separated path - // and set its `NAME_KEY` - obj.toString = makeToString(paths.join('.')); - obj[NAME_KEY] = paths.join('.'); - // Support nested namespaces - } else if (obj && obj.isNamespace) { - // Skip aliased namespaces - if (seen[guidFor(obj)]) { continue; } - seen[guidFor(obj)] = true; +var get = Ember.get, set = Ember.set; - // Process the child namespace - processNamespace(paths, obj, seen); - } - } +/** + The `Ember.Freezable` mixin implements some basic methods for marking an + object as frozen. Once an object is frozen it should be read only. No changes + may be made the internal state of the object. - paths.length = idx; // cut out last item -} + ## Enforcement -function findNamespaces() { - var Namespace = Ember.Namespace, lookup = Ember.lookup, obj, isNamespace; + To fully support freezing in your subclass, you must include this mixin and + override any method that might alter any property on the object to instead + raise an exception. You can check the state of an object by checking the + `isFrozen` property. - if (Namespace.PROCESSED) { return; } + Although future versions of JavaScript may support language-level freezing + object objects, that is not the case today. Even if an object is freezable, + it is still technically possible to modify the object, even though it could + break other parts of your application that do not expect a frozen object to + change. It is, therefore, very important that you always respect the + `isFrozen` property on all freezable objects. - for (var prop in lookup) { - // These don't raise exceptions but can cause warnings - if (prop === "parent" || prop === "top" || prop === "frameElement" || prop === "webkitStorageInfo") { continue; } + ## Example Usage - // get(window.globalStorage, 'isNamespace') would try to read the storage for domain isNamespace and cause exception in Firefox. - // globalStorage is a storage obsoleted by the WhatWG storage specification. See https://developer.mozilla.org/en/DOM/Storage#globalStorage - if (prop === "globalStorage" && lookup.StorageList && lookup.globalStorage instanceof lookup.StorageList) { continue; } - // Unfortunately, some versions of IE don't support window.hasOwnProperty - if (lookup.hasOwnProperty && !lookup.hasOwnProperty(prop)) { continue; } + The example below shows a simple object that implement the `Ember.Freezable` + protocol. - // At times we are not allowed to access certain properties for security reasons. - // There are also times where even if we can access them, we are not allowed to access their properties. - try { - obj = Ember.lookup[prop]; - isNamespace = obj && obj.isNamespace; - } catch (e) { - continue; - } + ```javascript + Contact = Ember.Object.extend(Ember.Freezable, { + firstName: null, + lastName: null, - if (isNamespace) { - Ember.deprecate("Namespaces should not begin with lowercase.", /^[A-Z]/.test(prop)); - obj[NAME_KEY] = prop; + // swaps the names + swapNames: function() { + if (this.get('isFrozen')) throw Ember.FROZEN_ERROR; + var tmp = this.get('firstName'); + this.set('firstName', this.get('lastName')); + this.set('lastName', tmp); + return this; } - } -} -var NAME_KEY = Ember.NAME_KEY = Ember.GUID_KEY + '_name'; - -function superClassString(mixin) { - var superclass = mixin.superclass; - if (superclass) { - if (superclass[NAME_KEY]) { return superclass[NAME_KEY]; } - else { return superClassString(superclass); } - } else { - return; - } -} + }); -function classToString() { - if (!Ember.BOOTED && !this[NAME_KEY]) { - processAllNamespaces(); - } + c = Contact.create({ firstName: "John", lastName: "Doe" }); + c.swapNames(); // returns c + c.freeze(); + c.swapNames(); // EXCEPTION + ``` - var ret; + ## Copying - if (this[NAME_KEY]) { - ret = this[NAME_KEY]; - } else if (this._toString) { - ret = this._toString; - } else { - var str = superClassString(this); - if (str) { - ret = "(subclass of " + str + ")"; - } else { - ret = "(unknown mixin)"; - } - this.toString = makeToString(ret); - } + Usually the `Ember.Freezable` protocol is implemented in cooperation with the + `Ember.Copyable` protocol, which defines a `frozenCopy()` method that will + return a frozen object, if the object implements this method as well. - return ret; -} + @class Freezable + @namespace Ember + @since Ember 0.9 +*/ +Ember.Freezable = Ember.Mixin.create({ -function processAllNamespaces() { - var unprocessedNamespaces = !Namespace.PROCESSED, - unprocessedMixins = Ember.anyUnprocessedMixins; + /** + Set to `true` when the object is frozen. Use this property to detect + whether your object is frozen or not. - if (unprocessedNamespaces) { - findNamespaces(); - Namespace.PROCESSED = true; - } + @property isFrozen + @type Boolean + */ + isFrozen: false, - if (unprocessedNamespaces || unprocessedMixins) { - var namespaces = Namespace.NAMESPACES, namespace; - for (var i=0, l=namespaces.length; i= length, then append to the end of the array. + @param {Number} amt Number of elements that should be removed from + the array, starting at *idx*. + @param {Array} objects An array of zero or more objects that should be + inserted into the array at *idx* + */ + replace: Ember.required(), - objectAt: function(idx) { - return get(this, 'content') && this.objectAtContent(idx); - }, + /** + Remove all elements from self. This is useful if you + want to reuse an existing array without having to recreate it. - length: Ember.computed(function() { - var arrangedContent = get(this, 'arrangedContent'); - return arrangedContent ? get(arrangedContent, 'length') : 0; - // No dependencies since Enumerable notifies length of change - }), + ```javascript + var colors = ["red", "green", "blue"]; + color.length(); // 3 + colors.clear(); // [] + colors.length(); // 0 + ``` - _replace: function(idx, amt, objects) { - var content = get(this, 'content'); - Ember.assert('The content property of '+ this.constructor + ' should be set before modifying it', content); - if (content) this.replaceContent(idx, amt, objects); + @method clear + @return {Ember.Array} An empty Array. + */ + clear: function () { + var len = get(this, 'length'); + if (len === 0) return this; + this.replace(0, len, EMPTY); return this; }, - replace: function() { - if (get(this, 'arrangedContent') === get(this, 'content')) { - this._replace.apply(this, arguments); - } else { - throw new Ember.Error("Using replace on an arranged ArrayProxy is not allowed."); - } - }, + /** + This will use the primitive `replace()` method to insert an object at the + specified index. - _insertAt: function(idx, object) { - if (idx > get(this, 'content.length')) throw new Ember.Error(OUT_OF_RANGE_EXCEPTION); - this._replace(idx, 0, [object]); - return this; - }, + ```javascript + var colors = ["red", "green", "blue"]; + colors.insertAt(2, "yellow"); // ["red", "green", "yellow", "blue"] + colors.insertAt(5, "orange"); // Error: Index out of range + ``` + @method insertAt + @param {Number} idx index of insert the object at. + @param {Object} object object to insert + @return this + */ insertAt: function(idx, object) { - if (get(this, 'arrangedContent') === get(this, 'content')) { - return this._insertAt(idx, object); - } else { - throw new Ember.Error("Using insertAt on an arranged ArrayProxy is not allowed."); - } + if (idx > get(this, 'length')) throw new Ember.Error(OUT_OF_RANGE_EXCEPTION) ; + this.replace(idx, 0, [object]) ; + return this ; }, + /** + Remove an object at the specified index using the `replace()` primitive + method. You can pass either a single index, or a start and a length. + + If you pass a start and length that is beyond the + length this method will throw an `OUT_OF_RANGE_EXCEPTION`. + + ```javascript + var colors = ["red", "green", "blue", "yellow", "orange"]; + colors.removeAt(0); // ["green", "blue", "yellow", "orange"] + colors.removeAt(2, 2); // ["green", "blue"] + colors.removeAt(4, 2); // Error: Index out of range + ``` + + @method removeAt + @param {Number} start index, start of range + @param {Number} len length of passing range + @return {Object} receiver + */ removeAt: function(start, len) { if ('number' === typeof start) { - var content = get(this, 'content'), - arrangedContent = get(this, 'arrangedContent'), - indices = [], i; if ((start < 0) || (start >= get(this, 'length'))) { throw new Ember.Error(OUT_OF_RANGE_EXCEPTION); } + // fast case if (len === undefined) len = 1; - - // Get a list of indices in original content to remove - for (i=start; i= 0) { + var curObject = this.objectAt(loc) ; + if (curObject === obj) this.removeAt(loc) ; + } + return this ; }, - willDestroy: function() { - this._teardownArrangedContent(); - this._teardownContent(); + addObject: function(obj) { + if (!this.contains(obj)) this.pushObject(obj); + return this ; } + }); })(); @@ -16730,147 +18061,137 @@ Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray,/** @scope Ember.Array @submodule ember-runtime */ -var get = Ember.get, - set = Ember.set, - fmt = Ember.String.fmt, - addBeforeObserver = Ember.addBeforeObserver, - addObserver = Ember.addObserver, - removeBeforeObserver = Ember.removeBeforeObserver, - removeObserver = Ember.removeObserver, - propertyWillChange = Ember.propertyWillChange, - propertyDidChange = Ember.propertyDidChange, - meta = Ember.meta, - defineProperty = Ember.defineProperty; - -function contentPropertyWillChange(content, contentKey) { - var key = contentKey.slice(8); // remove "content." - if (key in this) { return; } // if shadowed in proxy - propertyWillChange(this, key); -} - -function contentPropertyDidChange(content, contentKey) { - var key = contentKey.slice(8); // remove "content." - if (key in this) { return; } // if shadowed in proxy - propertyDidChange(this, key); -} +var get = Ember.get, set = Ember.set; /** - `Ember.ObjectProxy` forwards all properties not defined by the proxy itself - to a proxied `content` object. +`Ember.TargetActionSupport` is a mixin that can be included in a class +to add a `triggerAction` method with semantics similar to the Handlebars +`{{action}}` helper. In normal Ember usage, the `{{action}}` helper is +usually the best choice. This mixin is most often useful when you are +doing more complex event handling in View objects. - ```javascript - object = Ember.Object.create({ - name: 'Foo' - }); +See also `Ember.ViewTargetActionSupport`, which has +view-aware defaults for target and actionContext. - proxy = Ember.ObjectProxy.create({ - content: object - }); +@class TargetActionSupport +@namespace Ember +@extends Ember.Mixin +*/ +Ember.TargetActionSupport = Ember.Mixin.create({ + target: null, + action: null, + actionContext: null, - // Access and change existing properties - proxy.get('name') // 'Foo' - proxy.set('name', 'Bar'); - object.get('name') // 'Bar' + targetObject: Ember.computed(function() { + var target = get(this, 'target'); - // Create new 'description' property on `object` - proxy.set('description', 'Foo is a whizboo baz'); - object.get('description') // 'Foo is a whizboo baz' - ``` + if (Ember.typeOf(target) === "string") { + var value = get(this, target); + if (value === undefined) { value = get(Ember.lookup, target); } + return value; + } else { + return target; + } + }).property('target'), - While `content` is unset, setting a property to be delegated will throw an - Error. + actionContextObject: Ember.computed(function() { + var actionContext = get(this, 'actionContext'); + + if (Ember.typeOf(actionContext) === "string") { + var value = get(this, actionContext); + if (value === undefined) { value = get(Ember.lookup, actionContext); } + return value; + } else { + return actionContext; + } + }).property('actionContext'), + + /** + Send an `action` with an `actionContext` to a `target`. The action, actionContext + and target will be retrieved from properties of the object. For example: ```javascript - proxy = Ember.ObjectProxy.create({ - content: null, - flag: null + App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { + target: Ember.computed.alias('controller'), + action: 'save', + actionContext: Ember.computed.alias('context'), + click: function() { + this.triggerAction(); // Sends the `save` action, along with the current context + // to the current controller + } }); - proxy.set('flag', true); - proxy.get('flag'); // true - proxy.get('foo'); // undefined - proxy.set('foo', 'data'); // throws Error ``` - Delegated properties can be bound to and will change when content is updated. - - Computed properties on the proxy itself can depend on delegated properties. + The `target`, `action`, and `actionContext` can be provided as properties of + an optional object argument to `triggerAction` as well. ```javascript - ProxyWithComputedProperty = Ember.ObjectProxy.extend({ - fullName: function () { - var firstName = this.get('firstName'), - lastName = this.get('lastName'); - if (firstName && lastName) { - return firstName + ' ' + lastName; - } - return firstName || lastName; - }.property('firstName', 'lastName') + App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { + click: function() { + this.triggerAction({ + action: 'save', + target: this.get('controller'), + actionContext: this.get('context'), + }); // Sends the `save` action, along with the current context + // to the current controller + } }); + ``` - proxy = ProxyWithComputedProperty.create(); - - proxy.get('fullName'); // undefined - proxy.set('content', { - firstName: 'Tom', lastName: 'Dale' - }); // triggers property change for fullName on proxy + The `actionContext` defaults to the object you mixing `TargetActionSupport` into. + But `target` and `action` must be specified either as properties or with the argument + to `triggerAction`, or a combination: - proxy.get('fullName'); // 'Tom Dale' + ```javascript + App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { + target: Ember.computed.alias('controller'), + click: function() { + this.triggerAction({ + action: 'save' + }); // Sends the `save` action, along with a reference to `this`, + // to the current controller + } + }); ``` - @class ObjectProxy - @namespace Ember - @extends Ember.Object -*/ -Ember.ObjectProxy = Ember.Object.extend(/** @scope Ember.ObjectProxy.prototype */ { - /** - The object whose properties will be forwarded. - - @property content - @type Ember.Object - @default null + @method triggerAction + @param opts {Hash} (optional, with the optional keys action, target and/or actionContext) + @return {Boolean} true if the action was sent successfully and did not return false */ - content: null, - _contentDidChange: Ember.observer(function() { - Ember.assert("Can't set ObjectProxy's content to itself", this.get('content') !== this); - }, 'content'), + triggerAction: function(opts) { + opts = opts || {}; + var action = opts.action || get(this, 'action'), + target = opts.target || get(this, 'targetObject'), + actionContext = opts.actionContext; - isTruthy: Ember.computed.bool('content'), + function args(options, actionName) { + var ret = []; + if (actionName) { ret.push(actionName); } - _debugContainerKey: null, + return ret.concat(options); + } - willWatchProperty: function (key) { - var contentKey = 'content.' + key; - addBeforeObserver(this, contentKey, null, contentPropertyWillChange); - addObserver(this, contentKey, null, contentPropertyDidChange); - }, + if (typeof actionContext === 'undefined') { + actionContext = get(this, 'actionContextObject') || this; + } - didUnwatchProperty: function (key) { - var contentKey = 'content.' + key; - removeBeforeObserver(this, contentKey, null, contentPropertyWillChange); - removeObserver(this, contentKey, null, contentPropertyDidChange); - }, + if (target && action) { + var ret; - unknownProperty: function (key) { - var content = get(this, 'content'); - if (content) { - return get(content, key); - } - }, + if (target.send) { + ret = target.send.apply(target, args(actionContext, action)); + } else { + Ember.assert("The action '" + action + "' did not exist on " + target, typeof target[action] === 'function'); + ret = target[action].apply(target, args(actionContext)); + } - setUnknownProperty: function (key, value) { - var m = meta(this); - if (m.proto === this) { - // if marked as prototype then just defineProperty - // rather than delegate - defineProperty(this, key, null, value); - return value; - } + if (ret !== false) ret = true; - var content = get(this, 'content'); - Ember.assert(fmt("Cannot delegate set('%@', %@) to the 'content' property of object proxy %@: its 'content' is undefined.", [key, value, this]), content); - return set(content, key, value); + return ret; + } else { + return false; + } } - }); })(); @@ -16883,205 +18204,259 @@ Ember.ObjectProxy = Ember.Object.extend(/** @scope Ember.ObjectProxy.prototype * @submodule ember-runtime */ +/** + This mixin allows for Ember objects to subscribe to and emit events. -var set = Ember.set, get = Ember.get, guidFor = Ember.guidFor; -var forEach = Ember.EnumerableUtils.forEach, - indexOf = Ember.ArrayPolyfills.indexOf; + ```javascript + App.Person = Ember.Object.extend(Ember.Evented, { + greet: function() { + // ... + this.trigger('greet'); + } + }); -var EachArray = Ember.Object.extend(Ember.Array, { + var person = App.Person.create(); - init: function(content, keyName, owner) { - this._super(); - this._keyName = keyName; - this._owner = owner; - this._content = content; - }, + person.on('greet', function() { + console.log('Our person has greeted'); + }); - objectAt: function(idx) { - var item = this._content.objectAt(idx); - return item && get(item, this._keyName); - }, + person.greet(); - length: Ember.computed(function() { - var content = this._content; - return content ? get(content, 'length') : 0; - }) + // outputs: 'Our person has greeted' + ``` -}); + You can also chain multiple event subscriptions: -var IS_OBSERVER = /^.+:(before|change)$/; + ```javascript + person.on('greet', function() { + console.log('Our person has greeted'); + }).one('greet', function() { + console.log('Offer one-time special'); + }).off('event', this, forgetThis); + ``` -function addObserverForContentKey(content, keyName, proxy, idx, loc) { - var objects = proxy._objects, guid; - if (!objects) objects = proxy._objects = {}; + @class Evented + @namespace Ember + */ +Ember.Evented = Ember.Mixin.create({ - while(--loc>=idx) { - var item = content.objectAt(loc); - if (item) { - Ember.assert('When using @each to observe the array ' + content + ', the array must return an object', Ember.typeOf(item) === 'instance' || Ember.typeOf(item) === 'object'); - Ember.addBeforeObserver(item, keyName, proxy, 'contentKeyWillChange'); - Ember.addObserver(item, keyName, proxy, 'contentKeyDidChange'); + /** + Subscribes to a named event with given function. - // keep track of the index each item was found at so we can map - // it back when the obj changes. - guid = guidFor(item); - if (!objects[guid]) objects[guid] = []; - objects[guid].push(loc); - } - } -} + ```javascript + person.on('didLoad', function() { + // fired once the person has loaded + }); + ``` -function removeObserverForContentKey(content, keyName, proxy, idx, loc) { - var objects = proxy._objects; - if (!objects) objects = proxy._objects = {}; - var indicies, guid; + An optional target can be passed in as the 2nd argument that will + be set as the "this" for the callback. This is a good way to give your + function access to the object triggering the event. When the target + parameter is used the callback becomes the third argument. - while(--loc>=idx) { - var item = content.objectAt(loc); - if (item) { - Ember.removeBeforeObserver(item, keyName, proxy, 'contentKeyWillChange'); - Ember.removeObserver(item, keyName, proxy, 'contentKeyDidChange'); + @method on + @param {String} name The name of the event + @param {Object} [target] The "this" binding for the callback + @param {Function} method The callback to execute + @return this + */ + on: function(name, target, method) { + Ember.addListener(this, name, target, method); + return this; + }, - guid = guidFor(item); - indicies = objects[guid]; - indicies[indexOf.call(indicies, loc)] = null; + /** + Subscribes a function to a named event and then cancels the subscription + after the first time the event is triggered. It is good to use ``one`` when + you only care about the first time an event has taken place. + + This function takes an optional 2nd argument that will become the "this" + value for the callback. If this argument is passed then the 3rd argument + becomes the function. + + @method one + @param {String} name The name of the event + @param {Object} [target] The "this" binding for the callback + @param {Function} method The callback to execute + @return this + */ + one: function(name, target, method) { + if (!method) { + method = target; + target = null; } - } -} -/** - This is the object instance returned when you get the `@each` property on an - array. It uses the unknownProperty handler to automatically create - EachArray instances for property names. + Ember.addListener(this, name, target, method, true); + return this; + }, - @private - @class EachProxy - @namespace Ember - @extends Ember.Object -*/ -Ember.EachProxy = Ember.Object.extend({ + /** + Triggers a named event for the object. Any additional arguments + will be passed as parameters to the functions that are subscribed to the + event. - init: function(content) { - this._super(); - this._content = content; - content.addArrayObserver(this); + ```javascript + person.on('didEat', function(food) { + console.log('person ate some ' + food); + }); - // in case someone is already observing some keys make sure they are - // added - forEach(Ember.watchedEvents(this), function(eventName) { - this.didAddListener(eventName); - }, this); + person.trigger('didEat', 'broccoli'); + + // outputs: person ate some broccoli + ``` + @method trigger + @param {String} name The name of the event + @param {Object...} args Optional arguments to pass on + */ + trigger: function(name) { + var args = [], i, l; + for (i = 1, l = arguments.length; i < l; i++) { + args.push(arguments[i]); + } + Ember.sendEvent(this, name, args); }, /** - You can directly access mapped properties by simply requesting them. - The `unknownProperty` handler will generate an EachArray of each item. + Cancels subscription for given name, target, and method. - @method unknownProperty - @param keyName {String} - @param value {*} + @method off + @param {String} name The name of the event + @param {Object} target The target of the subscription + @param {Function} method The function of the subscription + @return this */ - unknownProperty: function(keyName, value) { - var ret; - ret = new EachArray(this._content, keyName, this); - Ember.defineProperty(this, keyName, null, ret); - this.beginObservingContentKey(keyName); - return ret; + off: function(name, target, method) { + Ember.removeListener(this, name, target, method); + return this; }, - // .......................................................... - // ARRAY CHANGES - // Invokes whenever the content array itself changes. + /** + Checks to see if object has any subscriptions for named event. - arrayWillChange: function(content, idx, removedCnt, addedCnt) { - var keys = this._keys, key, lim; + @method has + @param {String} name The name of the event + @return {Boolean} does the object have a subscription for event + */ + has: function(name) { + return Ember.hasListeners(this, name); + } +}); - lim = removedCnt>0 ? idx+removedCnt : -1; - Ember.beginPropertyChanges(this); +})(); - for(key in keys) { - if (!keys.hasOwnProperty(key)) { continue; } - if (lim>0) { removeObserverForContentKey(content, key, this, idx, lim); } - Ember.propertyWillChange(this, key); +(function() { +var RSVP = requireModule("rsvp"); + +if (Ember.FEATURES['ember-runtime-test-friendly-promises']) { + + var asyncStart = function() { + if (Ember.Test && Ember.Test.adapter) { + Ember.Test.adapter.asyncStart(); } + }; - Ember.propertyWillChange(this._content, '@each'); - Ember.endPropertyChanges(this); - }, + var asyncEnd = function() { + if (Ember.Test && Ember.Test.adapter) { + Ember.Test.adapter.asyncEnd(); + } + }; - arrayDidChange: function(content, idx, removedCnt, addedCnt) { - var keys = this._keys, lim; + RSVP.configure('async', function(callback, promise) { + var async = !Ember.run.currentRunLoop; - lim = addedCnt>0 ? idx+addedCnt : -1; - Ember.changeProperties(function() { - for(var key in keys) { - if (!keys.hasOwnProperty(key)) { continue; } + if (Ember.testing && async) { asyncStart(); } - if (lim>0) { addObserverForContentKey(content, key, this, idx, lim); } + Ember.run.backburner.schedule('actions', function(){ + if (Ember.testing && async) { asyncEnd(); } + callback(promise); + }); + }); +} else { + RSVP.configure('async', function(callback, promise) { + Ember.run.backburner.schedule('actions', function(){ + callback(promise); + }); + }); +} - Ember.propertyDidChange(this, key); - } +RSVP.Promise.prototype.fail = function(callback, label){ + Ember.deprecate('RSVP.Promise.fail has been renamed as RSVP.Promise.catch'); + return this['catch'](callback, label); +}; - Ember.propertyDidChange(this._content, '@each'); - }, this); - }, +/** +@module ember +@submodule ember-runtime +*/ - // .......................................................... - // LISTEN FOR NEW OBSERVERS AND OTHER EVENT LISTENERS - // Start monitoring keys based on who is listening... +var get = Ember.get; - didAddListener: function(eventName) { - if (IS_OBSERVER.test(eventName)) { - this.beginObservingContentKey(eventName.slice(0, -7)); - } - }, +/** + @class Deferred + @namespace Ember + */ +Ember.DeferredMixin = Ember.Mixin.create({ + /** + Add handlers to be called when the Deferred object is resolved or rejected. - didRemoveListener: function(eventName) { - if (IS_OBSERVER.test(eventName)) { - this.stopObservingContentKey(eventName.slice(0, -7)); + @method then + @param {Function} resolve a callback function to be called when done + @param {Function} reject a callback function to be called when failed + */ + then: function(resolve, reject, label) { + var deferred, promise, entity; + + entity = this; + deferred = get(this, '_deferred'); + promise = deferred.promise; + + function fulfillmentHandler(fulfillment) { + if (fulfillment === promise) { + return resolve(entity); + } else { + return resolve(fulfillment); + } } + + return promise.then(resolve && fulfillmentHandler, reject, label); }, - // .......................................................... - // CONTENT KEY OBSERVING - // Actual watch keys on the source content. + /** + Resolve a Deferred object and call any `doneCallbacks` with the given args. - beginObservingContentKey: function(keyName) { - var keys = this._keys; - if (!keys) keys = this._keys = {}; - if (!keys[keyName]) { - keys[keyName] = 1; - var content = this._content, - len = get(content, 'length'); - addObserverForContentKey(content, keyName, this, 0, len); + @method resolve + */ + resolve: function(value) { + var deferred, promise; + + deferred = get(this, '_deferred'); + promise = deferred.promise; + + if (value === this) { + deferred.resolve(promise); } else { - keys[keyName]++; + deferred.resolve(value); } }, - stopObservingContentKey: function(keyName) { - var keys = this._keys; - if (keys && (keys[keyName]>0) && (--keys[keyName]<=0)) { - var content = this._content, - len = get(content, 'length'); - removeObserverForContentKey(content, keyName, this, 0, len); - } - }, + /** + Reject a Deferred object and call any `failCallbacks` with the given args. - contentKeyWillChange: function(obj, keyName) { - Ember.propertyWillChange(this, keyName); + @method reject + */ + reject: function(value) { + get(this, '_deferred').reject(value); }, - contentKeyDidChange: function(obj, keyName) { - Ember.propertyDidChange(this, keyName); - } - + _deferred: Ember.computed(function() { + return RSVP.defer('Ember: DeferredMixin - ' + this); + }) }); - })(); @@ -17092,796 +18467,971 @@ Ember.EachProxy = Ember.Object.extend({ @submodule ember-runtime */ +var get = Ember.get, typeOf = Ember.typeOf; -var get = Ember.get, set = Ember.set, replace = Ember.EnumerableUtils._replace; +/** + The `Ember.ActionHandler` mixin implements support for moving an `actions` + property to an `_actions` property at extend time, and adding `_actions` + to the object's mergedProperties list. -// Add Ember.Array to Array.prototype. Remove methods with native -// implementations and supply some more optimized versions of generic methods -// because they are so common. -var NativeArray = Ember.Mixin.create(Ember.MutableArray, Ember.Observable, Ember.Copyable, { + `Ember.ActionHandler` is available on some familiar classes including + `Ember.Route`, `Ember.View`, `Ember.Component`, and controllers such as + `Ember.Controller` and `Ember.ObjectController`. + (Internally the mixin is used by `Ember.CoreView`, `Ember.ControllerMixin`, + and `Ember.Route` and available to the above classes through + inheritance.) - // because length is a built-in property we need to know to just get the - // original property. - get: function(key) { - if (key==='length') return this.length; - else if ('number' === typeof key) return this[key]; - else return this._super(key); - }, + @class ActionHandler + @namespace Ember +*/ +Ember.ActionHandler = Ember.Mixin.create({ + mergedProperties: ['_actions'], - objectAt: function(idx) { - return this[idx]; - }, + /** + The collection of functions, keyed by name, available on this + `ActionHandler` as action targets. - // primitive for array support. - replace: function(idx, amt, objects) { + These functions will be invoked when a matching `{{action}}` is triggered + from within a template and the application's current route is this route. - if (this.isFrozen) throw Ember.FROZEN_ERROR; + Actions can also be invoked from other parts of your application + via `ActionHandler#send`. - // if we replaced exactly the same number of items, then pass only the - // replaced range. Otherwise, pass the full remaining array length - // since everything has shifted - var len = objects ? get(objects, 'length') : 0; - this.arrayContentWillChange(idx, amt, len); + The `actions` hash will inherit action handlers from + the `actions` hash defined on extended parent classes + or mixins rather than just replace the entire hash, e.g.: - if (!objects || objects.length === 0) { - this.splice(idx, amt); - } else { - replace(this, idx, amt, objects); - } + ```js + App.CanDisplayBanner = Ember.Mixin.create({ + actions: { + displayBanner: function(msg) { + // ... + } + } + }); - this.arrayContentDidChange(idx, amt, len); - return this; - }, + App.WelcomeRoute = Ember.Route.extend(App.CanDisplayBanner, { + actions: { + playMusic: function() { + // ... + } + } + }); - // If you ask for an unknown property, then try to collect the value - // from member items. - unknownProperty: function(key, value) { - var ret;// = this.reducedProperty(key, value) ; - if ((value !== undefined) && ret === undefined) { - ret = this[key] = value; - } - return ret ; - }, + // `WelcomeRoute`, when active, will be able to respond + // to both actions, since the actions hash is merged rather + // then replaced when extending mixins / parent classes. + this.send('displayBanner'); + this.send('playMusic'); + ``` - // If browser did not implement indexOf natively, then override with - // specialized version - indexOf: function(object, startAt) { - var idx, len = this.length; + Within a Controller, Route, View or Component's action handler, + the value of the `this` context is the Controller, Route, View or + Component object: - if (startAt === undefined) startAt = 0; - else startAt = (startAt < 0) ? Math.ceil(startAt) : Math.floor(startAt); - if (startAt < 0) startAt += len; + ```js + App.SongRoute = Ember.Route.extend({ + actions: { + myAction: function() { + this.controllerFor("song"); + this.transitionTo("other.route"); + ... + } + } + }); + ``` - for(idx=startAt;idx=0;idx--) { - if (this[idx] === object) return idx ; - } - return -1; - }, + App.AnnoyingDebugRoute = Ember.Route.extend(App.DebugRoute, { + actions: { + debugRouteInformation: function() { + // also call the debugRouteInformation of mixed in App.DebugRoute + this._super(); - copy: function(deep) { - if (deep) { - return this.map(function(item) { return Ember.copy(item, true); }); - } + // show additional annoyance + window.alert(...); + } + } + }); + ``` - return this.slice(); - } -}); + ## Bubbling -// Remove any methods implemented natively so we don't override them -var ignore = ['length']; -Ember.EnumerableUtils.forEach(NativeArray.keys(), function(methodName) { - if (Array.prototype[methodName]) ignore.push(methodName); -}); + By default, an action will stop bubbling once a handler defined + on the `actions` hash handles it. To continue bubbling the action, + you must return `true` from the handler: -if (ignore.length>0) { - NativeArray = NativeArray.without.apply(NativeArray, ignore); -} + ```js + App.Router.map(function() { + this.resource("album", function() { + this.route("song"); + }); + }); -/** - The NativeArray mixin contains the properties needed to to make the native - Array support Ember.MutableArray and all of its dependent APIs. Unless you - have `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Array` set to - false, this will be applied automatically. Otherwise you can apply the mixin - at anytime by calling `Ember.NativeArray.activate`. + App.AlbumRoute = Ember.Route.extend({ + actions: { + startPlaying: function() { + } + } + }); - @class NativeArray - @namespace Ember - @uses Ember.MutableArray - @uses Ember.Observable - @uses Ember.Copyable -*/ -Ember.NativeArray = NativeArray; + App.AlbumSongRoute = Ember.Route.extend({ + actions: { + startPlaying: function() { + // ... -/** - Creates an `Ember.NativeArray` from an Array like object. - Does not modify the original object. Ember.A is not needed if - `Ember.EXTEND_PROTOTYPES` is `true` (the default value). However, - it is recommended that you use Ember.A when creating addons for - ember or when you can not garentee that `Ember.EXTEND_PROTOTYPES` - will be `true`. + if (actionShouldAlsoBeTriggeredOnParentRoute) { + return true; + } + } + } + }); + ``` - Example + @property actions + @type Hash + @default null + */ - ```js - var Pagination = Ember.CollectionView.extend({ - tagName: 'ul', - classNames: ['pagination'], - init: function() { - this._super(); - if (!this.get('content')) { - this.set('content', Ember.A([])); + /** + Moves `actions` to `_actions` at extend time. Note that this currently + modifies the mixin themselves, which is technically dubious but + is practically of little consequence. This may change in the future. + + @private + @method willMergeMixin + */ + willMergeMixin: function(props) { + var hashName; + + if (!props._actions) { + Ember.assert("'actions' should not be a function", typeof(props.actions) !== 'function'); + + if (typeOf(props.actions) === 'object') { + hashName = 'actions'; + } else if (typeOf(props.events) === 'object') { + Ember.deprecate('Action handlers contained in an `events` object are deprecated in favor of putting them in an `actions` object', false); + hashName = 'events'; + } + + if (hashName) { + props._actions = Ember.merge(props._actions || {}, props[hashName]); } + + delete props[hashName]; } - }); - ``` + }, - @method A - @for Ember - @return {Ember.NativeArray} -*/ -Ember.A = function(arr) { - if (arr === undefined) { arr = []; } - return Ember.Array.detect(arr) ? arr : Ember.NativeArray.apply(arr); -}; + /** + Triggers a named action on the `ActionHandler`. Any parameters + supplied after the `actionName` string will be passed as arguments + to the action target function. -/** - Activates the mixin on the Array.prototype if not already applied. Calling - this method more than once is safe. This will be called when ember is loaded - unless you have `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Array` - set to `false`. + If the `ActionHandler` has its `target` property set, actions may + bubble to the `target`. Bubbling happens when an `actionName` can + not be found in the `ActionHandler`'s `actions` hash or if the + action target function returns `true`. - Example + Example - ```js - if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) { - Ember.NativeArray.activate(); - } - ``` + ```js + App.WelcomeRoute = Ember.Route.extend({ + actions: { + playTheme: function() { + this.send('playMusic', 'theme.mp3'); + }, + playMusic: function(track) { + // ... + } + } + }); + ``` - @method activate - @for Ember.NativeArray - @static - @return {void} -*/ -Ember.NativeArray.activate = function() { - NativeArray.apply(Array.prototype); + @method send + @param {String} actionName The action to trigger + @param {*} context a context to send with the action + */ + send: function(actionName) { + var args = [].slice.call(arguments, 1), target; - Ember.A = function(arr) { return arr || []; }; -}; + if (this._actions && this._actions[actionName]) { + if (this._actions[actionName].apply(this, args) === true) { + // handler returned true, so this action will bubble + } else { + return; + } + } else if (!Ember.FEATURES.isEnabled('ember-routing-drop-deprecated-action-style') && this.deprecatedSend && this.deprecatedSendHandles && this.deprecatedSendHandles(actionName)) { + Ember.warn("The current default is deprecated but will prefer to handle actions directly on the controller instead of a similarly named action in the actions hash. To turn off this deprecated feature set: Ember.FEATURES['ember-routing-drop-deprecated-action-style'] = true"); + if (this.deprecatedSend.apply(this, [].slice.call(arguments)) === true) { + // handler return true, so this action will bubble + } else { + return; + } + } -if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) { - Ember.NativeArray.activate(); -} + if (target = get(this, 'target')) { + Ember.assert("The `target` for " + this + " (" + target + ") does not have a `send` method", typeof target.send === 'function'); + target.send.apply(target, arguments); + } + } +}); })(); (function() { +var set = Ember.set, get = Ember.get, + not = Ember.computed.not, + or = Ember.computed.or; + /** -@module ember -@submodule ember-runtime -*/ + @module ember + @submodule ember-runtime + */ -var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, isNone = Ember.isNone, fmt = Ember.String.fmt; +function tap(proxy, promise) { + set(proxy, 'isFulfilled', false); + set(proxy, 'isRejected', false); + + return promise.then(function(value) { + set(proxy, 'isFulfilled', true); + set(proxy, 'content', value); + return value; + }, function(reason) { + set(proxy, 'isRejected', true); + set(proxy, 'reason', reason); + throw reason; + }, "Ember: PromiseProxy"); +} /** - An unordered collection of objects. + A low level mixin making ObjectProxy, ObjectController or ArrayController's promise aware. - A Set works a bit like an array except that its items are not ordered. You - can create a set to efficiently test for membership for an object. You can - also iterate through a set just like an array, even accessing objects by - index, however there is no guarantee as to their order. + ```javascript + var ObjectPromiseController = Ember.ObjectController.extend(Ember.PromiseProxyMixin); - All Sets are observable via the Enumerable Observer API - which works - on any enumerable object including both Sets and Arrays. + var controller = ObjectPromiseController.create({ + promise: $.getJSON('/some/remote/data.json') + }); - ## Creating a Set + controller.then(function(json){ + // the json + }, function(reason) { + // the reason why you have no json + }); + ``` - You can create a set like you would most objects using - `new Ember.Set()`. Most new sets you create will be empty, but you can - also initialize the set with some content by passing an array or other - enumerable of objects to the constructor. + the controller has bindable attributes which + track the promises life cycle - Finally, you can pass in an existing set and the set will be copied. You - can also create a copy of a set by calling `Ember.Set#copy()`. + ```javascript + controller.get('isPending') //=> true + controller.get('isSettled') //=> false + controller.get('isRejected') //=> false + controller.get('isFulfilled') //=> false + ``` + + When the the $.getJSON completes, and the promise is fulfilled + with json, the life cycle attributes will update accordingly. ```javascript - // creates a new empty set - var foundNames = new Ember.Set(); + controller.get('isPending') //=> false + controller.get('isSettled') //=> true + controller.get('isRejected') //=> false + controller.get('isFulfilled') //=> true + ``` - // creates a set with four names in it. - var names = new Ember.Set(["Charles", "Tom", "Juan", "Alex"]); // :P + As the controller is an ObjectController, and the json now its content, + all the json properties will be available directly from the controller. - // creates a copy of the names set. - var namesCopy = new Ember.Set(names); + ```javascript + // Assuming the following json: + { + firstName: 'Stefan', + lastName: 'Penner' + } - // same as above. - var anotherNamesCopy = names.copy(); + // both properties will accessible on the controller + controller.get('firstName') //=> 'Stefan' + controller.get('lastName') //=> 'Penner' ``` - ## Adding/Removing Objects + If the controller is backing a template, the attributes are + bindable from within that template - You generally add or remove objects from a set using `add()` or - `remove()`. You can add any type of object including primitives such as - numbers, strings, and booleans. + ```handlebars + {{#if isPending}} + loading... + {{else}} + firstName: {{firstName}} + lastName: {{lastName}} + {{/if}} + ``` + @class Ember.PromiseProxyMixin +*/ +Ember.PromiseProxyMixin = Ember.Mixin.create({ + /** + If the proxied promise is rejected this will contain the reason + provided. - Unlike arrays, objects can only exist one time in a set. If you call `add()` - on a set with the same object multiple times, the object will only be added - once. Likewise, calling `remove()` with the same object multiple times will - remove the object the first time and have no effect on future calls until - you add the object to the set again. + @property reason + @default null + */ + reason: null, - NOTE: You cannot add/remove `null` or `undefined` to a set. Any attempt to do - so will be ignored. + /** + Once the proxied promise has settled this will become `false`. - In addition to add/remove you can also call `push()`/`pop()`. Push behaves - just like `add()` but `pop()`, unlike `remove()` will pick an arbitrary - object, remove it and return it. This is a good way to use a set as a job - queue when you don't care which order the jobs are executed in. + @property isPending + @default true + */ + isPending: not('isSettled').readOnly(), - ## Testing for an Object + /** + Once the proxied promise has settled this will become `true`. - To test for an object's presence in a set you simply call - `Ember.Set#contains()`. + @property isSettled + @default false + */ + isSettled: or('isRejected', 'isFulfilled').readOnly(), - ## Observing changes + /** + Will become `true` if the proxied promise is rejected. - When using `Ember.Set`, you can observe the `"[]"` property to be - alerted whenever the content changes. You can also add an enumerable - observer to the set to be notified of specific objects that are added and - removed from the set. See [Ember.Enumerable](/api/classes/Ember.Enumerable.html) - for more information on enumerables. + @property isRejected + @default false + */ + isRejected: false, - This is often unhelpful. If you are filtering sets of objects, for instance, - it is very inefficient to re-filter all of the items each time the set - changes. It would be better if you could just adjust the filtered set based - on what was changed on the original set. The same issue applies to merging - sets, as well. + /** + Will become `true` if the proxied promise is fulfilled. - ## Other Methods + @property isFullfilled + @default false + */ + isFulfilled: false, - `Ember.Set` primary implements other mixin APIs. For a complete reference - on the methods you will use with `Ember.Set`, please consult these mixins. - The most useful ones will be `Ember.Enumerable` and - `Ember.MutableEnumerable` which implement most of the common iterator - methods you are used to on Array. + /** + The promise whose fulfillment value is being proxied by this object. - Note that you can also use the `Ember.Copyable` and `Ember.Freezable` - APIs on `Ember.Set` as well. Once a set is frozen it can no longer be - modified. The benefit of this is that when you call `frozenCopy()` on it, - Ember will avoid making copies of the set. This allows you to write - code that can know with certainty when the underlying set data will or - will not be modified. + This property must be specified upon creation, and should not be + changed once created. - @class Set - @namespace Ember - @extends Ember.CoreObject - @uses Ember.MutableEnumerable - @uses Ember.Copyable - @uses Ember.Freezable - @since Ember 0.9 -*/ -Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Ember.Freezable, - /** @scope Ember.Set.prototype */ { + Example: - // .......................................................... - // IMPLEMENT ENUMERABLE APIS - // + ```javascript + Ember.ObjectController.extend(Ember.PromiseProxyMixin).create({ + promise: + }); + ``` + + @property promise + */ + promise: Ember.computed(function(key, promise) { + if (arguments.length === 2) { + return tap(this, promise); + } else { + throw new Ember.Error("PromiseProxy's promise must be set"); + } + }), /** - This property will change as the number of objects in the set changes. + An alias to the proxied promise's `then`. - @property length - @type number - @default 0 + See RSVP.Promise.then. + + @method then + @param {Function} callback + @return {RSVP.Promise} */ - length: 0, + then: promiseAlias('then'), /** - Clears the set. This is useful if you want to reuse an existing set - without having to recreate it. + An alias to the proxied promise's `catch`. - ```javascript - var colors = new Ember.Set(["red", "green", "blue"]); - colors.length; // 3 - colors.clear(); - colors.length; // 0 - ``` + See RSVP.Promise.catch. - @method clear - @return {Ember.Set} An empty Set + @method catch + @param {Function} callback + @return {RSVP.Promise} */ - clear: function() { - if (this.isFrozen) { throw new Ember.Error(Ember.FROZEN_ERROR); } + 'catch': promiseAlias('catch'), - var len = get(this, 'length'); - if (len === 0) { return this; } + /** + An alias to the proxied promise's `finally`. - var guid; + See RSVP.Promise.finally. - this.enumerableContentWillChange(len, 0); - Ember.propertyWillChange(this, 'firstObject'); - Ember.propertyWillChange(this, 'lastObject'); + @method finally + @param {Function} callback + @return {RSVP.Promise} + */ + 'finally': promiseAlias('finally') - for (var i=0; i < len; i++) { - guid = guidFor(this[i]); - delete this[guid]; - delete this[i]; - } +}); - set(this, 'length', 0); +function promiseAlias(name) { + return function () { + var promise = get(this, 'promise'); + return promise[name].apply(promise, arguments); + }; +} - Ember.propertyDidChange(this, 'firstObject'); - Ember.propertyDidChange(this, 'lastObject'); - this.enumerableContentDidChange(len, 0); +})(); - return this; - }, - /** - Returns true if the passed object is also an enumerable that contains the - same objects as the receiver. - ```javascript - var colors = ["red", "green", "blue"], - same_colors = new Ember.Set(colors); +(function() { - same_colors.isEqual(colors); // true - same_colors.isEqual(["purple", "brown"]); // false - ``` +})(); - @method isEqual - @param {Ember.Set} obj the other object. - @return {Boolean} - */ - isEqual: function(obj) { - // fail fast - if (!Ember.Enumerable.detect(obj)) return false; - var loc = get(this, 'length'); - if (get(obj, 'length') !== loc) return false; - while(--loc >= 0) { - if (!obj.contains(this[loc])) return false; - } +(function() { +var get = Ember.get, + forEach = Ember.EnumerableUtils.forEach, + RETAIN = 'r', + INSERT = 'i', + DELETE = 'd'; - return true; - }, +/** + An `Ember.TrackedArray` tracks array operations. It's useful when you want to + lazily compute the indexes of items in an array after they've been shifted by + subsequent operations. - /** - Adds an object to the set. Only non-`null` objects can be added to a set - and those can only be added once. If the object is already in the set or - the passed value is null this method will have no effect. + @class TrackedArray + @namespace Ember + @param {array} [items=[]] The array to be tracked. This is used just to get + the initial items for the starting state of retain:n. +*/ +Ember.TrackedArray = function (items) { + if (arguments.length < 1) { items = []; } - This is an alias for `Ember.MutableEnumerable.addObject()`. + var length = get(items, 'length'); - ```javascript - var colors = new Ember.Set(); - colors.add("blue"); // ["blue"] - colors.add("blue"); // ["blue"] - colors.add("red"); // ["blue", "red"] - colors.add(null); // ["blue", "red"] - colors.add(undefined); // ["blue", "red"] - ``` + if (length) { + this._operations = [new ArrayOperation(RETAIN, length, items)]; + } else { + this._operations = []; + } +}; - @method add - @param {Object} obj The object to add. - @return {Ember.Set} The set itself. - */ - add: Ember.aliasMethod('addObject'), +Ember.TrackedArray.RETAIN = RETAIN; +Ember.TrackedArray.INSERT = INSERT; +Ember.TrackedArray.DELETE = DELETE; - /** - Removes the object from the set if it is found. If you pass a `null` value - or an object that is already not in the set, this method will have no - effect. This is an alias for `Ember.MutableEnumerable.removeObject()`. +Ember.TrackedArray.prototype = { - ```javascript - var colors = new Ember.Set(["red", "green", "blue"]); - colors.remove("red"); // ["blue", "green"] - colors.remove("purple"); // ["blue", "green"] - colors.remove(null); // ["blue", "green"] - ``` + /** + Track that `newItems` were added to the tracked array at `index`. - @method remove - @param {Object} obj The object to remove - @return {Ember.Set} The set itself. + @method addItems + @param index + @param newItems */ - remove: Ember.aliasMethod('removeObject'), + addItems: function (index, newItems) { + var count = get(newItems, 'length'); + if (count < 1) { return; } - /** - Removes the last element from the set and returns it, or `null` if it's empty. + var match = this._findArrayOperation(index), + arrayOperation = match.operation, + arrayOperationIndex = match.index, + arrayOperationRangeStart = match.rangeStart, + composeIndex, + splitIndex, + splitItems, + splitArrayOperation, + newArrayOperation; - ```javascript - var colors = new Ember.Set(["green", "blue"]); - colors.pop(); // "blue" - colors.pop(); // "green" - colors.pop(); // null - ``` + newArrayOperation = new ArrayOperation(INSERT, count, newItems); - @method pop - @return {Object} The removed object from the set or null. - */ - pop: function() { - if (get(this, 'isFrozen')) throw new Ember.Error(Ember.FROZEN_ERROR); - var obj = this.length > 0 ? this[this.length-1] : null; - this.remove(obj); - return obj; + if (arrayOperation) { + if (!match.split) { + // insert left of arrayOperation + this._operations.splice(arrayOperationIndex, 0, newArrayOperation); + composeIndex = arrayOperationIndex; + } else { + this._split(arrayOperationIndex, index - arrayOperationRangeStart, newArrayOperation); + composeIndex = arrayOperationIndex + 1; + } + } else { + // insert at end + this._operations.push(newArrayOperation); + composeIndex = arrayOperationIndex; + } + + this._composeInsert(composeIndex); }, /** - Inserts the given object on to the end of the set. It returns - the set itself. - - This is an alias for `Ember.MutableEnumerable.addObject()`. - - ```javascript - var colors = new Ember.Set(); - colors.push("red"); // ["red"] - colors.push("green"); // ["red", "green"] - colors.push("blue"); // ["red", "green", "blue"] - ``` + Track that `count` items were removed at `index`. - @method push - @return {Ember.Set} The set itself. + @method removeItems + @param index + @param count */ - push: Ember.aliasMethod('addObject'), - - /** - Removes the last element from the set and returns it, or `null` if it's empty. + removeItems: function (index, count) { + if (count < 1) { return; } - This is an alias for `Ember.Set.pop()`. + var match = this._findArrayOperation(index), + arrayOperation = match.operation, + arrayOperationIndex = match.index, + arrayOperationRangeStart = match.rangeStart, + newArrayOperation, + composeIndex; - ```javascript - var colors = new Ember.Set(["green", "blue"]); - colors.shift(); // "blue" - colors.shift(); // "green" - colors.shift(); // null - ``` + newArrayOperation = new ArrayOperation(DELETE, count); + if (!match.split) { + // insert left of arrayOperation + this._operations.splice(arrayOperationIndex, 0, newArrayOperation); + composeIndex = arrayOperationIndex; + } else { + this._split(arrayOperationIndex, index - arrayOperationRangeStart, newArrayOperation); + composeIndex = arrayOperationIndex + 1; + } - @method shift - @return {Object} The removed object from the set or null. - */ - shift: Ember.aliasMethod('pop'), + return this._composeDelete(composeIndex); + }, /** - Inserts the given object on to the end of the set. It returns - the set itself. + Apply all operations, reducing them to retain:n, for `n`, the number of + items in the array. - This is an alias of `Ember.Set.push()` + `callback` will be called for each operation and will be passed the following arguments: - ```javascript - var colors = new Ember.Set(); - colors.unshift("red"); // ["red"] - colors.unshift("green"); // ["red", "green"] - colors.unshift("blue"); // ["red", "green", "blue"] - ``` + * {array} items The items for the given operation + * {number} offset The computed offset of the items, ie the index in the + array of the first item for this operation. + * {string} operation The type of the operation. One of + `Ember.TrackedArray.{RETAIN, DELETE, INSERT}` - @method unshift - @return {Ember.Set} The set itself. + @method apply + @param {function} callback */ - unshift: Ember.aliasMethod('push'), - - /** - Adds each object in the passed enumerable to the set. + apply: function (callback) { + var items = [], + offset = 0; - This is an alias of `Ember.MutableEnumerable.addObjects()` + forEach(this._operations, function (arrayOperation) { + callback(arrayOperation.items, offset, arrayOperation.type); - ```javascript - var colors = new Ember.Set(); - colors.addEach(["red", "green", "blue"]); // ["red", "green", "blue"] - ``` + if (arrayOperation.type !== DELETE) { + offset += arrayOperation.count; + items = items.concat(arrayOperation.items); + } + }); - @method addEach - @param {Ember.Enumerable} objects the objects to add. - @return {Ember.Set} The set itself. - */ - addEach: Ember.aliasMethod('addObjects'), + this._operations = [new ArrayOperation(RETAIN, items.length, items)]; + }, /** - Removes each object in the passed enumerable to the set. + Return an `ArrayOperationMatch` for the operation that contains the item at `index`. - This is an alias of `Ember.MutableEnumerable.removeObjects()` - - ```javascript - var colors = new Ember.Set(["red", "green", "blue"]); - colors.removeEach(["red", "blue"]); // ["green"] - ``` + @method _findArrayOperation - @method removeEach - @param {Ember.Enumerable} objects the objects to remove. - @return {Ember.Set} The set itself. + @param {number} index the index of the item whose operation information + should be returned. + @private */ - removeEach: Ember.aliasMethod('removeObjects'), - - // .......................................................... - // PRIVATE ENUMERABLE SUPPORT - // - - init: function(items) { - this._super(); - if (items) this.addObjects(items); - }, + _findArrayOperation: function (index) { + var arrayOperationIndex, + len, + split = false, + arrayOperation, + arrayOperationRangeStart, + arrayOperationRangeEnd; - // implement Ember.Enumerable - nextObject: function(idx) { - return this[idx]; - }, + // OPTIMIZE: we could search these faster if we kept a balanced tree. + // find leftmost arrayOperation to the right of `index` + for (arrayOperationIndex = arrayOperationRangeStart = 0, len = this._operations.length; arrayOperationIndex < len; ++arrayOperationIndex) { + arrayOperation = this._operations[arrayOperationIndex]; - // more optimized version - firstObject: Ember.computed(function() { - return this.length > 0 ? this[0] : undefined; - }), + if (arrayOperation.type === DELETE) { continue; } - // more optimized version - lastObject: Ember.computed(function() { - return this.length > 0 ? this[this.length-1] : undefined; - }), + arrayOperationRangeEnd = arrayOperationRangeStart + arrayOperation.count - 1; - // implements Ember.MutableEnumerable - addObject: function(obj) { - if (get(this, 'isFrozen')) throw new Ember.Error(Ember.FROZEN_ERROR); - if (isNone(obj)) return this; // nothing to do + if (index === arrayOperationRangeStart) { + break; + } else if (index > arrayOperationRangeStart && index <= arrayOperationRangeEnd) { + split = true; + break; + } else { + arrayOperationRangeStart = arrayOperationRangeEnd + 1; + } + } - var guid = guidFor(obj), - idx = this[guid], - len = get(this, 'length'), - added ; + return new ArrayOperationMatch(arrayOperation, arrayOperationIndex, split, arrayOperationRangeStart); + }, - if (idx>=0 && idx=0 && idx 0; ++i) { + nextArrayOperation = this._operations[i]; + nextOp = nextArrayOperation.type; + nextCount = nextArrayOperation.count; - // swap items - basically move the item to the end so it can be removed - if (idx < len-1) { - last = this[len-1]; - this[idx] = last; - this[guidFor(last)] = idx; + if (nextOp === DELETE) { + arrayOperation.count += nextCount; + continue; } - delete this[guid]; - delete this[len-1]; - set(this, 'length', len-1); - - if (isFirst) { Ember.propertyDidChange(this, 'firstObject'); } - if (isLast) { Ember.propertyDidChange(this, 'lastObject'); } - this.enumerableContentDidChange(removed, null); - } + if (nextCount > deletesToGo) { + // d:2 {r,i}:5 we reduce the retain or insert, but it stays + removedItems = removedItems.concat(nextArrayOperation.items.splice(0, deletesToGo)); + nextArrayOperation.count -= deletesToGo; - return this; - }, + // In the case where we truncate the last arrayOperation, we don't need to + // remove it; also the deletesToGo reduction is not the entirety of + // nextCount + i -= 1; + nextCount = deletesToGo; - // optimized version - contains: function(obj) { - return this[guidFor(obj)]>=0; - }, + deletesToGo = 0; + } else { + if (nextCount === deletesToGo) { + // Handle edge case of d:2 i:2 in which case both operations go away + // during composition. + removeNewAndNextOp = true; + } + removedItems = removedItems.concat(nextArrayOperation.items); + deletesToGo -= nextCount; + } - copy: function() { - var C = this.constructor, ret = new C(), loc = get(this, 'length'); - set(ret, 'length', loc); - while(--loc>=0) { - ret[loc] = this[loc]; - ret[guidFor(this[loc])] = loc; + if (nextOp === INSERT) { + // d:2 i:3 will result in delete going away + arrayOperation.count -= nextCount; + } } - return ret; - }, - toString: function() { - var len = this.length, idx, array = []; - for(idx = 0; idx < len; idx++) { - array[idx] = this[idx]; + if (arrayOperation.count > 0) { + // compose our new delete with possibly several operations to the right of + // disparate types + this._operations.splice(index+1, i-1-index); + } else { + // The delete operation can go away; it has merely reduced some other + // operation, as in d:3 i:4; it may also have eliminated that operation, + // as in d:3 i:3. + this._operations.splice(index, removeNewAndNextOp ? 2 : 1); } - return fmt("Ember.Set<%@>", [array.join(',')]); - } - -}); - -})(); + return removedItems; + }, + toString: function () { + var str = ""; + forEach(this._operations, function (operation) { + str += " " + operation.type + ":" + operation.count; + }); + return str.substring(1); + } +}; -(function() { -var DeferredMixin = Ember.DeferredMixin, // mixins/deferred - get = Ember.get; +/** + Internal data structure to represent an array operation. -var Deferred = Ember.Object.extend(DeferredMixin); + @method ArrayOperation + @private + @param {string} type The type of the operation. One of + `Ember.TrackedArray.{RETAIN, INSERT, DELETE}` + @param {number} count The number of items in this operation. + @param {array} items The items of the operation, if included. RETAIN and + INSERT include their items, DELETE does not. +*/ +function ArrayOperation (operation, count, items) { + this.type = operation; // RETAIN | INSERT | DELETE + this.count = count; + this.items = items; +} -Deferred.reopenClass({ - promise: function(callback, binding) { - var deferred = Deferred.create(); - callback.call(binding, deferred); - return deferred; - } -}); +/** + Internal data structure used to include information when looking up operations + by item index. -Ember.Deferred = Deferred; + @method ArrayOperationMatch + @private + @param {ArrayOperation} operation + @param {number} index The index of `operation` in the array of operations. + @param {boolean} split Whether or not the item index searched for would + require a split for a new operation type. + @param {number} rangeStart The index of the first item in the operation, + with respect to the tracked array. The index of the last item can be computed + from `rangeStart` and `operation.count`. +*/ +function ArrayOperationMatch(operation, index, split, rangeStart) { + this.operation = operation; + this.index = index; + this.split = split; + this.rangeStart = rangeStart; +} })(); (function() { -var forEach = Ember.ArrayPolyfills.forEach; - -/** -@module ember -@submodule ember-runtime -*/ +var get = Ember.get, + forEach = Ember.EnumerableUtils.forEach, + RETAIN = 'r', + FILTER = 'f'; -var loadHooks = Ember.ENV.EMBER_LOAD_HOOKS || {}; -var loaded = {}; +function Operation (type, count) { + this.type = type; + this.count = count; +} /** + An `Ember.SubArray` tracks an array in a way similar to, but more specialized + than, `Ember.TrackedArray`. It is useful for keeping track of the indexes of + items within a filtered array. -Detects when a specific package of Ember (e.g. 'Ember.Handlebars') -has fully loaded and is available for extension. - -The provided `callback` will be called with the `name` passed -resolved from a string into the object: + @class SubArray + @namespace Ember +*/ +Ember.SubArray = function (length) { + if (arguments.length < 1) { length = 0; } -```javascript -Ember.onLoad('Ember.Handlebars' function(hbars){ - hbars.registerHelper(...); -}); -``` + if (length > 0) { + this._operations = [new Operation(RETAIN, length)]; + } else { + this._operations = []; + } +}; +Ember.SubArray.prototype = { + /** + Track that an item was added to the tracked array. -@method onLoad -@for Ember -@param name {String} name of hook -@param callback {Function} callback to be called -*/ -Ember.onLoad = function(name, callback) { - var object; + @method addItem - loadHooks[name] = loadHooks[name] || Ember.A(); - loadHooks[name].pushObject(callback); + @param {number} index The index of the item in the tracked array. + @param {boolean} match `true` iff the item is included in the subarray. - if (object = loaded[name]) { - callback(object); - } -}; + @return {number} The index of the item in the subarray. + */ + addItem: function(index, match) { + var returnValue = -1, + itemType = match ? RETAIN : FILTER, + self = this; -/** + this._findOperation(index, function(operation, operationIndex, rangeStart, rangeEnd, seenInSubArray) { + var newOperation, splitOperation; -Called when an Ember.js package (e.g Ember.Handlebars) has finished -loading. Triggers any callbacks registered for this event. + if (itemType === operation.type) { + ++operation.count; + } else if (index === rangeStart) { + // insert to the left of `operation` + self._operations.splice(operationIndex, 0, new Operation(itemType, 1)); + } else { + newOperation = new Operation(itemType, 1); + splitOperation = new Operation(operation.type, rangeEnd - index + 1); + operation.count = index - rangeStart; -@method runLoadHooks -@for Ember -@param name {String} name of hook -@param object {Object} object to pass to callbacks -*/ -Ember.runLoadHooks = function(name, object) { - loaded[name] = object; + self._operations.splice(operationIndex + 1, 0, newOperation, splitOperation); + } - if (loadHooks[name]) { - forEach.call(loadHooks[name], function(callback) { - callback(object); - }); - } -}; + if (match) { + if (operation.type === RETAIN) { + returnValue = seenInSubArray + (index - rangeStart); + } else { + returnValue = seenInSubArray; + } + } -})(); + self._composeAt(operationIndex); + }, function(seenInSubArray) { + self._operations.push(new Operation(itemType, 1)); + if (match) { + returnValue = seenInSubArray; + } + self._composeAt(self._operations.length-1); + }); -(function() { + return returnValue; + }, -})(); + /** + Track that an item was removed from the tracked array. + @method removeItem + @param {number} index The index of the item in the tracked array. -(function() { -var get = Ember.get; + @return {number} The index of the item in the subarray, or `-1` if the item + was not in the subarray. + */ + removeItem: function(index) { + var returnValue = -1, + self = this; -/** -@module ember -@submodule ember-runtime -*/ + this._findOperation(index, function (operation, operationIndex, rangeStart, rangeEnd, seenInSubArray) { + if (operation.type === RETAIN) { + returnValue = seenInSubArray + (index - rangeStart); + } -/** - `Ember.ControllerMixin` provides a standard interface for all classes that - compose Ember's controller layer: `Ember.Controller`, - `Ember.ArrayController`, and `Ember.ObjectController`. + if (operation.count > 1) { + --operation.count; + } else { + self._operations.splice(operationIndex, 1); + self._composeAt(operationIndex); + } + }, function() { + throw new Ember.Error("Can't remove an item that has never been added."); + }); - @class ControllerMixin - @namespace Ember -*/ -Ember.ControllerMixin = Ember.Mixin.create(Ember.ActionHandler, { - /* ducktype as a controller */ - isController: true, + return returnValue; + }, - /** - The object to which actions from the view should be sent. - For example, when a Handlebars template uses the `{{action}}` helper, - it will attempt to send the action to the view's controller's `target`. + _findOperation: function (index, foundCallback, notFoundCallback) { + var operationIndex, + len, + operation, + rangeStart, + rangeEnd, + seenInSubArray = 0; - By default, a controller's `target` is set to the router after it is - instantiated by `Ember.Application#initialize`. + // OPTIMIZE: change to balanced tree + // find leftmost operation to the right of `index` + for (operationIndex = rangeStart = 0, len = this._operations.length; operationIndex < len; rangeStart = rangeEnd + 1, ++operationIndex) { + operation = this._operations[operationIndex]; + rangeEnd = rangeStart + operation.count - 1; - @property target - @default null - */ - target: null, + if (index >= rangeStart && index <= rangeEnd) { + foundCallback(operation, operationIndex, rangeStart, rangeEnd, seenInSubArray); + return; + } else if (operation.type === RETAIN) { + seenInSubArray += operation.count; + } + } - container: null, + notFoundCallback(seenInSubArray); + }, - parentController: null, + _composeAt: function(index) { + var op = this._operations[index], + otherOp; - store: null, + if (!op) { + // Composing out of bounds is a no-op, as when removing the last operation + // in the list. + return; + } - model: Ember.computed.alias('content'), + if (index > 0) { + otherOp = this._operations[index-1]; + if (otherOp.type === op.type) { + op.count += otherOp.count; + this._operations.splice(index-1, 1); + --index; + } + } - deprecatedSendHandles: function(actionName) { - return !!this[actionName]; + if (index < this._operations.length-1) { + otherOp = this._operations[index+1]; + if (otherOp.type === op.type) { + op.count += otherOp.count; + this._operations.splice(index+1, 1); + } + } }, - deprecatedSend: function(actionName) { - var args = [].slice.call(arguments, 1); - Ember.assert('' + this + " has the action " + actionName + " but it is not a function", typeof this[actionName] === 'function'); - Ember.deprecate('Action handlers implemented directly on controllers are deprecated in favor of action handlers on an `actions` object (' + actionName + ' on ' + this + ')', false); - this[actionName].apply(this, args); - return; + toString: function () { + var str = ""; + forEach(this._operations, function (operation) { + str += " " + operation.type + ":" + operation.count; + }); + return str.substring(1); } -}); +}; -/** - @class Controller - @namespace Ember - @extends Ember.Object - @uses Ember.ControllerMixin -*/ -Ember.Controller = Ember.Object.extend(Ember.ControllerMixin); +})(); + + + +(function() { +Ember.Container = requireModule('container')['default']; +Ember.Container.set = Ember.set; + +})(); + + + +(function() { +Ember.Application = Ember.Namespace.extend(); })(); @@ -17893,275 +19443,331 @@ Ember.Controller = Ember.Object.extend(Ember.ControllerMixin); @submodule ember-runtime */ -var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; +var OUT_OF_RANGE_EXCEPTION = "Index out of range"; +var EMPTY = []; -/** - `Ember.SortableMixin` provides a standard interface for array proxies - to specify a sort order and maintain this sorting when objects are added, - removed, or updated without changing the implicit order of their underlying - content array: +var get = Ember.get, set = Ember.set; - ```javascript - songs = [ - {trackNumber: 4, title: 'Ob-La-Di, Ob-La-Da'}, - {trackNumber: 2, title: 'Back in the U.S.S.R.'}, - {trackNumber: 3, title: 'Glass Onion'}, - ]; +/** + An ArrayProxy wraps any other object that implements `Ember.Array` and/or + `Ember.MutableArray,` forwarding all requests. This makes it very useful for + a number of binding use cases or other cases where being able to swap + out the underlying array is useful. - songsController = Ember.ArrayController.create({ - content: songs, - sortProperties: ['trackNumber'], - sortAscending: true - }); + A simple example of usage: - songsController.get('firstObject'); // {trackNumber: 2, title: 'Back in the U.S.S.R.'} + ```javascript + var pets = ['dog', 'cat', 'fish']; + var ap = Ember.ArrayProxy.create({ content: Ember.A(pets) }); - songsController.addObject({trackNumber: 1, title: 'Dear Prudence'}); - songsController.get('firstObject'); // {trackNumber: 1, title: 'Dear Prudence'} + ap.get('firstObject'); // 'dog' + ap.set('content', ['amoeba', 'paramecium']); + ap.get('firstObject'); // 'amoeba' ``` - If you add or remove the properties to sort by or change the sort direction the content - sort order will be automatically updated. + This class can also be useful as a layer to transform the contents of + an array, as they are accessed. This can be done by overriding + `objectAtContent`: ```javascript - songsController.set('sortProperties', ['title']); - songsController.get('firstObject'); // {trackNumber: 2, title: 'Back in the U.S.S.R.'} + var pets = ['dog', 'cat', 'fish']; + var ap = Ember.ArrayProxy.create({ + content: Ember.A(pets), + objectAtContent: function(idx) { + return this.get('content').objectAt(idx).toUpperCase(); + } + }); - songsController.toggleProperty('sortAscending'); - songsController.get('firstObject'); // {trackNumber: 4, title: 'Ob-La-Di, Ob-La-Da'} + ap.get('firstObject'); // . 'DOG' ``` - SortableMixin works by sorting the arrangedContent array, which is the array that - arrayProxy displays. Due to the fact that the underlying 'content' array is not changed, that - array will not display the sorted list: - - ```javascript - songsController.get('content').get('firstObject'); // Returns the unsorted original content - songsController.get('firstObject'); // Returns the sorted content. - ``` - - Although the sorted content can also be accessed through the arrangedContent property, - it is preferable to use the proxied class and not the arrangedContent array directly. - - @class SortableMixin + @class ArrayProxy @namespace Ember - @uses Ember.MutableEnumerable + @extends Ember.Object + @uses Ember.MutableArray */ -Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { +Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray, { /** - Specifies which properties dictate the arrangedContent's sort order. - - When specifying multiple properties the sorting will use properties - from the `sortProperties` array prioritized from first to last. + The content array. Must be an object that implements `Ember.Array` and/or + `Ember.MutableArray.` - @property {Array} sortProperties + @property content + @type Ember.Array */ - sortProperties: null, + content: null, /** - Specifies the arrangedContent's sort direction + The array that the proxy pretends to be. In the default `ArrayProxy` + implementation, this and `content` are the same. Subclasses of `ArrayProxy` + can override this property to provide things like sorting and filtering. - @property {Boolean} sortAscending + @property arrangedContent */ - sortAscending: true, + arrangedContent: Ember.computed.alias('content'), /** - The function used to compare two values. You can override this if you - want to do custom comparisons. Functions must be of the type expected by - Array#sort, i.e. - return 0 if the two parameters are equal, - return a negative value if the first parameter is smaller than the second or - return a positive value otherwise: + Should actually retrieve the object at the specified index from the + content. You can override this method in subclasses to transform the + content item to something new. - ```javascript - function(x,y) { // These are assumed to be integers - if (x === y) - return 0; - return x < y ? -1 : 1; - } - ``` + This method will only be called if content is non-`null`. - @property sortFunction - @type {Function} - @default Ember.compare + @method objectAtContent + @param {Number} idx The index to retrieve. + @return {Object} the value or undefined if none found */ - sortFunction: Ember.compare, - - orderBy: function(item1, item2) { - var result = 0, - sortProperties = get(this, 'sortProperties'), - sortAscending = get(this, 'sortAscending'), - sortFunction = get(this, 'sortFunction'); + objectAtContent: function(idx) { + return get(this, 'arrangedContent').objectAt(idx); + }, - Ember.assert("you need to define `sortProperties`", !!sortProperties); + /** + Should actually replace the specified objects on the content array. + You can override this method in subclasses to transform the content item + into something new. - forEach(sortProperties, function(propertyName) { - if (result === 0) { - result = sortFunction(get(item1, propertyName), get(item2, propertyName)); - if ((result !== 0) && !sortAscending) { - result = (-1) * result; - } - } - }); + This method will only be called if content is non-`null`. - return result; + @method replaceContent + @param {Number} idx The starting index + @param {Number} amt The number of items to remove from the content. + @param {Array} objects Optional array of objects to insert or null if no + objects. + @return {void} + */ + replaceContent: function(idx, amt, objects) { + get(this, 'content').replace(idx, amt, objects); }, - destroy: function() { - var content = get(this, 'content'), - sortProperties = get(this, 'sortProperties'); + /** + Invoked when the content property is about to change. Notifies observers that the + entire array content will change. - if (content && sortProperties) { - forEach(content, function(item) { - forEach(sortProperties, function(sortProperty) { - Ember.removeObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); - }, this); - }, this); - } + @private + @method _contentWillChange + */ + _contentWillChange: Ember.beforeObserver('content', function() { + this._teardownContent(); + }), - return this._super(); + _teardownContent: function() { + var content = get(this, 'content'); + + if (content) { + content.removeArrayObserver(this, { + willChange: 'contentArrayWillChange', + didChange: 'contentArrayDidChange' + }); + } }, - isSorted: Ember.computed.bool('sortProperties'), + contentArrayWillChange: Ember.K, + contentArrayDidChange: Ember.K, /** - Overrides the default arrangedContent from arrayProxy in order to sort by sortFunction. - Also sets up observers for each sortProperty on each item in the content Array. - - @property arrangedContent - */ + Invoked when the content property changes. Notifies observers that the + entire array content has changed. - arrangedContent: Ember.computed('content', 'sortProperties.@each', function(key, value) { - var content = get(this, 'content'), - isSorted = get(this, 'isSorted'), - sortProperties = get(this, 'sortProperties'), - self = this; + @private + @method _contentDidChange + */ + _contentDidChange: Ember.observer('content', function() { + var content = get(this, 'content'); - if (content && isSorted) { - content = content.slice(); - content.sort(function(item1, item2) { - return self.orderBy(item1, item2); - }); - forEach(content, function(item) { - forEach(sortProperties, function(sortProperty) { - Ember.addObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); - }, this); - }, this); - return Ember.A(content); - } + Ember.assert("Can't set ArrayProxy's content to itself", content !== this); - return content; + this._setupContent(); }), - _contentWillChange: Ember.beforeObserver(function() { - var content = get(this, 'content'), - sortProperties = get(this, 'sortProperties'); + _setupContent: function() { + var content = get(this, 'content'); - if (content && sortProperties) { - forEach(content, function(item) { - forEach(sortProperties, function(sortProperty) { - Ember.removeObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); - }, this); - }, this); + if (content) { + Ember.assert(Ember.String.fmt('ArrayProxy expects an Array or ' + + 'Ember.ArrayProxy, but you passed %@', [typeof content]), + Ember.isArray(content) || content.isDestroyed); + + content.addArrayObserver(this, { + willChange: 'contentArrayWillChange', + didChange: 'contentArrayDidChange' + }); } + }, - this._super(); - }, 'content'), + _arrangedContentWillChange: Ember.beforeObserver('arrangedContent', function() { + var arrangedContent = get(this, 'arrangedContent'), + len = arrangedContent ? get(arrangedContent, 'length') : 0; - sortAscendingWillChange: Ember.beforeObserver(function() { - this._lastSortAscending = get(this, 'sortAscending'); - }, 'sortAscending'), + this.arrangedContentArrayWillChange(this, 0, len, undefined); + this.arrangedContentWillChange(this); - sortAscendingDidChange: Ember.observer(function() { - if (get(this, 'sortAscending') !== this._lastSortAscending) { - var arrangedContent = get(this, 'arrangedContent'); - arrangedContent.reverseObjects(); - } - }, 'sortAscending'), + this._teardownArrangedContent(arrangedContent); + }), - contentArrayWillChange: function(array, idx, removedCount, addedCount) { - var isSorted = get(this, 'isSorted'); + _arrangedContentDidChange: Ember.observer('arrangedContent', function() { + var arrangedContent = get(this, 'arrangedContent'), + len = arrangedContent ? get(arrangedContent, 'length') : 0; - if (isSorted) { - var arrangedContent = get(this, 'arrangedContent'); - var removedObjects = array.slice(idx, idx+removedCount); - var sortProperties = get(this, 'sortProperties'); + Ember.assert("Can't set ArrayProxy's content to itself", arrangedContent !== this); - forEach(removedObjects, function(item) { - arrangedContent.removeObject(item); + this._setupArrangedContent(); - forEach(sortProperties, function(sortProperty) { - Ember.removeObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); - }, this); - }, this); - } + this.arrangedContentDidChange(this); + this.arrangedContentArrayDidChange(this, 0, undefined, len); + }), - return this._super(array, idx, removedCount, addedCount); - }, + _setupArrangedContent: function() { + var arrangedContent = get(this, 'arrangedContent'); - contentArrayDidChange: function(array, idx, removedCount, addedCount) { - var isSorted = get(this, 'isSorted'), - sortProperties = get(this, 'sortProperties'); + if (arrangedContent) { + Ember.assert(Ember.String.fmt('ArrayProxy expects an Array or ' + + 'Ember.ArrayProxy, but you passed %@', [typeof arrangedContent]), + Ember.isArray(arrangedContent) || arrangedContent.isDestroyed); - if (isSorted) { - var addedObjects = array.slice(idx, idx+addedCount); + arrangedContent.addArrayObserver(this, { + willChange: 'arrangedContentArrayWillChange', + didChange: 'arrangedContentArrayDidChange' + }); + } + }, - forEach(addedObjects, function(item) { - this.insertItemSorted(item); + _teardownArrangedContent: function() { + var arrangedContent = get(this, 'arrangedContent'); - forEach(sortProperties, function(sortProperty) { - Ember.addObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); - }, this); - }, this); + if (arrangedContent) { + arrangedContent.removeArrayObserver(this, { + willChange: 'arrangedContentArrayWillChange', + didChange: 'arrangedContentArrayDidChange' + }); } - - return this._super(array, idx, removedCount, addedCount); }, - insertItemSorted: function(item) { - var arrangedContent = get(this, 'arrangedContent'); - var length = get(arrangedContent, 'length'); + arrangedContentWillChange: Ember.K, + arrangedContentDidChange: Ember.K, - var idx = this._binarySearch(item, 0, length); - arrangedContent.insertAt(idx, item); + objectAt: function(idx) { + return get(this, 'content') && this.objectAtContent(idx); }, - contentItemSortPropertyDidChange: function(item) { - var arrangedContent = get(this, 'arrangedContent'), - oldIndex = arrangedContent.indexOf(item), - leftItem = arrangedContent.objectAt(oldIndex - 1), - rightItem = arrangedContent.objectAt(oldIndex + 1), - leftResult = leftItem && this.orderBy(item, leftItem), - rightResult = rightItem && this.orderBy(item, rightItem); + length: Ember.computed(function() { + var arrangedContent = get(this, 'arrangedContent'); + return arrangedContent ? get(arrangedContent, 'length') : 0; + // No dependencies since Enumerable notifies length of change + }), - if (leftResult < 0 || rightResult > 0) { - arrangedContent.removeObject(item); - this.insertItemSorted(item); + _replace: function(idx, amt, objects) { + var content = get(this, 'content'); + Ember.assert('The content property of '+ this.constructor + ' should be set before modifying it', content); + if (content) this.replaceContent(idx, amt, objects); + return this; + }, + + replace: function() { + if (get(this, 'arrangedContent') === get(this, 'content')) { + this._replace.apply(this, arguments); + } else { + throw new Ember.Error("Using replace on an arranged ArrayProxy is not allowed."); } }, - _binarySearch: function(item, low, high) { - var mid, midItem, res, arrangedContent; + _insertAt: function(idx, object) { + if (idx > get(this, 'content.length')) throw new Ember.Error(OUT_OF_RANGE_EXCEPTION); + this._replace(idx, 0, [object]); + return this; + }, - if (low === high) { - return low; + insertAt: function(idx, object) { + if (get(this, 'arrangedContent') === get(this, 'content')) { + return this._insertAt(idx, object); + } else { + throw new Ember.Error("Using insertAt on an arranged ArrayProxy is not allowed."); } + }, - arrangedContent = get(this, 'arrangedContent'); + removeAt: function(start, len) { + if ('number' === typeof start) { + var content = get(this, 'content'), + arrangedContent = get(this, 'arrangedContent'), + indices = [], i; - mid = low + Math.floor((high - low) / 2); - midItem = arrangedContent.objectAt(mid); + if ((start < 0) || (start >= get(this, 'length'))) { + throw new Ember.Error(OUT_OF_RANGE_EXCEPTION); + } - res = this.orderBy(midItem, item); + if (len === undefined) len = 1; - if (res < 0) { - return this._binarySearch(item, mid+1, high); - } else if (res > 0) { - return this._binarySearch(item, low, mid); + // Get a list of indices in original content to remove + for (i=start; i{{title}} ({{titleLength}} characters) - {{/each}} - ``` +var IS_OBSERVER = /^.+:(before|change)$/; - ```javascript - App.PostsController = Ember.ArrayController.extend({ - itemController: 'post' - }); +function addObserverForContentKey(content, keyName, proxy, idx, loc) { + var objects = proxy._objects, guid; + if (!objects) objects = proxy._objects = {}; - App.PostController = Ember.ObjectController.extend({ - // the `title` property will be proxied to the underlying post. + while(--loc>=idx) { + var item = content.objectAt(loc); + if (item) { + Ember.assert('When using @each to observe the array ' + content + ', the array must return an object', Ember.typeOf(item) === 'instance' || Ember.typeOf(item) === 'object'); + Ember.addBeforeObserver(item, keyName, proxy, 'contentKeyWillChange'); + Ember.addObserver(item, keyName, proxy, 'contentKeyDidChange'); - titleLength: function() { - return this.get('title').length; - }.property('title') - }); - ``` + // keep track of the index each item was found at so we can map + // it back when the obj changes. + guid = guidFor(item); + if (!objects[guid]) objects[guid] = []; + objects[guid].push(loc); + } + } +} - In some cases it is helpful to return a different `itemController` depending - on the particular item. Subclasses can do this by overriding - `lookupItemController`. +function removeObserverForContentKey(content, keyName, proxy, idx, loc) { + var objects = proxy._objects; + if (!objects) objects = proxy._objects = {}; + var indicies, guid; - For example: + while(--loc>=idx) { + var item = content.objectAt(loc); + if (item) { + Ember.removeBeforeObserver(item, keyName, proxy, 'contentKeyWillChange'); + Ember.removeObserver(item, keyName, proxy, 'contentKeyDidChange'); - ```javascript - App.MyArrayController = Ember.ArrayController.extend({ - lookupItemController: function( object ) { - if (object.get('isSpecial')) { - return "special"; // use App.SpecialController - } else { - return "regular"; // use App.RegularController - } + guid = guidFor(item); + indicies = objects[guid]; + indicies[indexOf.call(indicies, loc)] = null; } - }); - ``` + } +} - The itemController instances will have a `parentController` property set to - either the the `parentController` property of the `ArrayController` - or to the `ArrayController` instance itself. +/** + This is the object instance returned when you get the `@each` property on an + array. It uses the unknownProperty handler to automatically create + EachArray instances for property names. - @class ArrayController + @private + @class EachProxy @namespace Ember - @extends Ember.ArrayProxy - @uses Ember.SortableMixin - @uses Ember.ControllerMixin + @extends Ember.Object */ +Ember.EachProxy = Ember.Object.extend({ -Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, - Ember.SortableMixin, { + init: function(content) { + this._super(); + this._content = content; + content.addArrayObserver(this); + + // in case someone is already observing some keys make sure they are + // added + forEach(Ember.watchedEvents(this), function(eventName) { + this.didAddListener(eventName); + }, this); + }, /** - The controller used to wrap items, if any. + You can directly access mapped properties by simply requesting them. + The `unknownProperty` handler will generate an EachArray of each item. - @property itemController - @type String - @default null + @method unknownProperty + @param keyName {String} + @param value {*} */ - itemController: null, + unknownProperty: function(keyName, value) { + var ret; + ret = new EachArray(this._content, keyName, this); + Ember.defineProperty(this, keyName, null, ret); + this.beginObservingContentKey(keyName); + return ret; + }, - /** - Return the name of the controller to wrap items, or `null` if items should - be returned directly. The default implementation simply returns the - `itemController` property, but subclasses can override this method to return - different controllers for different objects. + // .......................................................... + // ARRAY CHANGES + // Invokes whenever the content array itself changes. - For example: + arrayWillChange: function(content, idx, removedCnt, addedCnt) { + var keys = this._keys, key, lim; - ```javascript - App.MyArrayController = Ember.ArrayController.extend({ - lookupItemController: function( object ) { - if (object.get('isSpecial')) { - return "special"; // use App.SpecialController - } else { - return "regular"; // use App.RegularController - } - } - }); - ``` + lim = removedCnt>0 ? idx+removedCnt : -1; + Ember.beginPropertyChanges(this); - @method lookupItemController - @param {Object} object - @return {String} - */ - lookupItemController: function(object) { - return get(this, 'itemController'); - }, + for(key in keys) { + if (!keys.hasOwnProperty(key)) { continue; } - objectAtContent: function(idx) { - var length = get(this, 'length'), - arrangedContent = get(this,'arrangedContent'), - object = arrangedContent && arrangedContent.objectAt(idx); + if (lim>0) { removeObserverForContentKey(content, key, this, idx, lim); } - if (idx >= 0 && idx < length) { - var controllerClass = this.lookupItemController(object); - if (controllerClass) { - return this.controllerAt(idx, object, controllerClass); - } + Ember.propertyWillChange(this, key); } - // When `controllerClass` is falsy, we have not opted in to using item - // controllers, so return the object directly. - - // When the index is out of range, we want to return the "out of range" - // value, whatever that might be. Rather than make assumptions - // (e.g. guessing `null` or `undefined`) we defer this to `arrangedContent`. - return object; + Ember.propertyWillChange(this._content, '@each'); + Ember.endPropertyChanges(this); }, - arrangedContentDidChange: function() { - this._super(); - this._resetSubControllers(); - }, + arrayDidChange: function(content, idx, removedCnt, addedCnt) { + var keys = this._keys, lim; - arrayContentDidChange: function(idx, removedCnt, addedCnt) { - var subControllers = get(this, '_subControllers'), - subControllersToRemove = subControllers.slice(idx, idx+removedCnt); + lim = addedCnt>0 ? idx+addedCnt : -1; + Ember.changeProperties(function() { + for(var key in keys) { + if (!keys.hasOwnProperty(key)) { continue; } - forEach(subControllersToRemove, function(subController) { - if (subController) { subController.destroy(); } - }); + if (lim>0) { addObserverForContentKey(content, key, this, idx, lim); } - replace(subControllers, idx, removedCnt, new Array(addedCnt)); + Ember.propertyDidChange(this, key); + } - // The shadow array of subcontrollers must be updated before we trigger - // observers, otherwise observers will get the wrong subcontainer when - // calling `objectAt` - this._super(idx, removedCnt, addedCnt); + Ember.propertyDidChange(this._content, '@each'); + }, this); }, - init: function() { - this._super(); + // .......................................................... + // LISTEN FOR NEW OBSERVERS AND OTHER EVENT LISTENERS + // Start monitoring keys based on who is listening... - this.set('_subControllers', Ember.A()); + didAddListener: function(eventName) { + if (IS_OBSERVER.test(eventName)) { + this.beginObservingContentKey(eventName.slice(0, -7)); + } }, - content: Ember.computed(function () { - return Ember.A(); - }), - - controllerAt: function(idx, object, controllerClass) { - var container = get(this, 'container'), - subControllers = get(this, '_subControllers'), - subController = subControllers[idx], - factory, fullName; - - if (subController) { return subController; } - - fullName = "controller:" + controllerClass; - - if (!container.has(fullName)) { - throw new Ember.Error('Could not resolve itemController: "' + controllerClass + '"'); + didRemoveListener: function(eventName) { + if (IS_OBSERVER.test(eventName)) { + this.stopObservingContentKey(eventName.slice(0, -7)); } + }, - subController = container.lookupFactory(fullName).create({ - target: this, - parentController: get(this, 'parentController') || this, - content: object - }); - - subControllers[idx] = subController; + // .......................................................... + // CONTENT KEY OBSERVING + // Actual watch keys on the source content. - return subController; + beginObservingContentKey: function(keyName) { + var keys = this._keys; + if (!keys) keys = this._keys = {}; + if (!keys[keyName]) { + keys[keyName] = 1; + var content = this._content, + len = get(content, 'length'); + addObserverForContentKey(content, keyName, this, 0, len); + } else { + keys[keyName]++; + } }, - _subControllers: null, - - _resetSubControllers: function() { - var subControllers = get(this, '_subControllers'); - if (subControllers) { - forEach(subControllers, function(subController) { - if (subController) { subController.destroy(); } - }); + stopObservingContentKey: function(keyName) { + var keys = this._keys; + if (keys && (keys[keyName]>0) && (--keys[keyName]<=0)) { + var content = this._content, + len = get(content, 'length'); + removeObserverForContentKey(content, keyName, this, 0, len); } + }, - this.set('_subControllers', Ember.A()); + contentKeyWillChange: function(obj, keyName) { + Ember.propertyWillChange(this, keyName); + }, + + contentKeyDidChange: function(obj, keyName) { + Ember.propertyDidChange(this, keyName); } + }); + + })(); @@ -18407,220 +19990,180 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, @submodule ember-runtime */ -/** - `Ember.ObjectController` is part of Ember's Controller layer. It is intended - to wrap a single object, proxying unhandled attempts to `get` and `set` to the underlying - content object, and to forward unhandled action attempts to its `target`. - `Ember.ObjectController` derives this functionality from its superclass - `Ember.ObjectProxy` and the `Ember.ControllerMixin` mixin. +var get = Ember.get, set = Ember.set, replace = Ember.EnumerableUtils._replace; - @class ObjectController - @namespace Ember - @extends Ember.ObjectProxy - @uses Ember.ControllerMixin -**/ -Ember.ObjectController = Ember.ObjectProxy.extend(Ember.ControllerMixin); +// Add Ember.Array to Array.prototype. Remove methods with native +// implementations and supply some more optimized versions of generic methods +// because they are so common. +var NativeArray = Ember.Mixin.create(Ember.MutableArray, Ember.Observable, Ember.Copyable, { -})(); + // because length is a built-in property we need to know to just get the + // original property. + get: function(key) { + if (key==='length') return this.length; + else if ('number' === typeof key) return this[key]; + else return this._super(key); + }, + objectAt: function(idx) { + return this[idx]; + }, + // primitive for array support. + replace: function(idx, amt, objects) { -(function() { + if (this.isFrozen) throw Ember.FROZEN_ERROR; -})(); + // if we replaced exactly the same number of items, then pass only the + // replaced range. Otherwise, pass the full remaining array length + // since everything has shifted + var len = objects ? get(objects, 'length') : 0; + this.arrayContentWillChange(idx, amt, len); + if (len === 0) { + this.splice(idx, amt); + } else { + replace(this, idx, amt, objects); + } + this.arrayContentDidChange(idx, amt, len); + return this; + }, -(function() { -/** -Ember Runtime + // If you ask for an unknown property, then try to collect the value + // from member items. + unknownProperty: function(key, value) { + var ret;// = this.reducedProperty(key, value) ; + if ((value !== undefined) && ret === undefined) { + ret = this[key] = value; + } + return ret ; + }, -@module ember -@submodule ember-runtime -@requires ember-metal -*/ + // If browser did not implement indexOf natively, then override with + // specialized version + indexOf: function(object, startAt) { + var idx, len = this.length; -})(); + if (startAt === undefined) startAt = 0; + else startAt = (startAt < 0) ? Math.ceil(startAt) : Math.floor(startAt); + if (startAt < 0) startAt += len; -(function() { -/** -@module ember -@submodule ember-views -*/ - -var jQuery = Ember.imports.jQuery; -Ember.assert("Ember Views require jQuery 1.7, 1.8, 1.9, 1.10, or 2.0", jQuery && (jQuery().jquery.match(/^((1\.(7|8|9|10))|2.0)(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_JQUERY)); + for(idx=startAt;idx=0;idx--) { + if (this[idx] === object) return idx ; + } + return -1; + }, + copy: function(deep) { + if (deep) { + return this.map(function(item) { return Ember.copy(item, true); }); + } + return this.slice(); + } +}); -(function() { -/** -@module ember -@submodule ember-views -*/ -if (Ember.$) { - // http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#dndevents - var dragEvents = Ember.String.w('dragstart drag dragenter dragleave dragover drop dragend'); +// Remove any methods implemented natively so we don't override them +var ignore = ['length']; +Ember.EnumerableUtils.forEach(NativeArray.keys(), function(methodName) { + if (Array.prototype[methodName]) ignore.push(methodName); +}); - // Copies the `dataTransfer` property from a browser event object onto the - // jQuery event object for the specified events - Ember.EnumerableUtils.forEach(dragEvents, function(eventName) { - Ember.$.event.fixHooks[eventName] = { props: ['dataTransfer'] }; - }); +if (ignore.length>0) { + NativeArray = NativeArray.without.apply(NativeArray, ignore); } -})(); - - - -(function() { /** -@module ember -@submodule ember-views -*/ - -/* BEGIN METAMORPH HELPERS */ - -// Internet Explorer prior to 9 does not allow setting innerHTML if the first element -// is a "zero-scope" element. This problem can be worked around by making -// the first node an invisible text node. We, like Modernizr, use ­ - -var needsShy = this.document && (function() { - var testEl = document.createElement('div'); - testEl.innerHTML = "
"; - testEl.firstChild.innerHTML = ""; - return testEl.firstChild.innerHTML === ''; -})(); - -// IE 8 (and likely earlier) likes to move whitespace preceeding -// a script tag to appear after it. This means that we can -// accidentally remove whitespace when updating a morph. -var movesWhitespace = this.document && (function() { - var testEl = document.createElement('div'); - testEl.innerHTML = "Test: Value"; - return testEl.childNodes[0].nodeValue === 'Test:' && - testEl.childNodes[2].nodeValue === ' Value'; -})(); - -// Use this to find children by ID instead of using jQuery -var findChildById = function(element, id) { - if (element.getAttribute('id') === id) { return element; } - - var len = element.childNodes.length, idx, node, found; - for (idx=0; idx 0) { - var len = matches.length, idx; - for (idx=0; idxTest'); - canSet = el.options.length === 1; + ```js + if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) { + Ember.NativeArray.activate(); } + ``` - innerHTMLTags[tagName] = canSet; - - return canSet; -}; - -var setInnerHTML = function(element, html) { - var tagName = element.tagName; - - if (canSetInnerHTML(tagName)) { - setInnerHTMLWithoutFix(element, html); - } else { - // Firefox versions < 11 do not have support for element.outerHTML. - var outerHTML = element.outerHTML || new XMLSerializer().serializeToString(element); - Ember.assert("Can't set innerHTML on "+element.tagName+" in this browser", outerHTML); - - var startTag = outerHTML.match(new RegExp("<"+tagName+"([^>]*)>", 'i'))[0], - endTag = ''; - - var wrapper = document.createElement('div'); - setInnerHTMLWithoutFix(wrapper, startTag + html + endTag); - element = wrapper.firstChild; - while (element.tagName !== tagName) { - element = element.nextSibling; - } - } + @method activate + @for Ember.NativeArray + @static + @return {void} +*/ +Ember.NativeArray.activate = function() { + NativeArray.apply(Array.prototype); - return element; + Ember.A = function(arr) { return arr || []; }; }; -function isSimpleClick(event) { - var modifier = event.shiftKey || event.metaKey || event.altKey || event.ctrlKey, - secondaryClick = event.which > 1; // IE9 may return undefined - - return !modifier && !secondaryClick; +if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) { + Ember.NativeArray.activate(); } -Ember.ViewUtils = { - setInnerHTML: setInnerHTML, - isSimpleClick: isSimpleClick -}; })(); @@ -18629,537 +20172,543 @@ Ember.ViewUtils = { (function() { /** @module ember -@submodule ember-views +@submodule ember-runtime */ -var get = Ember.get, set = Ember.set; +var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, isNone = Ember.isNone, fmt = Ember.String.fmt; -var ClassSet = function() { - this.seen = {}; - this.list = []; -}; +/** + An unordered collection of objects. -ClassSet.prototype = { - add: function(string) { - if (string in this.seen) { return; } - this.seen[string] = true; + A Set works a bit like an array except that its items are not ordered. You + can create a set to efficiently test for membership for an object. You can + also iterate through a set just like an array, even accessing objects by + index, however there is no guarantee as to their order. - this.list.push(string); - }, + All Sets are observable via the Enumerable Observer API - which works + on any enumerable object including both Sets and Arrays. - toDOM: function() { - return this.list.join(" "); - } -}; + ## Creating a Set -var BAD_TAG_NAME_TEST_REGEXP = /[^a-zA-Z0-9\-]/; -var BAD_TAG_NAME_REPLACE_REGEXP = /[^a-zA-Z0-9\-]/g; + You can create a set like you would most objects using + `new Ember.Set()`. Most new sets you create will be empty, but you can + also initialize the set with some content by passing an array or other + enumerable of objects to the constructor. -function stripTagName(tagName) { - if (!tagName) { - return tagName; - } + Finally, you can pass in an existing set and the set will be copied. You + can also create a copy of a set by calling `Ember.Set#copy()`. - if (!BAD_TAG_NAME_TEST_REGEXP.test(tagName)) { - return tagName; - } + ```javascript + // creates a new empty set + var foundNames = new Ember.Set(); - return tagName.replace(BAD_TAG_NAME_REPLACE_REGEXP, ''); -} + // creates a set with four names in it. + var names = new Ember.Set(["Charles", "Tom", "Juan", "Alex"]); // :P -var BAD_CHARS_REGEXP = /&(?!\w+;)|[<>"'`]/g; -var POSSIBLE_CHARS_REGEXP = /[&<>"'`]/; + // creates a copy of the names set. + var namesCopy = new Ember.Set(names); -function escapeAttribute(value) { - // Stolen shamelessly from Handlebars + // same as above. + var anotherNamesCopy = names.copy(); + ``` - var escape = { - "<": "<", - ">": ">", - '"': """, - "'": "'", - "`": "`" - }; + ## Adding/Removing Objects - var escapeChar = function(chr) { - return escape[chr] || "&"; - }; + You generally add or remove objects from a set using `add()` or + `remove()`. You can add any type of object including primitives such as + numbers, strings, and booleans. - var string = value.toString(); + Unlike arrays, objects can only exist one time in a set. If you call `add()` + on a set with the same object multiple times, the object will only be added + once. Likewise, calling `remove()` with the same object multiple times will + remove the object the first time and have no effect on future calls until + you add the object to the set again. - if(!POSSIBLE_CHARS_REGEXP.test(string)) { return string; } - return string.replace(BAD_CHARS_REGEXP, escapeChar); -} + NOTE: You cannot add/remove `null` or `undefined` to a set. Any attempt to do + so will be ignored. -// IE 6/7 have bugs arond setting names on inputs during creation. -// From http://msdn.microsoft.com/en-us/library/ie/ms536389(v=vs.85).aspx: -// "To include the NAME attribute at run time on objects created with the createElement method, use the eTag." -var canSetNameOnInputs = (function() { - var div = document.createElement('div'), - el = document.createElement('input'); + In addition to add/remove you can also call `push()`/`pop()`. Push behaves + just like `add()` but `pop()`, unlike `remove()` will pick an arbitrary + object, remove it and return it. This is a good way to use a set as a job + queue when you don't care which order the jobs are executed in. - el.setAttribute('name', 'foo'); - div.appendChild(el); + ## Testing for an Object - return !!div.innerHTML.match('foo'); -})(); + To test for an object's presence in a set you simply call + `Ember.Set#contains()`. -/** - `Ember.RenderBuffer` gathers information regarding the a view and generates the - final representation. `Ember.RenderBuffer` will generate HTML which can be pushed - to the DOM. + ## Observing changes - ```javascript - var buffer = Ember.RenderBuffer('div'); - ``` + When using `Ember.Set`, you can observe the `"[]"` property to be + alerted whenever the content changes. You can also add an enumerable + observer to the set to be notified of specific objects that are added and + removed from the set. See [Ember.Enumerable](/api/classes/Ember.Enumerable.html) + for more information on enumerables. - @class RenderBuffer - @namespace Ember - @constructor - @param {String} tagName tag name (such as 'div' or 'p') used for the buffer -*/ -Ember.RenderBuffer = function(tagName) { - return new Ember._RenderBuffer(tagName); -}; + This is often unhelpful. If you are filtering sets of objects, for instance, + it is very inefficient to re-filter all of the items each time the set + changes. It would be better if you could just adjust the filtered set based + on what was changed on the original set. The same issue applies to merging + sets, as well. -Ember._RenderBuffer = function(tagName) { - this.tagNames = [tagName || null]; - this.buffer = ""; -}; + ## Other Methods -Ember._RenderBuffer.prototype = -/** @scope Ember.RenderBuffer.prototype */ { + `Ember.Set` primary implements other mixin APIs. For a complete reference + on the methods you will use with `Ember.Set`, please consult these mixins. + The most useful ones will be `Ember.Enumerable` and + `Ember.MutableEnumerable` which implement most of the common iterator + methods you are used to on Array. - // The root view's element - _element: null, + Note that you can also use the `Ember.Copyable` and `Ember.Freezable` + APIs on `Ember.Set` as well. Once a set is frozen it can no longer be + modified. The benefit of this is that when you call `frozenCopy()` on it, + Ember will avoid making copies of the set. This allows you to write + code that can know with certainty when the underlying set data will or + will not be modified. - _hasElement: true, + @class Set + @namespace Ember + @extends Ember.CoreObject + @uses Ember.MutableEnumerable + @uses Ember.Copyable + @uses Ember.Freezable + @since Ember 0.9 +*/ +Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Ember.Freezable, + { - /** - @private + // .......................................................... + // IMPLEMENT ENUMERABLE APIS + // - An internal set used to de-dupe class names when `addClass()` is - used. After each call to `addClass()`, the `classes` property - will be updated. + /** + This property will change as the number of objects in the set changes. - @property elementClasses - @type Array - @default [] + @property length + @type number + @default 0 */ - elementClasses: null, + length: 0, /** - Array of class names which will be applied in the class attribute. + Clears the set. This is useful if you want to reuse an existing set + without having to recreate it. - You can use `setClasses()` to set this property directly. If you - use `addClass()`, it will be maintained for you. + ```javascript + var colors = new Ember.Set(["red", "green", "blue"]); + colors.length; // 3 + colors.clear(); + colors.length; // 0 + ``` - @property classes - @type Array - @default [] + @method clear + @return {Ember.Set} An empty Set */ - classes: null, + clear: function() { + if (this.isFrozen) { throw new Ember.Error(Ember.FROZEN_ERROR); } - /** - The id in of the element, to be applied in the id attribute. + var len = get(this, 'length'); + if (len === 0) { return this; } - You should not set this property yourself, rather, you should use - the `id()` method of `Ember.RenderBuffer`. + var guid; - @property elementId - @type String - @default null - */ - elementId: null, + this.enumerableContentWillChange(len, 0); + Ember.propertyWillChange(this, 'firstObject'); + Ember.propertyWillChange(this, 'lastObject'); - /** - A hash keyed on the name of the attribute and whose value will be - applied to that attribute. For example, if you wanted to apply a - `data-view="Foo.bar"` property to an element, you would set the - elementAttributes hash to `{'data-view':'Foo.bar'}`. + for (var i=0; i < len; i++) { + guid = guidFor(this[i]); + delete this[guid]; + delete this[i]; + } - You should not maintain this hash yourself, rather, you should use - the `attr()` method of `Ember.RenderBuffer`. + set(this, 'length', 0); - @property elementAttributes - @type Hash - @default {} - */ - elementAttributes: null, + Ember.propertyDidChange(this, 'firstObject'); + Ember.propertyDidChange(this, 'lastObject'); + this.enumerableContentDidChange(len, 0); + + return this; + }, /** - A hash keyed on the name of the properties and whose value will be - applied to that property. For example, if you wanted to apply a - `checked=true` property to an element, you would set the - elementProperties hash to `{'checked':true}`. + Returns true if the passed object is also an enumerable that contains the + same objects as the receiver. - You should not maintain this hash yourself, rather, you should use - the `prop()` method of `Ember.RenderBuffer`. + ```javascript + var colors = ["red", "green", "blue"], + same_colors = new Ember.Set(colors); - @property elementProperties - @type Hash - @default {} + same_colors.isEqual(colors); // true + same_colors.isEqual(["purple", "brown"]); // false + ``` + + @method isEqual + @param {Ember.Set} obj the other object. + @return {Boolean} */ - elementProperties: null, + isEqual: function(obj) { + // fail fast + if (!Ember.Enumerable.detect(obj)) return false; + + var loc = get(this, 'length'); + if (get(obj, 'length') !== loc) return false; + + while(--loc >= 0) { + if (!obj.contains(this[loc])) return false; + } + + return true; + }, /** - The tagname of the element an instance of `Ember.RenderBuffer` represents. + Adds an object to the set. Only non-`null` objects can be added to a set + and those can only be added once. If the object is already in the set or + the passed value is null this method will have no effect. - Usually, this gets set as the first parameter to `Ember.RenderBuffer`. For - example, if you wanted to create a `p` tag, then you would call + This is an alias for `Ember.MutableEnumerable.addObject()`. ```javascript - Ember.RenderBuffer('p') + var colors = new Ember.Set(); + colors.add("blue"); // ["blue"] + colors.add("blue"); // ["blue"] + colors.add("red"); // ["blue", "red"] + colors.add(null); // ["blue", "red"] + colors.add(undefined); // ["blue", "red"] ``` - @property elementTag - @type String - @default null + @method add + @param {Object} obj The object to add. + @return {Ember.Set} The set itself. */ - elementTag: null, + add: Ember.aliasMethod('addObject'), /** - A hash keyed on the name of the style attribute and whose value will - be applied to that attribute. For example, if you wanted to apply a - `background-color:black;` style to an element, you would set the - elementStyle hash to `{'background-color':'black'}`. + Removes the object from the set if it is found. If you pass a `null` value + or an object that is already not in the set, this method will have no + effect. This is an alias for `Ember.MutableEnumerable.removeObject()`. - You should not maintain this hash yourself, rather, you should use - the `style()` method of `Ember.RenderBuffer`. + ```javascript + var colors = new Ember.Set(["red", "green", "blue"]); + colors.remove("red"); // ["blue", "green"] + colors.remove("purple"); // ["blue", "green"] + colors.remove(null); // ["blue", "green"] + ``` - @property elementStyle - @type Hash - @default {} + @method remove + @param {Object} obj The object to remove + @return {Ember.Set} The set itself. */ - elementStyle: null, + remove: Ember.aliasMethod('removeObject'), /** - Nested `RenderBuffers` will set this to their parent `RenderBuffer` - instance. + Removes the last element from the set and returns it, or `null` if it's empty. - @property parentBuffer - @type Ember._RenderBuffer + ```javascript + var colors = new Ember.Set(["green", "blue"]); + colors.pop(); // "blue" + colors.pop(); // "green" + colors.pop(); // null + ``` + + @method pop + @return {Object} The removed object from the set or null. */ - parentBuffer: null, + pop: function() { + if (get(this, 'isFrozen')) throw new Ember.Error(Ember.FROZEN_ERROR); + var obj = this.length > 0 ? this[this.length-1] : null; + this.remove(obj); + return obj; + }, /** - Adds a string of HTML to the `RenderBuffer`. + Inserts the given object on to the end of the set. It returns + the set itself. + + This is an alias for `Ember.MutableEnumerable.addObject()`. + + ```javascript + var colors = new Ember.Set(); + colors.push("red"); // ["red"] + colors.push("green"); // ["red", "green"] + colors.push("blue"); // ["red", "green", "blue"] + ``` @method push - @param {String} string HTML to push into the buffer - @chainable + @return {Ember.Set} The set itself. */ - push: function(string) { - this.buffer += string; - return this; - }, + push: Ember.aliasMethod('addObject'), /** - Adds a class to the buffer, which will be rendered to the class attribute. + Removes the last element from the set and returns it, or `null` if it's empty. - @method addClass - @param {String} className Class name to add to the buffer - @chainable - */ - addClass: function(className) { - // lazily create elementClasses - this.elementClasses = (this.elementClasses || new ClassSet()); - this.elementClasses.add(className); - this.classes = this.elementClasses.list; + This is an alias for `Ember.Set.pop()`. - return this; - }, + ```javascript + var colors = new Ember.Set(["green", "blue"]); + colors.shift(); // "blue" + colors.shift(); // "green" + colors.shift(); // null + ``` - setClasses: function(classNames) { - this.classes = classNames; - }, + @method shift + @return {Object} The removed object from the set or null. + */ + shift: Ember.aliasMethod('pop'), /** - Sets the elementID to be used for the element. + Inserts the given object on to the end of the set. It returns + the set itself. - @method id - @param {String} id - @chainable - */ - id: function(id) { - this.elementId = id; - return this; - }, + This is an alias of `Ember.Set.push()` - // duck type attribute functionality like jQuery so a render buffer - // can be used like a jQuery object in attribute binding scenarios. + ```javascript + var colors = new Ember.Set(); + colors.unshift("red"); // ["red"] + colors.unshift("green"); // ["red", "green"] + colors.unshift("blue"); // ["red", "green", "blue"] + ``` + + @method unshift + @return {Ember.Set} The set itself. + */ + unshift: Ember.aliasMethod('push'), /** - Adds an attribute which will be rendered to the element. + Adds each object in the passed enumerable to the set. - @method attr - @param {String} name The name of the attribute - @param {String} value The value to add to the attribute - @chainable - @return {Ember.RenderBuffer|String} this or the current attribute value - */ - attr: function(name, value) { - var attributes = this.elementAttributes = (this.elementAttributes || {}); + This is an alias of `Ember.MutableEnumerable.addObjects()` - if (arguments.length === 1) { - return attributes[name]; - } else { - attributes[name] = value; - } + ```javascript + var colors = new Ember.Set(); + colors.addEach(["red", "green", "blue"]); // ["red", "green", "blue"] + ``` - return this; - }, + @method addEach + @param {Ember.Enumerable} objects the objects to add. + @return {Ember.Set} The set itself. + */ + addEach: Ember.aliasMethod('addObjects'), /** - Remove an attribute from the list of attributes to render. + Removes each object in the passed enumerable to the set. - @method removeAttr - @param {String} name The name of the attribute - @chainable + This is an alias of `Ember.MutableEnumerable.removeObjects()` + + ```javascript + var colors = new Ember.Set(["red", "green", "blue"]); + colors.removeEach(["red", "blue"]); // ["green"] + ``` + + @method removeEach + @param {Ember.Enumerable} objects the objects to remove. + @return {Ember.Set} The set itself. */ - removeAttr: function(name) { - var attributes = this.elementAttributes; - if (attributes) { delete attributes[name]; } + removeEach: Ember.aliasMethod('removeObjects'), - return this; + // .......................................................... + // PRIVATE ENUMERABLE SUPPORT + // + + init: function(items) { + this._super(); + if (items) this.addObjects(items); }, - /** - Adds an property which will be rendered to the element. + // implement Ember.Enumerable + nextObject: function(idx) { + return this[idx]; + }, - @method prop - @param {String} name The name of the property - @param {String} value The value to add to the property - @chainable - @return {Ember.RenderBuffer|String} this or the current property value - */ - prop: function(name, value) { - var properties = this.elementProperties = (this.elementProperties || {}); + // more optimized version + firstObject: Ember.computed(function() { + return this.length > 0 ? this[0] : undefined; + }), - if (arguments.length === 1) { - return properties[name]; - } else { - properties[name] = value; - } + // more optimized version + lastObject: Ember.computed(function() { + return this.length > 0 ? this[this.length-1] : undefined; + }), - return this; - }, + // implements Ember.MutableEnumerable + addObject: function(obj) { + if (get(this, 'isFrozen')) throw new Ember.Error(Ember.FROZEN_ERROR); + if (isNone(obj)) return this; // nothing to do - /** - Remove an property from the list of properties to render. + var guid = guidFor(obj), + idx = this[guid], + len = get(this, 'length'), + added ; - @method removeProp - @param {String} name The name of the property - @chainable - */ - removeProp: function(name) { - var properties = this.elementProperties; - if (properties) { delete properties[name]; } + if (idx>=0 && idx=0 && idx=0; + }, - if (props) { - for (prop in props) { - if (props.hasOwnProperty(prop)) { - var value = props[prop]; - if (value || typeof(value) === 'number') { - if (value === true) { - buffer += ' ' + prop + '="' + prop + '"'; - } else { - buffer += ' ' + prop + '="' + escapeAttribute(props[prop]) + '"'; - } - } - } - } + copy: function() { + var C = this.constructor, ret = new C(), loc = get(this, 'length'); + set(ret, 'length', loc); + while(--loc>=0) { + ret[loc] = this[loc]; + ret[guidFor(this[loc])] = loc; + } + return ret; + }, - this.elementProperties = null; + toString: function() { + var len = this.length, idx, array = []; + for(idx = 0; idx < len; idx++) { + array[idx] = this[idx]; } + return fmt("Ember.Set<%@>", [array.join(',')]); + } - buffer += '>'; - this.buffer = buffer; - }, +}); - pushClosingTag: function() { - var tagName = this.tagNames.pop(); - if (tagName) { this.buffer += ''; } - }, +})(); - currentTagName: function() { - return this.tagNames[this.tagNames.length-1]; - }, - generateElement: function() { - var tagName = this.tagNames.pop(), // pop since we don't need to close - id = this.elementId, - classes = this.classes, - attrs = this.elementAttributes, - props = this.elementProperties, - style = this.elementStyle, - styleBuffer = '', attr, prop, tagString; - if (attrs && attrs.name && !canSetNameOnInputs) { - // IE allows passing a tag to createElement. See note on `canSetNameOnInputs` above as well. - tagString = '<'+stripTagName(tagName)+' name="'+escapeAttribute(attrs.name)+'">'; - } else { - tagString = tagName; - } +(function() { +var DeferredMixin = Ember.DeferredMixin, // mixins/deferred + get = Ember.get; - var element = document.createElement(tagString), - $element = Ember.$(element); +var Deferred = Ember.Object.extend(DeferredMixin); - if (id) { - $element.attr('id', id); - this.elementId = null; - } - if (classes) { - $element.attr('class', classes.join(' ')); - this.classes = null; - } +Deferred.reopenClass({ + promise: function(callback, binding) { + var deferred = Deferred.create(); + callback.call(binding, deferred); + return deferred; + } +}); - if (style) { - for (prop in style) { - if (style.hasOwnProperty(prop)) { - styleBuffer += (prop + ':' + style[prop] + ';'); - } - } +Ember.Deferred = Deferred; - $element.attr('style', styleBuffer); +})(); - this.elementStyle = null; - } - if (attrs) { - for (attr in attrs) { - if (attrs.hasOwnProperty(attr)) { - $element.attr(attr, attrs[attr]); - } - } - this.elementAttributes = null; - } +(function() { +/*globals CustomEvent */ - if (props) { - for (prop in props) { - if (props.hasOwnProperty(prop)) { - $element.prop(prop, props[prop]); - } - } +var forEach = Ember.ArrayPolyfills.forEach; - this.elementProperties = null; - } +/** + @module ember + @submodule ember-runtime +*/ - return element; - }, +var loadHooks = Ember.ENV.EMBER_LOAD_HOOKS || {}; +var loaded = {}; - /** - @method element - @return {DOMElement} The element corresponding to the generated HTML - of this buffer - */ - element: function() { - var html = this.innerString(); +/** + Detects when a specific package of Ember (e.g. 'Ember.Handlebars') + has fully loaded and is available for extension. - if (html) { - this._element = Ember.ViewUtils.setInnerHTML(this._element, html); - } + The provided `callback` will be called with the `name` passed + resolved from a string into the object: - return this._element; - }, + ``` javascript + Ember.onLoad('Ember.Handlebars' function(hbars){ + hbars.registerHelper(...); + }); + ``` - /** - Generates the HTML content for this buffer. + @method onLoad + @for Ember + @param name {String} name of hook + @param callback {Function} callback to be called +*/ +Ember.onLoad = function(name, callback) { + var object; - @method string - @return {String} The generated HTML - */ - string: function() { - if (this._hasElement && this._element) { - // Firefox versions < 11 do not have support for element.outerHTML. - var thisElement = this.element(), outerHTML = thisElement.outerHTML; - if (typeof outerHTML === 'undefined') { - return Ember.$('
').append(thisElement).html(); - } - return outerHTML; - } else { - return this.innerString(); - } - }, + loadHooks[name] = loadHooks[name] || Ember.A(); + loadHooks[name].pushObject(callback); - innerString: function() { - return this.buffer; + if (object = loaded[name]) { + callback(object); + } +}; + +/** + Called when an Ember.js package (e.g Ember.Handlebars) has finished + loading. Triggers any callbacks registered for this event. + + @method runLoadHooks + @for Ember + @param name {String} name of hook + @param object {Object} object to pass to callbacks +*/ +Ember.runLoadHooks = function(name, object) { + loaded[name] = object; + + if (typeof window === 'object' && typeof window.dispatchEvent === 'function' && typeof CustomEvent === "function") { + var event = new CustomEvent(name, {detail: object, name: name}); + window.dispatchEvent(event); + } + + if (loadHooks[name]) { + forEach.call(loadHooks[name], function(callback) { + callback(object); + }); } }; @@ -19168,2802 +20717,7124 @@ Ember._RenderBuffer.prototype = (function() { + +})(); + + + +(function() { +var get = Ember.get; + /** @module ember -@submodule ember-views +@submodule ember-runtime */ -var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; - /** - `Ember.EventDispatcher` handles delegating browser events to their - corresponding `Ember.Views.` For example, when you click on a view, - `Ember.EventDispatcher` ensures that that view's `mouseDown` method gets - called. + `Ember.ControllerMixin` provides a standard interface for all classes that + compose Ember's controller layer: `Ember.Controller`, + `Ember.ArrayController`, and `Ember.ObjectController`. - @class EventDispatcher + @class ControllerMixin @namespace Ember - @private - @extends Ember.Object + @uses Ember.ActionHandler */ -Ember.EventDispatcher = Ember.Object.extend(/** @scope Ember.EventDispatcher.prototype */{ +Ember.ControllerMixin = Ember.Mixin.create(Ember.ActionHandler, { + /* ducktype as a controller */ + isController: true, /** - The set of events names (and associated handler function names) to be setup - and dispatched by the `EventDispatcher`. Custom events can added to this list at setup - time, generally via the `Ember.Application.customEvents` hash. Only override this - default set to prevent the EventDispatcher from listening on some events all together. + The object to which actions from the view should be sent. - This set will be modified by `setup` to also include any events added at that time. + For example, when a Handlebars template uses the `{{action}}` helper, + it will attempt to send the action to the view's controller's `target`. - @property events - @type Object + By default, a controller's `target` is set to the router after it is + instantiated by `Ember.Application#initialize`. + + @property target + @default null */ - events: { - touchstart : 'touchStart', - touchmove : 'touchMove', - touchend : 'touchEnd', - touchcancel : 'touchCancel', - keydown : 'keyDown', - keyup : 'keyUp', - keypress : 'keyPress', - mousedown : 'mouseDown', - mouseup : 'mouseUp', - contextmenu : 'contextMenu', - click : 'click', - dblclick : 'doubleClick', - mousemove : 'mouseMove', - focusin : 'focusIn', - focusout : 'focusOut', - mouseenter : 'mouseEnter', - mouseleave : 'mouseLeave', - submit : 'submit', - input : 'input', - change : 'change', - dragstart : 'dragStart', - drag : 'drag', - dragenter : 'dragEnter', - dragleave : 'dragLeave', - dragover : 'dragOver', - drop : 'drop', - dragend : 'dragEnd' + target: null, + + container: null, + + parentController: null, + + store: null, + + model: Ember.computed.alias('content'), + + deprecatedSendHandles: function(actionName) { + return !!this[actionName]; }, - /** - @private + deprecatedSend: function(actionName) { + var args = [].slice.call(arguments, 1); + Ember.assert('' + this + " has the action " + actionName + " but it is not a function", typeof this[actionName] === 'function'); + Ember.deprecate('Action handlers implemented directly on controllers are deprecated in favor of action handlers on an `actions` object ( action: `' + actionName + '` on ' + this + ')', false); + this[actionName].apply(this, args); + return; + } +}); - The root DOM element to which event listeners should be attached. Event - listeners will be attached to the document unless this is overridden. +/** + @class Controller + @namespace Ember + @extends Ember.Object + @uses Ember.ControllerMixin +*/ +Ember.Controller = Ember.Object.extend(Ember.ControllerMixin); - Can be specified as a DOMElement or a selector string. +})(); - The default body is a string since this may be evaluated before document.body - exists in the DOM. - @property rootElement - @type DOMElement - @default 'body' - */ - rootElement: 'body', - /** - @private +(function() { +/** +@module ember +@submodule ember-runtime +*/ - Sets up event listeners for standard browser events. +var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; - This will be called after the browser sends a `DOMContentReady` event. By - default, it will set up all of the listeners on the document body. If you - would like to register the listeners on a different element, set the event - dispatcher's `root` property. +/** + `Ember.SortableMixin` provides a standard interface for array proxies + to specify a sort order and maintain this sorting when objects are added, + removed, or updated without changing the implicit order of their underlying + content array: - @method setup - @param addedEvents {Hash} - */ - setup: function(addedEvents, rootElement) { - var event, events = get(this, 'events'); + ```javascript + songs = [ + {trackNumber: 4, title: 'Ob-La-Di, Ob-La-Da'}, + {trackNumber: 2, title: 'Back in the U.S.S.R.'}, + {trackNumber: 3, title: 'Glass Onion'}, + ]; - Ember.$.extend(events, addedEvents || {}); + songsController = Ember.ArrayController.create({ + content: songs, + sortProperties: ['trackNumber'], + sortAscending: true + }); + songsController.get('firstObject'); // {trackNumber: 2, title: 'Back in the U.S.S.R.'} - if (!Ember.isNone(rootElement)) { - set(this, 'rootElement', rootElement); - } + songsController.addObject({trackNumber: 1, title: 'Dear Prudence'}); + songsController.get('firstObject'); // {trackNumber: 1, title: 'Dear Prudence'} + ``` - rootElement = Ember.$(get(this, 'rootElement')); + If you add or remove the properties to sort by or change the sort direction the content + sort order will be automatically updated. - Ember.assert(fmt('You cannot use the same root element (%@) multiple times in an Ember.Application', [rootElement.selector || rootElement[0].tagName]), !rootElement.is('.ember-application')); - Ember.assert('You cannot make a new Ember.Application using a root element that is a descendent of an existing Ember.Application', !rootElement.closest('.ember-application').length); - Ember.assert('You cannot make a new Ember.Application using a root element that is an ancestor of an existing Ember.Application', !rootElement.find('.ember-application').length); + ```javascript + songsController.set('sortProperties', ['title']); + songsController.get('firstObject'); // {trackNumber: 2, title: 'Back in the U.S.S.R.'} - rootElement.addClass('ember-application'); + songsController.toggleProperty('sortAscending'); + songsController.get('firstObject'); // {trackNumber: 4, title: 'Ob-La-Di, Ob-La-Da'} + ``` - Ember.assert('Unable to add "ember-application" class to rootElement. Make sure you set rootElement to the body or an element in the body.', rootElement.is('.ember-application')); + SortableMixin works by sorting the arrangedContent array, which is the array that + arrayProxy displays. Due to the fact that the underlying 'content' array is not changed, that + array will not display the sorted list: - for (event in events) { - if (events.hasOwnProperty(event)) { - this.setupHandler(rootElement, event, events[event]); - } - } - }, + ```javascript + songsController.get('content').get('firstObject'); // Returns the unsorted original content + songsController.get('firstObject'); // Returns the sorted content. + ``` + + Although the sorted content can also be accessed through the arrangedContent property, + it is preferable to use the proxied class and not the arrangedContent array directly. + + @class SortableMixin + @namespace Ember + @uses Ember.MutableEnumerable +*/ +Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { /** - @private + Specifies which properties dictate the arrangedContent's sort order. - Registers an event listener on the document. If the given event is - triggered, the provided event handler will be triggered on the target view. + When specifying multiple properties the sorting will use properties + from the `sortProperties` array prioritized from first to last. - If the target view does not implement the event handler, or if the handler - returns `false`, the parent view will be called. The event will continue to - bubble to each successive parent view until it reaches the top. + @property {Array} sortProperties + */ + sortProperties: null, - For example, to have the `mouseDown` method called on the target view when - a `mousedown` event is received from the browser, do the following: + /** + Specifies the arrangedContent's sort direction + + @property {Boolean} sortAscending + */ + sortAscending: true, + + /** + The function used to compare two values. You can override this if you + want to do custom comparisons. Functions must be of the type expected by + Array#sort, i.e. + return 0 if the two parameters are equal, + return a negative value if the first parameter is smaller than the second or + return a positive value otherwise: ```javascript - setupHandler('mousedown', 'mouseDown'); + function(x,y) { // These are assumed to be integers + if (x === y) + return 0; + return x < y ? -1 : 1; + } ``` - @method setupHandler - @param {Element} rootElement - @param {String} event the browser-originated event to listen to - @param {String} eventName the name of the method to call on the view + @property sortFunction + @type {Function} + @default Ember.compare */ - setupHandler: function(rootElement, event, eventName) { - var self = this; + sortFunction: Ember.compare, - rootElement.on(event + '.ember', '.ember-view', function(evt, triggeringManager) { - return Ember.handleErrors(function() { - var view = Ember.View.views[this.id], - result = true, manager = null; + orderBy: function(item1, item2) { + var result = 0, + sortProperties = get(this, 'sortProperties'), + sortAscending = get(this, 'sortAscending'), + sortFunction = get(this, 'sortFunction'); - manager = self._findNearestEventManager(view,eventName); + Ember.assert("you need to define `sortProperties`", !!sortProperties); - if (manager && manager !== triggeringManager) { - result = self._dispatchEvent(manager, evt, eventName, view); - } else if (view) { - result = self._bubbleEvent(view,evt,eventName); - } else { - evt.stopPropagation(); + forEach(sortProperties, function(propertyName) { + if (result === 0) { + result = sortFunction(get(item1, propertyName), get(item2, propertyName)); + if ((result !== 0) && !sortAscending) { + result = (-1) * result; } - - return result; - }, this); + } }); - rootElement.on(event + '.ember', '[data-ember-action]', function(evt) { - return Ember.handleErrors(function() { - var actionId = Ember.$(evt.currentTarget).attr('data-ember-action'), - action = Ember.Handlebars.ActionHelper.registeredActions[actionId]; - - // We have to check for action here since in some cases, jQuery will trigger - // an event on `removeChild` (i.e. focusout) after we've already torn down the - // action handlers for the view. - if (action && action.eventName === eventName) { - return action.handler(evt); - } - }, this); - }); + return result; }, - _findNearestEventManager: function(view, eventName) { - var manager = null; - - while (view) { - manager = get(view, 'eventManager'); - if (manager && manager[eventName]) { break; } + destroy: function() { + var content = get(this, 'content'), + sortProperties = get(this, 'sortProperties'); - view = get(view, 'parentView'); + if (content && sortProperties) { + forEach(content, function(item) { + forEach(sortProperties, function(sortProperty) { + Ember.removeObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); + }, this); + }, this); } - return manager; + return this._super(); }, - _dispatchEvent: function(object, evt, eventName, view) { - var result = true; + isSorted: Ember.computed.bool('sortProperties'), - var handler = object[eventName]; - if (Ember.typeOf(handler) === 'function') { - result = Ember.run(function() { - return handler.call(object, evt, view); + /** + Overrides the default arrangedContent from arrayProxy in order to sort by sortFunction. + Also sets up observers for each sortProperty on each item in the content Array. + + @property arrangedContent + */ + + arrangedContent: Ember.computed('content', 'sortProperties.@each', function(key, value) { + var content = get(this, 'content'), + isSorted = get(this, 'isSorted'), + sortProperties = get(this, 'sortProperties'), + self = this; + + if (content && isSorted) { + content = content.slice(); + content.sort(function(item1, item2) { + return self.orderBy(item1, item2); }); - // Do not preventDefault in eventManagers. - evt.stopPropagation(); + forEach(content, function(item) { + forEach(sortProperties, function(sortProperty) { + Ember.addObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); + }, this); + }, this); + return Ember.A(content); } - else { - result = this._bubbleEvent(view, evt, eventName); + + return content; + }), + + _contentWillChange: Ember.beforeObserver('content', function() { + var content = get(this, 'content'), + sortProperties = get(this, 'sortProperties'); + + if (content && sortProperties) { + forEach(content, function(item) { + forEach(sortProperties, function(sortProperty) { + Ember.removeObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); + }, this); + }, this); } - return result; - }, + this._super(); + }), - _bubbleEvent: function(view, evt, eventName) { - return Ember.run(function() { - return view.handleEvent(eventName, evt); - }); - }, + sortAscendingWillChange: Ember.beforeObserver('sortAscending', function() { + this._lastSortAscending = get(this, 'sortAscending'); + }), - destroy: function() { - var rootElement = get(this, 'rootElement'); - Ember.$(rootElement).off('.ember', '**').removeClass('ember-application'); - return this._super(); - } -}); + sortAscendingDidChange: Ember.observer('sortAscending', function() { + if (get(this, 'sortAscending') !== this._lastSortAscending) { + var arrangedContent = get(this, 'arrangedContent'); + arrangedContent.reverseObjects(); + } + }), -})(); + contentArrayWillChange: function(array, idx, removedCount, addedCount) { + var isSorted = get(this, 'isSorted'); + if (isSorted) { + var arrangedContent = get(this, 'arrangedContent'); + var removedObjects = array.slice(idx, idx+removedCount); + var sortProperties = get(this, 'sortProperties'); + forEach(removedObjects, function(item) { + arrangedContent.removeObject(item); -(function() { -/** -@module ember -@submodule ember-views -*/ + forEach(sortProperties, function(sortProperty) { + Ember.removeObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); + }, this); + }, this); + } -// Add a new named queue for rendering views that happens -// after bindings have synced, and a queue for scheduling actions -// that that should occur after view rendering. -var queues = Ember.run.queues, - indexOf = Ember.ArrayPolyfills.indexOf; -queues.splice(indexOf.call(queues, 'actions')+1, 0, 'render', 'afterRender'); + return this._super(array, idx, removedCount, addedCount); + }, -})(); + contentArrayDidChange: function(array, idx, removedCount, addedCount) { + var isSorted = get(this, 'isSorted'), + sortProperties = get(this, 'sortProperties'); + if (isSorted) { + var addedObjects = array.slice(idx, idx+addedCount); + forEach(addedObjects, function(item) { + this.insertItemSorted(item); -(function() { -/** -@module ember -@submodule ember-views -*/ + forEach(sortProperties, function(sortProperty) { + Ember.addObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); + }, this); + }, this); + } -var get = Ember.get, set = Ember.set; + return this._super(array, idx, removedCount, addedCount); + }, -// Original class declaration and documentation in runtime/lib/controllers/controller.js -// NOTE: It may be possible with YUIDoc to combine docs in two locations + insertItemSorted: function(item) { + var arrangedContent = get(this, 'arrangedContent'); + var length = get(arrangedContent, 'length'); -/** -Additional methods for the ControllerMixin + var idx = this._binarySearch(item, 0, length); + arrangedContent.insertAt(idx, item); + }, -@class ControllerMixin -@namespace Ember -*/ -Ember.ControllerMixin.reopen({ - target: null, - namespace: null, - view: null, - container: null, - _childContainers: null, + contentItemSortPropertyDidChange: function(item) { + var arrangedContent = get(this, 'arrangedContent'), + oldIndex = arrangedContent.indexOf(item), + leftItem = arrangedContent.objectAt(oldIndex - 1), + rightItem = arrangedContent.objectAt(oldIndex + 1), + leftResult = leftItem && this.orderBy(item, leftItem), + rightResult = rightItem && this.orderBy(item, rightItem); - init: function() { - this._super(); - set(this, '_childContainers', {}); + if (leftResult < 0 || rightResult > 0) { + arrangedContent.removeObject(item); + this.insertItemSorted(item); + } }, - _modelDidChange: Ember.observer(function() { - var containers = get(this, '_childContainers'); + _binarySearch: function(item, low, high) { + var mid, midItem, res, arrangedContent; - for (var prop in containers) { - if (!containers.hasOwnProperty(prop)) { continue; } - containers[prop].destroy(); + if (low === high) { + return low; } - set(this, '_childContainers', {}); - }, 'model') -}); + arrangedContent = get(this, 'arrangedContent'); -})(); + mid = low + Math.floor((high - low) / 2); + midItem = arrangedContent.objectAt(mid); + res = this.orderBy(midItem, item); + if (res < 0) { + return this._binarySearch(item, mid+1, high); + } else if (res > 0) { + return this._binarySearch(item, low, mid); + } -(function() { + return mid; + } +}); })(); (function() { -var states = {}; - /** @module ember -@submodule ember-views +@submodule ember-runtime */ -var get = Ember.get, set = Ember.set; -var guidFor = Ember.guidFor; -var a_forEach = Ember.EnumerableUtils.forEach; -var a_addObject = Ember.EnumerableUtils.addObject; -var meta = Ember.meta; +var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach, + replace = Ember.EnumerableUtils.replace; -var childViewsProperty = Ember.computed(function() { - var childViews = this._childViews, ret = Ember.A(), view = this; +/** + `Ember.ArrayController` provides a way for you to publish a collection of + objects so that you can easily bind to the collection from a Handlebars + `#each` helper, an `Ember.CollectionView`, or other controllers. - a_forEach(childViews, function(view) { - var currentChildViews; - if (view.isVirtual) { - if (currentChildViews = get(view, 'childViews')) { - ret.pushObjects(currentChildViews); - } - } else { - ret.push(view); - } - }); + The advantage of using an `ArrayController` is that you only have to set up + your view bindings once; to change what's displayed, simply swap out the + `content` property on the controller. - ret.replace = function (idx, removedCount, addedViews) { - if (view instanceof Ember.ContainerView) { - Ember.deprecate("Manipulating an Ember.ContainerView through its childViews property is deprecated. Please use the ContainerView instance itself as an Ember.MutableArray."); - return view.replace(idx, removedCount, addedViews); - } - throw new Ember.Error("childViews is immutable"); - }; + For example, imagine you wanted to display a list of items fetched via an XHR + request. Create an `Ember.ArrayController` and set its `content` property: - return ret; -}); + ```javascript + MyApp.listController = Ember.ArrayController.create(); -Ember.warn("The VIEW_PRESERVES_CONTEXT flag has been removed and the functionality can no longer be disabled.", Ember.ENV.VIEW_PRESERVES_CONTEXT !== false); + $.get('people.json', function(data) { + MyApp.listController.set('content', data); + }); + ``` -/** - Global hash of shared templates. This will automatically be populated - by the build tools so that you can store your Handlebars templates in - separate files that get loaded into JavaScript at buildtime. + Then, create a view that binds to your new controller: - @property TEMPLATES - @for Ember - @type Hash -*/ -Ember.TEMPLATES = {}; + ```handlebars + {{#each MyApp.listController}} + {{firstName}} {{lastName}} + {{/each}} + ``` -/** - `Ember.CoreView` is an abstract class that exists to give view-like behavior - to both Ember's main view class `Ember.View` and other classes like - `Ember._SimpleMetamorphView` that don't need the fully functionaltiy of - `Ember.View`. + Although you are binding to the controller, the behavior of this controller + is to pass through any methods or properties to the underlying array. This + capability comes from `Ember.ArrayProxy`, which this class inherits from. - Unless you have specific needs for `CoreView`, you will use `Ember.View` - in your applications. + Sometimes you want to display computed properties within the body of an + `#each` helper that depend on the underlying items in `content`, but are not + present on those items. To do this, set `itemController` to the name of a + controller (probably an `ObjectController`) that will wrap each individual item. - @class CoreView - @namespace Ember - @extends Ember.Object - @uses Ember.Evented -*/ + For example: -Ember.CoreView = Ember.Object.extend(Ember.Evented, Ember.ActionHandler, { - isView: true, + ```handlebars + {{#each post in controller}} +
  • {{title}} ({{titleLength}} characters)
  • + {{/each}} + ``` - states: states, + ```javascript + App.PostsController = Ember.ArrayController.extend({ + itemController: 'post' + }); - init: function() { - this._super(); - this.transitionTo('preRender'); - }, + App.PostController = Ember.ObjectController.extend({ + // the `title` property will be proxied to the underlying post. - /** - If the view is currently inserted into the DOM of a parent view, this - property will point to the parent of the view. + titleLength: function() { + return this.get('title').length; + }.property('title') + }); + ``` - @property parentView - @type Ember.View - @default null - */ - parentView: Ember.computed(function() { - var parent = this._parentView; + In some cases it is helpful to return a different `itemController` depending + on the particular item. Subclasses can do this by overriding + `lookupItemController`. - if (parent && parent.isVirtual) { - return get(parent, 'parentView'); - } else { - return parent; + For example: + + ```javascript + App.MyArrayController = Ember.ArrayController.extend({ + lookupItemController: function( object ) { + if (object.get('isSpecial')) { + return "special"; // use App.SpecialController + } else { + return "regular"; // use App.RegularController + } } - }).property('_parentView'), + }); + ``` - state: null, + The itemController instances will have a `parentController` property set to + the `ArrayController` instance. - _parentView: null, + @class ArrayController + @namespace Ember + @extends Ember.ArrayProxy + @uses Ember.SortableMixin + @uses Ember.ControllerMixin +*/ - // return the current view, not including virtual views - concreteView: Ember.computed(function() { - if (!this.isVirtual) { return this; } - else { return get(this, 'parentView'); } - }).property('parentView'), +Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, + Ember.SortableMixin, { - instrumentName: 'core_view', + /** + The controller used to wrap items, if any. - instrumentDetails: function(hash) { - hash.object = this.toString(); - }, + @property itemController + @type String + @default null + */ + itemController: null, /** - @private + Return the name of the controller to wrap items, or `null` if items should + be returned directly. The default implementation simply returns the + `itemController` property, but subclasses can override this method to return + different controllers for different objects. - Invoked by the view system when this view needs to produce an HTML - representation. This method will create a new render buffer, if needed, - then apply any default attributes, such as class names and visibility. - Finally, the `render()` method is invoked, which is responsible for - doing the bulk of the rendering. + For example: - You should not need to override this method; instead, implement the - `template` property, or if you need more control, override the `render` - method. + ```javascript + App.MyArrayController = Ember.ArrayController.extend({ + lookupItemController: function( object ) { + if (object.get('isSpecial')) { + return "special"; // use App.SpecialController + } else { + return "regular"; // use App.RegularController + } + } + }); + ``` - @method renderToBuffer - @param {Ember.RenderBuffer} buffer the render buffer. If no buffer is - passed, a default buffer, using the current view's `tagName`, will - be used. + @method lookupItemController + @param {Object} object + @return {String} */ - renderToBuffer: function(parentBuffer, bufferOperation) { - var name = 'render.' + this.instrumentName, - details = {}; - - this.instrumentDetails(details); - - return Ember.instrument(name, details, function() { - return this._renderToBuffer(parentBuffer, bufferOperation); - }, this); + lookupItemController: function(object) { + return get(this, 'itemController'); }, - _renderToBuffer: function(parentBuffer, bufferOperation) { - // If this is the top-most view, start a new buffer. Otherwise, - // create a new buffer relative to the original using the - // provided buffer operation (for example, `insertAfter` will - // insert a new buffer after the "parent buffer"). - var tagName = this.tagName; + objectAtContent: function(idx) { + var length = get(this, 'length'), + arrangedContent = get(this,'arrangedContent'), + object = arrangedContent && arrangedContent.objectAt(idx); - if (tagName === null || tagName === undefined) { - tagName = 'div'; + if (idx >= 0 && idx < length) { + var controllerClass = this.lookupItemController(object); + if (controllerClass) { + return this.controllerAt(idx, object, controllerClass); + } } - var buffer = this.buffer = parentBuffer && parentBuffer.begin(tagName) || Ember.RenderBuffer(tagName); - this.transitionTo('inBuffer', false); + // When `controllerClass` is falsy, we have not opted in to using item + // controllers, so return the object directly. - this.beforeRender(buffer); - this.render(buffer); - this.afterRender(buffer); + // When the index is out of range, we want to return the "out of range" + // value, whatever that might be. Rather than make assumptions + // (e.g. guessing `null` or `undefined`) we defer this to `arrangedContent`. + return object; + }, - return buffer; + arrangedContentDidChange: function() { + this._super(); + this._resetSubControllers(); }, - /** - @private + arrayContentDidChange: function(idx, removedCnt, addedCnt) { + var subControllers = get(this, '_subControllers'), + subControllersToRemove = subControllers.slice(idx, idx+removedCnt); - Override the default event firing from `Ember.Evented` to - also call methods with the given name. + forEach(subControllersToRemove, function(subController) { + if (subController) { subController.destroy(); } + }); - @method trigger - @param name {String} - */ - trigger: function(name) { - this._super.apply(this, arguments); - var method = this[name]; - if (method) { - var args = [], i, l; - for (i = 1, l = arguments.length; i < l; i++) { - args.push(arguments[i]); - } - return method.apply(this, args); - } - }, + replace(subControllers, idx, removedCnt, new Array(addedCnt)); - deprecatedSendHandles: function(actionName) { - return !!this[actionName]; + // The shadow array of subcontrollers must be updated before we trigger + // observers, otherwise observers will get the wrong subcontainer when + // calling `objectAt` + this._super(idx, removedCnt, addedCnt); }, - deprecatedSend: function(actionName) { - var args = [].slice.call(arguments, 1); - Ember.assert('' + this + " has the action " + actionName + " but it is not a function", typeof this[actionName] === 'function'); - Ember.deprecate('Action handlers implemented directly on views are deprecated in favor of action handlers on an `actions` object (' + actionName + ' on ' + this + ')', false); - this[actionName].apply(this, args); - return; - }, + init: function() { + this._super(); - has: function(name) { - return Ember.typeOf(this[name]) === 'function' || this._super(name); + this.set('_subControllers', Ember.A()); }, - destroy: function() { - var parent = this._parentView; - - if (!this._super()) { return; } - - // destroy the element -- this will avoid each child view destroying - // the element over and over again... - if (!this.removedFromDOM) { this.destroyElement(); } - - // remove from parent if found. Don't call removeFromParent, - // as removeFromParent will try to remove the element from - // the DOM again. - if (parent) { parent.removeChild(this); } - - this.transitionTo('destroying', false); + content: Ember.computed(function () { + return Ember.A(); + }), - return this; - }, + /** + * Flag to mark as being "virtual". Used to keep this instance + * from participating in the parentController hierarchy. + * + * @private + * @type Boolean + */ + _isVirtual: false, - clearRenderedChildren: Ember.K, - triggerRecursively: Ember.K, - invokeRecursively: Ember.K, - transitionTo: Ember.K, - destroyElement: Ember.K -}); + controllerAt: function(idx, object, controllerClass) { + var container = get(this, 'container'), + subControllers = get(this, '_subControllers'), + subController = subControllers[idx], + fullName; -var ViewCollection = Ember._ViewCollection = function(initialViews) { - var views = this.views = initialViews || []; - this.length = views.length; -}; + if (subController) { return subController; } -ViewCollection.prototype = { - length: 0, + fullName = "controller:" + controllerClass; - trigger: function(eventName) { - var views = this.views, view; - for (var i = 0, l = views.length; i < l; i++) { - view = views[i]; - if (view.trigger) { view.trigger(eventName); } + if (!container.has(fullName)) { + throw new Ember.Error('Could not resolve itemController: "' + controllerClass + '"'); } - }, - - triggerRecursively: function(eventName) { - var views = this.views; - for (var i = 0, l = views.length; i < l; i++) { - views[i].triggerRecursively(eventName); + var parentController; + if (this._isVirtual) { + parentController = get(this, 'parentController'); } - }, + parentController = parentController || this; + subController = container.lookupFactory(fullName).create({ + target: this, + parentController: parentController, + content: object + }); - invokeRecursively: function(fn) { - var views = this.views, view; + subControllers[idx] = subController; - for (var i = 0, l = views.length; i < l; i++) { - view = views[i]; - fn(view); - } + return subController; }, - transitionTo: function(state, children) { - var views = this.views; - for (var i = 0, l = views.length; i < l; i++) { - views[i].transitionTo(state, children); - } - }, + _subControllers: null, - push: function() { - this.length += arguments.length; - var views = this.views; - return views.push.apply(views, arguments); - }, + _resetSubControllers: function() { + var subControllers = get(this, '_subControllers'); + if (subControllers) { + forEach(subControllers, function(subController) { + if (subController) { subController.destroy(); } + }); + } - objectAt: function(idx) { - return this.views[idx]; - }, + this.set('_subControllers', Ember.A()); + } +}); - forEach: function(callback) { - var views = this.views; - return a_forEach(views, callback); - }, +})(); - clear: function() { - this.length = 0; - this.views.length = 0; - } -}; -var EMPTY_ARRAY = []; +(function() { /** - `Ember.View` is the class in Ember responsible for encapsulating templates of - HTML content, combining templates with data to render as sections of a page's - DOM, and registering and responding to user-initiated events. - - ## HTML Tag +@module ember +@submodule ember-runtime +*/ - The default HTML tag name used for a view's DOM representation is `div`. This - can be customized by setting the `tagName` property. The following view - class: +/** + `Ember.ObjectController` is part of Ember's Controller layer. It is intended + to wrap a single object, proxying unhandled attempts to `get` and `set` to the underlying + content object, and to forward unhandled action attempts to its `target`. - ```javascript - ParagraphView = Ember.View.extend({ - tagName: 'em' - }); - ``` + `Ember.ObjectController` derives this functionality from its superclass + `Ember.ObjectProxy` and the `Ember.ControllerMixin` mixin. - Would result in instances with the following HTML: + @class ObjectController + @namespace Ember + @extends Ember.ObjectProxy + @uses Ember.ControllerMixin +**/ +Ember.ObjectController = Ember.ObjectProxy.extend(Ember.ControllerMixin); - ```html - - ``` +})(); - ## HTML `class` Attribute - The HTML `class` attribute of a view's tag can be set by providing a - `classNames` property that is set to an array of strings: - ```javascript - MyView = Ember.View.extend({ - classNames: ['my-class', 'my-other-class'] - }); - ``` +(function() { - Will result in view instances with an HTML representation of: +})(); - ```html -
    - ``` - `class` attribute values can also be set by providing a `classNameBindings` - property set to an array of properties names for the view. The return value - of these properties will be added as part of the value for the view's `class` - attribute. These properties can be computed properties: - ```javascript - MyView = Ember.View.extend({ - classNameBindings: ['propertyA', 'propertyB'], - propertyA: 'from-a', - propertyB: function() { - if (someLogic) { return 'from-b'; } - }.property() - }); - ``` +(function() { +/** +Ember Runtime - Will result in view instances with an HTML representation of: +@module ember +@submodule ember-runtime +@requires ember-metal +*/ - ```html -
    - ``` +})(); - If the value of a class name binding returns a boolean the property name - itself will be used as the class name if the property is true. The class name - will not be added if the value is `false` or `undefined`. +(function() { +/** +@module ember +@submodule ember-views +*/ - ```javascript - MyView = Ember.View.extend({ - classNameBindings: ['hovered'], - hovered: true - }); - ``` +var jQuery = (Ember.imports && Ember.imports.jQuery) || (this && this.jQuery); +if (!jQuery && typeof require === 'function') { + jQuery = require('jquery'); +} - Will result in view instances with an HTML representation of: +Ember.assert("Ember Views require jQuery between 1.7 and 2.1", jQuery && (jQuery().jquery.match(/^((1\.(7|8|9|10|11))|(2\.(0|1)))(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_JQUERY)); - ```html -
    - ``` +/** + Alias for jQuery - When using boolean class name bindings you can supply a string value other - than the property name for use as the `class` HTML attribute by appending the - preferred value after a ":" character when defining the binding: + @method $ + @for Ember +*/ +Ember.$ = jQuery; - ```javascript - MyView = Ember.View.extend({ - classNameBindings: ['awesome:so-very-cool'], - awesome: true - }); - ``` +})(); - Will result in view instances with an HTML representation of: - ```html -
    - ``` - Boolean value class name bindings whose property names are in a - camelCase-style format will be converted to a dasherized format: +(function() { +/** +@module ember +@submodule ember-views +*/ +if (Ember.$) { + // http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#dndevents + var dragEvents = Ember.String.w('dragstart drag dragenter dragleave dragover drop dragend'); - ```javascript - MyView = Ember.View.extend({ - classNameBindings: ['isUrgent'], - isUrgent: true + // Copies the `dataTransfer` property from a browser event object onto the + // jQuery event object for the specified events + Ember.EnumerableUtils.forEach(dragEvents, function(eventName) { + Ember.$.event.fixHooks[eventName] = { props: ['dataTransfer'] }; }); - ``` +} - Will result in view instances with an HTML representation of: +})(); - ```html -
    - ``` - Class name bindings can also refer to object values that are found by - traversing a path relative to the view itself: - ```javascript - MyView = Ember.View.extend({ - classNameBindings: ['messages.empty'] - messages: Ember.Object.create({ - empty: true - }) - }); - ``` +(function() { +/** +@module ember +@submodule ember-views +*/ - Will result in view instances with an HTML representation of: +/* BEGIN METAMORPH HELPERS */ - ```html -
    - ``` +// Internet Explorer prior to 9 does not allow setting innerHTML if the first element +// is a "zero-scope" element. This problem can be worked around by making +// the first node an invisible text node. We, like Modernizr, use ­ - If you want to add a class name for a property which evaluates to true and - and a different class name if it evaluates to false, you can pass a binding - like this: +var needsShy = typeof document !== 'undefined' && (function() { + var testEl = document.createElement('div'); + testEl.innerHTML = "
    "; + testEl.firstChild.innerHTML = ""; + return testEl.firstChild.innerHTML === ''; +})(); - ```javascript - // Applies 'enabled' class when isEnabled is true and 'disabled' when isEnabled is false - Ember.View.extend({ - classNameBindings: ['isEnabled:enabled:disabled'] - isEnabled: true - }); - ``` +// IE 8 (and likely earlier) likes to move whitespace preceeding +// a script tag to appear after it. This means that we can +// accidentally remove whitespace when updating a morph. +var movesWhitespace = typeof document !== 'undefined' && (function() { + var testEl = document.createElement('div'); + testEl.innerHTML = "Test: Value"; + return testEl.childNodes[0].nodeValue === 'Test:' && + testEl.childNodes[2].nodeValue === ' Value'; +})(); - Will result in view instances with an HTML representation of: +// Use this to find children by ID instead of using jQuery +var findChildById = function(element, id) { + if (element.getAttribute('id') === id) { return element; } - ```html -
    - ``` + var len = element.childNodes.length, idx, node, found; + for (idx=0; idx
    - ``` + var matches = []; + if (movesWhitespace) { + // Right now we only check for script tags with ids with the + // goal of targeting morphs. + html = html.replace(/(\s+)( - ``` +ClassSet.prototype = { + add: function(string) { + if (string in this.seen) { return; } + this.seen[string] = true; - And associate it by name using a view's `templateName` property: + this.list.push(string); + }, - ```javascript - AView = Ember.View.extend({ - templateName: 'some-template' - }); - ``` + toDOM: function() { + return this.list.join(" "); + } +}; - Using a value for `templateName` that does not have a Handlebars template - with a matching `data-template-name` attribute will throw an error. +var BAD_TAG_NAME_TEST_REGEXP = /[^a-zA-Z0-9\-]/; +var BAD_TAG_NAME_REPLACE_REGEXP = /[^a-zA-Z0-9\-]/g; - For views classes that may have a template later defined (e.g. as the block - portion of a `{{view}}` Handlebars helper call in another template or in - a subclass), you can provide a `defaultTemplate` property set to compiled - template function. If a template is not later provided for the view instance - the `defaultTemplate` value will be used: +function stripTagName(tagName) { + if (!tagName) { + return tagName; + } - ```javascript - AView = Ember.View.extend({ - defaultTemplate: Ember.Handlebars.compile('I was the default'), - template: null, - templateName: null - }); - ``` + if (!BAD_TAG_NAME_TEST_REGEXP.test(tagName)) { + return tagName; + } - Will result in instances with an HTML representation of: + return tagName.replace(BAD_TAG_NAME_REPLACE_REGEXP, ''); +} - ```html -
    I was the default
    - ``` +var BAD_CHARS_REGEXP = /&(?!\w+;)|[<>"'`]/g; +var POSSIBLE_CHARS_REGEXP = /[&<>"'`]/; - If a `template` or `templateName` is provided it will take precedence over - `defaultTemplate`: +function escapeAttribute(value) { + // Stolen shamelessly from Handlebars - ```javascript - AView = Ember.View.extend({ - defaultTemplate: Ember.Handlebars.compile('I was the default') - }); + var escape = { + "<": "<", + ">": ">", + '"': """, + "'": "'", + "`": "`" + }; - aView = AView.create({ - template: Ember.Handlebars.compile('I was the template, not default') - }); - ``` + var escapeChar = function(chr) { + return escape[chr] || "&"; + }; - Will result in the following HTML representation when rendered: + var string = value.toString(); - ```html -
    I was the template, not default
    - ``` + if(!POSSIBLE_CHARS_REGEXP.test(string)) { return string; } + return string.replace(BAD_CHARS_REGEXP, escapeChar); +} - ## View Context +// IE 6/7 have bugs around setting names on inputs during creation. +// From http://msdn.microsoft.com/en-us/library/ie/ms536389(v=vs.85).aspx: +// "To include the NAME attribute at run time on objects created with the createElement method, use the eTag." +var canSetNameOnInputs = (function() { + var div = document.createElement('div'), + el = document.createElement('input'); - The default context of the compiled template is the view's controller: + el.setAttribute('name', 'foo'); + div.appendChild(el); - ```javascript - AView = Ember.View.extend({ - template: Ember.Handlebars.compile('Hello {{excitedGreeting}}') - }); + return !!div.innerHTML.match('foo'); +})(); - aController = Ember.Object.create({ - firstName: 'Barry', - excitedGreeting: function() { - return this.get("content.firstName") + "!!!" - }.property() - }); +/** + `Ember.RenderBuffer` gathers information regarding the a view and generates the + final representation. `Ember.RenderBuffer` will generate HTML which can be pushed + to the DOM. - aView = AView.create({ - controller: aController, - }); + ```javascript + var buffer = Ember.RenderBuffer('div'); ``` - Will result in an HTML representation of: + @class RenderBuffer + @namespace Ember + @constructor + @param {String} tagName tag name (such as 'div' or 'p') used for the buffer +*/ +Ember.RenderBuffer = function(tagName) { + return new Ember._RenderBuffer(tagName); +}; - ```html -
    Hello Barry!!!
    - ``` +Ember._RenderBuffer = function(tagName) { + this.tagNames = [tagName || null]; + this.buffer = ""; +}; - A context can also be explicitly supplied through the view's `context` - property. If the view has neither `context` nor `controller` properties, the - `parentView`'s context will be used. +Ember._RenderBuffer.prototype = { - ## Layouts + // The root view's element + _element: null, - Views can have a secondary template that wraps their main template. Like - primary templates, layouts can be any function that accepts an optional - context parameter and returns a string of HTML that will be inserted inside - view's tag. Views whose HTML element is self closing (e.g. ``) - cannot have a layout and this property will be ignored. + _hasElement: true, - Most typically in Ember a layout will be a compiled `Ember.Handlebars` - template. + /** + An internal set used to de-dupe class names when `addClass()` is + used. After each call to `addClass()`, the `classes` property + will be updated. - A view's layout can be set directly with the `layout` property or reference - an existing Handlebars template by name with the `layoutName` property. + @private + @property elementClasses + @type Array + @default [] + */ + elementClasses: null, - A template used as a layout must contain a single use of the Handlebars - `{{yield}}` helper. The HTML contents of a view's rendered `template` will be - inserted at this location: + /** + Array of class names which will be applied in the class attribute. - ```javascript - AViewWithLayout = Ember.View.extend({ - layout: Ember.Handlebars.compile("
    {{yield}}
    ") - template: Ember.Handlebars.compile("I got wrapped"), - }); - ``` + You can use `setClasses()` to set this property directly. If you + use `addClass()`, it will be maintained for you. - Will result in view instances with an HTML representation of: + @property classes + @type Array + @default [] + */ + classes: null, - ```html -
    -
    - I got wrapped -
    -
    - ``` + /** + The id in of the element, to be applied in the id attribute. - See [Ember.Handlebars.helpers.yield](/api/classes/Ember.Handlebars.helpers.html#method_yield) - for more information. + You should not set this property yourself, rather, you should use + the `id()` method of `Ember.RenderBuffer`. - ## Responding to Browser Events + @property elementId + @type String + @default null + */ + elementId: null, - Views can respond to user-initiated events in one of three ways: method - implementation, through an event manager, and through `{{action}}` helper use - in their template or layout. + /** + A hash keyed on the name of the attribute and whose value will be + applied to that attribute. For example, if you wanted to apply a + `data-view="Foo.bar"` property to an element, you would set the + elementAttributes hash to `{'data-view':'Foo.bar'}`. - ### Method Implementation + You should not maintain this hash yourself, rather, you should use + the `attr()` method of `Ember.RenderBuffer`. - Views can respond to user-initiated events by implementing a method that - matches the event name. A `jQuery.Event` object will be passed as the - argument to this method. + @property elementAttributes + @type Hash + @default {} + */ + elementAttributes: null, - ```javascript - AView = Ember.View.extend({ - click: function(event) { - // will be called when when an instance's - // rendered element is clicked - } - }); - ``` + /** + A hash keyed on the name of the properties and whose value will be + applied to that property. For example, if you wanted to apply a + `checked=true` property to an element, you would set the + elementProperties hash to `{'checked':true}`. - ### Event Managers + You should not maintain this hash yourself, rather, you should use + the `prop()` method of `Ember.RenderBuffer`. - Views can define an object as their `eventManager` property. This object can - then implement methods that match the desired event names. Matching events - that occur on the view's rendered HTML or the rendered HTML of any of its DOM - descendants will trigger this method. A `jQuery.Event` object will be passed - as the first argument to the method and an `Ember.View` object as the - second. The `Ember.View` will be the view whose rendered HTML was interacted - with. This may be the view with the `eventManager` property or one of its - descendent views. + @property elementProperties + @type Hash + @default {} + */ + elementProperties: null, - ```javascript - AView = Ember.View.extend({ - eventManager: Ember.Object.create({ - doubleClick: function(event, view) { - // will be called when when an instance's - // rendered element or any rendering - // of this views's descendent - // elements is clicked - } - }) - }); - ``` + /** + The tagname of the element an instance of `Ember.RenderBuffer` represents. - An event defined for an event manager takes precedence over events of the - same name handled through methods on the view. + Usually, this gets set as the first parameter to `Ember.RenderBuffer`. For + example, if you wanted to create a `p` tag, then you would call - ```javascript - AView = Ember.View.extend({ - mouseEnter: function(event) { - // will never trigger. - }, - eventManager: Ember.Object.create({ - mouseEnter: function(event, view) { - // takes precedence over AView#mouseEnter - } - }) - }); - ``` + ```javascript + Ember.RenderBuffer('p') + ``` - Similarly a view's event manager will take precedence for events of any views - rendered as a descendent. A method name that matches an event name will not - be called if the view instance was rendered inside the HTML representation of - a view that has an `eventManager` property defined that handles events of the - name. Events not handled by the event manager will still trigger method calls - on the descendent. + @property elementTag + @type String + @default null + */ + elementTag: null, - ```javascript - OuterView = Ember.View.extend({ - template: Ember.Handlebars.compile("outer {{#view InnerView}}inner{{/view}} outer"), - eventManager: Ember.Object.create({ - mouseEnter: function(event, view) { - // view might be instance of either - // OuterView or InnerView depending on - // where on the page the user interaction occured - } - }) - }); + /** + A hash keyed on the name of the style attribute and whose value will + be applied to that attribute. For example, if you wanted to apply a + `background-color:black;` style to an element, you would set the + elementStyle hash to `{'background-color':'black'}`. - InnerView = Ember.View.extend({ - click: function(event) { - // will be called if rendered inside - // an OuterView because OuterView's - // eventManager doesn't handle click events - }, - mouseEnter: function(event) { - // will never be called if rendered inside - // an OuterView. - } - }); - ``` + You should not maintain this hash yourself, rather, you should use + the `style()` method of `Ember.RenderBuffer`. - ### Handlebars `{{action}}` Helper + @property elementStyle + @type Hash + @default {} + */ + elementStyle: null, - See [Handlebars.helpers.action](/api/classes/Ember.Handlebars.helpers.html#method_action). + /** + Nested `RenderBuffers` will set this to their parent `RenderBuffer` + instance. - ### Event Names + @property parentBuffer + @type Ember._RenderBuffer + */ + parentBuffer: null, - All of the event handling approaches described above respond to the same set - of events. The names of the built-in events are listed below. (The hash of - built-in events exists in `Ember.EventDispatcher`.) Additional, custom events - can be registered by using `Ember.Application.customEvents`. + /** + Adds a string of HTML to the `RenderBuffer`. - Touch events: + @method push + @param {String} string HTML to push into the buffer + @chainable + */ + push: function(string) { + this.buffer += string; + return this; + }, - * `touchStart` - * `touchMove` - * `touchEnd` - * `touchCancel` + /** + Adds a class to the buffer, which will be rendered to the class attribute. - Keyboard events + @method addClass + @param {String} className Class name to add to the buffer + @chainable + */ + addClass: function(className) { + // lazily create elementClasses + this.elementClasses = (this.elementClasses || new ClassSet()); + this.elementClasses.add(className); + this.classes = this.elementClasses.list; - * `keyDown` - * `keyUp` - * `keyPress` + return this; + }, - Mouse events - - * `mouseDown` - * `mouseUp` - * `contextMenu` - * `click` - * `doubleClick` - * `mouseMove` - * `focusIn` - * `focusOut` - * `mouseEnter` - * `mouseLeave` - - Form events: - - * `submit` - * `change` - * `focusIn` - * `focusOut` - * `input` - - HTML5 drag and drop events: - - * `dragStart` - * `drag` - * `dragEnter` - * `dragLeave` - * `drop` - * `dragEnd` - - ## Handlebars `{{view}}` Helper - - Other `Ember.View` instances can be included as part of a view's template by - using the `{{view}}` Handlebars helper. See [Ember.Handlebars.helpers.view](/api/classes/Ember.Handlebars.helpers.html#method_view) - for additional information. - - @class View - @namespace Ember - @extends Ember.CoreView -*/ -Ember.View = Ember.CoreView.extend( -/** @scope Ember.View.prototype */ { - - concatenatedProperties: ['classNames', 'classNameBindings', 'attributeBindings'], + setClasses: function(classNames) { + this.elementClasses = null; + var len = classNames.length, i; + for (i = 0; i < len; i++) { + this.addClass(classNames[i]); + } + }, /** - @property isView - @type Boolean - @default true - @final + Sets the elementID to be used for the element. + + @method id + @param {String} id + @chainable */ - isView: true, + id: function(id) { + this.elementId = id; + return this; + }, - // .......................................................... - // TEMPLATE SUPPORT - // + // duck type attribute functionality like jQuery so a render buffer + // can be used like a jQuery object in attribute binding scenarios. /** - The name of the template to lookup if no template is provided. - - `Ember.View` will look for a template with this name in this view's - `templates` object. By default, this will be a global object - shared in `Ember.TEMPLATES`. + Adds an attribute which will be rendered to the element. - @property templateName - @type String - @default null + @method attr + @param {String} name The name of the attribute + @param {String} value The value to add to the attribute + @chainable + @return {Ember.RenderBuffer|String} this or the current attribute value */ - templateName: null, - - /** - The name of the layout to lookup if no layout is provided. + attr: function(name, value) { + var attributes = this.elementAttributes = (this.elementAttributes || {}); - `Ember.View` will look for a template with this name in this view's - `templates` object. By default, this will be a global object - shared in `Ember.TEMPLATES`. + if (arguments.length === 1) { + return attributes[name]; + } else { + attributes[name] = value; + } - @property layoutName - @type String - @default null - */ - layoutName: null, + return this; + }, /** - The hash in which to look for `templateName`. + Remove an attribute from the list of attributes to render. - @property templates - @type Ember.Object - @default Ember.TEMPLATES + @method removeAttr + @param {String} name The name of the attribute + @chainable */ - templates: Ember.TEMPLATES, + removeAttr: function(name) { + var attributes = this.elementAttributes; + if (attributes) { delete attributes[name]; } - /** - The template used to render the view. This should be a function that - accepts an optional context parameter and returns a string of HTML that - will be inserted into the DOM relative to its parent view. + return this; + }, - In general, you should set the `templateName` property instead of setting - the template yourself. + /** + Adds a property which will be rendered to the element. - @property template - @type Function + @method prop + @param {String} name The name of the property + @param {String} value The value to add to the property + @chainable + @return {Ember.RenderBuffer|String} this or the current property value */ - template: Ember.computed(function(key, value) { - if (value !== undefined) { return value; } - - var templateName = get(this, 'templateName'), - template = this.templateForName(templateName, 'template'); + prop: function(name, value) { + var properties = this.elementProperties = (this.elementProperties || {}); - Ember.assert("You specified the templateName " + templateName + " for " + this + ", but it did not exist.", !templateName || template); + if (arguments.length === 1) { + return properties[name]; + } else { + properties[name] = value; + } - return template || get(this, 'defaultTemplate'); - }).property('templateName'), + return this; + }, /** - The controller managing this view. If this property is set, it will be - made available for use by the template. + Remove an property from the list of properties to render. - @property controller - @type Object + @method removeProp + @param {String} name The name of the property + @chainable */ - controller: Ember.computed(function(key) { - var parentView = get(this, '_parentView'); - return parentView ? get(parentView, 'controller') : null; - }).property('_parentView'), + removeProp: function(name) { + var properties = this.elementProperties; + if (properties) { delete properties[name]; } - /** - A view may contain a layout. A layout is a regular template but - supersedes the `template` property during rendering. It is the - responsibility of the layout template to retrieve the `template` - property from the view (or alternatively, call `Handlebars.helpers.yield`, - `{{yield}}`) to render it in the correct location. + return this; + }, - This is useful for a view that has a shared wrapper, but which delegates - the rendering of the contents of the wrapper to the `template` property - on a subclass. + /** + Adds a style to the style attribute which will be rendered to the element. - @property layout - @type Function + @method style + @param {String} name Name of the style + @param {String} value + @chainable */ - layout: Ember.computed(function(key) { - var layoutName = get(this, 'layoutName'), - layout = this.templateForName(layoutName, 'layout'); - - Ember.assert("You specified the layoutName " + layoutName + " for " + this + ", but it did not exist.", !layoutName || layout); - - return layout || get(this, 'defaultLayout'); - }).property('layoutName'), + style: function(name, value) { + this.elementStyle = (this.elementStyle || {}); - _yield: function(context, options) { - var template = get(this, 'template'); - if (template) { template(context, options); } + this.elementStyle[name] = value; + return this; }, - templateForName: function(name, type) { - if (!name) { return; } - Ember.assert("templateNames are not allowed to contain periods: "+name, name.indexOf('.') === -1); - - // the defaultContainer is deprecated - var container = this.container || (Ember.Container && Ember.Container.defaultContainer); - return container && container.lookup('template:' + name); + begin: function(tagName) { + this.tagNames.push(tagName || null); + return this; }, - /** - The object from which templates should access properties. - - This object will be passed to the template function each time the render - method is called, but it is up to the individual function to decide what - to do with it. - - By default, this will be the view's controller. + pushOpeningTag: function() { + var tagName = this.currentTagName(); + if (!tagName) { return; } - @property context - @type Object - */ - context: Ember.computed(function(key, value) { - if (arguments.length === 2) { - set(this, '_context', value); - return value; - } else { - return get(this, '_context'); + if (this._hasElement && !this._element && this.buffer.length === 0) { + this._element = this.generateElement(); + return; } - }).volatile(), - - /** - @private - Private copy of the view's template context. This can be set directly - by Handlebars without triggering the observer that causes the view - to be re-rendered. + var buffer = this.buffer, + id = this.elementId, + classes = this.classes, + attrs = this.elementAttributes, + props = this.elementProperties, + style = this.elementStyle, + attr, prop; - The context of a view is looked up as follows: + buffer += '<' + stripTagName(tagName); - 1. Supplied context (usually by Handlebars) - 2. Specified controller - 3. `parentView`'s context (for a child of a ContainerView) + if (id) { + buffer += ' id="' + escapeAttribute(id) + '"'; + this.elementId = null; + } + if (classes) { + buffer += ' class="' + escapeAttribute(classes.join(' ')) + '"'; + this.classes = null; + this.elementClasses = null; + } - The code in Handlebars that overrides the `_context` property first - checks to see whether the view has a specified controller. This is - something of a hack and should be revisited. + if (style) { + buffer += ' style="'; - @property _context - */ - _context: Ember.computed(function(key) { - var parentView, controller; + for (prop in style) { + if (style.hasOwnProperty(prop)) { + buffer += prop + ':' + escapeAttribute(style[prop]) + ';'; + } + } - if (controller = get(this, 'controller')) { - return controller; - } + buffer += '"'; - parentView = this._parentView; - if (parentView) { - return get(parentView, '_context'); + this.elementStyle = null; } - return null; - }), + if (attrs) { + for (attr in attrs) { + if (attrs.hasOwnProperty(attr)) { + buffer += ' ' + attr + '="' + escapeAttribute(attrs[attr]) + '"'; + } + } - /** - @private + this.elementAttributes = null; + } - If a value that affects template rendering changes, the view should be - re-rendered to reflect the new value. + if (props) { + for (prop in props) { + if (props.hasOwnProperty(prop)) { + var value = props[prop]; + if (value || typeof(value) === 'number') { + if (value === true) { + buffer += ' ' + prop + '="' + prop + '"'; + } else { + buffer += ' ' + prop + '="' + escapeAttribute(props[prop]) + '"'; + } + } + } + } - @method _contextDidChange - */ - _contextDidChange: Ember.observer(function() { - this.rerender(); - }, 'context'), + this.elementProperties = null; + } - /** - If `false`, the view will appear hidden in DOM. + buffer += '>'; + this.buffer = buffer; + }, - @property isVisible - @type Boolean - @default null - */ - isVisible: true, + pushClosingTag: function() { + var tagName = this.tagNames.pop(); + if (tagName) { this.buffer += ''; } + }, - /** - @private + currentTagName: function() { + return this.tagNames[this.tagNames.length-1]; + }, - Array of child views. You should never edit this array directly. - Instead, use `appendChild` and `removeFromParent`. + generateElement: function() { + var tagName = this.tagNames.pop(), // pop since we don't need to close + id = this.elementId, + classes = this.classes, + attrs = this.elementAttributes, + props = this.elementProperties, + style = this.elementStyle, + styleBuffer = '', attr, prop, tagString; - @property childViews - @type Array - @default [] - */ - childViews: childViewsProperty, + if (attrs && attrs.name && !canSetNameOnInputs) { + // IE allows passing a tag to createElement. See note on `canSetNameOnInputs` above as well. + tagString = '<'+stripTagName(tagName)+' name="'+escapeAttribute(attrs.name)+'">'; + } else { + tagString = tagName; + } - _childViews: EMPTY_ARRAY, + var element = document.createElement(tagString), + $element = Ember.$(element); - // When it's a virtual view, we need to notify the parent that their - // childViews will change. - _childViewsWillChange: Ember.beforeObserver(function() { - if (this.isVirtual) { - var parentView = get(this, 'parentView'); - if (parentView) { Ember.propertyWillChange(parentView, 'childViews'); } + if (id) { + $element.attr('id', id); + this.elementId = null; } - }, 'childViews'), - - // When it's a virtual view, we need to notify the parent that their - // childViews did change. - _childViewsDidChange: Ember.observer(function() { - if (this.isVirtual) { - var parentView = get(this, 'parentView'); - if (parentView) { Ember.propertyDidChange(parentView, 'childViews'); } + if (classes) { + $element.attr('class', classes.join(' ')); + this.classes = null; + this.elementClasses = null; } - }, 'childViews'), - /** - Return the nearest ancestor that is an instance of the provided - class. + if (style) { + for (prop in style) { + if (style.hasOwnProperty(prop)) { + styleBuffer += (prop + ':' + style[prop] + ';'); + } + } - @property nearestInstanceOf - @param {Class} klass Subclass of Ember.View (or Ember.View itself) - @return Ember.View - @deprecated - */ - nearestInstanceOf: function(klass) { - Ember.deprecate("nearestInstanceOf is deprecated and will be removed from future releases. Use nearestOfType."); - var view = get(this, 'parentView'); + $element.attr('style', styleBuffer); - while (view) { - if (view instanceof klass) { return view; } - view = get(view, 'parentView'); + this.elementStyle = null; } - }, - /** - Return the nearest ancestor that is an instance of the provided - class or mixin. - - @property nearestOfType - @param {Class,Mixin} klass Subclass of Ember.View (or Ember.View itself), - or an instance of Ember.Mixin. - @return Ember.View - */ - nearestOfType: function(klass) { - var view = get(this, 'parentView'), - isOfType = klass instanceof Ember.Mixin ? - function(view) { return klass.detect(view); } : - function(view) { return klass.detect(view.constructor); }; + if (attrs) { + for (attr in attrs) { + if (attrs.hasOwnProperty(attr)) { + $element.attr(attr, attrs[attr]); + } + } - while (view) { - if (isOfType(view)) { return view; } - view = get(view, 'parentView'); + this.elementAttributes = null; } - }, - /** - Return the nearest ancestor that has a given property. - - @property nearestWithProperty - @param {String} property A property name - @return Ember.View - */ - nearestWithProperty: function(property) { - var view = get(this, 'parentView'); + if (props) { + for (prop in props) { + if (props.hasOwnProperty(prop)) { + $element.prop(prop, props[prop]); + } + } - while (view) { - if (property in view) { return view; } - view = get(view, 'parentView'); + this.elementProperties = null; } + + return element; }, /** - Return the nearest ancestor whose parent is an instance of - `klass`. - - @property nearestChildOf - @param {Class} klass Subclass of Ember.View (or Ember.View itself) - @return Ember.View + @method element + @return {DOMElement} The element corresponding to the generated HTML + of this buffer */ - nearestChildOf: function(klass) { - var view = get(this, 'parentView'); + element: function() { + var html = this.innerString(); - while (view) { - if (get(view, 'parentView') instanceof klass) { return view; } - view = get(view, 'parentView'); + if (html) { + this._element = Ember.ViewUtils.setInnerHTML(this._element, html); } + + return this._element; }, /** - @private - - When the parent view changes, recursively invalidate `controller` + Generates the HTML content for this buffer. - @method _parentViewDidChange + @method string + @return {String} The generated HTML */ - _parentViewDidChange: Ember.observer(function() { - if (this.isDestroying) { return; } + string: function() { + if (this._hasElement && this._element) { + // Firefox versions < 11 do not have support for element.outerHTML. + var thisElement = this.element(), outerHTML = thisElement.outerHTML; + if (typeof outerHTML === 'undefined') { + return Ember.$('
    ').append(thisElement).html(); + } + return outerHTML; + } else { + return this.innerString(); + } + }, - this.trigger('parentViewDidChange'); + innerString: function() { + return this.buffer; + } +}; - if (get(this, 'parentView.controller') && !get(this, 'controller')) { - this.notifyPropertyChange('controller'); - } - }, '_parentView'), +})(); - _controllerDidChange: Ember.observer(function() { - if (this.isDestroying) { return; } - this.rerender(); - this.forEachChildView(function(view) { - view.propertyDidChange('controller'); - }); - }, 'controller'), +(function() { +/** +@module ember +@submodule ember-views +*/ - cloneKeywords: function() { - var templateData = get(this, 'templateData'); +var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; - var keywords = templateData ? Ember.copy(templateData.keywords) : {}; - set(keywords, 'view', get(this, 'concreteView')); - set(keywords, '_view', this); - set(keywords, 'controller', get(this, 'controller')); +/** + `Ember.EventDispatcher` handles delegating browser events to their + corresponding `Ember.Views.` For example, when you click on a view, + `Ember.EventDispatcher` ensures that that view's `mouseDown` method gets + called. - return keywords; - }, + @class EventDispatcher + @namespace Ember + @private + @extends Ember.Object +*/ +Ember.EventDispatcher = Ember.Object.extend({ /** - Called on your view when it should push strings of HTML into a - `Ember.RenderBuffer`. Most users will want to override the `template` - or `templateName` properties instead of this method. + The set of events names (and associated handler function names) to be setup + and dispatched by the `EventDispatcher`. Custom events can added to this list at setup + time, generally via the `Ember.Application.customEvents` hash. Only override this + default set to prevent the EventDispatcher from listening on some events all together. - By default, `Ember.View` will look for a function in the `template` - property and invoke it with the value of `context`. The value of - `context` will be the view's controller unless you override it. + This set will be modified by `setup` to also include any events added at that time. - @method render - @param {Ember.RenderBuffer} buffer The render buffer + @property events + @type Object */ - render: function(buffer) { - // If this view has a layout, it is the responsibility of the - // the layout to render the view's template. Otherwise, render the template - // directly. - var template = get(this, 'layout') || get(this, 'template'); - - if (template) { - var context = get(this, 'context'); - var keywords = this.cloneKeywords(); - var output; - - var data = { - view: this, - buffer: buffer, - isRenderData: true, - keywords: keywords, - insideGroup: get(this, 'templateData.insideGroup') - }; - - // Invoke the template with the provided template context, which - // is the view's controller by default. A hash of data is also passed that provides - // the template with access to the view and render buffer. - - Ember.assert('template must be a function. Did you mean to call Ember.Handlebars.compile("...") or specify templateName instead?', typeof template === 'function'); - // The template should write directly to the render buffer instead - // of returning a string. - output = template(context, { data: data }); - - // If the template returned a string instead of writing to the buffer, - // push the string onto the buffer. - if (output !== undefined) { buffer.push(output); } - } + events: { + touchstart : 'touchStart', + touchmove : 'touchMove', + touchend : 'touchEnd', + touchcancel : 'touchCancel', + keydown : 'keyDown', + keyup : 'keyUp', + keypress : 'keyPress', + mousedown : 'mouseDown', + mouseup : 'mouseUp', + contextmenu : 'contextMenu', + click : 'click', + dblclick : 'doubleClick', + mousemove : 'mouseMove', + focusin : 'focusIn', + focusout : 'focusOut', + mouseenter : 'mouseEnter', + mouseleave : 'mouseLeave', + submit : 'submit', + input : 'input', + change : 'change', + dragstart : 'dragStart', + drag : 'drag', + dragenter : 'dragEnter', + dragleave : 'dragLeave', + dragover : 'dragOver', + drop : 'drop', + dragend : 'dragEnd' }, /** - Renders the view again. This will work regardless of whether the - view is already in the DOM or not. If the view is in the DOM, the - rendering process will be deferred to give bindings a chance - to synchronize. + The root DOM element to which event listeners should be attached. Event + listeners will be attached to the document unless this is overridden. - If children were added during the rendering process using `appendChild`, - `rerender` will remove them, because they will be added again - if needed by the next `render`. + Can be specified as a DOMElement or a selector string. - In general, if the display of your view changes, you should modify - the DOM element directly instead of manually calling `rerender`, which can - be slow. + The default body is a string since this may be evaluated before document.body + exists in the DOM. - @method rerender + @private + @property rootElement + @type DOMElement + @default 'body' */ - rerender: function() { - return this.currentState.rerender(this); - }, - - clearRenderedChildren: function() { - var lengthBefore = this.lengthBeforeRender, - lengthAfter = this.lengthAfterRender; - - // If there were child views created during the last call to render(), - // remove them under the assumption that they will be re-created when - // we re-render. - - // VIEW-TODO: Unit test this path. - var childViews = this._childViews; - for (var i=lengthAfter-1; i>=lengthBefore; i--) { - if (childViews[i]) { childViews[i].destroy(); } - } - }, + rootElement: 'body', /** - @private + Sets up event listeners for standard browser events. - Iterates over the view's `classNameBindings` array, inserts the value - of the specified property into the `classNames` array, then creates an - observer to update the view's element if the bound property ever changes - in the future. + This will be called after the browser sends a `DOMContentReady` event. By + default, it will set up all of the listeners on the document body. If you + would like to register the listeners on a different element, set the event + dispatcher's `root` property. - @method _applyClassNameBindings + @private + @method setup + @param addedEvents {Hash} */ - _applyClassNameBindings: function(classBindings) { - var classNames = this.classNames, - elem, newClass, dasherizedClass; + setup: function(addedEvents, rootElement) { + var event, events = get(this, 'events'); - // Loop through all of the configured bindings. These will be either - // property names ('isUrgent') or property paths relative to the view - // ('content.isUrgent') - a_forEach(classBindings, function(binding) { + Ember.$.extend(events, addedEvents || {}); - // Variable in which the old class value is saved. The observer function - // closes over this variable, so it knows which string to remove when - // the property changes. - var oldClass; - // Extract just the property name from bindings like 'foo:bar' - var parsedPath = Ember.View._parsePropertyPath(binding); - // Set up an observer on the context. If the property changes, toggle the - // class name. - var observer = function() { - // Get the current value of the property - newClass = this._classStringForProperty(binding); - elem = this.$(); + if (!Ember.isNone(rootElement)) { + set(this, 'rootElement', rootElement); + } - // If we had previously added a class to the element, remove it. - if (oldClass) { - elem.removeClass(oldClass); - // Also remove from classNames so that if the view gets rerendered, - // the class doesn't get added back to the DOM. - classNames.removeObject(oldClass); - } + rootElement = Ember.$(get(this, 'rootElement')); - // If necessary, add a new class. Make sure we keep track of it so - // it can be removed in the future. - if (newClass) { - elem.addClass(newClass); - oldClass = newClass; - } else { - oldClass = null; - } - }; + Ember.assert(fmt('You cannot use the same root element (%@) multiple times in an Ember.Application', [rootElement.selector || rootElement[0].tagName]), !rootElement.is('.ember-application')); + Ember.assert('You cannot make a new Ember.Application using a root element that is a descendent of an existing Ember.Application', !rootElement.closest('.ember-application').length); + Ember.assert('You cannot make a new Ember.Application using a root element that is an ancestor of an existing Ember.Application', !rootElement.find('.ember-application').length); - // Get the class name for the property at its current value - dasherizedClass = this._classStringForProperty(binding); + rootElement.addClass('ember-application'); - if (dasherizedClass) { - // Ensure that it gets into the classNames array - // so it is displayed when we render. - a_addObject(classNames, dasherizedClass); + Ember.assert('Unable to add "ember-application" class to rootElement. Make sure you set rootElement to the body or an element in the body.', rootElement.is('.ember-application')); - // Save a reference to the class name so we can remove it - // if the observer fires. Remember that this variable has - // been closed over by the observer. - oldClass = dasherizedClass; + for (event in events) { + if (events.hasOwnProperty(event)) { + this.setupHandler(rootElement, event, events[event]); } - - this.registerObserver(this, parsedPath.path, observer); - // Remove className so when the view is rerendered, - // the className is added based on binding reevaluation - this.one('willClearRender', function() { - if (oldClass) { - classNames.removeObject(oldClass); - oldClass = null; - } - }); - - }, this); + } }, /** - @private + Registers an event listener on the document. If the given event is + triggered, the provided event handler will be triggered on the target view. - Iterates through the view's attribute bindings, sets up observers for each, - then applies the current value of the attributes to the passed render buffer. + If the target view does not implement the event handler, or if the handler + returns `false`, the parent view will be called. The event will continue to + bubble to each successive parent view until it reaches the top. - @method _applyAttributeBindings - @param {Ember.RenderBuffer} buffer - */ - _applyAttributeBindings: function(buffer, attributeBindings) { - var attributeValue, elem; + For example, to have the `mouseDown` method called on the target view when + a `mousedown` event is received from the browser, do the following: - a_forEach(attributeBindings, function(binding) { - var split = binding.split(':'), - property = split[0], - attributeName = split[1] || property; + ```javascript + setupHandler('mousedown', 'mouseDown'); + ``` - // Create an observer to add/remove/change the attribute if the - // JavaScript property changes. - var observer = function() { - elem = this.$(); + @private + @method setupHandler + @param {Element} rootElement + @param {String} event the browser-originated event to listen to + @param {String} eventName the name of the method to call on the view + */ + setupHandler: function(rootElement, event, eventName) { + var self = this; - attributeValue = get(this, property); + rootElement.on(event + '.ember', '.ember-view', function(evt, triggeringManager) { + var view = Ember.View.views[this.id], + result = true, manager = null; - Ember.View.applyAttributeBindings(elem, attributeName, attributeValue); - }; + manager = self._findNearestEventManager(view, eventName); - this.registerObserver(this, property, observer); + if (manager && manager !== triggeringManager) { + result = self._dispatchEvent(manager, evt, eventName, view); + } else if (view) { + result = self._bubbleEvent(view, evt, eventName); + } else { + evt.stopPropagation(); + } - // Determine the current value and add it to the render buffer - // if necessary. - attributeValue = get(this, property); - Ember.View.applyAttributeBindings(buffer, attributeName, attributeValue); - }, this); - }, + return result; + }); - /** - @private + rootElement.on(event + '.ember', '[data-ember-action]', function(evt) { + var actionId = Ember.$(evt.currentTarget).attr('data-ember-action'), + action = Ember.Handlebars.ActionHelper.registeredActions[actionId]; - Given a property name, returns a dasherized version of that - property name if the property evaluates to a non-falsy value. + // We have to check for action here since in some cases, jQuery will trigger + // an event on `removeChild` (i.e. focusout) after we've already torn down the + // action handlers for the view. + if (action && action.eventName === eventName) { + return action.handler(evt); + } + }); + }, - For example, if the view has property `isUrgent` that evaluates to true, - passing `isUrgent` to this method will return `"is-urgent"`. + _findNearestEventManager: function(view, eventName) { + var manager = null; - @method _classStringForProperty - @param property - */ - _classStringForProperty: function(property) { - var parsedPath = Ember.View._parsePropertyPath(property); - var path = parsedPath.path; + while (view) { + manager = get(view, 'eventManager'); + if (manager && manager[eventName]) { break; } - var val = get(this, path); - if (val === undefined && Ember.isGlobalPath(path)) { - val = get(Ember.lookup, path); + view = get(view, 'parentView'); } - return Ember.View._classStringForValue(path, val, parsedPath.className, parsedPath.falsyClassName); + return manager; }, - // .......................................................... - // ELEMENT SUPPORT - // - - /** - Returns the current DOM element for the view. + _dispatchEvent: function(object, evt, eventName, view) { + var result = true; - @property element - @type DOMElement - */ - element: Ember.computed(function(key, value) { - if (value !== undefined) { - return this.currentState.setElement(this, value); - } else { - return this.currentState.getElement(this); + var handler = object[eventName]; + if (Ember.typeOf(handler) === 'function') { + result = Ember.run(object, handler, evt, view); + // Do not preventDefault in eventManagers. + evt.stopPropagation(); + } + else { + result = this._bubbleEvent(view, evt, eventName); } - }).property('_parentView'), - - /** - Returns a jQuery object for this view's element. If you pass in a selector - string, this method will return a jQuery object, using the current element - as its buffer. - For example, calling `view.$('li')` will return a jQuery object containing - all of the `li` elements inside the DOM element of this view. + return result; + }, - @method $ - @param {String} [selector] a jQuery-compatible selector string - @return {jQuery} the jQuery object for the DOM node - */ - $: function(sel) { - return this.currentState.$(this, sel); + _bubbleEvent: function(view, evt, eventName) { + return Ember.run(view, view.handleEvent, eventName, evt); }, - mutateChildViews: function(callback) { - var childViews = this._childViews, - idx = childViews.length, - view; + destroy: function() { + var rootElement = get(this, 'rootElement'); + Ember.$(rootElement).off('.ember', '**').removeClass('ember-application'); + return this._super(); + } +}); - while(--idx >= 0) { - view = childViews[idx]; - callback(this, view, idx); - } +})(); - return this; - }, - forEachChildView: function(callback) { - var childViews = this._childViews; - if (!childViews) { return this; } +(function() { +/** +@module ember +@submodule ember-views +*/ - var len = childViews.length, - view, idx; +// Add a new named queue for rendering views that happens +// after bindings have synced, and a queue for scheduling actions +// that that should occur after view rendering. +var queues = Ember.run.queues, + indexOf = Ember.ArrayPolyfills.indexOf; +queues.splice(indexOf.call(queues, 'actions')+1, 0, 'render', 'afterRender'); - for (idx = 0; idx < len; idx++) { - view = childViews[idx]; - callback(view); - } +})(); - return this; - }, - /** - Appends the view's element to the specified parent element. - If the view does not have an HTML representation yet, `createElement()` - will be called automatically. +(function() { - Note that this method just schedules the view to be appended; the DOM - element will not be appended to the given element until all bindings have - finished synchronizing. +})(); - This is not typically a function that you will need to call directly when - building your application. You might consider using `Ember.ContainerView` - instead. If you do need to use `appendTo`, be sure that the target element - you are providing is associated with an `Ember.Application` and does not - have an ancestor element that is associated with an Ember view. - @method appendTo - @param {String|DOMElement|jQuery} A selector, element, HTML string, or jQuery object - @return {Ember.View} receiver - */ - appendTo: function(target) { - // Schedule the DOM element to be created and appended to the given - // element after bindings have synchronized. - this._insertElementLater(function() { - Ember.assert("You tried to append to (" + target + ") but that isn't in the DOM", Ember.$(target).length > 0); - Ember.assert("You cannot append to an existing Ember.View. Consider using Ember.ContainerView instead.", !Ember.$(target).is('.ember-view') && !Ember.$(target).parents().is('.ember-view')); - this.$().appendTo(target); - }); - return this; - }, +(function() { +var states = {}; - /** - Replaces the content of the specified parent element with this view's - element. If the view does not have an HTML representation yet, - `createElement()` will be called automatically. +/** +@module ember +@submodule ember-views +*/ - Note that this method just schedules the view to be appended; the DOM - element will not be appended to the given element until all bindings have - finished synchronizing +var get = Ember.get, set = Ember.set, + guidFor = Ember.guidFor, + a_forEach = Ember.EnumerableUtils.forEach, + a_addObject = Ember.EnumerableUtils.addObject, + meta = Ember.meta, + defineProperty = Ember.defineProperty; - @method replaceIn - @param {String|DOMElement|jQuery} A selector, element, HTML string, or jQuery object - @return {Ember.View} received - */ - replaceIn: function(target) { - Ember.assert("You tried to replace in (" + target + ") but that isn't in the DOM", Ember.$(target).length > 0); - Ember.assert("You cannot replace an existing Ember.View. Consider using Ember.ContainerView instead.", !Ember.$(target).is('.ember-view') && !Ember.$(target).parents().is('.ember-view')); +function nullViewsBuffer(view) { + view.buffer = null; +} - this._insertElementLater(function() { - Ember.$(target).empty(); - this.$().appendTo(target); - }); +var childViewsProperty = Ember.computed(function() { + var childViews = this._childViews, ret = Ember.A(), view = this; - return this; - }, + a_forEach(childViews, function(view) { + var currentChildViews; + if (view.isVirtual) { + if (currentChildViews = get(view, 'childViews')) { + ret.pushObjects(currentChildViews); + } + } else { + ret.push(view); + } + }); - /** - @private + ret.replace = function (idx, removedCount, addedViews) { + if (view instanceof Ember.ContainerView) { + Ember.deprecate("Manipulating an Ember.ContainerView through its childViews property is deprecated. Please use the ContainerView instance itself as an Ember.MutableArray."); + return view.replace(idx, removedCount, addedViews); + } + throw new Ember.Error("childViews is immutable"); + }; - Schedules a DOM operation to occur during the next render phase. This - ensures that all bindings have finished synchronizing before the view is - rendered. + return ret; +}); - To use, pass a function that performs a DOM operation. +Ember.warn("The VIEW_PRESERVES_CONTEXT flag has been removed and the functionality can no longer be disabled.", Ember.ENV.VIEW_PRESERVES_CONTEXT !== false); - Before your function is called, this view and all child views will receive - the `willInsertElement` event. After your function is invoked, this view - and all of its child views will receive the `didInsertElement` event. +/** + Global hash of shared templates. This will automatically be populated + by the build tools so that you can store your Handlebars templates in + separate files that get loaded into JavaScript at buildtime. - ```javascript - view._insertElementLater(function() { - this.createElement(); - this.$().appendTo('body'); - }); - ``` + @property TEMPLATES + @for Ember + @type Hash +*/ +Ember.TEMPLATES = {}; - @method _insertElementLater - @param {Function} fn the function that inserts the element into the DOM - */ - _insertElementLater: function(fn) { - this._scheduledInsert = Ember.run.scheduleOnce('render', this, '_insertElement', fn); - }, +/** + `Ember.CoreView` is an abstract class that exists to give view-like behavior + to both Ember's main view class `Ember.View` and other classes like + `Ember._SimpleMetamorphView` that don't need the fully functionaltiy of + `Ember.View`. - _insertElement: function (fn) { - this._scheduledInsert = null; - this.currentState.insertElement(this, fn); - }, + Unless you have specific needs for `CoreView`, you will use `Ember.View` + in your applications. - /** - Appends the view's element to the document body. If the view does - not have an HTML representation yet, `createElement()` will be called - automatically. + @class CoreView + @namespace Ember + @extends Ember.Object + @uses Ember.Evented + @uses Ember.ActionHandler +*/ - If your application uses the `rootElement` property, you must append - the view within that element. Rendering views outside of the `rootElement` - is not supported. +Ember.CoreView = Ember.Object.extend(Ember.Evented, Ember.ActionHandler, { + isView: true, - Note that this method just schedules the view to be appended; the DOM - element will not be appended to the document body until all bindings have - finished synchronizing. + states: states, - @method append - @return {Ember.View} receiver - */ - append: function() { - return this.appendTo(document.body); + init: function() { + this._super(); + this.transitionTo('preRender'); + this._isVisible = get(this, 'isVisible'); }, /** - Removes the view's element from the element to which it is attached. + If the view is currently inserted into the DOM of a parent view, this + property will point to the parent of the view. - @method remove - @return {Ember.View} receiver + @property parentView + @type Ember.View + @default null */ - remove: function() { - // What we should really do here is wait until the end of the run loop - // to determine if the element has been re-appended to a different - // element. - // In the interim, we will just re-render if that happens. It is more - // important than elements get garbage collected. - if (!this.removedFromDOM) { this.destroyElement(); } - this.invokeRecursively(function(view) { - if (view.clearRenderedChildren) { view.clearRenderedChildren(); } - }); - }, + parentView: Ember.computed('_parentView', function() { + var parent = this._parentView; - elementId: null, + if (parent && parent.isVirtual) { + return get(parent, 'parentView'); + } else { + return parent; + } + }), - /** - Attempts to discover the element in the parent element. The default - implementation looks for an element with an ID of `elementId` (or the - view's guid if `elementId` is null). You can override this method to - provide your own form of lookup. For example, if you want to discover your - element using a CSS class name instead of an ID. + state: null, - @method findElementInParentElement - @param {DOMElement} parentElement The parent's DOM element - @return {DOMElement} The discovered element - */ - findElementInParentElement: function(parentElem) { - var id = "#" + this.elementId; - return Ember.$(id)[0] || Ember.$(id, parentElem)[0]; + _parentView: null, + + // return the current view, not including virtual views + concreteView: Ember.computed('parentView', function() { + if (!this.isVirtual) { return this; } + else { return get(this, 'parentView'); } + }), + + instrumentName: 'core_view', + + instrumentDetails: function(hash) { + hash.object = this.toString(); }, /** - Creates a DOM representation of the view and all of its - child views by recursively calling the `render()` method. + Invoked by the view system when this view needs to produce an HTML + representation. This method will create a new render buffer, if needed, + then apply any default attributes, such as class names and visibility. + Finally, the `render()` method is invoked, which is responsible for + doing the bulk of the rendering. - After the element has been created, `didInsertElement` will - be called on this view and all of its child views. + You should not need to override this method; instead, implement the + `template` property, or if you need more control, override the `render` + method. - @method createElement - @return {Ember.View} receiver + @method renderToBuffer + @param {Ember.RenderBuffer} buffer the render buffer. If no buffer is + passed, a default buffer, using the current view's `tagName`, will + be used. + @private */ - createElement: function() { - if (get(this, 'element')) { return this; } + renderToBuffer: function(parentBuffer, bufferOperation) { + var name = 'render.' + this.instrumentName, + details = {}; - var buffer = this.renderToBuffer(); - set(this, 'element', buffer.element()); + this.instrumentDetails(details); - return this; + return Ember.instrument(name, details, function instrumentRenderToBuffer() { + return this._renderToBuffer(parentBuffer, bufferOperation); + }, this); }, - /** - Called when a view is going to insert an element into the DOM. + _renderToBuffer: function(parentBuffer, bufferOperation) { + // If this is the top-most view, start a new buffer. Otherwise, + // create a new buffer relative to the original using the + // provided buffer operation (for example, `insertAfter` will + // insert a new buffer after the "parent buffer"). + var tagName = this.tagName; - @event willInsertElement - */ - willInsertElement: Ember.K, + if (tagName === null || tagName === undefined) { + tagName = 'div'; + } - /** - Called when the element of the view has been inserted into the DOM - or after the view was re-rendered. Override this function to do any - set up that requires an element in the document body. + var buffer = this.buffer = parentBuffer && parentBuffer.begin(tagName) || Ember.RenderBuffer(tagName); + this.transitionTo('inBuffer', false); - @event didInsertElement - */ - didInsertElement: Ember.K, + this.beforeRender(buffer); + this.render(buffer); + this.afterRender(buffer); + + return buffer; + }, /** - Called when the view is about to rerender, but before anything has - been torn down. This is a good opportunity to tear down any manual - observers you have installed based on the DOM state + Override the default event firing from `Ember.Evented` to + also call methods with the given name. - @event willClearRender + @method trigger + @param name {String} + @private */ - willClearRender: Ember.K, + trigger: function(name) { + this._super.apply(this, arguments); + var method = this[name]; + if (method) { + var args = [], i, l; + for (i = 1, l = arguments.length; i < l; i++) { + args.push(arguments[i]); + } + return method.apply(this, args); + } + }, - /** - @private + deprecatedSendHandles: function(actionName) { + return !!this[actionName]; + }, - Run this callback on the current view (unless includeSelf is false) and recursively on child views. + deprecatedSend: function(actionName) { + var args = [].slice.call(arguments, 1); + Ember.assert('' + this + " has the action " + actionName + " but it is not a function", typeof this[actionName] === 'function'); + Ember.deprecate('Action handlers implemented directly on views are deprecated in favor of action handlers on an `actions` object ( action: `' + actionName + '` on ' + this + ')', false); + this[actionName].apply(this, args); + return; + }, - @method invokeRecursively - @param fn {Function} - @param includeSelf (optional, default true) - */ - invokeRecursively: function(fn, includeSelf) { - var childViews = (includeSelf === false) ? this._childViews : [this]; - var currentViews, view, currentChildViews; + has: function(name) { + return Ember.typeOf(this[name]) === 'function' || this._super(name); + }, - while (childViews.length) { - currentViews = childViews.slice(); - childViews = []; + destroy: function() { + var parent = this._parentView; - for (var i=0, l=currentViews.length; i + ``` + + ## HTML `class` Attribute + + The HTML `class` attribute of a view's tag can be set by providing a + `classNames` property that is set to an array of strings: + + ```javascript + MyView = Ember.View.extend({ + classNames: ['my-class', 'my-other-class'] + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + `class` attribute values can also be set by providing a `classNameBindings` + property set to an array of properties names for the view. The return value + of these properties will be added as part of the value for the view's `class` + attribute. These properties can be computed properties: + + ```javascript + MyView = Ember.View.extend({ + classNameBindings: ['propertyA', 'propertyB'], + propertyA: 'from-a', + propertyB: function() { + if (someLogic) { return 'from-b'; } + }.property() + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + If the value of a class name binding returns a boolean the property name + itself will be used as the class name if the property is true. The class name + will not be added if the value is `false` or `undefined`. + + ```javascript + MyView = Ember.View.extend({ + classNameBindings: ['hovered'], + hovered: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + When using boolean class name bindings you can supply a string value other + than the property name for use as the `class` HTML attribute by appending the + preferred value after a ":" character when defining the binding: + + ```javascript + MyView = Ember.View.extend({ + classNameBindings: ['awesome:so-very-cool'], + awesome: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + Boolean value class name bindings whose property names are in a + camelCase-style format will be converted to a dasherized format: + + ```javascript + MyView = Ember.View.extend({ + classNameBindings: ['isUrgent'], + isUrgent: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + Class name bindings can also refer to object values that are found by + traversing a path relative to the view itself: + + ```javascript + MyView = Ember.View.extend({ + classNameBindings: ['messages.empty'] + messages: Ember.Object.create({ + empty: true + }) + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + If you want to add a class name for a property which evaluates to true and + and a different class name if it evaluates to false, you can pass a binding + like this: + + ```javascript + // Applies 'enabled' class when isEnabled is true and 'disabled' when isEnabled is false + Ember.View.extend({ + classNameBindings: ['isEnabled:enabled:disabled'] + isEnabled: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + When isEnabled is `false`, the resulting HTML reprensentation looks like + this: + + ```html +
    + ``` + + This syntax offers the convenience to add a class if a property is `false`: + + ```javascript + // Applies no class when isEnabled is true and class 'disabled' when isEnabled is false + Ember.View.extend({ + classNameBindings: ['isEnabled::disabled'] + isEnabled: true + }); + ``` + + Will result in view instances with an HTML representation of: + ```html +
    + ``` + + When the `isEnabled` property on the view is set to `false`, it will result + in view instances with an HTML representation of: + + ```html +
    + ``` + + Updates to the the value of a class name binding will result in automatic + update of the HTML `class` attribute in the view's rendered HTML + representation. If the value becomes `false` or `undefined` the class name + will be removed. + + Both `classNames` and `classNameBindings` are concatenated properties. See + [Ember.Object](/api/classes/Ember.Object.html) documentation for more + information about concatenated properties. + + ## HTML Attributes + + The HTML attribute section of a view's tag can be set by providing an + `attributeBindings` property set to an array of property names on the view. + The return value of these properties will be used as the value of the view's + HTML associated attribute: + + ```javascript + AnchorView = Ember.View.extend({ + tagName: 'a', + attributeBindings: ['href'], + href: 'http://google.com' + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html + + ``` + + If the return value of an `attributeBindings` monitored property is a boolean + the property will follow HTML's pattern of repeating the attribute's name as + its value: + + ```javascript + MyTextInput = Ember.View.extend({ + tagName: 'input', + attributeBindings: ['disabled'], + disabled: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html + + ``` + + `attributeBindings` can refer to computed properties: + + ```javascript + MyTextInput = Ember.View.extend({ + tagName: 'input', + attributeBindings: ['disabled'], + disabled: function() { + if (someLogic) { + return true; + } else { + return false; } + }.property() + }); + ``` + + Updates to the the property of an attribute binding will result in automatic + update of the HTML attribute in the view's rendered HTML representation. + + `attributeBindings` is a concatenated property. See [Ember.Object](/api/classes/Ember.Object.html) + documentation for more information about concatenated properties. + + ## Templates + + The HTML contents of a view's rendered representation are determined by its + template. Templates can be any function that accepts an optional context + parameter and returns a string of HTML that will be inserted within the + view's tag. Most typically in Ember this function will be a compiled + `Ember.Handlebars` template. + + ```javascript + AView = Ember.View.extend({ + template: Ember.Handlebars.compile('I am the template') + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    I am the template
    + ``` + + Within an Ember application is more common to define a Handlebars templates as + part of a page: + + ```html + + ``` + + And associate it by name using a view's `templateName` property: + + ```javascript + AView = Ember.View.extend({ + templateName: 'some-template' + }); + ``` + + If you have nested resources, your Handlebars template will look like this: + + ```html + + ``` + + And `templateName` property: + + ```javascript + AView = Ember.View.extend({ + templateName: 'posts/new' + }); + ``` + + Using a value for `templateName` that does not have a Handlebars template + with a matching `data-template-name` attribute will throw an error. + + For views classes that may have a template later defined (e.g. as the block + portion of a `{{view}}` Handlebars helper call in another template or in + a subclass), you can provide a `defaultTemplate` property set to compiled + template function. If a template is not later provided for the view instance + the `defaultTemplate` value will be used: + + ```javascript + AView = Ember.View.extend({ + defaultTemplate: Ember.Handlebars.compile('I was the default'), + template: null, + templateName: null + }); + ``` + + Will result in instances with an HTML representation of: + + ```html +
    I was the default
    + ``` + + If a `template` or `templateName` is provided it will take precedence over + `defaultTemplate`: + + ```javascript + AView = Ember.View.extend({ + defaultTemplate: Ember.Handlebars.compile('I was the default') + }); + + aView = AView.create({ + template: Ember.Handlebars.compile('I was the template, not default') + }); + ``` + + Will result in the following HTML representation when rendered: + + ```html +
    I was the template, not default
    + ``` + + ## View Context + + The default context of the compiled template is the view's controller: + + ```javascript + AView = Ember.View.extend({ + template: Ember.Handlebars.compile('Hello {{excitedGreeting}}') + }); + + aController = Ember.Object.create({ + firstName: 'Barry', + excitedGreeting: function() { + return this.get("content.firstName") + "!!!" + }.property() + }); + + aView = AView.create({ + controller: aController, + }); + ``` + + Will result in an HTML representation of: + + ```html +
    Hello Barry!!!
    + ``` + + A context can also be explicitly supplied through the view's `context` + property. If the view has neither `context` nor `controller` properties, the + `parentView`'s context will be used. + + ## Layouts + + Views can have a secondary template that wraps their main template. Like + primary templates, layouts can be any function that accepts an optional + context parameter and returns a string of HTML that will be inserted inside + view's tag. Views whose HTML element is self closing (e.g. ``) + cannot have a layout and this property will be ignored. + + Most typically in Ember a layout will be a compiled `Ember.Handlebars` + template. + + A view's layout can be set directly with the `layout` property or reference + an existing Handlebars template by name with the `layoutName` property. + + A template used as a layout must contain a single use of the Handlebars + `{{yield}}` helper. The HTML contents of a view's rendered `template` will be + inserted at this location: + + ```javascript + AViewWithLayout = Ember.View.extend({ + layout: Ember.Handlebars.compile("
    {{yield}}
    ") + template: Ember.Handlebars.compile("I got wrapped"), + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    +
    + I got wrapped +
    +
    + ``` + + See [Ember.Handlebars.helpers.yield](/api/classes/Ember.Handlebars.helpers.html#method_yield) + for more information. + + ## Responding to Browser Events + + Views can respond to user-initiated events in one of three ways: method + implementation, through an event manager, and through `{{action}}` helper use + in their template or layout. + + ### Method Implementation + + Views can respond to user-initiated events by implementing a method that + matches the event name. A `jQuery.Event` object will be passed as the + argument to this method. + + ```javascript + AView = Ember.View.extend({ + click: function(event) { + // will be called when when an instance's + // rendered element is clicked + } + }); + ``` + + ### Event Managers + + Views can define an object as their `eventManager` property. This object can + then implement methods that match the desired event names. Matching events + that occur on the view's rendered HTML or the rendered HTML of any of its DOM + descendants will trigger this method. A `jQuery.Event` object will be passed + as the first argument to the method and an `Ember.View` object as the + second. The `Ember.View` will be the view whose rendered HTML was interacted + with. This may be the view with the `eventManager` property or one of its + descendent views. + + ```javascript + AView = Ember.View.extend({ + eventManager: Ember.Object.create({ + doubleClick: function(event, view) { + // will be called when when an instance's + // rendered element or any rendering + // of this views's descendent + // elements is clicked + } + }) + }); + ``` + + An event defined for an event manager takes precedence over events of the + same name handled through methods on the view. + + ```javascript + AView = Ember.View.extend({ + mouseEnter: function(event) { + // will never trigger. + }, + eventManager: Ember.Object.create({ + mouseEnter: function(event, view) { + // takes precedence over AView#mouseEnter + } + }) + }); + ``` + + Similarly a view's event manager will take precedence for events of any views + rendered as a descendent. A method name that matches an event name will not + be called if the view instance was rendered inside the HTML representation of + a view that has an `eventManager` property defined that handles events of the + name. Events not handled by the event manager will still trigger method calls + on the descendent. + + ```javascript + OuterView = Ember.View.extend({ + template: Ember.Handlebars.compile("outer {{#view InnerView}}inner{{/view}} outer"), + eventManager: Ember.Object.create({ + mouseEnter: function(event, view) { + // view might be instance of either + // OuterView or InnerView depending on + // where on the page the user interaction occured + } + }) + }); + + InnerView = Ember.View.extend({ + click: function(event) { + // will be called if rendered inside + // an OuterView because OuterView's + // eventManager doesn't handle click events + }, + mouseEnter: function(event) { + // will never be called if rendered inside + // an OuterView. + } + }); + ``` + + ### Handlebars `{{action}}` Helper + + See [Handlebars.helpers.action](/api/classes/Ember.Handlebars.helpers.html#method_action). + + ### Event Names + + All of the event handling approaches described above respond to the same set + of events. The names of the built-in events are listed below. (The hash of + built-in events exists in `Ember.EventDispatcher`.) Additional, custom events + can be registered by using `Ember.Application.customEvents`. + + Touch events: + + * `touchStart` + * `touchMove` + * `touchEnd` + * `touchCancel` + + Keyboard events + + * `keyDown` + * `keyUp` + * `keyPress` + + Mouse events + + * `mouseDown` + * `mouseUp` + * `contextMenu` + * `click` + * `doubleClick` + * `mouseMove` + * `focusIn` + * `focusOut` + * `mouseEnter` + * `mouseLeave` + + Form events: + + * `submit` + * `change` + * `focusIn` + * `focusOut` + * `input` + + HTML5 drag and drop events: + + * `dragStart` + * `drag` + * `dragEnter` + * `dragLeave` + * `drop` + * `dragEnd` + + ## Handlebars `{{view}}` Helper + + Other `Ember.View` instances can be included as part of a view's template by + using the `{{view}}` Handlebars helper. See [Ember.Handlebars.helpers.view](/api/classes/Ember.Handlebars.helpers.html#method_view) + for additional information. + + @class View + @namespace Ember + @extends Ember.CoreView +*/ +Ember.View = Ember.CoreView.extend({ + + concatenatedProperties: ['classNames', 'classNameBindings', 'attributeBindings'], + + /** + @property isView + @type Boolean + @default true + @static + */ + isView: true, + + // .......................................................... + // TEMPLATE SUPPORT + // + + /** + The name of the template to lookup if no template is provided. + + By default `Ember.View` will lookup a template with this name in + `Ember.TEMPLATES` (a shared global object). + + @property templateName + @type String + @default null + */ + templateName: null, + + /** + The name of the layout to lookup if no layout is provided. + + By default `Ember.View` will lookup a template with this name in + `Ember.TEMPLATES` (a shared global object). + + @property layoutName + @type String + @default null + */ + layoutName: null, + + /** + The template used to render the view. This should be a function that + accepts an optional context parameter and returns a string of HTML that + will be inserted into the DOM relative to its parent view. + + In general, you should set the `templateName` property instead of setting + the template yourself. + + @property template + @type Function + */ + template: Ember.computed('templateName', function(key, value) { + if (value !== undefined) { return value; } + + var templateName = get(this, 'templateName'), + template = this.templateForName(templateName, 'template'); + + Ember.assert("You specified the templateName " + templateName + " for " + this + ", but it did not exist.", !templateName || template); + + return template || get(this, 'defaultTemplate'); + }), + + /** + The controller managing this view. If this property is set, it will be + made available for use by the template. + + @property controller + @type Object + */ + controller: Ember.computed('_parentView', function(key) { + var parentView = get(this, '_parentView'); + return parentView ? get(parentView, 'controller') : null; + }), + + /** + A view may contain a layout. A layout is a regular template but + supersedes the `template` property during rendering. It is the + responsibility of the layout template to retrieve the `template` + property from the view (or alternatively, call `Handlebars.helpers.yield`, + `{{yield}}`) to render it in the correct location. + + This is useful for a view that has a shared wrapper, but which delegates + the rendering of the contents of the wrapper to the `template` property + on a subclass. + + @property layout + @type Function + */ + layout: Ember.computed(function(key) { + var layoutName = get(this, 'layoutName'), + layout = this.templateForName(layoutName, 'layout'); + + Ember.assert("You specified the layoutName " + layoutName + " for " + this + ", but it did not exist.", !layoutName || layout); + + return layout || get(this, 'defaultLayout'); + }).property('layoutName'), + + _yield: function(context, options) { + var template = get(this, 'template'); + if (template) { template(context, options); } + }, + + templateForName: function(name, type) { + if (!name) { return; } + Ember.assert("templateNames are not allowed to contain periods: "+name, name.indexOf('.') === -1); + + // the defaultContainer is deprecated + var container = this.container || (Ember.Container && Ember.Container.defaultContainer); + return container && container.lookup('template:' + name); + }, + + /** + The object from which templates should access properties. + + This object will be passed to the template function each time the render + method is called, but it is up to the individual function to decide what + to do with it. + + By default, this will be the view's controller. + + @property context + @type Object + */ + context: Ember.computed(function(key, value) { + if (arguments.length === 2) { + set(this, '_context', value); + return value; + } else { + return get(this, '_context'); + } + }).volatile(), + + /** + Private copy of the view's template context. This can be set directly + by Handlebars without triggering the observer that causes the view + to be re-rendered. + + The context of a view is looked up as follows: + + 1. Supplied context (usually by Handlebars) + 2. Specified controller + 3. `parentView`'s context (for a child of a ContainerView) + + The code in Handlebars that overrides the `_context` property first + checks to see whether the view has a specified controller. This is + something of a hack and should be revisited. + + @property _context + @private + */ + _context: Ember.computed(function(key) { + var parentView, controller; + + if (controller = get(this, 'controller')) { + return controller; + } + + parentView = this._parentView; + if (parentView) { + return get(parentView, '_context'); + } + + return null; + }), + + /** + If a value that affects template rendering changes, the view should be + re-rendered to reflect the new value. + + @method _contextDidChange + @private + */ + _contextDidChange: Ember.observer('context', function() { + this.rerender(); + }), + + /** + If `false`, the view will appear hidden in DOM. + + @property isVisible + @type Boolean + @default null + */ + isVisible: true, + + /** + Array of child views. You should never edit this array directly. + Instead, use `appendChild` and `removeFromParent`. + + @property childViews + @type Array + @default [] + @private + */ + childViews: childViewsProperty, + + _childViews: EMPTY_ARRAY, + + // When it's a virtual view, we need to notify the parent that their + // childViews will change. + _childViewsWillChange: Ember.beforeObserver('childViews', function() { + if (this.isVirtual) { + var parentView = get(this, 'parentView'); + if (parentView) { Ember.propertyWillChange(parentView, 'childViews'); } + } + }), + + // When it's a virtual view, we need to notify the parent that their + // childViews did change. + _childViewsDidChange: Ember.observer('childViews', function() { + if (this.isVirtual) { + var parentView = get(this, 'parentView'); + if (parentView) { Ember.propertyDidChange(parentView, 'childViews'); } + } + }), + + /** + Return the nearest ancestor that is an instance of the provided + class. + + @method nearestInstanceOf + @param {Class} klass Subclass of Ember.View (or Ember.View itself) + @return Ember.View + @deprecated + */ + nearestInstanceOf: function(klass) { + Ember.deprecate("nearestInstanceOf is deprecated and will be removed from future releases. Use nearestOfType."); + var view = get(this, 'parentView'); + + while (view) { + if (view instanceof klass) { return view; } + view = get(view, 'parentView'); + } + }, + + /** + Return the nearest ancestor that is an instance of the provided + class or mixin. + + @method nearestOfType + @param {Class,Mixin} klass Subclass of Ember.View (or Ember.View itself), + or an instance of Ember.Mixin. + @return Ember.View + */ + nearestOfType: function(klass) { + var view = get(this, 'parentView'), + isOfType = klass instanceof Ember.Mixin ? + function(view) { return klass.detect(view); } : + function(view) { return klass.detect(view.constructor); }; + + while (view) { + if (isOfType(view)) { return view; } + view = get(view, 'parentView'); + } + }, + + /** + Return the nearest ancestor that has a given property. + + @function nearestWithProperty + @param {String} property A property name + @return Ember.View + */ + nearestWithProperty: function(property) { + var view = get(this, 'parentView'); + + while (view) { + if (property in view) { return view; } + view = get(view, 'parentView'); + } + }, + + /** + Return the nearest ancestor whose parent is an instance of + `klass`. + + @method nearestChildOf + @param {Class} klass Subclass of Ember.View (or Ember.View itself) + @return Ember.View + */ + nearestChildOf: function(klass) { + var view = get(this, 'parentView'); + + while (view) { + if (get(view, 'parentView') instanceof klass) { return view; } + view = get(view, 'parentView'); + } + }, + + /** + When the parent view changes, recursively invalidate `controller` + + @method _parentViewDidChange + @private + */ + _parentViewDidChange: Ember.observer('_parentView', function() { + if (this.isDestroying) { return; } + + this.trigger('parentViewDidChange'); + + if (get(this, 'parentView.controller') && !get(this, 'controller')) { + this.notifyPropertyChange('controller'); + } + }), + + _controllerDidChange: Ember.observer('controller', function() { + if (this.isDestroying) { return; } + + this.rerender(); + + this.forEachChildView(function(view) { + view.propertyDidChange('controller'); + }); + }), + + cloneKeywords: function() { + var templateData = get(this, 'templateData'); + + var keywords = templateData ? Ember.copy(templateData.keywords) : {}; + set(keywords, 'view', get(this, 'concreteView')); + set(keywords, '_view', this); + set(keywords, 'controller', get(this, 'controller')); + + return keywords; + }, + + /** + Called on your view when it should push strings of HTML into a + `Ember.RenderBuffer`. Most users will want to override the `template` + or `templateName` properties instead of this method. + + By default, `Ember.View` will look for a function in the `template` + property and invoke it with the value of `context`. The value of + `context` will be the view's controller unless you override it. + + @method render + @param {Ember.RenderBuffer} buffer The render buffer + */ + render: function(buffer) { + // If this view has a layout, it is the responsibility of the + // the layout to render the view's template. Otherwise, render the template + // directly. + var template = get(this, 'layout') || get(this, 'template'); + + if (template) { + var context = get(this, 'context'); + var keywords = this.cloneKeywords(); + var output; + + var data = { + view: this, + buffer: buffer, + isRenderData: true, + keywords: keywords, + insideGroup: get(this, 'templateData.insideGroup') + }; + + // Invoke the template with the provided template context, which + // is the view's controller by default. A hash of data is also passed that provides + // the template with access to the view and render buffer. + + Ember.assert('template must be a function. Did you mean to call Ember.Handlebars.compile("...") or specify templateName instead?', typeof template === 'function'); + // The template should write directly to the render buffer instead + // of returning a string. + output = template(context, { data: data }); + + // If the template returned a string instead of writing to the buffer, + // push the string onto the buffer. + if (output !== undefined) { buffer.push(output); } + } + }, + + /** + Renders the view again. This will work regardless of whether the + view is already in the DOM or not. If the view is in the DOM, the + rendering process will be deferred to give bindings a chance + to synchronize. + + If children were added during the rendering process using `appendChild`, + `rerender` will remove them, because they will be added again + if needed by the next `render`. + + In general, if the display of your view changes, you should modify + the DOM element directly instead of manually calling `rerender`, which can + be slow. + + @method rerender + */ + rerender: function() { + return this.currentState.rerender(this); + }, + + clearRenderedChildren: function() { + var lengthBefore = this.lengthBeforeRender, + lengthAfter = this.lengthAfterRender; + + // If there were child views created during the last call to render(), + // remove them under the assumption that they will be re-created when + // we re-render. + + // VIEW-TODO: Unit test this path. + var childViews = this._childViews; + for (var i=lengthAfter-1; i>=lengthBefore; i--) { + if (childViews[i]) { childViews[i].destroy(); } + } + }, + + /** + Iterates over the view's `classNameBindings` array, inserts the value + of the specified property into the `classNames` array, then creates an + observer to update the view's element if the bound property ever changes + in the future. + + @method _applyClassNameBindings + @private + */ + _applyClassNameBindings: function(classBindings) { + var classNames = this.classNames, + elem, newClass, dasherizedClass; + + // Loop through all of the configured bindings. These will be either + // property names ('isUrgent') or property paths relative to the view + // ('content.isUrgent') + a_forEach(classBindings, function(binding) { + + Ember.assert("classNameBindings must not have spaces in them. Multiple class name bindings can be provided as elements of an array, e.g. ['foo', ':bar']", binding.indexOf(' ') === -1); + + // Variable in which the old class value is saved. The observer function + // closes over this variable, so it knows which string to remove when + // the property changes. + var oldClass; + // Extract just the property name from bindings like 'foo:bar' + var parsedPath = Ember.View._parsePropertyPath(binding); + + // Set up an observer on the context. If the property changes, toggle the + // class name. + var observer = function() { + // Get the current value of the property + newClass = this._classStringForProperty(binding); + elem = this.$(); + + // If we had previously added a class to the element, remove it. + if (oldClass) { + elem.removeClass(oldClass); + // Also remove from classNames so that if the view gets rerendered, + // the class doesn't get added back to the DOM. + classNames.removeObject(oldClass); + } + + // If necessary, add a new class. Make sure we keep track of it so + // it can be removed in the future. + if (newClass) { + elem.addClass(newClass); + oldClass = newClass; + } else { + oldClass = null; + } + }; + + // Get the class name for the property at its current value + dasherizedClass = this._classStringForProperty(binding); + + if (dasherizedClass) { + // Ensure that it gets into the classNames array + // so it is displayed when we render. + a_addObject(classNames, dasherizedClass); + + // Save a reference to the class name so we can remove it + // if the observer fires. Remember that this variable has + // been closed over by the observer. + oldClass = dasherizedClass; + } + + this.registerObserver(this, parsedPath.path, observer); + // Remove className so when the view is rerendered, + // the className is added based on binding reevaluation + this.one('willClearRender', function() { + if (oldClass) { + classNames.removeObject(oldClass); + oldClass = null; + } + }); + + }, this); + }, + + _unspecifiedAttributeBindings: null, + + /** + Iterates through the view's attribute bindings, sets up observers for each, + then applies the current value of the attributes to the passed render buffer. + + @method _applyAttributeBindings + @param {Ember.RenderBuffer} buffer + @private + */ + _applyAttributeBindings: function(buffer, attributeBindings) { + var attributeValue, + unspecifiedAttributeBindings = this._unspecifiedAttributeBindings = this._unspecifiedAttributeBindings || {}; + + a_forEach(attributeBindings, function(binding) { + var split = binding.split(':'), + property = split[0], + attributeName = split[1] || property; + + if (property in this) { + this._setupAttributeBindingObservation(property, attributeName); + + // Determine the current value and add it to the render buffer + // if necessary. + attributeValue = get(this, property); + Ember.View.applyAttributeBindings(buffer, attributeName, attributeValue); + } else { + unspecifiedAttributeBindings[property] = attributeName; + } + }, this); + + // Lazily setup setUnknownProperty after attributeBindings are initially applied + this.setUnknownProperty = this._setUnknownProperty; + }, + + _setupAttributeBindingObservation: function(property, attributeName) { + var attributeValue, elem; + + // Create an observer to add/remove/change the attribute if the + // JavaScript property changes. + var observer = function() { + elem = this.$(); + + attributeValue = get(this, property); + + Ember.View.applyAttributeBindings(elem, attributeName, attributeValue); + }; + + this.registerObserver(this, property, observer); + }, + + /** + We're using setUnknownProperty as a hook to setup attributeBinding observers for + properties that aren't defined on a view at initialization time. + + Note: setUnknownProperty will only be called once for each property. + + @method setUnknownProperty + @param key + @param value + @private + */ + setUnknownProperty: null, // Gets defined after initialization by _applyAttributeBindings + + _setUnknownProperty: function(key, value) { + var attributeName = this._unspecifiedAttributeBindings && this._unspecifiedAttributeBindings[key]; + if (attributeName) { + this._setupAttributeBindingObservation(key, attributeName); + } + + defineProperty(this, key); + return set(this, key, value); + }, + + /** + Given a property name, returns a dasherized version of that + property name if the property evaluates to a non-falsy value. + + For example, if the view has property `isUrgent` that evaluates to true, + passing `isUrgent` to this method will return `"is-urgent"`. + + @method _classStringForProperty + @param property + @private + */ + _classStringForProperty: function(property) { + var parsedPath = Ember.View._parsePropertyPath(property); + var path = parsedPath.path; + + var val = get(this, path); + if (val === undefined && Ember.isGlobalPath(path)) { + val = get(Ember.lookup, path); + } + + return Ember.View._classStringForValue(path, val, parsedPath.className, parsedPath.falsyClassName); + }, + + // .......................................................... + // ELEMENT SUPPORT + // + + /** + Returns the current DOM element for the view. + + @property element + @type DOMElement + */ + element: Ember.computed('_parentView', function(key, value) { + if (value !== undefined) { + return this.currentState.setElement(this, value); + } else { + return this.currentState.getElement(this); + } + }), + + /** + Returns a jQuery object for this view's element. If you pass in a selector + string, this method will return a jQuery object, using the current element + as its buffer. + + For example, calling `view.$('li')` will return a jQuery object containing + all of the `li` elements inside the DOM element of this view. + + @method $ + @param {String} [selector] a jQuery-compatible selector string + @return {jQuery} the jQuery object for the DOM node + */ + $: function(sel) { + return this.currentState.$(this, sel); + }, + + mutateChildViews: function(callback) { + var childViews = this._childViews, + idx = childViews.length, + view; + + while(--idx >= 0) { + view = childViews[idx]; + callback(this, view, idx); + } + + return this; + }, + + forEachChildView: function(callback) { + var childViews = this._childViews; + + if (!childViews) { return this; } + + var len = childViews.length, + view, idx; + + for (idx = 0; idx < len; idx++) { + view = childViews[idx]; + callback(view); + } + + return this; + }, + + /** + Appends the view's element to the specified parent element. + + If the view does not have an HTML representation yet, `createElement()` + will be called automatically. + + Note that this method just schedules the view to be appended; the DOM + element will not be appended to the given element until all bindings have + finished synchronizing. + + This is not typically a function that you will need to call directly when + building your application. You might consider using `Ember.ContainerView` + instead. If you do need to use `appendTo`, be sure that the target element + you are providing is associated with an `Ember.Application` and does not + have an ancestor element that is associated with an Ember view. + + @method appendTo + @param {String|DOMElement|jQuery} A selector, element, HTML string, or jQuery object + @return {Ember.View} receiver + */ + appendTo: function(target) { + // Schedule the DOM element to be created and appended to the given + // element after bindings have synchronized. + this._insertElementLater(function() { + Ember.assert("You tried to append to (" + target + ") but that isn't in the DOM", Ember.$(target).length > 0); + Ember.assert("You cannot append to an existing Ember.View. Consider using Ember.ContainerView instead.", !Ember.$(target).is('.ember-view') && !Ember.$(target).parents().is('.ember-view')); + this.$().appendTo(target); + }); + + return this; + }, + + /** + Replaces the content of the specified parent element with this view's + element. If the view does not have an HTML representation yet, + `createElement()` will be called automatically. + + Note that this method just schedules the view to be appended; the DOM + element will not be appended to the given element until all bindings have + finished synchronizing + + @method replaceIn + @param {String|DOMElement|jQuery} target A selector, element, HTML string, or jQuery object + @return {Ember.View} received + */ + replaceIn: function(target) { + Ember.assert("You tried to replace in (" + target + ") but that isn't in the DOM", Ember.$(target).length > 0); + Ember.assert("You cannot replace an existing Ember.View. Consider using Ember.ContainerView instead.", !Ember.$(target).is('.ember-view') && !Ember.$(target).parents().is('.ember-view')); + + this._insertElementLater(function() { + Ember.$(target).empty(); + this.$().appendTo(target); + }); + + return this; + }, + + /** + Schedules a DOM operation to occur during the next render phase. This + ensures that all bindings have finished synchronizing before the view is + rendered. + + To use, pass a function that performs a DOM operation. + + Before your function is called, this view and all child views will receive + the `willInsertElement` event. After your function is invoked, this view + and all of its child views will receive the `didInsertElement` event. + + ```javascript + view._insertElementLater(function() { + this.createElement(); + this.$().appendTo('body'); + }); + ``` + + @method _insertElementLater + @param {Function} fn the function that inserts the element into the DOM + @private + */ + _insertElementLater: function(fn) { + this._scheduledInsert = Ember.run.scheduleOnce('render', this, '_insertElement', fn); + }, + + _insertElement: function (fn) { + this._scheduledInsert = null; + this.currentState.insertElement(this, fn); + }, + + /** + Appends the view's element to the document body. If the view does + not have an HTML representation yet, `createElement()` will be called + automatically. + + If your application uses the `rootElement` property, you must append + the view within that element. Rendering views outside of the `rootElement` + is not supported. + + Note that this method just schedules the view to be appended; the DOM + element will not be appended to the document body until all bindings have + finished synchronizing. + + @method append + @return {Ember.View} receiver + */ + append: function() { + return this.appendTo(document.body); + }, + + /** + Removes the view's element from the element to which it is attached. + + @method remove + @return {Ember.View} receiver + */ + remove: function() { + // What we should really do here is wait until the end of the run loop + // to determine if the element has been re-appended to a different + // element. + // In the interim, we will just re-render if that happens. It is more + // important than elements get garbage collected. + if (!this.removedFromDOM) { this.destroyElement(); } + this.invokeRecursively(function(view) { + if (view.clearRenderedChildren) { view.clearRenderedChildren(); } + }); + }, + + elementId: null, + + /** + Attempts to discover the element in the parent element. The default + implementation looks for an element with an ID of `elementId` (or the + view's guid if `elementId` is null). You can override this method to + provide your own form of lookup. For example, if you want to discover your + element using a CSS class name instead of an ID. + + @method findElementInParentElement + @param {DOMElement} parentElement The parent's DOM element + @return {DOMElement} The discovered element + */ + findElementInParentElement: function(parentElem) { + var id = "#" + this.elementId; + return Ember.$(id)[0] || Ember.$(id, parentElem)[0]; + }, + + /** + Creates a DOM representation of the view and all of its + child views by recursively calling the `render()` method. + + After the element has been created, `didInsertElement` will + be called on this view and all of its child views. + + @method createElement + @return {Ember.View} receiver + */ + createElement: function() { + if (get(this, 'element')) { return this; } + + var buffer = this.renderToBuffer(); + set(this, 'element', buffer.element()); + + return this; + }, + + /** + Called when a view is going to insert an element into the DOM. + + @event willInsertElement + */ + willInsertElement: Ember.K, + + /** + Called when the element of the view has been inserted into the DOM + or after the view was re-rendered. Override this function to do any + set up that requires an element in the document body. + + @event didInsertElement + */ + didInsertElement: Ember.K, + + /** + Called when the view is about to rerender, but before anything has + been torn down. This is a good opportunity to tear down any manual + observers you have installed based on the DOM state + + @event willClearRender + */ + willClearRender: Ember.K, + + /** + Run this callback on the current view (unless includeSelf is false) and recursively on child views. + + @method invokeRecursively + @param fn {Function} + @param includeSelf {Boolean} Includes itself if true. + @private + */ + invokeRecursively: function(fn, includeSelf) { + var childViews = (includeSelf === false) ? this._childViews : [this]; + var currentViews, view, currentChildViews; + + while (childViews.length) { + currentViews = childViews.slice(); + childViews = []; + + for (var i=0, l=currentViews.length; i` tag for views. + + @property tagName + @type String + @default null + */ + + // We leave this null by default so we can tell the difference between + // the default case and a user-specified tag. + tagName: null, + + /** + The WAI-ARIA role of the control represented by this view. For example, a + button may have a role of type 'button', or a pane may have a role of + type 'alertdialog'. This property is used by assistive software to help + visually challenged users navigate rich web applications. + + The full list of valid WAI-ARIA roles is available at: + [http://www.w3.org/TR/wai-aria/roles#roles_categorization](http://www.w3.org/TR/wai-aria/roles#roles_categorization) + + @property ariaRole + @type String + @default null + */ + ariaRole: null, + + /** + Standard CSS class names to apply to the view's outer element. This + property automatically inherits any class names defined by the view's + superclasses as well. + + @property classNames + @type Array + @default ['ember-view'] + */ + classNames: ['ember-view'], + + /** + A list of properties of the view to apply as class names. If the property + is a string value, the value of that string will be applied as a class + name. + + ```javascript + // Applies the 'high' class to the view element + Ember.View.extend({ + classNameBindings: ['priority'] + priority: 'high' + }); + ``` + + If the value of the property is a Boolean, the name of that property is + added as a dasherized class name. + + ```javascript + // Applies the 'is-urgent' class to the view element + Ember.View.extend({ + classNameBindings: ['isUrgent'] + isUrgent: true + }); + ``` + + If you would prefer to use a custom value instead of the dasherized + property name, you can pass a binding like this: + + ```javascript + // Applies the 'urgent' class to the view element + Ember.View.extend({ + classNameBindings: ['isUrgent:urgent'] + isUrgent: true + }); + ``` + + This list of properties is inherited from the view's superclasses as well. + + @property classNameBindings + @type Array + @default [] + */ + classNameBindings: EMPTY_ARRAY, + + /** + A list of properties of the view to apply as attributes. If the property is + a string value, the value of that string will be applied as the attribute. + + ```javascript + // Applies the type attribute to the element + // with the value "button", like
    + Ember.View.extend({ + attributeBindings: ['type'], + type: 'button' + }); + ``` + + If the value of the property is a Boolean, the name of that property is + added as an attribute. + + ```javascript + // Renders something like
    + Ember.View.extend({ + attributeBindings: ['enabled'], + enabled: true + }); + ``` + + @property attributeBindings + */ + attributeBindings: EMPTY_ARRAY, + + // ....................................................... + // CORE DISPLAY METHODS + // + + /** + Setup a view, but do not finish waking it up. + + * configure `childViews` + * register the view with the global views hash, which is used for event + dispatch + + @method init + @private + */ + init: function() { + this.elementId = this.elementId || guidFor(this); + + this._super(); + + // setup child views. be sure to clone the child views array first + this._childViews = this._childViews.slice(); + + Ember.assert("Only arrays are allowed for 'classNameBindings'", Ember.typeOf(this.classNameBindings) === 'array'); + this.classNameBindings = Ember.A(this.classNameBindings.slice()); + + Ember.assert("Only arrays are allowed for 'classNames'", Ember.typeOf(this.classNames) === 'array'); + this.classNames = Ember.A(this.classNames.slice()); + }, + + appendChild: function(view, options) { + return this.currentState.appendChild(this, view, options); + }, + + /** + Removes the child view from the parent view. + + @method removeChild + @param {Ember.View} view + @return {Ember.View} receiver + */ + removeChild: function(view) { + // If we're destroying, the entire subtree will be + // freed, and the DOM will be handled separately, + // so no need to mess with childViews. + if (this.isDestroying) { return; } + + // update parent node + set(view, '_parentView', null); + + // remove view from childViews array. + var childViews = this._childViews; + + Ember.EnumerableUtils.removeObject(childViews, view); + + this.propertyDidChange('childViews'); // HUH?! what happened to will change? + + return this; + }, + + /** + Removes all children from the `parentView`. + + @method removeAllChildren + @return {Ember.View} receiver + */ + removeAllChildren: function() { + return this.mutateChildViews(function(parentView, view) { + parentView.removeChild(view); + }); + }, + + destroyAllChildren: function() { + return this.mutateChildViews(function(parentView, view) { + view.destroy(); + }); + }, + + /** + Removes the view from its `parentView`, if one is found. Otherwise + does nothing. + + @method removeFromParent + @return {Ember.View} receiver + */ + removeFromParent: function() { + var parent = this._parentView; + + // Remove DOM element from parent + this.remove(); + + if (parent) { parent.removeChild(this); } + return this; + }, + + /** + You must call `destroy` on a view to destroy the view (and all of its + child views). This will remove the view from any parent node, then make + sure that the DOM element managed by the view can be released by the + memory manager. + + @method destroy + */ + destroy: function() { + var childViews = this._childViews, + // get parentView before calling super because it'll be destroyed + nonVirtualParentView = get(this, 'parentView'), + viewName = this.viewName, + childLen, i; + + if (!this._super()) { return; } + + childLen = childViews.length; + for (i=childLen-1; i>=0; i--) { + childViews[i].removedFromDOM = true; + } + + // remove from non-virtual parent view if viewName was specified + if (viewName && nonVirtualParentView) { + nonVirtualParentView.set(viewName, null); + } + + childLen = childViews.length; + for (i=childLen-1; i>=0; i--) { + childViews[i].destroy(); + } + + return this; + }, + + /** + Instantiates a view to be added to the childViews array during view + initialization. You generally will not call this method directly unless + you are overriding `createChildViews()`. Note that this method will + automatically configure the correct settings on the new view instance to + act as a child of the parent. + + @method createChildView + @param {Class|String} viewClass + @param {Hash} [attrs] Attributes to add + @return {Ember.View} new instance + */ + createChildView: function(view, attrs) { + if (!view) { + throw new TypeError("createChildViews first argument must exist"); + } + + if (view.isView && view._parentView === this && view.container === this.container) { + return view; + } + + attrs = attrs || {}; + attrs._parentView = this; + + if (Ember.CoreView.detect(view)) { + attrs.templateData = attrs.templateData || get(this, 'templateData'); + + attrs.container = this.container; + view = view.create(attrs); + + // don't set the property on a virtual view, as they are invisible to + // consumers of the view API + if (view.viewName) { + set(get(this, 'concreteView'), view.viewName, view); + } + } else if ('string' === typeof view) { + var fullName = 'view:' + view; + var View = this.container.lookupFactory(fullName); + + Ember.assert("Could not find view: '" + fullName + "'", !!View); + + attrs.templateData = get(this, 'templateData'); + view = View.create(attrs); + } else { + Ember.assert('You must pass instance or subclass of View', view.isView); + attrs.container = this.container; + + if (!get(view, 'templateData')) { + attrs.templateData = get(this, 'templateData'); + } + + Ember.setProperties(view, attrs); + + } + + return view; + }, + + becameVisible: Ember.K, + becameHidden: Ember.K, + + /** + When the view's `isVisible` property changes, toggle the visibility + element of the actual DOM element. + + @method _isVisibleDidChange + @private + */ + _isVisibleDidChange: Ember.observer('isVisible', function() { + if (this._isVisible === get(this, 'isVisible')) { return ; } + Ember.run.scheduleOnce('render', this, this._toggleVisibility); + }), + + _toggleVisibility: function() { + var $el = this.$(); + if (!$el) { return; } + + var isVisible = get(this, 'isVisible'); + + if (this._isVisible === isVisible) { return ; } + + $el.toggle(isVisible); + + this._isVisible = isVisible; + + if (this._isAncestorHidden()) { return; } + + if (isVisible) { + this._notifyBecameVisible(); + } else { + this._notifyBecameHidden(); + } + }, + + _notifyBecameVisible: function() { + this.trigger('becameVisible'); + + this.forEachChildView(function(view) { + var isVisible = get(view, 'isVisible'); + + if (isVisible || isVisible === null) { + view._notifyBecameVisible(); + } + }); + }, + + _notifyBecameHidden: function() { + this.trigger('becameHidden'); + this.forEachChildView(function(view) { + var isVisible = get(view, 'isVisible'); + + if (isVisible || isVisible === null) { + view._notifyBecameHidden(); + } + }); + }, + + _isAncestorHidden: function() { + var parent = get(this, 'parentView'); + + while (parent) { + if (get(parent, 'isVisible') === false) { return true; } + + parent = get(parent, 'parentView'); + } + + return false; + }, + + clearBuffer: function() { + this.invokeRecursively(nullViewsBuffer); + }, + + transitionTo: function(state, children) { + var priorState = this.currentState, + currentState = this.currentState = this.states[state]; + this.state = state; + + if (priorState && priorState.exit) { priorState.exit(this); } + if (currentState.enter) { currentState.enter(this); } + if (state === 'inDOM') { delete Ember.meta(this).cache.element; } + + if (children !== false) { + this.forEachChildView(function(view) { + view.transitionTo(state); + }); + } + }, + + // ....................................................... + // EVENT HANDLING + // + + /** + Handle events from `Ember.EventDispatcher` + + @method handleEvent + @param eventName {String} + @param evt {Event} + @private + */ + handleEvent: function(eventName, evt) { + return this.currentState.handleEvent(this, eventName, evt); + }, + + registerObserver: function(root, path, target, observer) { + if (!observer && 'function' === typeof target) { + observer = target; + target = null; + } + + if (!root || typeof root !== 'object') { + return; + } + + var view = this, + stateCheckedObserver = function() { + view.currentState.invokeObserver(this, observer); + }, + scheduledObserver = function() { + Ember.run.scheduleOnce('render', this, stateCheckedObserver); + }; + + Ember.addObserver(root, path, target, scheduledObserver); + + this.one('willClearRender', function() { + Ember.removeObserver(root, path, target, scheduledObserver); + }); + } + +}); + +/* + Describe how the specified actions should behave in the various + states that a view can exist in. Possible states: + + * preRender: when a view is first instantiated, and after its + element was destroyed, it is in the preRender state + * inBuffer: once a view has been rendered, but before it has + been inserted into the DOM, it is in the inBuffer state + * hasElement: the DOM representation of the view is created, + and is ready to be inserted + * inDOM: once a view has been inserted into the DOM it is in + the inDOM state. A view spends the vast majority of its + existence in this state. + * destroyed: once a view has been destroyed (using the destroy + method), it is in this state. No further actions can be invoked + on a destroyed view. +*/ + + // in the destroyed state, everything is illegal + + // before rendering has begun, all legal manipulations are noops. + + // inside the buffer, legal manipulations are done on the buffer + + // once the view has been inserted into the DOM, legal manipulations + // are done on the DOM element. + +function notifyMutationListeners() { + Ember.run.once(Ember.View, 'notifyMutationListeners'); +} + +var DOMManager = { + prepend: function(view, html) { + view.$().prepend(html); + notifyMutationListeners(); + }, + + after: function(view, html) { + view.$().after(html); + notifyMutationListeners(); + }, + + html: function(view, html) { + view.$().html(html); + notifyMutationListeners(); + }, + + replace: function(view) { + var element = get(view, 'element'); + + set(view, 'element', null); + + view._insertElementLater(function() { + Ember.$(element).replaceWith(get(view, 'element')); + notifyMutationListeners(); + }); + }, + + remove: function(view) { + view.$().remove(); + notifyMutationListeners(); + }, + + empty: function(view) { + view.$().empty(); + notifyMutationListeners(); + } +}; + +Ember.View.reopen({ + domManager: DOMManager +}); + +Ember.View.reopenClass({ + + /** + Parse a path and return an object which holds the parsed properties. + + For example a path like "content.isEnabled:enabled:disabled" will return the + following object: + + ```javascript + { + path: "content.isEnabled", + className: "enabled", + falsyClassName: "disabled", + classNames: ":enabled:disabled" + } + ``` + + @method _parsePropertyPath + @static + @private + */ + _parsePropertyPath: function(path) { + var split = path.split(':'), + propertyPath = split[0], + classNames = "", + className, + falsyClassName; + + // check if the property is defined as prop:class or prop:trueClass:falseClass + if (split.length > 1) { + className = split[1]; + if (split.length === 3) { falsyClassName = split[2]; } + + classNames = ':' + className; + if (falsyClassName) { classNames += ":" + falsyClassName; } + } + + return { + path: propertyPath, + classNames: classNames, + className: (className === '') ? undefined : className, + falsyClassName: falsyClassName + }; + }, + + /** + Get the class name for a given value, based on the path, optional + `className` and optional `falsyClassName`. + + - if a `className` or `falsyClassName` has been specified: + - if the value is truthy and `className` has been specified, + `className` is returned + - if the value is falsy and `falsyClassName` has been specified, + `falsyClassName` is returned + - otherwise `null` is returned + - if the value is `true`, the dasherized last part of the supplied path + is returned + - if the value is not `false`, `undefined` or `null`, the `value` + is returned + - if none of the above rules apply, `null` is returned + + @method _classStringForValue + @param path + @param val + @param className + @param falsyClassName + @static + @private + */ + _classStringForValue: function(path, val, className, falsyClassName) { + // When using the colon syntax, evaluate the truthiness or falsiness + // of the value to determine which className to return + if (className || falsyClassName) { + if (className && !!val) { + return className; + + } else if (falsyClassName && !val) { + return falsyClassName; + + } else { + return null; + } + + // If value is a Boolean and true, return the dasherized property + // name. + } else if (val === true) { + // Normalize property path to be suitable for use + // as a class name. For exaple, content.foo.barBaz + // becomes bar-baz. + var parts = path.split('.'); + return Ember.String.dasherize(parts[parts.length-1]); + + // If the value is not false, undefined, or null, return the current + // value of the property. + } else if (val !== false && val != null) { + return val; + + // Nothing to display. Return null so that the old class is removed + // but no new class is added. + } else { + return null; + } + } +}); + +var mutation = Ember.Object.extend(Ember.Evented).create(); + +Ember.View.addMutationListener = function(callback) { + mutation.on('change', callback); +}; + +Ember.View.removeMutationListener = function(callback) { + mutation.off('change', callback); +}; + +Ember.View.notifyMutationListeners = function() { + mutation.trigger('change'); +}; + +/** + Global views hash + + @property views + @static + @type Hash +*/ +Ember.View.views = {}; + +// If someone overrides the child views computed property when +// defining their class, we want to be able to process the user's +// supplied childViews and then restore the original computed property +// at view initialization time. This happens in Ember.ContainerView's init +// method. +Ember.View.childViewsProperty = childViewsProperty; + +Ember.View.applyAttributeBindings = function(elem, name, value) { + var type = Ember.typeOf(value); + + // if this changes, also change the logic in ember-handlebars/lib/helpers/binding.js + if (name !== 'value' && (type === 'string' || (type === 'number' && !isNaN(value)))) { + if (value !== elem.attr(name)) { + elem.attr(name, value); + } + } else if (name === 'value' || type === 'boolean') { + if (Ember.isNone(value) || value === false) { + // `null`, `undefined` or `false` should remove attribute + elem.removeAttr(name); + elem.prop(name, ''); + } else if (value !== elem.prop(name)) { + // value should always be properties + elem.prop(name, value); + } + } else if (!value) { + elem.removeAttr(name); + } +}; + +Ember.View.states = states; + +})(); + + + +(function() { +/** +@module ember +@submodule ember-views +*/ + +var get = Ember.get, set = Ember.set; + +Ember.View.states._default = { + // appendChild is only legal while rendering the buffer. + appendChild: function() { + throw "You can't use appendChild outside of the rendering process"; + }, + + $: function() { + return undefined; + }, + + getElement: function() { + return null; + }, + + // Handle events from `Ember.EventDispatcher` + handleEvent: function() { + return true; // continue event propagation + }, + + destroyElement: function(view) { + set(view, 'element', null); + if (view._scheduledInsert) { + Ember.run.cancel(view._scheduledInsert); + view._scheduledInsert = null; + } + return view; + }, + + renderToBufferIfNeeded: function () { + return false; + }, + + rerender: Ember.K, + invokeObserver: Ember.K +}; + +})(); + + + +(function() { +/** +@module ember +@submodule ember-views +*/ + +var preRender = Ember.View.states.preRender = Ember.create(Ember.View.states._default); + +Ember.merge(preRender, { + // a view leaves the preRender state once its element has been + // created (createElement). + insertElement: function(view, fn) { + view.createElement(); + var viewCollection = view.viewHierarchyCollection(); + + viewCollection.trigger('willInsertElement'); + + fn.call(view); + + // We transition to `inDOM` if the element exists in the DOM + var element = view.get('element'); + if (document.body.contains(element)) { + viewCollection.transitionTo('inDOM', false); + viewCollection.trigger('didInsertElement'); + } + }, + + renderToBufferIfNeeded: function(view, buffer) { + view.renderToBuffer(buffer); + return true; + }, + + empty: Ember.K, + + setElement: function(view, value) { + if (value !== null) { + view.transitionTo('hasElement'); + } + return value; + } +}); + +})(); + + + +(function() { +/** +@module ember +@submodule ember-views +*/ + +var get = Ember.get, set = Ember.set; + +var inBuffer = Ember.View.states.inBuffer = Ember.create(Ember.View.states._default); + +Ember.merge(inBuffer, { + $: function(view, sel) { + // if we don't have an element yet, someone calling this.$() is + // trying to update an element that isn't in the DOM. Instead, + // rerender the view to allow the render method to reflect the + // changes. + view.rerender(); + return Ember.$(); + }, + + // when a view is rendered in a buffer, rerendering it simply + // replaces the existing buffer with a new one + rerender: function(view) { + throw new Ember.Error("Something you did caused a view to re-render after it rendered but before it was inserted into the DOM."); + }, + + // when a view is rendered in a buffer, appending a child + // view will render that view and append the resulting + // buffer into its buffer. + appendChild: function(view, childView, options) { + var buffer = view.buffer, _childViews = view._childViews; + + childView = view.createChildView(childView, options); + if (!_childViews.length) { _childViews = view._childViews = _childViews.slice(); } + _childViews.push(childView); + + childView.renderToBuffer(buffer); + + view.propertyDidChange('childViews'); + + return childView; + }, + + // when a view is rendered in a buffer, destroying the + // element will simply destroy the buffer and put the + // state back into the preRender state. + destroyElement: function(view) { + view.clearBuffer(); + var viewCollection = view._notifyWillDestroyElement(); + viewCollection.transitionTo('preRender', false); + + return view; + }, + + empty: function() { + Ember.assert("Emptying a view in the inBuffer state is not allowed and " + + "should not happen under normal circumstances. Most likely " + + "there is a bug in your application. This may be due to " + + "excessive property change notifications."); + }, + + renderToBufferIfNeeded: function (view, buffer) { + return false; + }, + + // It should be impossible for a rendered view to be scheduled for + // insertion. + insertElement: function() { + throw "You can't insert an element that has already been rendered"; + }, + + setElement: function(view, value) { + if (value === null) { + view.transitionTo('preRender'); + } else { + view.clearBuffer(); + view.transitionTo('hasElement'); + } + + return value; + }, + + invokeObserver: function(target, observer) { + observer.call(target); + } +}); + + +})(); + + + +(function() { +/** +@module ember +@submodule ember-views +*/ + +var get = Ember.get, set = Ember.set; + +var hasElement = Ember.View.states.hasElement = Ember.create(Ember.View.states._default); + +Ember.merge(hasElement, { + $: function(view, sel) { + var elem = get(view, 'element'); + return sel ? Ember.$(sel, elem) : Ember.$(elem); + }, + + getElement: function(view) { + var parent = get(view, 'parentView'); + if (parent) { parent = get(parent, 'element'); } + if (parent) { return view.findElementInParentElement(parent); } + return Ember.$("#" + get(view, 'elementId'))[0]; + }, + + setElement: function(view, value) { + if (value === null) { + view.transitionTo('preRender'); + } else { + throw "You cannot set an element to a non-null value when the element is already in the DOM."; + } + + return value; + }, + + // once the view has been inserted into the DOM, rerendering is + // deferred to allow bindings to synchronize. + rerender: function(view) { + view.triggerRecursively('willClearRender'); + + view.clearRenderedChildren(); + + view.domManager.replace(view); + return view; + }, + + // once the view is already in the DOM, destroying it removes it + // from the DOM, nukes its element, and puts it back into the + // preRender state if inDOM. + + destroyElement: function(view) { + view._notifyWillDestroyElement(); + view.domManager.remove(view); + set(view, 'element', null); + if (view._scheduledInsert) { + Ember.run.cancel(view._scheduledInsert); + view._scheduledInsert = null; + } + return view; + }, + + empty: function(view) { + var _childViews = view._childViews, len, idx; + if (_childViews) { + len = _childViews.length; + for (idx = 0; idx < len; idx++) { + _childViews[idx]._notifyWillDestroyElement(); + } + } + view.domManager.empty(view); + }, + + // Handle events from `Ember.EventDispatcher` + handleEvent: function(view, eventName, evt) { + if (view.has(eventName)) { + // Handler should be able to re-dispatch events, so we don't + // preventDefault or stopPropagation. + return view.trigger(eventName, evt); + } else { + return true; // continue event propagation + } + }, + + invokeObserver: function(target, observer) { + observer.call(target); + } +}); +})(); + + + +(function() { +/** +@module ember +@submodule ember-views +*/ + +var hasElement = Ember.View.states.hasElement; +var inDOM = Ember.View.states.inDOM = Ember.create(hasElement); + +Ember.merge(inDOM, { + enter: function(view) { + // Register the view for event handling. This hash is used by + // Ember.EventDispatcher to dispatch incoming events. + if (!view.isVirtual) { + Ember.assert("Attempted to register a view with an id already in use: "+view.elementId, !Ember.View.views[view.elementId]); + Ember.View.views[view.elementId] = view; + } + + view.addBeforeObserver('elementId', function() { + throw new Ember.Error("Changing a view's elementId after creation is not allowed"); + }); + }, + + exit: function(view) { + if (!this.isVirtual) delete Ember.View.views[view.elementId]; + }, + + insertElement: function(view, fn) { + throw "You can't insert an element into the DOM that has already been inserted"; + } +}); + +})(); + + + +(function() { +/** +@module ember +@submodule ember-views +*/ + +var destroyingError = "You can't call %@ on a view being destroyed", fmt = Ember.String.fmt; + +var destroying = Ember.View.states.destroying = Ember.create(Ember.View.states._default); + +Ember.merge(destroying, { + appendChild: function() { + throw fmt(destroyingError, ['appendChild']); + }, + rerender: function() { + throw fmt(destroyingError, ['rerender']); + }, + destroyElement: function() { + throw fmt(destroyingError, ['destroyElement']); + }, + empty: function() { + throw fmt(destroyingError, ['empty']); + }, + + setElement: function() { + throw fmt(destroyingError, ["set('element', ...)"]); + }, + + renderToBufferIfNeeded: function() { + return false; + }, + + // Since element insertion is scheduled, don't do anything if + // the view has been destroyed between scheduling and execution + insertElement: Ember.K +}); + + +})(); + + + +(function() { +Ember.View.cloneStates = function(from) { + var into = {}; + + into._default = {}; + into.preRender = Ember.create(into._default); + into.destroying = Ember.create(into._default); + into.inBuffer = Ember.create(into._default); + into.hasElement = Ember.create(into._default); + into.inDOM = Ember.create(into.hasElement); + + for (var stateName in from) { + if (!from.hasOwnProperty(stateName)) { continue; } + Ember.merge(into[stateName], from[stateName]); + } + + return into; +}; + +})(); + + + +(function() { +var states = Ember.View.cloneStates(Ember.View.states); + +/** +@module ember +@submodule ember-views +*/ + +var get = Ember.get, set = Ember.set; +var forEach = Ember.EnumerableUtils.forEach; +var ViewCollection = Ember._ViewCollection; + +/** + A `ContainerView` is an `Ember.View` subclass that implements `Ember.MutableArray` + allowing programmatic management of its child views. + + ## Setting Initial Child Views + + The initial array of child views can be set in one of two ways. You can + provide a `childViews` property at creation time that contains instance of + `Ember.View`: + + ```javascript + aContainer = Ember.ContainerView.create({ + childViews: [Ember.View.create(), Ember.View.create()] + }); + ``` + + You can also provide a list of property names whose values are instances of + `Ember.View`: + + ```javascript + aContainer = Ember.ContainerView.create({ + childViews: ['aView', 'bView', 'cView'], + aView: Ember.View.create(), + bView: Ember.View.create(), + cView: Ember.View.create() + }); + ``` + + The two strategies can be combined: + + ```javascript + aContainer = Ember.ContainerView.create({ + childViews: ['aView', Ember.View.create()], + aView: Ember.View.create() + }); + ``` + + Each child view's rendering will be inserted into the container's rendered + HTML in the same order as its position in the `childViews` property. + + ## Adding and Removing Child Views + + The container view implements `Ember.MutableArray` allowing programmatic management of its child views. + + To remove a view, pass that view into a `removeObject` call on the container view. + + Given an empty `` the following code + + ```javascript + aContainer = Ember.ContainerView.create({ + classNames: ['the-container'], + childViews: ['aView', 'bView'], + aView: Ember.View.create({ + template: Ember.Handlebars.compile("A") + }), + bView: Ember.View.create({ + template: Ember.Handlebars.compile("B") + }) + }); + + aContainer.appendTo('body'); + ``` + + Results in the HTML + + ```html +
    +
    A
    +
    B
    +
    + ``` + + Removing a view + + ```javascript + aContainer.toArray(); // [aContainer.aView, aContainer.bView] + aContainer.removeObject(aContainer.get('bView')); + aContainer.toArray(); // [aContainer.aView] + ``` + + Will result in the following HTML + + ```html +
    +
    A
    +
    + ``` + + Similarly, adding a child view is accomplished by adding `Ember.View` instances to the + container view. + + Given an empty `` the following code + + ```javascript + aContainer = Ember.ContainerView.create({ + classNames: ['the-container'], + childViews: ['aView', 'bView'], + aView: Ember.View.create({ + template: Ember.Handlebars.compile("A") + }), + bView: Ember.View.create({ + template: Ember.Handlebars.compile("B") + }) + }); + + aContainer.appendTo('body'); + ``` + + Results in the HTML + + ```html +
    +
    A
    +
    B
    +
    + ``` + + Adding a view + + ```javascript + AnotherViewClass = Ember.View.extend({ + template: Ember.Handlebars.compile("Another view") + }); + + aContainer.toArray(); // [aContainer.aView, aContainer.bView] + aContainer.pushObject(AnotherViewClass.create()); + aContainer.toArray(); // [aContainer.aView, aContainer.bView, ] + ``` + + Will result in the following HTML + + ```html +
    +
    A
    +
    B
    +
    Another view
    +
    + ``` + + ## Templates and Layout + + A `template`, `templateName`, `defaultTemplate`, `layout`, `layoutName` or + `defaultLayout` property on a container view will not result in the template + or layout being rendered. The HTML contents of a `Ember.ContainerView`'s DOM + representation will only be the rendered HTML of its child views. + + @class ContainerView + @namespace Ember + @extends Ember.View +*/ +Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { + states: states, + + init: function() { + this._super(); + + var childViews = get(this, 'childViews'); + + // redefine view's childViews property that was obliterated + Ember.defineProperty(this, 'childViews', Ember.View.childViewsProperty); + + var _childViews = this._childViews; + + forEach(childViews, function(viewName, idx) { + var view; + + if ('string' === typeof viewName) { + view = get(this, viewName); + view = this.createChildView(view); + set(this, viewName, view); + } else { + view = this.createChildView(viewName); + } + + _childViews[idx] = view; + }, this); + + var currentView = get(this, 'currentView'); + if (currentView) { + if (!_childViews.length) { _childViews = this._childViews = this._childViews.slice(); } + _childViews.push(this.createChildView(currentView)); + } + }, + + replace: function(idx, removedCount, addedViews) { + var addedCount = addedViews ? get(addedViews, 'length') : 0; + var self = this; + Ember.assert("You can't add a child to a container that is already a child of another view", Ember.A(addedViews).every(function(item) { return !get(item, '_parentView') || get(item, '_parentView') === self; })); + + this.arrayContentWillChange(idx, removedCount, addedCount); + this.childViewsWillChange(this._childViews, idx, removedCount); + + if (addedCount === 0) { + this._childViews.splice(idx, removedCount) ; + } else { + var args = [idx, removedCount].concat(addedViews); + if (addedViews.length && !this._childViews.length) { this._childViews = this._childViews.slice(); } + this._childViews.splice.apply(this._childViews, args); + } + + this.arrayContentDidChange(idx, removedCount, addedCount); + this.childViewsDidChange(this._childViews, idx, removedCount, addedCount); + + return this; + }, + + objectAt: function(idx) { + return this._childViews[idx]; + }, + + length: Ember.computed(function () { + return this._childViews.length; + }).volatile(), + + /** + Instructs each child view to render to the passed render buffer. + + @private + @method render + @param {Ember.RenderBuffer} buffer the buffer to render to + */ + render: function(buffer) { + this.forEachChildView(function(view) { + view.renderToBuffer(buffer); + }); + }, + + instrumentName: 'container', + + /** + When a child view is removed, destroy its element so that + it is removed from the DOM. + + The array observer that triggers this action is set up in the + `renderToBuffer` method. + + @private + @method childViewsWillChange + @param {Ember.Array} views the child views array before mutation + @param {Number} start the start position of the mutation + @param {Number} removed the number of child views removed + **/ + childViewsWillChange: function(views, start, removed) { + this.propertyWillChange('childViews'); + + if (removed > 0) { + var changedViews = views.slice(start, start+removed); + // transition to preRender before clearing parentView + this.currentState.childViewsWillChange(this, views, start, removed); + this.initializeViews(changedViews, null, null); + } + }, + + removeChild: function(child) { + this.removeObject(child); + return this; + }, + + /** + When a child view is added, make sure the DOM gets updated appropriately. + + If the view has already rendered an element, we tell the child view to + create an element and insert it into the DOM. If the enclosing container + view has already written to a buffer, but not yet converted that buffer + into an element, we insert the string representation of the child into the + appropriate place in the buffer. + + @private + @method childViewsDidChange + @param {Ember.Array} views the array of child views afte the mutation has occurred + @param {Number} start the start position of the mutation + @param {Number} removed the number of child views removed + @param {Number} the number of child views added + */ + childViewsDidChange: function(views, start, removed, added) { + if (added > 0) { + var changedViews = views.slice(start, start+added); + this.initializeViews(changedViews, this, get(this, 'templateData')); + this.currentState.childViewsDidChange(this, views, start, added); + } + this.propertyDidChange('childViews'); + }, + + initializeViews: function(views, parentView, templateData) { + forEach(views, function(view) { + set(view, '_parentView', parentView); + + if (!view.container && parentView) { + set(view, 'container', parentView.container); + } + + if (!get(view, 'templateData')) { + set(view, 'templateData', templateData); + } + }); + }, + + currentView: null, + + _currentViewWillChange: Ember.beforeObserver('currentView', function() { + var currentView = get(this, 'currentView'); + if (currentView) { + currentView.destroy(); + } + }), + + _currentViewDidChange: Ember.observer('currentView', function() { + var currentView = get(this, 'currentView'); + if (currentView) { + Ember.assert("You tried to set a current view that already has a parent. Make sure you don't have multiple outlets in the same view.", !get(currentView, '_parentView')); + this.pushObject(currentView); + } + }), + + _ensureChildrenAreInDOM: function () { + this.currentState.ensureChildrenAreInDOM(this); + } +}); + +Ember.merge(states._default, { + childViewsWillChange: Ember.K, + childViewsDidChange: Ember.K, + ensureChildrenAreInDOM: Ember.K +}); + +Ember.merge(states.inBuffer, { + childViewsDidChange: function(parentView, views, start, added) { + throw new Ember.Error('You cannot modify child views while in the inBuffer state'); + } +}); + +Ember.merge(states.hasElement, { + childViewsWillChange: function(view, views, start, removed) { + for (var i=start; i` and the following code: + + ```javascript + someItemsView = Ember.CollectionView.create({ + classNames: ['a-collection'], + content: ['A','B','C'], + itemViewClass: Ember.View.extend({ + template: Ember.Handlebars.compile("the letter: {{view.content}}") + }) + }); + + someItemsView.appendTo('body'); + ``` + + Will result in the following HTML structure + + ```html +
    +
    the letter: A
    +
    the letter: B
    +
    the letter: C
    +
    + ``` + + ## Automatic matching of parent/child tagNames + + Setting the `tagName` property of a `CollectionView` to any of + "ul", "ol", "table", "thead", "tbody", "tfoot", "tr", or "select" will result + in the item views receiving an appropriately matched `tagName` property. + + Given an empty `` and the following code: + + ```javascript + anUnorderedListView = Ember.CollectionView.create({ + tagName: 'ul', + content: ['A','B','C'], + itemViewClass: Ember.View.extend({ + template: Ember.Handlebars.compile("the letter: {{view.content}}") + }) + }); + + anUnorderedListView.appendTo('body'); + ``` + + Will result in the following HTML structure + + ```html +
      +
    • the letter: A
    • +
    • the letter: B
    • +
    • the letter: C
    • +
    + ``` + + Additional `tagName` pairs can be provided by adding to + `Ember.CollectionView.CONTAINER_MAP ` + + ```javascript + Ember.CollectionView.CONTAINER_MAP['article'] = 'section' + ``` + + ## Programmatic creation of child views + + For cases where additional customization beyond the use of a single + `itemViewClass` or `tagName` matching is required CollectionView's + `createChildView` method can be overidden: + + ```javascript + CustomCollectionView = Ember.CollectionView.extend({ + createChildView: function(viewClass, attrs) { + if (attrs.content.kind == 'album') { + viewClass = App.AlbumView; + } else { + viewClass = App.SongView; + } + return this._super(viewClass, attrs); + } + }); + ``` + + ## Empty View + + You can provide an `Ember.View` subclass to the `Ember.CollectionView` + instance as its `emptyView` property. If the `content` property of a + `CollectionView` is set to `null` or an empty array, an instance of this view + will be the `CollectionView`s only child. + + ```javascript + aListWithNothing = Ember.CollectionView.create({ + classNames: ['nothing'] + content: null, + emptyView: Ember.View.extend({ + template: Ember.Handlebars.compile("The collection is empty") + }) + }); + + aListWithNothing.appendTo('body'); + ``` + + Will result in the following HTML structure + + ```html +
    +
    + The collection is empty +
    +
    + ``` + + ## Adding and Removing items + + The `childViews` property of a `CollectionView` should not be directly + manipulated. Instead, add, remove, replace items from its `content` property. + This will trigger appropriate changes to its rendered HTML. + + + @class CollectionView + @namespace Ember + @extends Ember.ContainerView + @since Ember 0.9 +*/ +Ember.CollectionView = Ember.ContainerView.extend({ + + /** + A list of items to be displayed by the `Ember.CollectionView`. + + @property content + @type Ember.Array + @default null + */ + content: null, + + /** + This provides metadata about what kind of empty view class this + collection would like if it is being instantiated from another + system (like Handlebars) + + @private + @property emptyViewClass + */ + emptyViewClass: Ember.View, + + /** + An optional view to display if content is set to an empty array. + + @property emptyView + @type Ember.View + @default null + */ + emptyView: null, + + /** + @property itemViewClass + @type Ember.View + @default Ember.View + */ + itemViewClass: Ember.View, + + /** + Setup a CollectionView + + @method init + */ + init: function() { + var ret = this._super(); + this._contentDidChange(); + return ret; + }, + + /** + Invoked when the content property is about to change. Notifies observers that the + entire array content will change. + + @private + @method _contentWillChange + */ + _contentWillChange: Ember.beforeObserver('content', function() { + var content = this.get('content'); + + if (content) { content.removeArrayObserver(this); } + var len = content ? get(content, 'length') : 0; + this.arrayWillChange(content, 0, len); + }), + + /** + Check to make sure that the content has changed, and if so, + update the children directly. This is always scheduled + asynchronously, to allow the element to be created before + bindings have synchronized and vice versa. + + @private + @method _contentDidChange + */ + _contentDidChange: Ember.observer('content', function() { + var content = get(this, 'content'); + + if (content) { + this._assertArrayLike(content); + content.addArrayObserver(this); + } + + var len = content ? get(content, 'length') : 0; + this.arrayDidChange(content, 0, null, len); + }), + + /** + Ensure that the content implements Ember.Array + + @private + @method _assertArrayLike + */ + _assertArrayLike: function(content) { + Ember.assert(fmt("an Ember.CollectionView's content must implement Ember.Array. You passed %@", [content]), Ember.Array.detect(content)); + }, + + /** + Removes the content and content observers. + + @method destroy + */ + destroy: function() { + if (!this._super()) { return; } + + var content = get(this, 'content'); + if (content) { content.removeArrayObserver(this); } + + if (this._createdEmptyView) { + this._createdEmptyView.destroy(); + } + + return this; + }, + + /** + Called when a mutation to the underlying content array will occur. + + This method will remove any views that are no longer in the underlying + content array. + + Invokes whenever the content array itself will change. + + @method arrayWillChange + @param {Array} content the managed collection of objects + @param {Number} start the index at which the changes will occurr + @param {Number} removed number of object to be removed from content + */ + arrayWillChange: function(content, start, removedCount) { + // If the contents were empty before and this template collection has an + // empty view remove it now. + var emptyView = get(this, 'emptyView'); + if (emptyView && emptyView instanceof Ember.View) { + emptyView.removeFromParent(); + } + + // Loop through child views that correspond with the removed items. + // Note that we loop from the end of the array to the beginning because + // we are mutating it as we go. + var childViews = this._childViews, childView, idx, len; + + len = this._childViews.length; + + var removingAll = removedCount === len; + + if (removingAll) { + this.currentState.empty(this); + this.invokeRecursively(function(view) { + view.removedFromDOM = true; + }, false); + } + + for (idx = start + removedCount - 1; idx >= start; idx--) { + childView = childViews[idx]; + childView.destroy(); + } + }, + + /** + Called when a mutation to the underlying content array occurs. + + This method will replay that mutation against the views that compose the + `Ember.CollectionView`, ensuring that the view reflects the model. + + This array observer is added in `contentDidChange`. + + @method arrayDidChange + @param {Array} content the managed collection of objects + @param {Number} start the index at which the changes occurred + @param {Number} removed number of object removed from content + @param {Number} added number of object added to content + */ + arrayDidChange: function(content, start, removed, added) { + var addedViews = [], view, item, idx, len, itemViewClass, + emptyView; + + len = content ? get(content, 'length') : 0; + + if (len) { + itemViewClass = get(this, 'itemViewClass'); + + if ('string' === typeof itemViewClass) { + itemViewClass = get(itemViewClass) || itemViewClass; + } + + Ember.assert(fmt("itemViewClass must be a subclass of Ember.View, not %@", + [itemViewClass]), + 'string' === typeof itemViewClass || Ember.View.detect(itemViewClass)); + + for (idx = start; idx < start+added; idx++) { + item = content.objectAt(idx); + + view = this.createChildView(itemViewClass, { + content: item, + contentIndex: idx + }); + + addedViews.push(view); + } + } else { + emptyView = get(this, 'emptyView'); + + if (!emptyView) { return; } + + if ('string' === typeof emptyView) { + emptyView = get(emptyView) || emptyView; + } + + emptyView = this.createChildView(emptyView); + addedViews.push(emptyView); + set(this, 'emptyView', emptyView); + + if (Ember.CoreView.detect(emptyView)) { + this._createdEmptyView = emptyView; + } + } + + this.replace(start, 0, addedViews); + }, + + /** + Instantiates a view to be added to the childViews array during view + initialization. You generally will not call this method directly unless + you are overriding `createChildViews()`. Note that this method will + automatically configure the correct settings on the new view instance to + act as a child of the parent. + + The tag name for the view will be set to the tagName of the viewClass + passed in. + + @method createChildView + @param {Class} viewClass + @param {Hash} [attrs] Attributes to add + @return {Ember.View} new instance + */ + createChildView: function(view, attrs) { + view = this._super(view, attrs); + + var itemTagName = get(view, 'tagName'); + + if (itemTagName === null || itemTagName === undefined) { + itemTagName = Ember.CollectionView.CONTAINER_MAP[get(this, 'tagName')]; + set(view, 'tagName', itemTagName); + } + + return view; + } +}); + +/** + A map of parent tags to their default child tags. You can add + additional parent tags if you want collection views that use + a particular parent tag to default to a child tag. + + @property CONTAINER_MAP + @type Hash + @static + @final +*/ +Ember.CollectionView.CONTAINER_MAP = { + ul: 'li', + ol: 'li', + table: 'tr', + thead: 'tr', + tbody: 'tr', + tfoot: 'tr', + tr: 'td', + select: 'option' +}; + +})(); + + + +(function() { +var get = Ember.get; + +/** + The ComponentTemplateDeprecation mixin is used to provide a useful + deprecation warning when using either `template` or `templateName` with + a component. The `template` and `templateName` properties specified at + extend time are moved to `layout` and `layoutName` respectively. + + `Ember.ComponentTemplateDeprecation` is used internally by Ember in + `Ember.Component`. + + @class ComponentTemplateDeprecation + @namespace Ember +*/ +Ember.ComponentTemplateDeprecation = Ember.Mixin.create({ + /** + @private + + Moves `templateName` to `layoutName` and `template` to `layout` at extend + time if a layout is not also specified. + + Note that this currently modifies the mixin themselves, which is technically + dubious but is practically of little consequence. This may change in the + future. + + @method willMergeMixin + */ + willMergeMixin: function(props) { + // must call _super here to ensure that the ActionHandler + // mixin is setup properly (moves actions -> _actions) + // + // Calling super is only OK here since we KNOW that + // there is another Mixin loaded first. + this._super.apply(this, arguments); + + var deprecatedProperty, replacementProperty, + layoutSpecified = (props.layoutName || props.layout || get(this, 'layoutName')); + + if (props.templateName && !layoutSpecified) { + deprecatedProperty = 'templateName'; + replacementProperty = 'layoutName'; + + props.layoutName = props.templateName; + delete props['templateName']; + } + + if (props.template && !layoutSpecified) { + deprecatedProperty = 'template'; + replacementProperty = 'layout'; + + props.layout = props.template; + delete props['template']; + } + + if (deprecatedProperty) { + Ember.deprecate('Do not specify ' + deprecatedProperty + ' on a Component, use ' + replacementProperty + ' instead.', false); + } + } +}); + + +})(); + + + +(function() { +var get = Ember.get, set = Ember.set, isNone = Ember.isNone, + a_slice = Array.prototype.slice; + + +/** +@module ember +@submodule ember-views +*/ + +/** + An `Ember.Component` is a view that is completely + isolated. Property access in its templates go + to the view object and actions are targeted at + the view object. There is no access to the + surrounding context or outer controller; all + contextual information must be passed in. + + The easiest way to create an `Ember.Component` is via + a template. If you name a template + `components/my-foo`, you will be able to use + `{{my-foo}}` in other templates, which will make + an instance of the isolated component. + + ```handlebars + {{app-profile person=currentUser}} + ``` + + ```handlebars + +

    {{person.title}}

    + +

    {{person.signature}}

    + ``` + + You can use `yield` inside a template to + include the **contents** of any block attached to + the component. The block will be executed in the + context of the surrounding context or outer controller: + + ```handlebars + {{#app-profile person=currentUser}} +

    Admin mode

    + {{! Executed in the controller's context. }} + {{/app-profile}} + ``` + + ```handlebars + +

    {{person.title}}

    + {{! Executed in the components context. }} + {{yield}} {{! block contents }} + ``` + + If you want to customize the component, in order to + handle events or actions, you implement a subclass + of `Ember.Component` named after the name of the + component. Note that `Component` needs to be appended to the name of + your subclass like `AppProfileComponent`. + + For example, you could implement the action + `hello` for the `app-profile` component: + + ```javascript + App.AppProfileComponent = Ember.Component.extend({ + actions: { + hello: function(name) { + console.log("Hello", name); + } + } + }); + ``` + + And then use it in the component's template: + + ```handlebars + + +

    {{person.title}}

    + {{yield}} + + + ``` + + Components must have a `-` in their name to avoid + conflicts with built-in controls that wrap HTML + elements. This is consistent with the same + requirement in web components. + + @class Component + @namespace Ember + @extends Ember.View +*/ +Ember.Component = Ember.View.extend(Ember.TargetActionSupport, Ember.ComponentTemplateDeprecation, { + init: function() { + this._super(); + set(this, 'context', this); + set(this, 'controller', this); + }, + + defaultLayout: function(context, options){ + Ember.Handlebars.helpers['yield'].call(context, options); + }, + + /** + A components template property is set by passing a block + during its invocation. It is executed within the parent context. + + Example: + + ```handlebars + {{#my-component}} + // something that is run in the context + // of the parent context + {{/my-component}} + ``` + + Specifying a template directly to a component is deprecated without + also specifying the layout property. + + @deprecated + @property template + */ + template: Ember.computed(function(key, value) { + if (value !== undefined) { return value; } + + var templateName = get(this, 'templateName'), + template = this.templateForName(templateName, 'template'); + + Ember.assert("You specified the templateName " + templateName + " for " + this + ", but it did not exist.", !templateName || template); + + return template || get(this, 'defaultTemplate'); + }).property('templateName'), + + /** + Specifying a components `templateName` is deprecated without also + providing the `layout` or `layoutName` properties. + + @deprecated + @property templateName + */ + templateName: null, + + // during render, isolate keywords + cloneKeywords: function() { + return { + view: this, + controller: this + }; + }, + + _yield: function(context, options) { + var view = options.data.view, + parentView = this._parentView, + template = get(this, 'template'); + + if (template) { + Ember.assert("A Component must have a parent view in order to yield.", parentView); + + view.appendChild(Ember.View, { + isVirtual: true, + tagName: '', + _contextView: parentView, + template: template, + context: get(parentView, 'context'), + controller: get(parentView, 'controller'), + templateData: { keywords: parentView.cloneKeywords() } + }); + } + }, + + /** + If the component is currently inserted into the DOM of a parent view, this + property will point to the controller of the parent view. + + @property targetObject + @type Ember.Controller + @default null + */ + targetObject: Ember.computed(function(key) { + var parentView = get(this, '_parentView'); + return parentView ? get(parentView, 'controller') : null; + }).property('_parentView'), + + /** + Triggers a named action on the controller context where the component is used if + this controller has registered for notifications of the action. + + For example a component for playing or pausing music may translate click events + into action notifications of "play" or "stop" depending on some internal state + of the component: + + + ```javascript + App.PlayButtonComponent = Ember.Component.extend({ + click: function(){ + if (this.get('isPlaying')) { + this.sendAction('play'); + } else { + this.sendAction('stop'); + } + } + }); + ``` + + When used inside a template these component actions are configured to + trigger actions in the outer application context: + + ```handlebars + {{! application.hbs }} + {{play-button play="musicStarted" stop="musicStopped"}} + ``` + + When the component receives a browser `click` event it translate this + interaction into application-specific semantics ("play" or "stop") and + triggers the specified action name on the controller for the template + where the component is used: + + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + actions: { + musicStarted: function(){ + // called when the play button is clicked + // and the music started playing + }, + musicStopped: function(){ + // called when the play button is clicked + // and the music stopped playing + } + } + }); + ``` + + If no action name is passed to `sendAction` a default name of "action" + is assumed. + + ```javascript + App.NextButtonComponent = Ember.Component.extend({ + click: function(){ + this.sendAction(); + } + }); + ``` + + ```handlebars + {{! application.hbs }} + {{next-button action="playNextSongInAlbum"}} + ``` + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + actions: { + playNextSongInAlbum: function(){ + ... + } + } + }); + ``` + + @method sendAction + @param [action] {String} the action to trigger + @param [context] {*} a context to send with the action + */ + sendAction: function(action) { + var actionName, + contexts = a_slice.call(arguments, 1); + + // Send the default action + if (action === undefined) { + actionName = get(this, 'action'); + Ember.assert("The default action was triggered on the component " + this.toString() + + ", but the action name (" + actionName + ") was not a string.", + isNone(actionName) || typeof actionName === 'string'); + } else { + actionName = get(this, action); + Ember.assert("The " + action + " action was triggered on the component " + + this.toString() + ", but the action name (" + actionName + + ") was not a string.", + isNone(actionName) || typeof actionName === 'string'); + } + + // If no action name for that action could be found, just abort. + if (actionName === undefined) { return; } + + this.triggerAction({ + action: actionName, + actionContext: contexts + }); + } +}); + +})(); + + + +(function() { + +})(); + + + +(function() { +/** +`Ember.ViewTargetActionSupport` is a mixin that can be included in a +view class to add a `triggerAction` method with semantics similar to +the Handlebars `{{action}}` helper. It provides intelligent defaults +for the action's target: the view's controller; and the context that is +sent with the action: the view's context. + +Note: In normal Ember usage, the `{{action}}` helper is usually the best +choice. This mixin is most often useful when you are doing more complex +event handling in custom View subclasses. + +For example: + +```javascript +App.SaveButtonView = Ember.View.extend(Ember.ViewTargetActionSupport, { + action: 'save', + click: function() { + this.triggerAction(); // Sends the `save` action, along with the current context + // to the current controller + } +}); +``` + +The `action` can be provided as properties of an optional object argument +to `triggerAction` as well. + +```javascript +App.SaveButtonView = Ember.View.extend(Ember.ViewTargetActionSupport, { + click: function() { + this.triggerAction({ + action: 'save' + }); // Sends the `save` action, along with the current context + // to the current controller + } +}); +``` + +@class ViewTargetActionSupport +@namespace Ember +@extends Ember.TargetActionSupport +*/ +Ember.ViewTargetActionSupport = Ember.Mixin.create(Ember.TargetActionSupport, { + /** + @property target + */ + target: Ember.computed.alias('controller'), + /** + @property actionContext + */ + actionContext: Ember.computed.alias('context') +}); + +})(); + + + +(function() { + +})(); + + + +(function() { +/** +Ember Views + +@module ember +@submodule ember-views +@requires ember-runtime +@main ember-views +*/ + +})(); + +(function() { +define("metamorph", + [], + function() { + "use strict"; + // ========================================================================== + // Project: metamorph + // Copyright: ©2014 Tilde, Inc. All rights reserved. + // ========================================================================== + + var K = function() {}, + guid = 0, + disableRange = (function(){ + if ('undefined' !== typeof MetamorphENV) { + return MetamorphENV.DISABLE_RANGE_API; + } else if ('undefined' !== ENV) { + return ENV.DISABLE_RANGE_API; + } else { + return false; + } + })(), + + // Feature-detect the W3C range API, the extended check is for IE9 which only partially supports ranges + supportsRange = (!disableRange) && typeof document !== 'undefined' && ('createRange' in document) && (typeof Range !== 'undefined') && Range.prototype.createContextualFragment, + + // Internet Explorer prior to 9 does not allow setting innerHTML if the first element + // is a "zero-scope" element. This problem can be worked around by making + // the first node an invisible text node. We, like Modernizr, use ­ + needsShy = typeof document !== 'undefined' && (function() { + var testEl = document.createElement('div'); + testEl.innerHTML = "
    "; + testEl.firstChild.innerHTML = ""; + return testEl.firstChild.innerHTML === ''; + })(), + + + // IE 8 (and likely earlier) likes to move whitespace preceeding + // a script tag to appear after it. This means that we can + // accidentally remove whitespace when updating a morph. + movesWhitespace = document && (function() { + var testEl = document.createElement('div'); + testEl.innerHTML = "Test: Value"; + return testEl.childNodes[0].nodeValue === 'Test:' && + testEl.childNodes[2].nodeValue === ' Value'; + })(); + + // Constructor that supports either Metamorph('foo') or new + // Metamorph('foo'); + // + // Takes a string of HTML as the argument. + + var Metamorph = function(html) { + var self; + + if (this instanceof Metamorph) { + self = this; + } else { + self = new K(); + } + + self.innerHTML = html; + var myGuid = 'metamorph-'+(guid++); + self.start = myGuid + '-start'; + self.end = myGuid + '-end'; + + return self; + }; + + K.prototype = Metamorph.prototype; + + var rangeFor, htmlFunc, removeFunc, outerHTMLFunc, appendToFunc, afterFunc, prependFunc, startTagFunc, endTagFunc; + + outerHTMLFunc = function() { + return this.startTag() + this.innerHTML + this.endTag(); + }; + + startTagFunc = function() { + /* + * We replace chevron by its hex code in order to prevent escaping problems. + * Check this thread for more explaination: + * http://stackoverflow.com/questions/8231048/why-use-x3c-instead-of-when-generating-html-from-javascript + */ + return "hi"; + * div.firstChild.firstChild.tagName //=> "" + * + * If our script markers are inside such a node, we need to find that + * node and use *it* as the marker. + */ + var realNode = function(start) { + while (start.parentNode.tagName === "") { + start = start.parentNode; + } + + return start; + }; + + /* + * When automatically adding a tbody, Internet Explorer inserts the + * tbody immediately before the first . Other browsers create it + * before the first node, no matter what. + * + * This means the the following code: + * + * div = document.createElement("div"); + * div.innerHTML = "
    hi
    + * + * Generates the following DOM in IE: + * + * + div + * + table + * - script id='first' + * + tbody + * + tr + * + td + * - "hi" + * - script id='last' + * + * Which means that the two script tags, even though they were + * inserted at the same point in the hierarchy in the original + * HTML, now have different parents. + * + * This code reparents the first script tag by making it the tbody's + * first child. + * + */ + var fixParentage = function(start, end) { + if (start.parentNode !== end.parentNode) { + end.parentNode.insertBefore(start, end.parentNode.firstChild); + } + }; + + htmlFunc = function(html, outerToo) { + // get the real starting node. see realNode for details. + var start = realNode(document.getElementById(this.start)); + var end = document.getElementById(this.end); + var parentNode = end.parentNode; + var node, nextSibling, last; + + // make sure that the start and end nodes share the same + // parent. If not, fix it. + fixParentage(start, end); + + // remove all of the nodes after the starting placeholder and + // before the ending placeholder. + node = start.nextSibling; + while (node) { + nextSibling = node.nextSibling; + last = node === end; + + // if this is the last node, and we want to remove it as well, + // set the `end` node to the next sibling. This is because + // for the rest of the function, we insert the new nodes + // before the end (note that insertBefore(node, null) is + // the same as appendChild(node)). + // + // if we do not want to remove it, just break. + if (last) { + if (outerToo) { end = node.nextSibling; } else { break; } + } + + node.parentNode.removeChild(node); + + // if this is the last node and we didn't break before + // (because we wanted to remove the outer nodes), break + // now. + if (last) { break; } + + node = nextSibling; + } + + // get the first node for the HTML string, even in cases like + // tables and lists where a simple innerHTML on a div would + // swallow some of the content. + node = firstNodeFor(start.parentNode, html); + + if (outerToo) { + start.parentNode.removeChild(start); + } + + // copy the nodes for the HTML between the starting and ending + // placeholder. + while (node) { + nextSibling = node.nextSibling; + parentNode.insertBefore(node, end); + node = nextSibling; + } + }; + + // remove the nodes in the DOM representing this metamorph. + // + // this includes the starting and ending placeholders. + removeFunc = function() { + var start = realNode(document.getElementById(this.start)); + var end = document.getElementById(this.end); + + this.html(''); + start.parentNode.removeChild(start); + end.parentNode.removeChild(end); + }; + + appendToFunc = function(parentNode) { + var node = firstNodeFor(parentNode, this.outerHTML()); + var nextSibling; + + while (node) { + nextSibling = node.nextSibling; + parentNode.appendChild(node); + node = nextSibling; + } + }; + + afterFunc = function(html) { + // get the real starting node. see realNode for details. + var end = document.getElementById(this.end); + var insertBefore = end.nextSibling; + var parentNode = end.parentNode; + var nextSibling; + var node; + + // get the first node for the HTML string, even in cases like + // tables and lists where a simple innerHTML on a div would + // swallow some of the content. + node = firstNodeFor(parentNode, html); + + // copy the nodes for the HTML between the starting and ending + // placeholder. + while (node) { + nextSibling = node.nextSibling; + parentNode.insertBefore(node, insertBefore); + node = nextSibling; + } + }; + + prependFunc = function(html) { + var start = document.getElementById(this.start); + var parentNode = start.parentNode; + var nextSibling; + var node; + + node = firstNodeFor(parentNode, html); + var insertBefore = start.nextSibling; + + while (node) { + nextSibling = node.nextSibling; + parentNode.insertBefore(node, insertBefore); + node = nextSibling; + } + }; } - }, - viewHierarchyCollection: function() { - var currentView, viewCollection = new ViewCollection([this]); + Metamorph.prototype.html = function(html) { + this.checkRemoved(); + if (html === undefined) { return this.innerHTML; } - for (var i = 0; i < viewCollection.length; i++) { - currentView = viewCollection.objectAt(i); - if (currentView._childViews) { - viewCollection.push.apply(viewCollection, currentView._childViews); - } - } + htmlFunc.call(this, html); - return viewCollection; - }, + this.innerHTML = html; + }; - /** - Destroys any existing element along with the element for any child views - as well. If the view does not currently have a element, then this method - will do nothing. + Metamorph.prototype.replaceWith = function(html) { + this.checkRemoved(); + htmlFunc.call(this, html, true); + }; - If you implement `willDestroyElement()` on your view, then this method will - be invoked on your view before your element is destroyed to give you a - chance to clean up any event handlers, etc. + Metamorph.prototype.remove = removeFunc; + Metamorph.prototype.outerHTML = outerHTMLFunc; + Metamorph.prototype.appendTo = appendToFunc; + Metamorph.prototype.after = afterFunc; + Metamorph.prototype.prepend = prependFunc; + Metamorph.prototype.startTag = startTagFunc; + Metamorph.prototype.endTag = endTagFunc; - If you write a `willDestroyElement()` handler, you can assume that your - `didInsertElement()` handler was called earlier for the same element. + Metamorph.prototype.isRemoved = function() { + var before = document.getElementById(this.start); + var after = document.getElementById(this.end); - You should not call or override this method yourself, but you may - want to implement the above callbacks. + return !before || !after; + }; - @method destroyElement - @return {Ember.View} receiver - */ - destroyElement: function() { - return this.currentState.destroyElement(this); - }, + Metamorph.prototype.checkRemoved = function() { + if (this.isRemoved()) { + throw new Error("Cannot perform operations on a Metamorph that is not in the DOM."); + } + }; - /** - Called when the element of the view is going to be destroyed. Override - this function to do any teardown that requires an element, like removing - event listeners. + return Metamorph; + }); - @event willDestroyElement - */ - willDestroyElement: Ember.K, +})(); - /** - @private +(function() { +/** +@module ember +@submodule ember-handlebars-compiler +*/ - Triggers the `willDestroyElement` event (which invokes the - `willDestroyElement()` method if it exists) on this view and all child - views. +// Eliminate dependency on any Ember to simplify precompilation workflow +var objectCreate = Object.create || function(parent) { + function F() {} + F.prototype = parent; + return new F(); +}; - Before triggering `willDestroyElement`, it first triggers the - `willClearRender` event recursively. +var Handlebars = (Ember.imports && Ember.imports.Handlebars) || (this && this.Handlebars); +if (!Handlebars && typeof require === 'function') { + Handlebars = require('handlebars'); +} - @method _notifyWillDestroyElement - */ - _notifyWillDestroyElement: function() { - var viewCollection = this.viewHierarchyCollection(); - viewCollection.trigger('willClearRender'); - viewCollection.trigger('willDestroyElement'); - return viewCollection; - }, +Ember.assert("Ember Handlebars requires Handlebars version 1.0 or 1.1. Include " + + "a SCRIPT tag in the HTML HEAD linking to the Handlebars file " + + "before you link to Ember.", Handlebars); - /** - @private +Ember.assert("Ember Handlebars requires Handlebars version 1.0 or 1.1, " + + "COMPILER_REVISION expected: 4, got: " + Handlebars.COMPILER_REVISION + + " - Please note: Builds of master may have other COMPILER_REVISION values.", + Handlebars.COMPILER_REVISION === 4); - If this view's element changes, we need to invalidate the caches of our - child views so that we do not retain references to DOM elements that are - no longer needed. +/** + Prepares the Handlebars templating library for use inside Ember's view + system. - @method _elementDidChange - */ - _elementDidChange: Ember.observer(function() { - this.forEachChildView(function(view) { - delete meta(view).cache.element; - }); - }, 'element'), + The `Ember.Handlebars` object is the standard Handlebars library, extended to + use Ember's `get()` method instead of direct property access, which allows + computed properties to be used inside templates. - /** - Called when the parentView property has changed. + To create an `Ember.Handlebars` template, call `Ember.Handlebars.compile()`. + This will return a function that can be used by `Ember.View` for rendering. - @event parentViewDidChange - */ - parentViewDidChange: Ember.K, + @class Handlebars + @namespace Ember +*/ +Ember.Handlebars = objectCreate(Handlebars); - instrumentName: 'view', +/** + Register a bound helper or custom view helper. - instrumentDetails: function(hash) { - hash.template = get(this, 'templateName'); - this._super(hash); - }, + ## Simple bound helper example - _renderToBuffer: function(parentBuffer, bufferOperation) { - this.lengthBeforeRender = this._childViews.length; - var buffer = this._super(parentBuffer, bufferOperation); - this.lengthAfterRender = this._childViews.length; + ```javascript + Ember.Handlebars.helper('capitalize', function(value) { + return value.toUpperCase(); + }); + ``` - return buffer; - }, + The above bound helper can be used inside of templates as follows: - renderToBufferIfNeeded: function (buffer) { - return this.currentState.renderToBufferIfNeeded(this, buffer); - }, + ```handlebars + {{capitalize name}} + ``` - beforeRender: function(buffer) { - this.applyAttributesToBuffer(buffer); - buffer.pushOpeningTag(); - }, + In this case, when the `name` property of the template's context changes, + the rendered value of the helper will update to reflect this change. - afterRender: function(buffer) { - buffer.pushClosingTag(); - }, + For more examples of bound helpers, see documentation for + `Ember.Handlebars.registerBoundHelper`. - applyAttributesToBuffer: function(buffer) { - // Creates observers for all registered class name and attribute bindings, - // then adds them to the element. - var classNameBindings = get(this, 'classNameBindings'); - if (classNameBindings.length) { - this._applyClassNameBindings(classNameBindings); - } + ## Custom view helper example - // Pass the render buffer so the method can apply attributes directly. - // This isn't needed for class name bindings because they use the - // existing classNames infrastructure. - var attributeBindings = get(this, 'attributeBindings'); - if (attributeBindings.length) { - this._applyAttributeBindings(buffer, attributeBindings); - } + Assuming a view subclass named `App.CalendarView` were defined, a helper + for rendering instances of this view could be registered as follows: - buffer.setClasses(this.classNames); - buffer.id(this.elementId); + ```javascript + Ember.Handlebars.helper('calendar', App.CalendarView): + ``` - var role = get(this, 'ariaRole'); - if (role) { - buffer.attr('role', role); - } + The above bound helper can be used inside of templates as follows: - if (get(this, 'isVisible') === false) { - buffer.style('display', 'none'); - } - }, + ```handlebars + {{calendar}} + ``` - // .......................................................... - // STANDARD RENDER PROPERTIES - // + Which is functionally equivalent to: - /** - Tag name for the view's outer element. The tag name is only used when an - element is first created. If you change the `tagName` for an element, you - must destroy and recreate the view element. + ```handlebars + {{view App.CalendarView}} + ``` - By default, the render buffer will use a `
    ` tag for views. + Options in the helper will be passed to the view in exactly the same + manner as with the `view` helper. - @property tagName - @type String - @default null - */ + @method helper + @for Ember.Handlebars + @param {String} name + @param {Function|Ember.View} function or view class constructor + @param {String} dependentKeys* +*/ +Ember.Handlebars.helper = function(name, value) { + Ember.assert("You tried to register a component named '" + name + "', but component names must include a '-'", !Ember.Component.detect(value) || name.match(/-/)); - // We leave this null by default so we can tell the difference between - // the default case and a user-specified tag. - tagName: null, + if (Ember.View.detect(value)) { + Ember.Handlebars.registerHelper(name, Ember.Handlebars.makeViewHelper(value)); + } else { + Ember.Handlebars.registerBoundHelper.apply(null, arguments); + } +}; - /** - The WAI-ARIA role of the control represented by this view. For example, a - button may have a role of type 'button', or a pane may have a role of - type 'alertdialog'. This property is used by assistive software to help - visually challenged users navigate rich web applications. +/** + Returns a helper function that renders the provided ViewClass. - The full list of valid WAI-ARIA roles is available at: - [http://www.w3.org/TR/wai-aria/roles#roles_categorization](http://www.w3.org/TR/wai-aria/roles#roles_categorization) + Used internally by Ember.Handlebars.helper and other methods + involving helper/component registration. - @property ariaRole - @type String - @default null - */ - ariaRole: null, + @private + @method makeViewHelper + @for Ember.Handlebars + @param {Function} ViewClass view class constructor +*/ +Ember.Handlebars.makeViewHelper = function(ViewClass) { + return function(options) { + Ember.assert("You can only pass attributes (such as name=value) not bare values to a helper for a View found in '" + ViewClass.toString() + "'", arguments.length < 2); + return Ember.Handlebars.helpers.view.call(this, ViewClass, options); + }; +}; - /** - Standard CSS class names to apply to the view's outer element. This - property automatically inherits any class names defined by the view's - superclasses as well. +/** +@class helpers +@namespace Ember.Handlebars +*/ +Ember.Handlebars.helpers = objectCreate(Handlebars.helpers); - @property classNames - @type Array - @default ['ember-view'] - */ - classNames: ['ember-view'], +/** + Override the the opcode compiler and JavaScript compiler for Handlebars. - /** - A list of properties of the view to apply as class names. If the property - is a string value, the value of that string will be applied as a class - name. + @class Compiler + @namespace Ember.Handlebars + @private + @constructor +*/ +Ember.Handlebars.Compiler = function() {}; - ```javascript - // Applies the 'high' class to the view element - Ember.View.extend({ - classNameBindings: ['priority'] - priority: 'high' - }); - ``` +// Handlebars.Compiler doesn't exist in runtime-only +if (Handlebars.Compiler) { + Ember.Handlebars.Compiler.prototype = objectCreate(Handlebars.Compiler.prototype); +} - If the value of the property is a Boolean, the name of that property is - added as a dasherized class name. +Ember.Handlebars.Compiler.prototype.compiler = Ember.Handlebars.Compiler; - ```javascript - // Applies the 'is-urgent' class to the view element - Ember.View.extend({ - classNameBindings: ['isUrgent'] - isUrgent: true - }); - ``` +/** + @class JavaScriptCompiler + @namespace Ember.Handlebars + @private + @constructor +*/ +Ember.Handlebars.JavaScriptCompiler = function() {}; - If you would prefer to use a custom value instead of the dasherized - property name, you can pass a binding like this: +// Handlebars.JavaScriptCompiler doesn't exist in runtime-only +if (Handlebars.JavaScriptCompiler) { + Ember.Handlebars.JavaScriptCompiler.prototype = objectCreate(Handlebars.JavaScriptCompiler.prototype); + Ember.Handlebars.JavaScriptCompiler.prototype.compiler = Ember.Handlebars.JavaScriptCompiler; +} - ```javascript - // Applies the 'urgent' class to the view element - Ember.View.extend({ - classNameBindings: ['isUrgent:urgent'] - isUrgent: true - }); - ``` - This list of properties is inherited from the view's superclasses as well. +Ember.Handlebars.JavaScriptCompiler.prototype.namespace = "Ember.Handlebars"; - @property classNameBindings - @type Array - @default [] - */ - classNameBindings: EMPTY_ARRAY, +Ember.Handlebars.JavaScriptCompiler.prototype.initializeBuffer = function() { + return "''"; +}; - /** - A list of properties of the view to apply as attributes. If the property is - a string value, the value of that string will be applied as the attribute. +/** + Override the default buffer for Ember Handlebars. By default, Handlebars + creates an empty String at the beginning of each invocation and appends to + it. Ember's Handlebars overrides this to append to a single shared buffer. - ```javascript - // Applies the type attribute to the element - // with the value "button", like
    - Ember.View.extend({ - attributeBindings: ['type'], - type: 'button' - }); - ``` + @private + @method appendToBuffer + @param string {String} +*/ +Ember.Handlebars.JavaScriptCompiler.prototype.appendToBuffer = function(string) { + return "data.buffer.push("+string+");"; +}; - If the value of the property is a Boolean, the name of that property is - added as an attribute. +// Hacks ahead: +// Handlebars presently has a bug where the `blockHelperMissing` hook +// doesn't get passed the name of the missing helper name, but rather +// gets passed the value of that missing helper evaluated on the current +// context, which is most likely `undefined` and totally useless. +// +// So we alter the compiled template function to pass the name of the helper +// instead, as expected. +// +// This can go away once the following is closed: +// https://github.com/wycats/handlebars.js/issues/634 - ```javascript - // Renders something like
    - Ember.View.extend({ - attributeBindings: ['enabled'], - enabled: true - }); - ``` +var DOT_LOOKUP_REGEX = /helpers\.(.*?)\)/, + BRACKET_STRING_LOOKUP_REGEX = /helpers\['(.*?)'/, + INVOCATION_SPLITTING_REGEX = /(.*blockHelperMissing\.call\(.*)(stack[0-9]+)(,.*)/; - @property attributeBindings - */ - attributeBindings: EMPTY_ARRAY, +Ember.Handlebars.JavaScriptCompiler.stringifyLastBlockHelperMissingInvocation = function(source) { + var helperInvocation = source[source.length - 1], + helperName = (DOT_LOOKUP_REGEX.exec(helperInvocation) || BRACKET_STRING_LOOKUP_REGEX.exec(helperInvocation))[1], + matches = INVOCATION_SPLITTING_REGEX.exec(helperInvocation); - // ....................................................... - // CORE DISPLAY METHODS - // + source[source.length - 1] = matches[1] + "'" + helperName + "'" + matches[3]; +}; +var stringifyBlockHelperMissing = Ember.Handlebars.JavaScriptCompiler.stringifyLastBlockHelperMissingInvocation; - /** - @private +var originalBlockValue = Ember.Handlebars.JavaScriptCompiler.prototype.blockValue; +Ember.Handlebars.JavaScriptCompiler.prototype.blockValue = function() { + originalBlockValue.apply(this, arguments); + stringifyBlockHelperMissing(this.source); +}; - Setup a view, but do not finish waking it up. - - configure `childViews` - - register the view with the global views hash, which is used for event - dispatch +var originalAmbiguousBlockValue = Ember.Handlebars.JavaScriptCompiler.prototype.ambiguousBlockValue; +Ember.Handlebars.JavaScriptCompiler.prototype.ambiguousBlockValue = function() { + originalAmbiguousBlockValue.apply(this, arguments); + stringifyBlockHelperMissing(this.source); +}; - @method init - */ - init: function() { - this.elementId = this.elementId || guidFor(this); +/** + Rewrite simple mustaches from `{{foo}}` to `{{bind "foo"}}`. This means that + all simple mustaches in Ember's Handlebars will also set up an observer to + keep the DOM up to date when the underlying property changes. - this._super(); + @private + @method mustache + @for Ember.Handlebars.Compiler + @param mustache +*/ +Ember.Handlebars.Compiler.prototype.mustache = function(mustache) { + if (!(mustache.params.length || mustache.hash)) { + var id = new Handlebars.AST.IdNode([{ part: '_triageMustache' }]); - // setup child views. be sure to clone the child views array first - this._childViews = this._childViews.slice(); + // Update the mustache node to include a hash value indicating whether the original node + // was escaped. This will allow us to properly escape values when the underlying value + // changes and we need to re-render the value. + if (!mustache.escaped) { + mustache.hash = mustache.hash || new Handlebars.AST.HashNode([]); + mustache.hash.pairs.push(["unescaped", new Handlebars.AST.StringNode("true")]); + } + mustache = new Handlebars.AST.MustacheNode([id].concat([mustache.id]), mustache.hash, !mustache.escaped); + } - Ember.assert("Only arrays are allowed for 'classNameBindings'", Ember.typeOf(this.classNameBindings) === 'array'); - this.classNameBindings = Ember.A(this.classNameBindings.slice()); + return Handlebars.Compiler.prototype.mustache.call(this, mustache); +}; - Ember.assert("Only arrays are allowed for 'classNames'", Ember.typeOf(this.classNames) === 'array'); - this.classNames = Ember.A(this.classNames.slice()); - }, +/** + Used for precompilation of Ember Handlebars templates. This will not be used + during normal app execution. - appendChild: function(view, options) { - return this.currentState.appendChild(this, view, options); - }, + @method precompile + @for Ember.Handlebars + @static + @param {String} string The template to precompile +*/ +Ember.Handlebars.precompile = function(string) { + var ast = Handlebars.parse(string); + + var options = { + knownHelpers: { + action: true, + unbound: true, + 'bind-attr': true, + template: true, + view: true, + _triageMustache: true + }, + data: true, + stringParams: true + }; + + var environment = new Ember.Handlebars.Compiler().compile(ast, options); + return new Ember.Handlebars.JavaScriptCompiler().compile(environment, options, undefined, true); +}; +// We don't support this for Handlebars runtime-only +if (Handlebars.compile) { /** - Removes the child view from the parent view. + The entry point for Ember Handlebars. This replaces the default + `Handlebars.compile` and turns on template-local data and String + parameters. - @method removeChild - @param {Ember.View} view - @return {Ember.View} receiver + @method compile + @for Ember.Handlebars + @static + @param {String} string The template to compile + @return {Function} */ - removeChild: function(view) { - // If we're destroying, the entire subtree will be - // freed, and the DOM will be handled separately, - // so no need to mess with childViews. - if (this.isDestroying) { return; } - - // update parent node - set(view, '_parentView', null); + Ember.Handlebars.compile = function(string) { + var ast = Handlebars.parse(string); + var options = { data: true, stringParams: true }; + var environment = new Ember.Handlebars.Compiler().compile(ast, options); + var templateSpec = new Ember.Handlebars.JavaScriptCompiler().compile(environment, options, undefined, true); - // remove view from childViews array. - var childViews = this._childViews; + var template = Ember.Handlebars.template(templateSpec); + template.isMethod = false; //Make sure we don't wrap templates with ._super - Ember.EnumerableUtils.removeObject(childViews, view); + return template; + }; +} - this.propertyDidChange('childViews'); // HUH?! what happened to will change? - return this; - }, +})(); - /** - Removes all children from the `parentView`. +(function() { +var slice = Array.prototype.slice, + originalTemplate = Ember.Handlebars.template; - @method removeAllChildren - @return {Ember.View} receiver - */ - removeAllChildren: function() { - return this.mutateChildViews(function(parentView, view) { - parentView.removeChild(view); - }); - }, +/** + If a path starts with a reserved keyword, returns the root + that should be used. - destroyAllChildren: function() { - return this.mutateChildViews(function(parentView, view) { - view.destroy(); - }); - }, + @private + @method normalizePath + @for Ember + @param root {Object} + @param path {String} + @param data {Hash} +*/ +var normalizePath = Ember.Handlebars.normalizePath = function(root, path, data) { + var keywords = (data && data.keywords) || {}, + keyword, isKeyword; - /** - Removes the view from its `parentView`, if one is found. Otherwise - does nothing. + // Get the first segment of the path. For example, if the + // path is "foo.bar.baz", returns "foo". + keyword = path.split('.', 1)[0]; - @method removeFromParent - @return {Ember.View} receiver - */ - removeFromParent: function() { - var parent = this._parentView; + // Test to see if the first path is a keyword that has been + // passed along in the view's data hash. If so, we will treat + // that object as the new root. + if (keywords.hasOwnProperty(keyword)) { + // Look up the value in the template's data hash. + root = keywords[keyword]; + isKeyword = true; - // Remove DOM element from parent - this.remove(); + // Handle cases where the entire path is the reserved + // word. In that case, return the object itself. + if (path === keyword) { + path = ''; + } else { + // Strip the keyword from the path and look up + // the remainder from the newly found root. + path = path.substr(keyword.length+1); + } + } - if (parent) { parent.removeChild(this); } - return this; - }, + return { root: root, path: path, isKeyword: isKeyword }; +}; - /** - You must call `destroy` on a view to destroy the view (and all of its - child views). This will remove the view from any parent node, then make - sure that the DOM element managed by the view can be released by the - memory manager. - @method destroy - */ - destroy: function() { - var childViews = this._childViews, - // get parentView before calling super because it'll be destroyed - nonVirtualParentView = get(this, 'parentView'), - viewName = this.viewName, - childLen, i; +/** + Lookup both on root and on window. If the path starts with + a keyword, the corresponding object will be looked up in the + template's data hash and used to resolve the path. - if (!this._super()) { return; } + @method get + @for Ember.Handlebars + @param {Object} root The object to look up the property on + @param {String} path The path to be lookedup + @param {Object} options The template's option hash +*/ +var handlebarsGet = Ember.Handlebars.get = function(root, path, options) { + var data = options && options.data, + normalizedPath = normalizePath(root, path, data), + value; - childLen = childViews.length; - for (i=childLen-1; i>=0; i--) { - childViews[i].removedFromDOM = true; - } + + root = normalizedPath.root; + path = normalizedPath.path; - // remove from non-virtual parent view if viewName was specified - if (viewName && nonVirtualParentView) { - nonVirtualParentView.set(viewName, null); - } + value = Ember.get(root, path); - childLen = childViews.length; - for (i=childLen-1; i>=0; i--) { - childViews[i].destroy(); + if (value === undefined && root !== Ember.lookup && Ember.isGlobalPath(path)) { + value = Ember.get(Ember.lookup, path); } + - return this; - }, - - /** - Instantiates a view to be added to the childViews array during view - initialization. You generally will not call this method directly unless - you are overriding `createChildViews()`. Note that this method will - automatically configure the correct settings on the new view instance to - act as a child of the parent. + return value; +}; - @method createChildView - @param {Class|String} viewClass - @param {Hash} [attrs] Attributes to add - @return {Ember.View} new instance - */ - createChildView: function(view, attrs) { - if (!view) { - throw new TypeError("createChildViews first argument must exist"); - } +/** + This method uses `Ember.Handlebars.get` to lookup a value, then ensures + that the value is escaped properly. - if (view.isView && view._parentView === this && view.container === this.container) { - return view; - } + If `unescaped` is a truthy value then the escaping will not be performed. - attrs = attrs || {}; - attrs._parentView = this; + @method getEscaped + @for Ember.Handlebars + @param {Object} root The object to look up the property on + @param {String} path The path to be lookedup + @param {Object} options The template's option hash +*/ +Ember.Handlebars.getEscaped = function(root, path, options) { + var result = handlebarsGet(root, path, options); - if (Ember.CoreView.detect(view)) { - attrs.templateData = attrs.templateData || get(this, 'templateData'); + if (result === null || result === undefined) { + result = ""; + } else if (!(result instanceof Handlebars.SafeString)) { + result = String(result); + } + if (!options.hash.unescaped){ + result = Handlebars.Utils.escapeExpression(result); + } - attrs.container = this.container; - view = view.create(attrs); + return result; +}; - // don't set the property on a virtual view, as they are invisible to - // consumers of the view API - if (view.viewName) { - set(get(this, 'concreteView'), view.viewName, view); - } - } else if ('string' === typeof view) { - var fullName = 'view:' + view; - var View = this.container.lookupFactory(fullName); +Ember.Handlebars.resolveParams = function(context, params, options) { + var resolvedParams = [], types = options.types, param, type; - Ember.assert("Could not find view: '" + fullName + "'", !!View); + for (var i=0, l=params.length; i 1) { - className = split[1]; - if (split.length === 3) { falsyClassName = split[2]; } + Ember.assert("registerBoundHelper-generated helpers do not support use with Handlebars blocks.", !options.fn); - classNames = ':' + className; - if (falsyClassName) { classNames += ":" + falsyClassName; } + // Detect bound options (e.g. countBinding="otherCount") + var boundOptions = hash.boundOptions = {}; + for (hashOption in hash) { + if (Ember.IS_BINDING.test(hashOption)) { + // Lop off 'Binding' suffix. + boundOptions[hashOption.slice(0, -7)] = hash[hashOption]; + } } - return { - path: propertyPath, - classNames: classNames, - className: (className === '') ? undefined : className, - falsyClassName: falsyClassName - }; - }, - - /** - @private - - Get the class name for a given value, based on the path, optional - `className` and optional `falsyClassName`. + // Expose property names on data.properties object. + var watchedProperties = []; + data.properties = []; + for (loc = 0; loc < numProperties; ++loc) { + data.properties.push(properties[loc]); + if (types[loc] === 'ID') { + var normalizedProp = normalizePath(currentContext, properties[loc], data); + normalizedProperties.push(normalizedProp); + watchedProperties.push(normalizedProp); + } else { + if(data.isUnbound) { + normalizedProperties.push({path: properties[loc]}); + }else { + normalizedProperties.push(null); + } + } + } - - if a `className` or `falsyClassName` has been specified: - - if the value is truthy and `className` has been specified, - `className` is returned - - if the value is falsy and `falsyClassName` has been specified, - `falsyClassName` is returned - - otherwise `null` is returned - - if the value is `true`, the dasherized last part of the supplied path - is returned - - if the value is not `false`, `undefined` or `null`, the `value` - is returned - - if none of the above rules apply, `null` is returned + // Handle case when helper invocation is preceded by `unbound`, e.g. + // {{unbound myHelper foo}} + if (data.isUnbound) { + return evaluateUnboundHelper(this, fn, normalizedProperties, options); + } - @method _classStringForValue - @param path - @param val - @param className - @param falsyClassName - @static - */ - _classStringForValue: function(path, val, className, falsyClassName) { - // When using the colon syntax, evaluate the truthiness or falsiness - // of the value to determine which className to return - if (className || falsyClassName) { - if (className && !!val) { - return className; + var bindView = new Ember._SimpleHandlebarsView(null, null, !options.hash.unescaped, options.data); - } else if (falsyClassName && !val) { - return falsyClassName; + // Override SimpleHandlebarsView's method for generating the view's content. + bindView.normalizedValue = function() { + var args = [], boundOption; - } else { - return null; + // Copy over bound hash options. + for (boundOption in boundOptions) { + if (!boundOptions.hasOwnProperty(boundOption)) { continue; } + property = normalizePath(currentContext, boundOptions[boundOption], data); + bindView.path = property.path; + bindView.pathRoot = property.root; + hash[boundOption] = normalizedValue.call(bindView); } - // If value is a Boolean and true, return the dasherized property - // name. - } else if (val === true) { - // Normalize property path to be suitable for use - // as a class name. For exaple, content.foo.barBaz - // becomes bar-baz. - var parts = path.split('.'); - return Ember.String.dasherize(parts[parts.length-1]); + for (loc = 0; loc < numProperties; ++loc) { + property = normalizedProperties[loc]; + if (property) { + bindView.path = property.path; + bindView.pathRoot = property.root; + args.push(normalizedValue.call(bindView)); + } else { + args.push(properties[loc]); + } + } + args.push(options); - // If the value is not false, undefined, or null, return the current - // value of the property. - } else if (val !== false && val != null) { - return val; + // Run the supplied helper function. + return fn.apply(currentContext, args); + }; - // Nothing to display. Return null so that the old class is removed - // but no new class is added. - } else { - return null; + view.appendChild(bindView); + + // Assemble list of watched properties that'll re-render this helper. + for (boundOption in boundOptions) { + if (boundOptions.hasOwnProperty(boundOption)) { + watchedProperties.push(normalizePath(currentContext, boundOptions[boundOption], data)); + } } - } -}); -var mutation = Ember.Object.extend(Ember.Evented).create(); + // Observe each property. + for (loc = 0, len = watchedProperties.length; loc < len; ++loc) { + property = watchedProperties[loc]; + view.registerObserver(property.root, property.path, bindView, bindView.rerender); + } -Ember.View.addMutationListener = function(callback) { - mutation.on('change', callback); -}; + if (types[0] !== 'ID' || normalizedProperties.length === 0) { + return; + } -Ember.View.removeMutationListener = function(callback) { - mutation.off('change', callback); -}; + // Add dependent key observers to the first param + var normalized = normalizedProperties[0], + pathRoot = normalized.root, + path = normalized.path; -Ember.View.notifyMutationListeners = function() { - mutation.trigger('change'); + if(!Ember.isEmpty(path)) { + prefixPathForDependentKeys = path + '.'; + } + for (var i=0, l=dependentKeys.length; isomeString
    ') + ``` + + @method htmlSafe + @for Ember.String + @static + @return {Handlebars.SafeString} a string that will not be html escaped by Handlebars */ +Ember.String.htmlSafe = function(str) { + return new Handlebars.SafeString(str); +}; -var get = Ember.get, set = Ember.set; +var htmlSafe = Ember.String.htmlSafe; -Ember.View.states._default = { - // appendChild is only legal while rendering the buffer. - appendChild: function() { - throw "You can't use appendChild outside of the rendering process"; - }, +if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { - $: function() { - return undefined; - }, + /** + Mark a string as being safe for unescaped output with Handlebars. - getElement: function() { - return null; - }, + ```javascript + '
    someString
    '.htmlSafe() + ``` - // Handle events from `Ember.EventDispatcher` - handleEvent: function() { - return true; // continue event propagation - }, + See [Ember.String.htmlSafe](/api/classes/Ember.String.html#method_htmlSafe). - destroyElement: function(view) { - set(view, 'element', null); - if (view._scheduledInsert) { - Ember.run.cancel(view._scheduledInsert); - view._scheduledInsert = null; - } - return view; - }, + @method htmlSafe + @for String + @return {Handlebars.SafeString} a string that will not be html escaped by Handlebars + */ + String.prototype.htmlSafe = function() { + return htmlSafe(this); + }; +} - renderToBufferIfNeeded: function () { - return false; - }, +})(); - rerender: Ember.K, - invokeObserver: Ember.K + + +(function() { +Ember.Handlebars.resolvePaths = function(options) { + var ret = [], + contexts = options.contexts, + roots = options.roots, + data = options.data; + + for (var i=0, l=contexts.length; i` the following code - ```javascript - aContainer = Ember.ContainerView.create({ - classNames: ['the-container'], - childViews: ['aView', 'bView'], - aView: Ember.View.create({ - template: Ember.Handlebars.compile("A") - }), - bView: Ember.View.create({ - template: Ember.Handlebars.compile("B") - }) - }); +(function() { +/** +@module ember +@submodule ember-handlebars +*/ - aContainer.appendTo('body'); - ``` +var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; +var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.normalizePath; +var handlebarsGetEscaped = Ember.Handlebars.getEscaped; +var forEach = Ember.ArrayPolyfills.forEach; +var o_create = Ember.create; - Results in the HTML +var EmberHandlebars = Ember.Handlebars, helpers = EmberHandlebars.helpers; - ```html -
    -
    A
    -
    B
    -
    - ``` +function exists(value) { + return !Ember.isNone(value); +} - Removing a view +// Binds a property into the DOM. This will create a hook in DOM that the +// KVO system will look for and update if the property changes. +function bind(property, options, preserveContext, shouldDisplay, valueNormalizer, childProperties) { + var data = options.data, + fn = options.fn, + inverse = options.inverse, + view = data.view, + currentContext = this, + normalized, observer, i; - ```javascript - aContainer.toArray(); // [aContainer.aView, aContainer.bView] - aContainer.removeObject(aContainer.get('bView')); - aContainer.toArray(); // [aContainer.aView] - ``` + normalized = normalizePath(currentContext, property, data); - Will result in the following HTML + // Set up observers for observable objects + if ('object' === typeof this) { + if (data.insideGroup) { + observer = function() { + Ember.run.once(view, 'rerender'); + }; - ```html -
    -
    A
    -
    - ``` + var template, context, result = handlebarsGet(currentContext, property, options); - Similarly, adding a child view is accomplished by adding `Ember.View` instances to the - container view. + result = valueNormalizer ? valueNormalizer(result) : result; - Given an empty `` the following code + context = preserveContext ? currentContext : result; + if (shouldDisplay(result)) { + template = fn; + } else if (inverse) { + template = inverse; + } - ```javascript - aContainer = Ember.ContainerView.create({ - classNames: ['the-container'], - childViews: ['aView', 'bView'], - aView: Ember.View.create({ - template: Ember.Handlebars.compile("A") - }), - bView: Ember.View.create({ - template: Ember.Handlebars.compile("B") - }) - }); + template(context, { data: options.data }); + } else { + // Create the view that will wrap the output of this template/property + // and add it to the nearest view's childViews array. + // See the documentation of Ember._HandlebarsBoundView for more. + var bindView = view.createChildView(Ember._HandlebarsBoundView, { + preserveContext: preserveContext, + shouldDisplayFunc: shouldDisplay, + valueNormalizerFunc: valueNormalizer, + displayTemplate: fn, + inverseTemplate: inverse, + path: property, + pathRoot: currentContext, + previousContext: currentContext, + isEscaped: !options.hash.unescaped, + templateData: options.data + }); - aContainer.appendTo('body'); - ``` + if (options.hash.controller) { + bindView.set('_contextController', this.container.lookupFactory('controller:'+options.hash.controller).create({ + container: currentContext.container, + parentController: currentContext, + target: currentContext + })); + } - Results in the HTML + view.appendChild(bindView); - ```html -
    -
    A
    -
    B
    -
    - ``` + observer = function() { + Ember.run.scheduleOnce('render', bindView, 'rerenderIfNeeded'); + }; + } - Adding a view + // Observes the given property on the context and + // tells the Ember._HandlebarsBoundView to re-render. If property + // is an empty string, we are printing the current context + // object ({{this}}) so updating it is not our responsibility. + if (normalized.path !== '') { + view.registerObserver(normalized.root, normalized.path, observer); + if (childProperties) { + for (i=0; i] - ``` +function simpleBind(currentContext, property, options) { + var data = options.data, + view = data.view, + normalized, observer, pathRoot, output; - Will result in the following HTML + normalized = normalizePath(currentContext, property, data); + pathRoot = normalized.root; - ```html -
    -
    A
    -
    B
    -
    Another view
    -
    - ``` + // Set up observers for observable objects + if (pathRoot && ('object' === typeof pathRoot)) { + if (data.insideGroup) { + observer = function() { + Ember.run.once(view, 'rerender'); + }; - ## Templates and Layout + output = handlebarsGetEscaped(currentContext, property, options); - A `template`, `templateName`, `defaultTemplate`, `layout`, `layoutName` or - `defaultLayout` property on a container view will not result in the template - or layout being rendered. The HTML contents of a `Ember.ContainerView`'s DOM - representation will only be the rendered HTML of its child views. + data.buffer.push(output); + } else { + var bindView = new Ember._SimpleHandlebarsView( + property, currentContext, !options.hash.unescaped, options.data + ); + + bindView._parentView = view; + view.appendChild(bindView); + + observer = function() { + Ember.run.scheduleOnce('render', bindView, 'rerender'); + }; + } + + // Observes the given property on the context and + // tells the Ember._HandlebarsBoundView to re-render. If property + // is an empty string, we are printing the current context + // object ({{this}}) so updating it is not our responsibility. + if (normalized.path !== '') { + view.registerObserver(normalized.root, normalized.path, observer); + } + } else { + // The object is not observable, so just render it out and + // be done with it. + output = handlebarsGetEscaped(currentContext, property, options); + data.buffer.push(output); + } +} - @class ContainerView - @namespace Ember - @extends Ember.View -*/ -Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { - states: states, +function shouldDisplayIfHelperContent(result) { + var truthy = result && get(result, 'isTruthy'); + if (typeof truthy === 'boolean') { return truthy; } - init: function() { - this._super(); + if (Ember.isArray(result)) { + return get(result, 'length') !== 0; + } else { + return !!result; + } +} - var childViews = get(this, 'childViews'); +/** + '_triageMustache' is used internally select between a binding, helper, or component for + the given context. Until this point, it would be hard to determine if the + mustache is a property reference or a regular helper reference. This triage + helper resolves that. - // redefine view's childViews property that was obliterated - Ember.defineProperty(this, 'childViews', Ember.View.childViewsProperty); + This would not be typically invoked by directly. - var _childViews = this._childViews; + @private + @method _triageMustache + @for Ember.Handlebars.helpers + @param {String} property Property/helperID to triage + @param {Object} options hash of template/rendering options + @return {String} HTML string +*/ +EmberHandlebars.registerHelper('_triageMustache', function(property, options) { + Ember.assert("You cannot pass more than one argument to the _triageMustache helper", arguments.length <= 2); - forEach(childViews, function(viewName, idx) { - var view; + if (helpers[property]) { + return helpers[property].call(this, options); + } - if ('string' === typeof viewName) { - view = get(this, viewName); - view = this.createChildView(view); - set(this, viewName, view); - } else { - view = this.createChildView(viewName); - } + var helper = Ember.Handlebars.resolveHelper(options.data.view.container, property); + if (helper) { + return helper.call(this, options); + } - _childViews[idx] = view; - }, this); + return helpers.bind.call(this, property, options); +}); - var currentView = get(this, 'currentView'); - if (currentView) { - if (!_childViews.length) { _childViews = this._childViews = this._childViews.slice(); } - _childViews.push(this.createChildView(currentView)); - } - }, +Ember.Handlebars.resolveHelper = function(container, name) { - replace: function(idx, removedCount, addedViews) { - var addedCount = addedViews ? get(addedViews, 'length') : 0; - var self = this; - Ember.assert("You can't add a child to a container that is already a child of another view", Ember.A(addedViews).every(function(item) { return !get(item, '_parentView') || get(item, '_parentView') === self; })); + if (!container || name.indexOf('-') === -1) { + return; + } - this.arrayContentWillChange(idx, removedCount, addedCount); - this.childViewsWillChange(this._childViews, idx, removedCount); + var helper = container.lookup('helper:' + name); + if (!helper) { + var componentLookup = container.lookup('component-lookup:main'); + Ember.assert("Could not find 'component-lookup:main' on the provided container, which is necessary for performing component lookups", componentLookup); - if (addedCount === 0) { - this._childViews.splice(idx, removedCount) ; - } else { - var args = [idx, removedCount].concat(addedViews); - if (addedViews.length && !this._childViews.length) { this._childViews = this._childViews.slice(); } - this._childViews.splice.apply(this._childViews, args); + var Component = componentLookup.lookupFactory(name, container); + if (Component) { + helper = EmberHandlebars.makeViewHelper(Component); + container.register('helper:' + name, helper); } + } + return helper; +}; - this.arrayContentDidChange(idx, removedCount, addedCount); - this.childViewsDidChange(this._childViews, idx, removedCount, addedCount); +/** + `bind` can be used to display a value, then update that value if it + changes. For example, if you wanted to print the `title` property of + `content`: - return this; - }, + ```handlebars + {{bind "content.title"}} + ``` - objectAt: function(idx) { - return this._childViews[idx]; - }, + This will return the `title` property as a string, then create a new observer + at the specified path. If it changes, it will update the value in DOM. Note + that if you need to support IE7 and IE8 you must modify the model objects + properties using `Ember.get()` and `Ember.set()` for this to work as it + relies on Ember's KVO system. For all other browsers this will be handled for + you automatically. - length: Ember.computed(function () { - return this._childViews.length; - }).volatile(), + @private + @method bind + @for Ember.Handlebars.helpers + @param {String} property Property to bind + @param {Function} fn Context to provide for rendering + @return {String} HTML string +*/ +EmberHandlebars.registerHelper('bind', function bindHelper(property, options) { + Ember.assert("You cannot pass more than one argument to the bind helper", arguments.length <= 2); - /** - @private + var context = (options.contexts && options.contexts.length) ? options.contexts[0] : this; - Instructs each child view to render to the passed render buffer. + if (!options.fn) { + return simpleBind(context, property, options); + } - @method render - @param {Ember.RenderBuffer} buffer the buffer to render to - */ - render: function(buffer) { - this.forEachChildView(function(view) { - view.renderToBuffer(buffer); - }); - }, + return bind.call(context, property, options, false, exists); +}); - instrumentName: 'container', +/** + Use the `boundIf` helper to create a conditional that re-evaluates + whenever the truthiness of the bound value changes. - /** - @private + ```handlebars + {{#boundIf "content.shouldDisplayTitle"}} + {{content.title}} + {{/boundIf}} + ``` - When a child view is removed, destroy its element so that - it is removed from the DOM. + @private + @method boundIf + @for Ember.Handlebars.helpers + @param {String} property Property to bind + @param {Function} fn Context to provide for rendering + @return {String} HTML string +*/ +EmberHandlebars.registerHelper('boundIf', function boundIfHelper(property, fn) { + var context = (fn.contexts && fn.contexts.length) ? fn.contexts[0] : this; - The array observer that triggers this action is set up in the - `renderToBuffer` method. + return bind.call(context, property, fn, true, shouldDisplayIfHelperContent, shouldDisplayIfHelperContent, ['isTruthy', 'length']); +}); - @method childViewsWillChange - @param {Ember.Array} views the child views array before mutation - @param {Number} start the start position of the mutation - @param {Number} removed the number of child views removed - **/ - childViewsWillChange: function(views, start, removed) { - this.propertyWillChange('childViews'); - if (removed > 0) { - var changedViews = views.slice(start, start+removed); - // transition to preRender before clearing parentView - this.currentState.childViewsWillChange(this, views, start, removed); - this.initializeViews(changedViews, null, null); - } - }, +/** + @private - removeChild: function(child) { - this.removeObject(child); - return this; - }, + Use the `unboundIf` helper to create a conditional that evaluates once. - /** - @private + ```handlebars + {{#unboundIf "content.shouldDisplayTitle"}} + {{content.title}} + {{/unboundIf}} + ``` - When a child view is added, make sure the DOM gets updated appropriately. + @method unboundIf + @for Ember.Handlebars.helpers + @param {String} property Property to bind + @param {Function} fn Context to provide for rendering + @return {String} HTML string +*/ +EmberHandlebars.registerHelper('unboundIf', function unboundIfHelper(property, fn) { + var context = (fn.contexts && fn.contexts.length) ? fn.contexts[0] : this, + data = fn.data, + template = fn.fn, + inverse = fn.inverse, + normalized, propertyValue, result; - If the view has already rendered an element, we tell the child view to - create an element and insert it into the DOM. If the enclosing container - view has already written to a buffer, but not yet converted that buffer - into an element, we insert the string representation of the child into the - appropriate place in the buffer. + normalized = normalizePath(context, property, data); + propertyValue = handlebarsGet(context, property, fn); - @method childViewsDidChange - @param {Ember.Array} views the array of child views afte the mutation has occurred - @param {Number} start the start position of the mutation - @param {Number} removed the number of child views removed - @param {Number} the number of child views added - */ - childViewsDidChange: function(views, start, removed, added) { - if (added > 0) { - var changedViews = views.slice(start, start+added); - this.initializeViews(changedViews, this, get(this, 'templateData')); - this.currentState.childViewsDidChange(this, views, start, added); - } - this.propertyDidChange('childViews'); - }, + if (!shouldDisplayIfHelperContent(propertyValue)) { + template = inverse; + } - initializeViews: function(views, parentView, templateData) { - forEach(views, function(view) { - set(view, '_parentView', parentView); + template(context, { data: data }); +}); - if (!view.container && parentView) { - set(view, 'container', parentView.container); - } +/** + Use the `{{with}}` helper when you want to scope context. Take the following code as an example: - if (!get(view, 'templateData')) { - set(view, 'templateData', templateData); - } - }); - }, + ```handlebars +
    {{user.name}}
    - currentView: null, +
    +
    {{user.role.label}}
    + {{user.role.id}} - _currentViewWillChange: Ember.beforeObserver(function() { - var currentView = get(this, 'currentView'); - if (currentView) { - currentView.destroy(); - } - }, 'currentView'), +

    {{user.role.description}}

    +
    + ``` - _currentViewDidChange: Ember.observer(function() { - var currentView = get(this, 'currentView'); - if (currentView) { - Ember.assert("You tried to set a current view that already has a parent. Make sure you don't have multiple outlets in the same view.", !get(currentView, '_parentView')); - this.pushObject(currentView); - } - }, 'currentView'), + `{{with}}` can be our best friend in these cases, + instead of writing `user.role.*` over and over, we use `{{#with user.role}}`. + Now the context within the `{{#with}} .. {{/with}}` block is `user.role` so you can do the following: - _ensureChildrenAreInDOM: function () { - this.currentState.ensureChildrenAreInDOM(this); - } -}); + ```handlebars +
    {{user.name}}
    -Ember.merge(states._default, { - childViewsWillChange: Ember.K, - childViewsDidChange: Ember.K, - ensureChildrenAreInDOM: Ember.K -}); +
    + {{#with user.role}} +
    {{label}}
    + {{id}} -Ember.merge(states.inBuffer, { - childViewsDidChange: function(parentView, views, start, added) { - throw new Ember.Error('You cannot modify child views while in the inBuffer state'); - } -}); +

    {{description}}

    + {{/with}} +
    + ``` -Ember.merge(states.hasElement, { - childViewsWillChange: function(view, views, start, removed) { - for (var i=start; i + There are {{blogPosts.length}} blog posts written by {{user.name}}. +
    - for (i = 0, len = childViews.length; i < len; i++) { - childView = childViews[i]; + {{#each post in blogPosts}} +
  • {{post.title}}
  • + {{/each}} + {{/with}} + ``` - if (!buffer) { buffer = Ember.RenderBuffer(); buffer._hasElement = false; } + Without the `as` operator, it would be impossible to reference `user.name` in the example above. - if (childView.renderToBufferIfNeeded(buffer)) { - viewCollection.push(childView); - } else if (viewCollection.length) { - insertViewCollection(view, viewCollection, previous, buffer); - buffer = null; - previous = childView; - viewCollection.clear(); - } else { - previous = childView; - } - } + NOTE: The alias should not reuse a name from the bound property path. + For example: `{{#with foo.bar as foo}}` is not supported because it attempts to alias using + the first part of the property path, `foo`. Instead, use `{{#with foo.bar as baz}}`. - if (viewCollection.length) { - insertViewCollection(view, viewCollection, previous, buffer); - } - } -}); + ### `controller` option -function insertViewCollection(view, viewCollection, previous, buffer) { - viewCollection.triggerRecursively('willInsertElement'); + Adding `controller='something'` instructs the `{{with}}` helper to create and use an instance of + the specified controller with the new context as its content. - if (previous) { - previous.domManager.after(previous, buffer.string()); - } else { - view.domManager.prepend(view, buffer.string()); - } + This is very similar to using an `itemController` option with the `{{each}}` helper. - viewCollection.forEach(function(v) { - v.transitionTo('inDOM'); - v.propertyDidChange('element'); - v.triggerRecursively('didInsertElement'); - }); -} + ```handlebars + {{#with users.posts controller='userBlogPosts'}} + {{!- The current context is wrapped in our controller instance }} + {{/with}} + ``` + In the above example, the template provided to the `{{with}}` block is now wrapped in the + `userBlogPost` controller, which provides a very elegant way to decorate the context with custom + functions/properties. -})(); + @method with + @for Ember.Handlebars.helpers + @param {Function} context + @param {Hash} options + @return {String} HTML string +*/ +EmberHandlebars.registerHelper('with', function withHelper(context, options) { + if (arguments.length === 4) { + var keywordName, path, rootPath, normalized, contextPath; + Ember.assert("If you pass more than one argument to the with helper, it must be in the form #with foo as bar", arguments[1] === "as"); + options = arguments[3]; + keywordName = arguments[2]; + path = arguments[0]; + Ember.assert("You must pass a block to the with helper", options.fn && options.fn !== Handlebars.VM.noop); -(function() { -/** -@module ember -@submodule ember-views -*/ + var localizedOptions = o_create(options); + localizedOptions.data = o_create(options.data); + localizedOptions.data.keywords = o_create(options.data.keywords || {}); -var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; + if (Ember.isGlobalPath(path)) { + contextPath = path; + } else { + normalized = normalizePath(this, path, options.data); + path = normalized.path; + rootPath = normalized.root; -/** - `Ember.CollectionView` is an `Ember.View` descendent responsible for managing - a collection (an array or array-like object) by maintaining a child view object - and associated DOM representation for each item in the array and ensuring - that child views and their associated rendered HTML are updated when items in - the array are added, removed, or replaced. + // This is a workaround for the fact that you cannot bind separate objects + // together. When we implement that functionality, we should use it here. + var contextKey = Ember.$.expando + Ember.guidFor(rootPath); + localizedOptions.data.keywords[contextKey] = rootPath; + // if the path is '' ("this"), just bind directly to the current context + contextPath = path ? contextKey + '.' + path : contextKey; + } - ## Setting content + Ember.bind(localizedOptions.data.keywords, keywordName, contextPath); - The managed collection of objects is referenced as the `Ember.CollectionView` - instance's `content` property. + return bind.call(this, path, localizedOptions, true, exists); + } else { + Ember.assert("You must pass exactly one argument to the with helper", arguments.length === 2); + Ember.assert("You must pass a block to the with helper", options.fn && options.fn !== Handlebars.VM.noop); + return helpers.bind.call(options.contexts[0], context, options); + } +}); - ```javascript - someItemsView = Ember.CollectionView.create({ - content: ['A', 'B','C'] - }) - ``` - The view for each item in the collection will have its `content` property set - to the item. +/** + See [boundIf](/api/classes/Ember.Handlebars.helpers.html#method_boundIf) + and [unboundIf](/api/classes/Ember.Handlebars.helpers.html#method_unboundIf) - ## Specifying itemViewClass + @method if + @for Ember.Handlebars.helpers + @param {Function} context + @param {Hash} options + @return {String} HTML string +*/ +EmberHandlebars.registerHelper('if', function ifHelper(context, options) { + Ember.assert("You must pass exactly one argument to the if helper", arguments.length === 2); + Ember.assert("You must pass a block to the if helper", options.fn && options.fn !== Handlebars.VM.noop); + if (options.data.isUnbound) { + return helpers.unboundIf.call(options.contexts[0], context, options); + } else { + return helpers.boundIf.call(options.contexts[0], context, options); + } +}); - By default the view class for each item in the managed collection will be an - instance of `Ember.View`. You can supply a different class by setting the - `CollectionView`'s `itemViewClass` property. +/** + @method unless + @for Ember.Handlebars.helpers + @param {Function} context + @param {Hash} options + @return {String} HTML string +*/ +EmberHandlebars.registerHelper('unless', function unlessHelper(context, options) { + Ember.assert("You must pass exactly one argument to the unless helper", arguments.length === 2); + Ember.assert("You must pass a block to the unless helper", options.fn && options.fn !== Handlebars.VM.noop); - Given an empty `` and the following code: + var fn = options.fn, inverse = options.inverse; - ```javascript - someItemsView = Ember.CollectionView.create({ - classNames: ['a-collection'], - content: ['A','B','C'], - itemViewClass: Ember.View.extend({ - template: Ember.Handlebars.compile("the letter: {{view.content}}") - }) - }); + options.fn = inverse; + options.inverse = fn; - someItemsView.appendTo('body'); - ``` + if (options.data.isUnbound) { + return helpers.unboundIf.call(options.contexts[0], context, options); + } else { + return helpers.boundIf.call(options.contexts[0], context, options); + } +}); - Will result in the following HTML structure +/** + `bind-attr` allows you to create a binding between DOM element attributes and + Ember objects. For example: - ```html -
    -
    the letter: A
    -
    the letter: B
    -
    the letter: C
    -
    + ```handlebars + imageTitle ``` - ## Automatic matching of parent/child tagNames - - Setting the `tagName` property of a `CollectionView` to any of - "ul", "ol", "table", "thead", "tbody", "tfoot", "tr", or "select" will result - in the item views receiving an appropriately matched `tagName` property. + The above handlebars template will fill the ``'s `src` attribute will + the value of the property referenced with `"imageUrl"` and its `alt` + attribute with the value of the property referenced with `"imageTitle"`. - Given an empty `` and the following code: + If the rendering context of this template is the following object: ```javascript - anUndorderedListView = Ember.CollectionView.create({ - tagName: 'ul', - content: ['A','B','C'], - itemViewClass: Ember.View.extend({ - template: Ember.Handlebars.compile("the letter: {{view.content}}") - }) - }); - - anUndorderedListView.appendTo('body'); + { + imageUrl: 'http://lolcats.info/haz-a-funny', + imageTitle: 'A humorous image of a cat' + } ``` - Will result in the following HTML structure + The resulting HTML output will be: ```html -
      -
    • the letter: A
    • -
    • the letter: B
    • -
    • the letter: C
    • -
    + A humorous image of a cat ``` - Additional `tagName` pairs can be provided by adding to - `Ember.CollectionView.CONTAINER_MAP ` + `bind-attr` cannot redeclare existing DOM element attributes. The use of `src` + in the following `bind-attr` example will be ignored and the hard coded value + of `src="/failwhale.gif"` will take precedence: - ```javascript - Ember.CollectionView.CONTAINER_MAP['article'] = 'section' + ```handlebars + imageTitle ``` - ## Programatic creation of child views - - For cases where additional customization beyond the use of a single - `itemViewClass` or `tagName` matching is required CollectionView's - `createChildView` method can be overidden: + ### `bind-attr` and the `class` attribute - ```javascript - CustomCollectionView = Ember.CollectionView.extend({ - createChildView: function(viewClass, attrs) { - if (attrs.content.kind == 'album') { - viewClass = App.AlbumView; - } else { - viewClass = App.SongView; - } - return this._super(viewClass, attrs); - } - }); - ``` + `bind-attr` supports a special syntax for handling a number of cases unique + to the `class` DOM element attribute. The `class` attribute combines + multiple discrete values into a single attribute as a space-delimited + list of strings. Each string can be: - ## Empty View + * a string return value of an object's property. + * a boolean return value of an object's property + * a hard-coded value - You can provide an `Ember.View` subclass to the `Ember.CollectionView` - instance as its `emptyView` property. If the `content` property of a - `CollectionView` is set to `null` or an empty array, an instance of this view - will be the `CollectionView`s only child. + A string return value works identically to other uses of `bind-attr`. The + return value of the property will become the value of the attribute. For + example, the following view and template: ```javascript - aListWithNothing = Ember.CollectionView.create({ - classNames: ['nothing'] - content: null, - emptyView: Ember.View.extend({ - template: Ember.Handlebars.compile("The collection is empty") + AView = Ember.View.extend({ + someProperty: function() { + return "aValue"; + }.property() }) - }); + ``` - aListWithNothing.appendTo('body'); + ```handlebars + -
    - The collection is empty -
    -
    + ``` - ## Adding and Removing items - - The `childViews` property of a `CollectionView` should not be directly - manipulated. Instead, add, remove, replace items from its `content` property. - This will trigger appropriate changes to its rendered HTML. - - - @class CollectionView - @namespace Ember - @extends Ember.ContainerView - @since Ember 0.9 -*/ -Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionView.prototype */ { - - /** - A list of items to be displayed by the `Ember.CollectionView`. - - @property content - @type Ember.Array - @default null - */ - content: null, + A boolean return value will insert a specified class name if the property + returns `true` and remove the class name if the property returns `false`. - /** - @private + A class name is provided via the syntax + `somePropertyName:class-name-if-true`. - This provides metadata about what kind of empty view class this - collection would like if it is being instantiated from another - system (like Handlebars) + ```javascript + AView = Ember.View.extend({ + someBool: true + }) + ``` - @property emptyViewClass - */ - emptyViewClass: Ember.View, + ```handlebars + + ``` - /** - An optional view to display if content is set to an empty array. + Result in the following rendered output: - @property emptyView - @type Ember.View - @default null - */ - emptyView: null, + ```html + + ``` - /** - @property itemViewClass - @type Ember.View - @default Ember.View - */ - itemViewClass: Ember.View, + An additional section of the binding can be provided if you want to + replace the existing class instead of removing it when the boolean + value changes: - /** - Setup a CollectionView + ```handlebars + + ``` - @method init - */ - init: function() { - var ret = this._super(); - this._contentDidChange(); - return ret; - }, + A hard-coded value can be used by prepending `:` to the desired + class name: `:class-name-to-always-apply`. - /** - @private + ```handlebars + + ``` - Invoked when the content property is about to change. Notifies observers that the - entire array content will change. + Results in the following rendered output: - @method _contentWillChange - */ - _contentWillChange: Ember.beforeObserver(function() { - var content = this.get('content'); + ```html + + ``` - if (content) { content.removeArrayObserver(this); } - var len = content ? get(content, 'length') : 0; - this.arrayWillChange(content, 0, len); - }, 'content'), + All three strategies - string return value, boolean return value, and + hard-coded value – can be combined in a single declaration: - /** - @private + ```handlebars + + ``` - Check to make sure that the content has changed, and if so, - update the children directly. This is always scheduled - asynchronously, to allow the element to be created before - bindings have synchronized and vice versa. + @method bind-attr + @for Ember.Handlebars.helpers + @param {Hash} options + @return {String} HTML string +*/ +EmberHandlebars.registerHelper('bind-attr', function bindAttrHelper(options) { - @method _contentDidChange - */ - _contentDidChange: Ember.observer(function() { - var content = get(this, 'content'); + var attrs = options.hash; - if (content) { - this._assertArrayLike(content); - content.addArrayObserver(this); - } + Ember.assert("You must specify at least one hash argument to bind-attr", !!Ember.keys(attrs).length); - var len = content ? get(content, 'length') : 0; - this.arrayDidChange(content, 0, null, len); - }, 'content'), + var view = options.data.view; + var ret = []; + var ctx = this; - /** - @private + // Generate a unique id for this element. This will be added as a + // data attribute to the element so it can be looked up when + // the bound property changes. + var dataId = ++Ember.uuid; - Ensure that the content implements Ember.Array + // Handle classes differently, as we can bind multiple classes + var classBindings = attrs['class']; + if (classBindings != null) { + var classResults = EmberHandlebars.bindClasses(this, classBindings, view, dataId, options); - @method _assertArrayLike - */ - _assertArrayLike: function(content) { - Ember.assert(fmt("an Ember.CollectionView's content must implement Ember.Array. You passed %@", [content]), Ember.Array.detect(content)); - }, + ret.push('class="' + Handlebars.Utils.escapeExpression(classResults.join(' ')) + '"'); + delete attrs['class']; + } - /** - Removes the content and content observers. + var attrKeys = Ember.keys(attrs); - @method destroy - */ - destroy: function() { - if (!this._super()) { return; } + // For each attribute passed, create an observer and emit the + // current value of the property as an attribute. + forEach.call(attrKeys, function(attr) { + var path = attrs[attr], + normalized; - var content = get(this, 'content'); - if (content) { content.removeArrayObserver(this); } + Ember.assert(fmt("You must provide an expression as the value of bound attribute. You specified: %@=%@", [attr, path]), typeof path === 'string'); - if (this._createdEmptyView) { - this._createdEmptyView.destroy(); - } + normalized = normalizePath(ctx, path, options.data); - return this; - }, + var value = (path === 'this') ? normalized.root : handlebarsGet(ctx, path, options), + type = Ember.typeOf(value); - /** - Called when a mutation to the underlying content array will occur. + Ember.assert(fmt("Attributes must be numbers, strings or booleans, not %@", [value]), value === null || value === undefined || type === 'number' || type === 'string' || type === 'boolean'); - This method will remove any views that are no longer in the underlying - content array. + var observer, invoker; - Invokes whenever the content array itself will change. + observer = function observer() { + var result = handlebarsGet(ctx, path, options); - @method arrayWillChange - @param {Array} content the managed collection of objects - @param {Number} start the index at which the changes will occurr - @param {Number} removed number of object to be removed from content - */ - arrayWillChange: function(content, start, removedCount) { - // If the contents were empty before and this template collection has an - // empty view remove it now. - var emptyView = get(this, 'emptyView'); - if (emptyView && emptyView instanceof Ember.View) { - emptyView.removeFromParent(); - } + Ember.assert(fmt("Attributes must be numbers, strings or booleans, not %@", [result]), + result === null || result === undefined || typeof result === 'number' || + typeof result === 'string' || typeof result === 'boolean'); - // Loop through child views that correspond with the removed items. - // Note that we loop from the end of the array to the beginning because - // we are mutating it as we go. - var childViews = this._childViews, childView, idx, len; + var elem = view.$("[data-bindattr-" + dataId + "='" + dataId + "']"); - len = this._childViews.length; + // If we aren't able to find the element, it means the element + // to which we were bound has been removed from the view. + // In that case, we can assume the template has been re-rendered + // and we need to clean up the observer. + if (!elem || elem.length === 0) { + Ember.removeObserver(normalized.root, normalized.path, invoker); + return; + } - var removingAll = removedCount === len; + Ember.View.applyAttributeBindings(elem, attr, result); + }; - if (removingAll) { - this.currentState.empty(this); - this.invokeRecursively(function(view) { - view.removedFromDOM = true; - }, false); + // Add an observer to the view for when the property changes. + // When the observer fires, find the element using the + // unique data id and update the attribute to the new value. + // Note: don't add observer when path is 'this' or path + // is whole keyword e.g. {{#each x in list}} ... {{bind-attr attr="x"}} + if (path !== 'this' && !(normalized.isKeyword && normalized.path === '' )) { + view.registerObserver(normalized.root, normalized.path, observer); } - for (idx = start + removedCount - 1; idx >= start; idx--) { - childView = childViews[idx]; - childView.destroy(); + // if this changes, also change the logic in ember-views/lib/views/view.js + if ((type === 'string' || (type === 'number' && !isNaN(value)))) { + ret.push(attr + '="' + Handlebars.Utils.escapeExpression(value) + '"'); + } else if (value && type === 'boolean') { + // The developer controls the attr name, so it should always be safe + ret.push(attr + '="' + attr + '"'); } - }, + }, this); - /** - Called when a mutation to the underlying content array occurs. + // Add the unique identifier + // NOTE: We use all lower-case since Firefox has problems with mixed case in SVG + ret.push('data-bindattr-' + dataId + '="' + dataId + '"'); + return new EmberHandlebars.SafeString(ret.join(' ')); +}); - This method will replay that mutation against the views that compose the - `Ember.CollectionView`, ensuring that the view reflects the model. +/** + See `bind-attr` - This array observer is added in `contentDidChange`. + @method bindAttr + @for Ember.Handlebars.helpers + @deprecated + @param {Function} context + @param {Hash} options + @return {String} HTML string +*/ +EmberHandlebars.registerHelper('bindAttr', function bindAttrHelper() { + Ember.warn("The 'bindAttr' view helper is deprecated in favor of 'bind-attr'"); + return EmberHandlebars.helpers['bind-attr'].apply(this, arguments); +}); - @method arrayDidChange - @param {Array} content the managed collection of objects - @param {Number} start the index at which the changes occurred - @param {Number} removed number of object removed from content - @param {Number} added number of object added to content - */ - arrayDidChange: function(content, start, removed, added) { - var addedViews = [], view, item, idx, len, itemViewClass, - emptyView; +/** + Helper that, given a space-separated string of property paths and a context, + returns an array of class names. Calling this method also has the side + effect of setting up observers at those property paths, such that if they + change, the correct class name will be reapplied to the DOM element. - len = content ? get(content, 'length') : 0; + For example, if you pass the string "fooBar", it will first look up the + "fooBar" value of the context. If that value is true, it will add the + "foo-bar" class to the current element (i.e., the dasherized form of + "fooBar"). If the value is a string, it will add that string as the class. + Otherwise, it will not add any new class name. - if (len) { - itemViewClass = get(this, 'itemViewClass'); + @private + @method bindClasses + @for Ember.Handlebars + @param {Ember.Object} context The context from which to lookup properties + @param {String} classBindings A string, space-separated, of class bindings + to use + @param {Ember.View} view The view in which observers should look for the + element to update + @param {Srting} bindAttrId Optional bindAttr id used to lookup elements + @return {Array} An array of class names to add +*/ +EmberHandlebars.bindClasses = function(context, classBindings, view, bindAttrId, options) { + var ret = [], newClass, value, elem; - if ('string' === typeof itemViewClass) { - itemViewClass = get(itemViewClass) || itemViewClass; - } + // Helper method to retrieve the property from the context and + // determine which class string to return, based on whether it is + // a Boolean or not. + var classStringForPath = function(root, parsedPath, options) { + var val, + path = parsedPath.path; - Ember.assert(fmt("itemViewClass must be a subclass of Ember.View, not %@", [itemViewClass]), 'string' === typeof itemViewClass || Ember.View.detect(itemViewClass)); + if (path === 'this') { + val = root; + } else if (path === '') { + val = true; + } else { + val = handlebarsGet(root, path, options); + } - for (idx = start; idx < start+added; idx++) { - item = content.objectAt(idx); + return Ember.View._classStringForValue(path, val, parsedPath.className, parsedPath.falsyClassName); + }; - view = this.createChildView(itemViewClass, { - content: item, - contentIndex: idx - }); + // For each property passed, loop through and setup + // an observer. + forEach.call(classBindings.split(' '), function(binding) { - addedViews.push(view); - } - } else { - emptyView = get(this, 'emptyView'); + // Variable in which the old class value is saved. The observer function + // closes over this variable, so it knows which string to remove when + // the property changes. + var oldClass; - if (!emptyView) { return; } + var observer, invoker; - if ('string' === typeof emptyView) { - emptyView = get(emptyView) || emptyView; - } + var parsedPath = Ember.View._parsePropertyPath(binding), + path = parsedPath.path, + pathRoot = context, + normalized; - emptyView = this.createChildView(emptyView); - addedViews.push(emptyView); - set(this, 'emptyView', emptyView); + if (path !== '' && path !== 'this') { + normalized = normalizePath(context, path, options.data); - if (Ember.CoreView.detect(emptyView)) { - this._createdEmptyView = emptyView; - } + pathRoot = normalized.root; + path = normalized.path; } - this.replace(start, 0, addedViews); - }, - - /** - Instantiates a view to be added to the childViews array during view - initialization. You generally will not call this method directly unless - you are overriding `createChildViews()`. Note that this method will - automatically configure the correct settings on the new view instance to - act as a child of the parent. - - The tag name for the view will be set to the tagName of the viewClass - passed in. + // Set up an observer on the context. If the property changes, toggle the + // class name. + observer = function() { + // Get the current value of the property + newClass = classStringForPath(context, parsedPath, options); + elem = bindAttrId ? view.$("[data-bindattr-" + bindAttrId + "='" + bindAttrId + "']") : view.$(); - @method createChildView - @param {Class} viewClass - @param {Hash} [attrs] Attributes to add - @return {Ember.View} new instance - */ - createChildView: function(view, attrs) { - view = this._super(view, attrs); + // If we can't find the element anymore, a parent template has been + // re-rendered and we've been nuked. Remove the observer. + if (!elem || elem.length === 0) { + Ember.removeObserver(pathRoot, path, invoker); + } else { + // If we had previously added a class to the element, remove it. + if (oldClass) { + elem.removeClass(oldClass); + } - var itemTagName = get(view, 'tagName'); + // If necessary, add a new class. Make sure we keep track of it so + // it can be removed in the future. + if (newClass) { + elem.addClass(newClass); + oldClass = newClass; + } else { + oldClass = null; + } + } + }; - if (itemTagName === null || itemTagName === undefined) { - itemTagName = Ember.CollectionView.CONTAINER_MAP[get(this, 'tagName')]; - set(view, 'tagName', itemTagName); + if (path !== '' && path !== 'this') { + view.registerObserver(pathRoot, path, observer); } - return view; - } -}); + // We've already setup the observer; now we just need to figure out the + // correct behavior right now on the first pass through. + value = classStringForPath(context, parsedPath, options); -/** - A map of parent tags to their default child tags. You can add - additional parent tags if you want collection views that use - a particular parent tag to default to a child tag. + if (value) { + ret.push(value); - @property CONTAINER_MAP - @type Hash - @static - @final -*/ -Ember.CollectionView.CONTAINER_MAP = { - ul: 'li', - ol: 'li', - table: 'tr', - thead: 'tr', - tbody: 'tr', - tfoot: 'tr', - tr: 'td', - select: 'option' + // Make sure we save the current value so that it can be removed if the + // observer fires. + oldClass = value; + } + }); + + return ret; }; + })(); (function() { -var get = Ember.get, set = Ember.set, isNone = Ember.isNone, - a_slice = Array.prototype.slice; - +/*globals Handlebars */ +// TODO: Don't require the entire module /** @module ember -@submodule ember-views +@submodule ember-handlebars */ -/** - An `Ember.Component` is a view that is completely - isolated. Property access in its templates go - to the view object and actions are targeted at - the view object. There is no access to the - surrounding context or outer controller; all - contextual information is passed in. - - The easiest way to create an `Ember.Component` is via - a template. If you name a template - `components/my-foo`, you will be able to use - `{{my-foo}}` in other templates, which will make - an instance of the isolated component. - - ```html - {{app-profile person=currentUser}} - ``` - - ```html - -

    {{person.title}}

    - -

    {{person.signature}}

    - ``` - - You can also use `yield` inside a template to - include the **contents** of the custom tag: - - ```html - {{#app-profile person=currentUser}} -

    Admin mode

    - {{/app-profile}} - ``` +var get = Ember.get, set = Ember.set; +var EmberHandlebars = Ember.Handlebars; +var LOWERCASE_A_Z = /^[a-z]/; +var VIEW_PREFIX = /^view\./; - ```html - -

    {{person.title}}

    - {{yield}} - ``` +function makeBindings(thisContext, options) { + var hash = options.hash, + hashType = options.hashTypes; - If you want to customize the component, in order to - handle events or actions, you implement a subclass - of `Ember.Component` named after the name of the - component. Note that `Component` needs to be appended to the name of - your subclass like `AppProfileComponent`. + for (var prop in hash) { + if (hashType[prop] === 'ID') { - For example, you could implement the action - `hello` for the `app-profile` component: + var value = hash[prop]; - ```javascript - App.AppProfileComponent = Ember.Component.extend({ - actions: { - hello: function(name) { - console.log("Hello", name); + if (Ember.IS_BINDING.test(prop)) { + Ember.warn("You're attempting to render a view by passing " + prop + "=" + value + " to a view helper, but this syntax is ambiguous. You should either surround " + value + " in quotes or remove `Binding` from " + prop + "."); + } else { + hash[prop + 'Binding'] = value; + hashType[prop + 'Binding'] = 'STRING'; + delete hash[prop]; + delete hashType[prop]; } } - }); - ``` - - And then use it in the component's template: - - ```html - + } -

    {{person.title}}

    - {{yield}} + if (hash.hasOwnProperty('idBinding')) { + // id can't be bound, so just perform one-time lookup. + hash.id = EmberHandlebars.get(thisContext, hash.idBinding, options); + hashType.id = 'STRING'; + delete hash.idBinding; + delete hashType.idBinding; + } +} - - ``` +EmberHandlebars.ViewHelper = Ember.Object.create({ - Components must have a `-` in their name to avoid - conflicts with built-in controls that wrap HTML - elements. This is consistent with the same - requirement in web components. + propertiesFromHTMLOptions: function(options) { + var hash = options.hash, data = options.data; + var extensions = {}, + classes = hash['class'], + dup = false; - @class Component - @namespace Ember - @extends Ember.View -*/ -Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { - init: function() { - this._super(); - set(this, 'context', this); - set(this, 'controller', this); - }, + if (hash.id) { + extensions.elementId = hash.id; + dup = true; + } - // during render, isolate keywords - cloneKeywords: function() { - return { - view: this, - controller: this - }; - }, + if (hash.tag) { + extensions.tagName = hash.tag; + dup = true; + } - _yield: function(context, options) { - var view = options.data.view, - parentView = this._parentView, - template = get(this, 'template'); + if (classes) { + classes = classes.split(' '); + extensions.classNames = classes; + dup = true; + } - if (template) { - Ember.assert("A Component must have a parent view in order to yield.", parentView); + if (hash.classBinding) { + extensions.classNameBindings = hash.classBinding.split(' '); + dup = true; + } - view.appendChild(Ember.View, { - isVirtual: true, - tagName: '', - _contextView: parentView, - template: template, - context: get(parentView, 'context'), - controller: get(parentView, 'controller'), - templateData: { keywords: parentView.cloneKeywords() } - }); + if (hash.classNameBindings) { + if (extensions.classNameBindings === undefined) extensions.classNameBindings = []; + extensions.classNameBindings = extensions.classNameBindings.concat(hash.classNameBindings.split(' ')); + dup = true; } - }, - /** - If the component is currently inserted into the DOM of a parent view, this - property will point to the controller of the parent view. + if (hash.attributeBindings) { + Ember.assert("Setting 'attributeBindings' via Handlebars is not allowed. Please subclass Ember.View and set it there instead."); + extensions.attributeBindings = null; + dup = true; + } - @property targetObject - @type Ember.Controller - @default null - */ - targetObject: Ember.computed(function(key) { - var parentView = get(this, '_parentView'); - return parentView ? get(parentView, 'controller') : null; - }).property('_parentView'), + if (dup) { + hash = Ember.$.extend({}, hash); + delete hash.id; + delete hash.tag; + delete hash['class']; + delete hash.classBinding; + } - /** - Sends an action to component's controller. A component inherits its - controller from the context in which it is used. + // Set the proper context for all bindings passed to the helper. This applies to regular attribute bindings + // as well as class name bindings. If the bindings are local, make them relative to the current context + // instead of the view. + var path; - By default, calling `sendAction()` will send an action with the name - of the component's `action` property. + // Evaluate the context of regular attribute bindings: + for (var prop in hash) { + if (!hash.hasOwnProperty(prop)) { continue; } - For example, if the component had a property `action` with the value - `"addItem"`, calling `sendAction()` would send the `addItem` action - to the component's controller. + // Test if the property ends in "Binding" + if (Ember.IS_BINDING.test(prop) && typeof hash[prop] === 'string') { + path = this.contextualizeBindingPath(hash[prop], data); + if (path) { hash[prop] = path; } + } + } - If you provide the `action` argument to `sendAction()`, that key will - be used to look up the action name. + // Evaluate the context of class name bindings: + if (extensions.classNameBindings) { + for (var b in extensions.classNameBindings) { + var full = extensions.classNameBindings[b]; + if (typeof full === 'string') { + // Contextualize the path of classNameBinding so this: + // + // classNameBinding="isGreen:green" + // + // is converted to this: + // + // classNameBinding="_parentView.context.isGreen:green" + var parsedPath = Ember.View._parsePropertyPath(full); + path = this.contextualizeBindingPath(parsedPath.path, data); + if (path) { extensions.classNameBindings[b] = path + parsedPath.classNames; } + } + } + } - For example, if the component had a property `playing` with the value - `didStartPlaying`, calling `sendAction('playing')` would send the - `didStartPlaying` action to the component's controller. + return Ember.$.extend(hash, extensions); + }, - Whether or not you are using the default action or a named action, if - the action name is not defined on the component, calling `sendAction()` - does not have any effect. + // Transform bindings from the current context to a context that can be evaluated within the view. + // Returns null if the path shouldn't be changed. + // + // TODO: consider the addition of a prefix that would allow this method to return `path`. + contextualizeBindingPath: function(path, data) { + var normalized = Ember.Handlebars.normalizePath(null, path, data); + if (normalized.isKeyword) { + return 'templateData.keywords.' + path; + } else if (Ember.isGlobalPath(path)) { + return null; + } else if (path === 'this' || path === '') { + return '_parentView.context'; + } else { + return '_parentView.context.' + path; + } + }, - For example, if you call `sendAction()` on a component that does not have - an `action` property defined, no action will be sent to the controller, - nor will an exception be raised. + helper: function(thisContext, path, options) { + var data = options.data, + fn = options.fn, + newView; - You can send a context object with the action by supplying the `context` - argument. The context will be supplied as the first argument in the - target's action method. Example: + makeBindings(thisContext, options); - ```javascript - App.MyTreeComponent = Ember.Component.extend({ - click: function() { - this.sendAction('didClickTreeNode', this.get('node')); - } - }); + if ('string' === typeof path) { - App.CategoriesController = Ember.Controller.extend({ - actions: { - didClickCategory: function(category) { - //Do something with the node/category that was clicked - } + // TODO: this is a lame conditional, this should likely change + // but something along these lines will likely need to be added + // as deprecation warnings + // + if (options.types[0] === 'STRING' && LOWERCASE_A_Z.test(path) && !VIEW_PREFIX.test(path)) { + Ember.assert("View requires a container", !!data.view.container); + newView = data.view.container.lookupFactory('view:' + path); + } else { + newView = EmberHandlebars.get(thisContext, path, options); } - }); - ``` - ```handlebars - {{! categories.hbs}} - {{my-tree didClickTreeNode='didClickCategory'}} - ``` + Ember.assert("Unable to find view at path '" + path + "'", !!newView); + } else { + newView = path; + } - @method sendAction - @param [action] {String} the action to trigger - @param [context] {*} a context to send with the action - */ - sendAction: function(action) { - var actionName, - contexts = a_slice.call(arguments, 1); + Ember.assert(Ember.String.fmt('You must pass a view to the #view helper, not %@ (%@)', [path, newView]), Ember.View.detect(newView) || Ember.View.detectInstance(newView)); - // Send the default action - if (action === undefined) { - actionName = get(this, 'action'); - Ember.assert("The default action was triggered on the component " + this.toString() + ", but the action name (" + actionName + ") was not a string.", isNone(actionName) || typeof actionName === 'string'); - } else { - actionName = get(this, action); - Ember.assert("The " + action + " action was triggered on the component " + this.toString() + ", but the action name (" + actionName + ") was not a string.", isNone(actionName) || typeof actionName === 'string'); + var viewOptions = this.propertiesFromHTMLOptions(options, thisContext); + var currentView = data.view; + viewOptions.templateData = data; + var newViewProto = newView.proto ? newView.proto() : newView; + + if (fn) { + Ember.assert("You cannot provide a template block if you also specified a templateName", !get(viewOptions, 'templateName') && !get(newViewProto, 'templateName')); + viewOptions.template = fn; } - // If no action name for that action could be found, just abort. - if (actionName === undefined) { return; } + // We only want to override the `_context` computed property if there is + // no specified controller. See View#_context for more information. + if (!newViewProto.controller && !newViewProto.controllerBinding && !viewOptions.controller && !viewOptions.controllerBinding) { + viewOptions._context = thisContext; + } - this.triggerAction({ - action: actionName, - actionContext: contexts - }); + currentView.appendChild(newView, viewOptions); } }); -})(); +/** + `{{view}}` inserts a new instance of `Ember.View` into a template passing its + options to the `Ember.View`'s `create` method and using the supplied block as + the view's own template. + An empty `` and the following template: + ```handlebars + A span: + {{#view tagName="span"}} + hello. + {{/view}} + ``` -(function() { + Will result in HTML structure: -})(); + ```html + + +
    + A span: + + Hello. + +
    + + ``` + ### `parentView` setting -(function() { -/** -`Ember.ViewTargetActionSupport` is a mixin that can be included in a -view class to add a `triggerAction` method with semantics similar to -the Handlebars `{{action}}` helper. It provides intelligent defaults -for the action's target: the view's controller; and the context that is -sent with the action: the view's context. + The `parentView` property of the new `Ember.View` instance created through + `{{view}}` will be set to the `Ember.View` instance of the template where + `{{view}}` was called. -Note: In normal Ember usage, the `{{action}}` helper is usually the best -choice. This mixin is most often useful when you are doing more complex -event handling in custom View subclasses. + ```javascript + aView = Ember.View.create({ + template: Ember.Handlebars.compile("{{#view}} my parent: {{parentView.elementId}} {{/view}}") + }); -For example: + aView.appendTo('body'); + ``` -```javascript -App.SaveButtonView = Ember.View.extend(Ember.ViewTargetActionSupport, { - action: 'save', - click: function() { - this.triggerAction(); // Sends the `save` action, along with the current context - // to the current controller - } -}); -``` + Will result in HTML structure: -The `action` can be provided as properties of an optional object argument -to `triggerAction` as well. + ```html +
    +
    + my parent: ember1 +
    +
    + ``` -```javascript -App.SaveButtonView = Ember.View.extend(Ember.ViewTargetActionSupport, { - click: function() { - this.triggerAction({ - action: 'save' - }); // Sends the `save` action, along with the current context - // to the current controller - } -}); -``` + ### Setting CSS id and class attributes -@class ViewTargetActionSupport -@namespace Ember -@extends Ember.TargetActionSupport -*/ -Ember.ViewTargetActionSupport = Ember.Mixin.create(Ember.TargetActionSupport, { - /** - @property target - */ - target: Ember.computed.alias('controller'), - /** - @property actionContext - */ - actionContext: Ember.computed.alias('context') -}); + The HTML `id` attribute can be set on the `{{view}}`'s resulting element with + the `id` option. This option will _not_ be passed to `Ember.View.create`. -})(); + ```handlebars + {{#view tagName="span" id="a-custom-id"}} + hello. + {{/view}} + ``` + Results in the following HTML structure: + ```html +
    + + hello. + +
    + ``` -(function() { + The HTML `class` attribute can be set on the `{{view}}`'s resulting element + with the `class` or `classNameBindings` options. The `class` option will + directly set the CSS `class` attribute and will not be passed to + `Ember.View.create`. `classNameBindings` will be passed to `create` and use + `Ember.View`'s class name binding functionality: -})(); + ```handlebars + {{#view tagName="span" class="a-custom-class"}} + hello. + {{/view}} + ``` + Results in the following HTML structure: + ```html +
    + + hello. + +
    + ``` -(function() { -/** -Ember Views + ### Supplying a different view class -@module ember -@submodule ember-views -@requires ember-runtime -@main ember-views -*/ + `{{view}}` can take an optional first argument before its supplied options to + specify a path to a custom view class. -})(); + ```handlebars + {{#view "MyApp.CustomView"}} + hello. + {{/view}} + ``` -(function() { -define("metamorph", - [], - function() { - "use strict"; - // ========================================================================== - // Project: metamorph - // Copyright: ©2011 My Company Inc. All rights reserved. - // ========================================================================== + The first argument can also be a relative path accessible from the current + context. - var K = function() {}, - guid = 0, - document = this.document, - disableRange = ('undefined' === typeof ENV ? {} : ENV).DISABLE_RANGE_API, + ```javascript + MyApp = Ember.Application.create({}); + MyApp.OuterView = Ember.View.extend({ + innerViewClass: Ember.View.extend({ + classNames: ['a-custom-view-class-as-property'] + }), + template: Ember.Handlebars.compile('{{#view "view.innerViewClass"}} hi {{/view}}') + }); - // Feature-detect the W3C range API, the extended check is for IE9 which only partially supports ranges - supportsRange = (!disableRange) && document && ('createRange' in document) && (typeof Range !== 'undefined') && Range.prototype.createContextualFragment, + MyApp.OuterView.create().appendTo('body'); + ``` - // Internet Explorer prior to 9 does not allow setting innerHTML if the first element - // is a "zero-scope" element. This problem can be worked around by making - // the first node an invisible text node. We, like Modernizr, use ­ - needsShy = document && (function() { - var testEl = document.createElement('div'); - testEl.innerHTML = "
    "; - testEl.firstChild.innerHTML = ""; - return testEl.firstChild.innerHTML === ''; - })(), + Will result in the following HTML: + ```html +
    +
    + hi +
    +
    + ``` - // IE 8 (and likely earlier) likes to move whitespace preceeding - // a script tag to appear after it. This means that we can - // accidentally remove whitespace when updating a morph. - movesWhitespace = document && (function() { - var testEl = document.createElement('div'); - testEl.innerHTML = "Test: Value"; - return testEl.childNodes[0].nodeValue === 'Test:' && - testEl.childNodes[2].nodeValue === ' Value'; - })(); + ### Blockless use - // Constructor that supports either Metamorph('foo') or new - // Metamorph('foo'); - // - // Takes a string of HTML as the argument. + If you supply a custom `Ember.View` subclass that specifies its own template + or provide a `templateName` option to `{{view}}` it can be used without + supplying a block. Attempts to use both a `templateName` option and supply a + block will throw an error. - var Metamorph = function(html) { - var self; + ```handlebars + {{view "MyApp.ViewWithATemplateDefined"}} + ``` - if (this instanceof Metamorph) { - self = this; - } else { - self = new K(); - } + ### `viewName` property - self.innerHTML = html; - var myGuid = 'metamorph-'+(guid++); - self.start = myGuid + '-start'; - self.end = myGuid + '-end'; + You can supply a `viewName` option to `{{view}}`. The `Ember.View` instance + will be referenced as a property of its parent view by this name. - return self; - }; + ```javascript + aView = Ember.View.create({ + template: Ember.Handlebars.compile('{{#view viewName="aChildByName"}} hi {{/view}}') + }); - K.prototype = Metamorph.prototype; + aView.appendTo('body'); + aView.get('aChildByName') // the instance of Ember.View created by {{view}} helper + ``` - var rangeFor, htmlFunc, removeFunc, outerHTMLFunc, appendToFunc, afterFunc, prependFunc, startTagFunc, endTagFunc; + @method view + @for Ember.Handlebars.helpers + @param {String} path + @param {Hash} options + @return {String} HTML string +*/ +EmberHandlebars.registerHelper('view', function viewHelper(path, options) { + Ember.assert("The view helper only takes a single argument", arguments.length <= 2); - outerHTMLFunc = function() { - return this.startTag() + this.innerHTML + this.endTag(); - }; + // If no path is provided, treat path param as options. + if (path && path.data && path.data.isRenderData) { + options = path; + path = "Ember.View"; + } - startTagFunc = function() { - /* - * We replace chevron by its hex code in order to prevent escaping problems. - * Check this thread for more explaination: - * http://stackoverflow.com/questions/8231048/why-use-x3c-instead-of-when-generating-html-from-javascript - */ - return "hi"; - * div.firstChild.firstChild.tagName //=> "" - * - * If our script markers are inside such a node, we need to find that - * node and use *it* as the marker. - */ - var realNode = function(start) { - while (start.parentNode.tagName === "") { - start = start.parentNode; - } + // If no path is provided, treat path param as options. + if (path && path.data && path.data.isRenderData) { + options = path; + path = undefined; + Ember.assert("You cannot pass more than one argument to the collection helper", arguments.length === 1); + } else { + Ember.assert("You cannot pass more than one argument to the collection helper", arguments.length === 2); + } - return start; - }; + var fn = options.fn; + var data = options.data; + var inverse = options.inverse; + var view = options.data.view; - /* - * When automatically adding a tbody, Internet Explorer inserts the - * tbody immediately before the first . Other browsers create it - * before the first node, no matter what. - * - * This means the the following code: - * - * div = document.createElement("div"); - * div.innerHTML = "
    hi
    - * - * Generates the following DOM in IE: - * - * + div - * + table - * - script id='first' - * + tbody - * + tr - * + td - * - "hi" - * - script id='last' - * - * Which means that the two script tags, even though they were - * inserted at the same point in the hierarchy in the original - * HTML, now have different parents. - * - * This code reparents the first script tag by making it the tbody's - * first child. - * - */ - var fixParentage = function(start, end) { - if (start.parentNode !== end.parentNode) { - end.parentNode.insertBefore(start, end.parentNode.firstChild); - } - }; - htmlFunc = function(html, outerToo) { - // get the real starting node. see realNode for details. - var start = realNode(document.getElementById(this.start)); - var end = document.getElementById(this.end); - var parentNode = end.parentNode; - var node, nextSibling, last; + var controller, container; + // If passed a path string, convert that into an object. + // Otherwise, just default to the standard class. + var collectionClass; + if (path) { + controller = data.keywords.controller; + container = controller && controller.container; + collectionClass = handlebarsGet(this, path, options) || container.lookupFactory('view:' + path); + Ember.assert(fmt("%@ #collection: Could not find collection class %@", [data.view, path]), !!collectionClass); + } + else { + collectionClass = Ember.CollectionView; + } - // make sure that the start and end nodes share the same - // parent. If not, fix it. - fixParentage(start, end); + var hash = options.hash, itemHash = {}, match; - // remove all of the nodes after the starting placeholder and - // before the ending placeholder. - node = start.nextSibling; - while (node) { - nextSibling = node.nextSibling; - last = node === end; + // Extract item view class if provided else default to the standard class + var collectionPrototype = collectionClass.proto(), + itemViewClass; - // if this is the last node, and we want to remove it as well, - // set the `end` node to the next sibling. This is because - // for the rest of the function, we insert the new nodes - // before the end (note that insertBefore(node, null) is - // the same as appendChild(node)). - // - // if we do not want to remove it, just break. - if (last) { - if (outerToo) { end = node.nextSibling; } else { break; } - } + if (hash.itemView) { + controller = data.keywords.controller; + Ember.assert('You specified an itemView, but the current context has no ' + + 'container to look the itemView up in. This probably means ' + + 'that you created a view manually, instead of through the ' + + 'container. Instead, use container.lookup("view:viewName"), ' + + 'which will properly instantiate your view.', + controller && controller.container); + container = controller.container; + itemViewClass = container.lookupFactory('view:' + hash.itemView); + Ember.assert('You specified the itemView ' + hash.itemView + ", but it was " + + "not found at " + container.describe("view:" + hash.itemView) + + " (and it was not registered in the container)", !!itemViewClass); + } else if (hash.itemViewClass) { + itemViewClass = handlebarsGet(collectionPrototype, hash.itemViewClass, options); + } else { + itemViewClass = collectionPrototype.itemViewClass; + } - node.parentNode.removeChild(node); + Ember.assert(fmt("%@ #collection: Could not find itemViewClass %@", [data.view, itemViewClass]), !!itemViewClass); - // if this is the last node and we didn't break before - // (because we wanted to remove the outer nodes), break - // now. - if (last) { break; } + delete hash.itemViewClass; + delete hash.itemView; - node = nextSibling; - } + // Go through options passed to the {{collection}} helper and extract options + // that configure item views instead of the collection itself. + for (var prop in hash) { + if (hash.hasOwnProperty(prop)) { + match = prop.match(/^item(.)(.*)$/); - // get the first node for the HTML string, even in cases like - // tables and lists where a simple innerHTML on a div would - // swallow some of the content. - node = firstNodeFor(start.parentNode, html); + if (match && prop !== 'itemController') { + // Convert itemShouldFoo -> shouldFoo + itemHash[match[1].toLowerCase() + match[2]] = hash[prop]; + // Delete from hash as this will end up getting passed to the + // {{view}} helper method. + delete hash[prop]; + } + } + } - // copy the nodes for the HTML between the starting and ending - // placeholder. - while (node) { - nextSibling = node.nextSibling; - parentNode.insertBefore(node, end); - node = nextSibling; - } - }; + if (fn) { + itemHash.template = fn; + delete options.fn; + } - // remove the nodes in the DOM representing this metamorph. - // - // this includes the starting and ending placeholders. - removeFunc = function() { - var start = realNode(document.getElementById(this.start)); - var end = document.getElementById(this.end); + var emptyViewClass; + if (inverse && inverse !== Ember.Handlebars.VM.noop) { + emptyViewClass = get(collectionPrototype, 'emptyViewClass'); + emptyViewClass = emptyViewClass.extend({ + template: inverse, + tagName: itemHash.tagName + }); + } else if (hash.emptyViewClass) { + emptyViewClass = handlebarsGet(this, hash.emptyViewClass, options); + } + if (emptyViewClass) { hash.emptyView = emptyViewClass; } - this.html(''); - start.parentNode.removeChild(start); - end.parentNode.removeChild(end); - }; + if (!hash.keyword) { + itemHash._context = Ember.computed.alias('content'); + } - appendToFunc = function(parentNode) { - var node = firstNodeFor(parentNode, this.outerHTML()); - var nextSibling; + var viewOptions = Ember.Handlebars.ViewHelper.propertiesFromHTMLOptions({ data: data, hash: itemHash }, this); + hash.itemViewClass = itemViewClass.extend(viewOptions); - while (node) { - nextSibling = node.nextSibling; - parentNode.appendChild(node); - node = nextSibling; - } - }; + return Ember.Handlebars.helpers.view.call(this, collectionClass, options); +}); - afterFunc = function(html) { - // get the real starting node. see realNode for details. - var end = document.getElementById(this.end); - var insertBefore = end.nextSibling; - var parentNode = end.parentNode; - var nextSibling; - var node; - // get the first node for the HTML string, even in cases like - // tables and lists where a simple innerHTML on a div would - // swallow some of the content. - node = firstNodeFor(parentNode, html); +})(); - // copy the nodes for the HTML between the starting and ending - // placeholder. - while (node) { - nextSibling = node.nextSibling; - parentNode.insertBefore(node, insertBefore); - node = nextSibling; - } - }; - prependFunc = function(html) { - var start = document.getElementById(this.start); - var parentNode = start.parentNode; - var nextSibling; - var node; - node = firstNodeFor(parentNode, html); - var insertBefore = start.nextSibling; +(function() { +/*globals Handlebars */ +/** +@module ember +@submodule ember-handlebars +*/ - while (node) { - nextSibling = node.nextSibling; - parentNode.insertBefore(node, insertBefore); - node = nextSibling; - } - }; - } +var handlebarsGet = Ember.Handlebars.get; - Metamorph.prototype.html = function(html) { - this.checkRemoved(); - if (html === undefined) { return this.innerHTML; } +/** + `unbound` allows you to output a property without binding. *Important:* The + output will not be updated if the property changes. Use with caution. - htmlFunc.call(this, html); + ```handlebars +
    {{unbound somePropertyThatDoesntChange}}
    + ``` - this.innerHTML = html; - }; + `unbound` can also be used in conjunction with a bound helper to + render it in its unbound form: - Metamorph.prototype.replaceWith = function(html) { - this.checkRemoved(); - htmlFunc.call(this, html, true); - }; + ```handlebars +
    {{unbound helperName somePropertyThatDoesntChange}}
    + ``` - Metamorph.prototype.remove = removeFunc; - Metamorph.prototype.outerHTML = outerHTMLFunc; - Metamorph.prototype.appendTo = appendToFunc; - Metamorph.prototype.after = afterFunc; - Metamorph.prototype.prepend = prependFunc; - Metamorph.prototype.startTag = startTagFunc; - Metamorph.prototype.endTag = endTagFunc; + @method unbound + @for Ember.Handlebars.helpers + @param {String} property + @return {String} HTML string +*/ +Ember.Handlebars.registerHelper('unbound', function unboundHelper(property, fn) { + var options = arguments[arguments.length - 1], helper, context, out; - Metamorph.prototype.isRemoved = function() { - var before = document.getElementById(this.start); - var after = document.getElementById(this.end); + if (arguments.length > 2) { + // Unbound helper call. + options.data.isUnbound = true; + helper = Ember.Handlebars.helpers[arguments[0]] || Ember.Handlebars.helpers.helperMissing; + out = helper.apply(this, Array.prototype.slice.call(arguments, 1)); + delete options.data.isUnbound; + return out; + } - return !before || !after; - }; + context = (fn.contexts && fn.contexts.length) ? fn.contexts[0] : this; + return handlebarsGet(context, property, fn); +}); - Metamorph.prototype.checkRemoved = function() { - if (this.isRemoved()) { - throw new Error("Cannot perform operations on a Metamorph that is not in the DOM."); - } - }; +})(); - return Metamorph; - }); -})(); (function() { +/*jshint debug:true*/ /** @module ember -@submodule ember-handlebars-compiler +@submodule ember-handlebars */ -// Eliminate dependency on any Ember to simplify precompilation workflow -var objectCreate = Object.create || function(parent) { - function F() {} - F.prototype = parent; - return new F(); -}; - -var Handlebars = this.Handlebars || (Ember.imports && Ember.imports.Handlebars); -if (!Handlebars && typeof require === 'function') { - Handlebars = require('handlebars'); -} - -Ember.assert("Ember Handlebars requires Handlebars version 1.0.0. Include a SCRIPT tag in the HTML HEAD linking to the Handlebars file before you link to Ember.", Handlebars); -Ember.assert("Ember Handlebars requires Handlebars version 1.0.0, COMPILER_REVISION expected: 4, got: " + Handlebars.COMPILER_REVISION + " - Please note: Builds of master may have other COMPILER_REVISION values.", Handlebars.COMPILER_REVISION === 4); +var get = Ember.get; +var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.normalizePath; +var a_slice = [].slice; /** - Prepares the Handlebars templating library for use inside Ember's view - system. - - The `Ember.Handlebars` object is the standard Handlebars library, extended to - use Ember's `get()` method instead of direct property access, which allows - computed properties to be used inside templates. + `log` allows you to output the value of variables in the current rendering + context. `log` also accepts primitive types such as strings or numbers. - To create an `Ember.Handlebars` template, call `Ember.Handlebars.compile()`. - This will return a function that can be used by `Ember.View` for rendering. + ```handlebars + {{log "myVariable:" myVariable }} + ``` - @class Handlebars - @namespace Ember + @method log + @for Ember.Handlebars.helpers + @param {String} property */ -Ember.Handlebars = objectCreate(Handlebars); +Ember.Handlebars.registerHelper('log', function logHelper() { + var params = a_slice.call(arguments, 0, -1), + options = arguments[arguments.length - 1], + logger = Ember.Logger.log, + values = [], + allowPrimitives = false; -/** - Register a bound helper or custom view helper. + + allowPrimitives = true; + - ## Simple bound helper example + for (var i = 0; i < params.length; i++) { + var type = options.types[i]; - ```javascript - Ember.Handlebars.helper('capitalize', function(value) { - return value.toUpperCase(); - }); - ``` + if (type === 'ID' || !allowPrimitives) { + var context = (options.contexts && options.contexts[i]) || this, + normalized = normalizePath(context, params[i], options.data); - The above bound helper can be used inside of templates as follows: + if (normalized.path === 'this') { + values.push(normalized.root); + } else { + values.push(handlebarsGet(normalized.root, normalized.path, options)); + } + } else { + values.push(params[i]); + } + } + + logger.apply(logger, values); +}); + +/** + Execute the `debugger` statement in the current context. ```handlebars - {{capitalize name}} + {{debugger}} ``` - In this case, when the `name` property of the template's context changes, - the rendered value of the helper will update to reflect this change. - - For more examples of bound helpers, see documentation for - `Ember.Handlebars.registerBoundHelper`. + Before invoking the `debugger` statement, there + are a few helpful variables defined in the + body of this helper that you can inspect while + debugging that describe how and where this + helper was invoked: - ## Custom view helper example + - templateContext: this is most likely a controller + from which this template looks up / displays properties + - typeOfTemplateContext: a string description of + what the templateContext is - Assuming a view subclass named `App.CalendarView` were defined, a helper - for rendering instances of this view could be registered as follows: + For example, if you're wondering why a value `{{foo}}` + isn't rendering as expected within a template, you + could place a `{{debugger}}` statement, and when + the `debugger;` breakpoint is hit, you can inspect + `templateContext`, determine if it's the object you + expect, and/or evaluate expressions in the console + to perform property lookups on the `templateContext`: - ```javascript - Ember.Handlebars.helper('calendar', App.CalendarView): ``` + > templateContext.get('foo') // -> "" + ``` + + @method debugger + @for Ember.Handlebars.helpers + @param {String} property +*/ +Ember.Handlebars.registerHelper('debugger', function debuggerHelper(options) { - The above bound helper can be used inside of templates as follows: + // These are helpful values you can inspect while debugging. + var templateContext = this; + var typeOfTemplateContext = Ember.inspect(templateContext); - ```handlebars - {{calendar}} - ``` + debugger; +}); - Which is functionally equivalent to: - ```handlebars - {{view App.CalendarView}} - ``` - Options in the helper will be passed to the view in exactly the same - manner as with the `view` helper. +})(); - @method helper - @for Ember.Handlebars - @param {String} name - @param {Function|Ember.View} function or view class constructor - @param {String} dependentKeys* -*/ -Ember.Handlebars.helper = function(name, value) { - Ember.assert("You tried to register a component named '" + name + "', but component names must include a '-'", !Ember.Component.detect(value) || name.match(/-/)); - if (Ember.View.detect(value)) { - Ember.Handlebars.registerHelper(name, function(options) { - Ember.assert("You can only pass attributes (such as name=value) not bare values to a helper for a View", arguments.length < 2); - return Ember.Handlebars.helpers.view.call(this, value, options); - }); - } else { - Ember.Handlebars.registerBoundHelper.apply(null, arguments); - } -}; +(function() { /** -@class helpers -@namespace Ember.Handlebars +@module ember +@submodule ember-handlebars */ -Ember.Handlebars.helpers = objectCreate(Handlebars.helpers); - -/** - Override the the opcode compiler and JavaScript compiler for Handlebars. - @class Compiler - @namespace Ember.Handlebars - @private - @constructor -*/ -Ember.Handlebars.Compiler = function() {}; +var get = Ember.get, set = Ember.set; +var fmt = Ember.String.fmt; -// Handlebars.Compiler doesn't exist in runtime-only -if (Handlebars.Compiler) { - Ember.Handlebars.Compiler.prototype = objectCreate(Handlebars.Compiler.prototype); -} +Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { + init: function() { + var itemController = get(this, 'itemController'); + var binding; -Ember.Handlebars.Compiler.prototype.compiler = Ember.Handlebars.Compiler; + if (itemController) { + var controller = get(this, 'controller.container').lookupFactory('controller:array').create({ + _isVirtual: true, + parentController: get(this, 'controller'), + itemController: itemController, + target: get(this, 'controller'), + _eachView: this + }); -/** - @class JavaScriptCompiler - @namespace Ember.Handlebars - @private - @constructor -*/ -Ember.Handlebars.JavaScriptCompiler = function() {}; + this.disableContentObservers(function() { + set(this, 'content', controller); + binding = new Ember.Binding('content', '_eachView.dataSource').oneWay(); + binding.connect(controller); + }); -// Handlebars.JavaScriptCompiler doesn't exist in runtime-only -if (Handlebars.JavaScriptCompiler) { - Ember.Handlebars.JavaScriptCompiler.prototype = objectCreate(Handlebars.JavaScriptCompiler.prototype); - Ember.Handlebars.JavaScriptCompiler.prototype.compiler = Ember.Handlebars.JavaScriptCompiler; -} + set(this, '_arrayController', controller); + } else { + this.disableContentObservers(function() { + binding = new Ember.Binding('content', 'dataSource').oneWay(); + binding.connect(this); + }); + } + return this._super(); + }, -Ember.Handlebars.JavaScriptCompiler.prototype.namespace = "Ember.Handlebars"; + _assertArrayLike: function(content) { + Ember.assert(fmt("The value that #each loops over must be an Array. You " + + "passed %@, but it should have been an ArrayController", + [content.constructor]), + !Ember.ControllerMixin.detect(content) || + (content && content.isGenerated) || + content instanceof Ember.ArrayController); + Ember.assert(fmt("The value that #each loops over must be an Array. You passed %@", [(Ember.ControllerMixin.detect(content) && content.get('model') !== undefined) ? fmt("'%@' (wrapped in %@)", [content.get('model'), content]) : content]), Ember.Array.detect(content)); + }, -Ember.Handlebars.JavaScriptCompiler.prototype.initializeBuffer = function() { - return "''"; -}; + disableContentObservers: function(callback) { + Ember.removeBeforeObserver(this, 'content', null, '_contentWillChange'); + Ember.removeObserver(this, 'content', null, '_contentDidChange'); -/** - @private + callback.call(this); - Override the default buffer for Ember Handlebars. By default, Handlebars - creates an empty String at the beginning of each invocation and appends to - it. Ember's Handlebars overrides this to append to a single shared buffer. + Ember.addBeforeObserver(this, 'content', null, '_contentWillChange'); + Ember.addObserver(this, 'content', null, '_contentDidChange'); + }, - @method appendToBuffer - @param string {String} -*/ -Ember.Handlebars.JavaScriptCompiler.prototype.appendToBuffer = function(string) { - return "data.buffer.push("+string+");"; -}; + itemViewClass: Ember._MetamorphView, + emptyViewClass: Ember._MetamorphView, -var prefix = "ember" + (+new Date()), incr = 1; + createChildView: function(view, attrs) { + view = this._super(view, attrs); -/** - @private + // At the moment, if a container view subclass wants + // to insert keywords, it is responsible for cloning + // the keywords hash. This will be fixed momentarily. + var keyword = get(this, 'keyword'); + var content = get(view, 'content'); - Rewrite simple mustaches from `{{foo}}` to `{{bind "foo"}}`. This means that - all simple mustaches in Ember's Handlebars will also set up an observer to - keep the DOM up to date when the underlying property changes. + if (keyword) { + var data = get(view, 'templateData'); - @method mustache - @for Ember.Handlebars.Compiler - @param mustache -*/ -Ember.Handlebars.Compiler.prototype.mustache = function(mustache) { - if (mustache.isHelper && mustache.id.string === 'control') { - mustache.hash = mustache.hash || new Handlebars.AST.HashNode([]); - mustache.hash.pairs.push(["controlID", new Handlebars.AST.StringNode(prefix + incr++)]); - } else if (mustache.params.length || mustache.hash) { - // no changes required - } else { - var id = new Handlebars.AST.IdNode([{ part: '_triageMustache' }]); + data = Ember.copy(data); + data.keywords = view.cloneKeywords(); + set(view, 'templateData', data); - // Update the mustache node to include a hash value indicating whether the original node - // was escaped. This will allow us to properly escape values when the underlying value - // changes and we need to re-render the value. - if (!mustache.escaped) { - mustache.hash = mustache.hash || new Handlebars.AST.HashNode([]); - mustache.hash.pairs.push(["unescaped", new Handlebars.AST.StringNode("true")]); + // In this case, we do not bind, because the `content` of + // a #each item cannot change. + data.keywords[keyword] = content; } - mustache = new Handlebars.AST.MustacheNode([id].concat([mustache.id]), mustache.hash, !mustache.escaped); - } - return Handlebars.Compiler.prototype.mustache.call(this, mustache); -}; + // If {{#each}} is looping over an array of controllers, + // point each child view at their respective controller. + if (content && content.isController) { + set(view, 'controller', content); + } -/** - Used for precompilation of Ember Handlebars templates. This will not be used - during normal app execution. + return view; + }, - @method precompile - @for Ember.Handlebars - @static - @param {String} string The template to precompile -*/ -Ember.Handlebars.precompile = function(string) { - var ast = Handlebars.parse(string); + destroy: function() { + if (!this._super()) { return; } - var options = { - knownHelpers: { - action: true, - unbound: true, - bindAttr: true, - template: true, - view: true, - _triageMustache: true - }, - data: true, - stringParams: true - }; + var arrayController = get(this, '_arrayController'); - var environment = new Ember.Handlebars.Compiler().compile(ast, options); - return new Ember.Handlebars.JavaScriptCompiler().compile(environment, options, undefined, true); -}; + if (arrayController) { + arrayController.destroy(); + } -// We don't support this for Handlebars runtime-only -if (Handlebars.compile) { - /** - The entry point for Ember Handlebars. This replaces the default - `Handlebars.compile` and turns on template-local data and String - parameters. + return this; + } +}); - @method compile - @for Ember.Handlebars - @static - @param {String} string The template to compile - @return {Function} - */ - Ember.Handlebars.compile = function(string) { - var ast = Handlebars.parse(string); - var options = { data: true, stringParams: true }; - var environment = new Ember.Handlebars.Compiler().compile(ast, options); - var templateSpec = new Ember.Handlebars.JavaScriptCompiler().compile(environment, options, undefined, true); +// Defeatureify doesn't seem to like nested functions that need to be removed +function _addMetamorphCheck() { + Ember.Handlebars.EachView.reopen({ + _checkMetamorph: Ember.on('didInsertElement', function() { + Ember.assert("The metamorph tags, " + + this.morph.start + " and " + this.morph.end + + ", have different parents.\nThe browser has fixed your template to output valid HTML (for example, check that you have properly closed all tags and have used a TBODY tag when creating a table with '{{#each}}')", + document.getElementById( this.morph.start ).parentNode === + document.getElementById( this.morph.end ).parentNode + ); + }) + }); +} - var template = Ember.Handlebars.template(templateSpec); - template.isMethod = false; //Make sure we don't wrap templates with ._super +Ember.runInDebug( function() { + _addMetamorphCheck(); +}); - return template; - }; -} +var GroupedEach = Ember.Handlebars.GroupedEach = function(context, path, options) { + var self = this, + normalized = Ember.Handlebars.normalizePath(context, path, options.data); + this.context = context; + this.path = path; + this.options = options; + this.template = options.fn; + this.containingView = options.data.view; + this.normalizedRoot = normalized.root; + this.normalizedPath = normalized.path; + this.content = this.lookupContent(); -})(); + this.addContentObservers(); + this.addArrayObservers(); -(function() { -var slice = Array.prototype.slice; + this.containingView.on('willClearRender', function() { + self.destroy(); + }); +}; -/** - @private +GroupedEach.prototype = { + contentWillChange: function() { + this.removeArrayObservers(); + }, - If a path starts with a reserved keyword, returns the root - that should be used. + contentDidChange: function() { + this.content = this.lookupContent(); + this.addArrayObservers(); + this.rerenderContainingView(); + }, - @method normalizePath - @for Ember - @param root {Object} - @param path {String} - @param data {Hash} -*/ -var normalizePath = Ember.Handlebars.normalizePath = function(root, path, data) { - var keywords = (data && data.keywords) || {}, - keyword, isKeyword; + contentArrayWillChange: Ember.K, - // Get the first segment of the path. For example, if the - // path is "foo.bar.baz", returns "foo". - keyword = path.split('.', 1)[0]; + contentArrayDidChange: function() { + this.rerenderContainingView(); + }, - // Test to see if the first path is a keyword that has been - // passed along in the view's data hash. If so, we will treat - // that object as the new root. - if (keywords.hasOwnProperty(keyword)) { - // Look up the value in the template's data hash. - root = keywords[keyword]; - isKeyword = true; + lookupContent: function() { + return Ember.Handlebars.get(this.normalizedRoot, this.normalizedPath, this.options); + }, - // Handle cases where the entire path is the reserved - // word. In that case, return the object itself. - if (path === keyword) { - path = ''; - } else { - // Strip the keyword from the path and look up - // the remainder from the newly found root. - path = path.substr(keyword.length+1); - } - } + addArrayObservers: function() { + if (!this.content) { return; } - return { root: root, path: path, isKeyword: isKeyword }; -}; + this.content.addArrayObserver(this, { + willChange: 'contentArrayWillChange', + didChange: 'contentArrayDidChange' + }); + }, + removeArrayObservers: function() { + if (!this.content) { return; } -/** - Lookup both on root and on window. If the path starts with - a keyword, the corresponding object will be looked up in the - template's data hash and used to resolve the path. + this.content.removeArrayObserver(this, { + willChange: 'contentArrayWillChange', + didChange: 'contentArrayDidChange' + }); + }, - @method get - @for Ember.Handlebars - @param {Object} root The object to look up the property on - @param {String} path The path to be lookedup - @param {Object} options The template's option hash -*/ -var handlebarsGet = Ember.Handlebars.get = function(root, path, options) { - var data = options && options.data, - normalizedPath = normalizePath(root, path, data), - value; + addContentObservers: function() { + Ember.addBeforeObserver(this.normalizedRoot, this.normalizedPath, this, this.contentWillChange); + Ember.addObserver(this.normalizedRoot, this.normalizedPath, this, this.contentDidChange); + }, - // In cases where the path begins with a keyword, change the - // root to the value represented by that keyword, and ensure - // the path is relative to it. - root = normalizedPath.root; - path = normalizedPath.path; + removeContentObservers: function() { + Ember.removeBeforeObserver(this.normalizedRoot, this.normalizedPath, this.contentWillChange); + Ember.removeObserver(this.normalizedRoot, this.normalizedPath, this.contentDidChange); + }, - value = Ember.get(root, path); + render: function() { + if (!this.content) { return; } - // If the path starts with a capital letter, look it up on Ember.lookup, - // which defaults to the `window` object in browsers. - if (value === undefined && root !== Ember.lookup && Ember.isGlobalPath(path)) { - value = Ember.get(Ember.lookup, path); - } - return value; -}; -Ember.Handlebars.getPath = Ember.deprecateFunc('`Ember.Handlebars.getPath` has been changed to `Ember.Handlebars.get` for consistency.', Ember.Handlebars.get); + var content = this.content, + contentLength = get(content, 'length'), + data = this.options.data, + template = this.template; -Ember.Handlebars.resolveParams = function(context, params, options) { - var resolvedParams = [], types = options.types, param, type; + data.insideEach = true; + for (var i = 0; i < contentLength; i++) { + template(content.objectAt(i), { data: data }); + } + }, - for (var i=0, l=params.length; iSorry, nobody is available for this task.

    + {{/each}} + ``` + ### Specifying a View class for items + If you provide an `itemViewClass` option that references a view class + with its own `template` you can omit the block. - error = "%@ Handlebars error: Could not find property '%@' on object %@."; - if (options.data) { - view = options.data.view; - } - throw new Ember.Error(Ember.String.fmt(error, [view, path, this])); -}); + The following template: -/** - Register a bound handlebars helper. Bound helpers behave similarly to regular - handlebars helpers, with the added ability to re-render when the underlying data - changes. + ```handlebars + {{#view App.MyView }} + {{each view.items itemViewClass="App.AnItemView"}} + {{/view}} + ``` - ## Simple example + And application code ```javascript - Ember.Handlebars.registerBoundHelper('capitalize', function(value) { - return value.toUpperCase(); + App = Ember.Application.create({ + MyView: Ember.View.extend({ + items: [ + Ember.Object.create({name: 'Dave'}), + Ember.Object.create({name: 'Mary'}), + Ember.Object.create({name: 'Sara'}) + ] + }) + }); + + App.AnItemView = Ember.View.extend({ + template: Ember.Handlebars.compile("Greetings {{name}}") }); ``` - The above bound helper can be used inside of templates as follows: + Will result in the HTML structure below - ```handlebars - {{capitalize name}} + ```html +
    +
    Greetings Dave
    +
    Greetings Mary
    +
    Greetings Sara
    +
    ``` - In this case, when the `name` property of the template's context changes, - the rendered value of the helper will update to reflect this change. + If an `itemViewClass` is defined on the helper, and therefore the helper is not + being used as a block, an `emptyViewClass` can also be provided optionally. + The `emptyViewClass` will match the behavior of the `{{else}}` condition + described above. That is, the `emptyViewClass` will render if the collection + is empty. - ## Example with options + ### Representing each item with a Controller. + By default the controller lookup within an `{{#each}}` block will be + the controller of the template where the `{{#each}}` was used. If each + item needs to be presented by a custom controller you can provide a + `itemController` option which references a controller by lookup name. + Each item in the loop will be wrapped in an instance of this controller + and the item itself will be set to the `content` property of that controller. - Like normal handlebars helpers, bound helpers have access to the options - passed into the helper call. + This is useful in cases where properties of model objects need transformation + or synthesis for display: ```javascript - Ember.Handlebars.registerBoundHelper('repeat', function(value, options) { - var count = options.hash.count; - var a = []; - while(a.length < count) { - a.push(value); - } - return a.join(''); - }); + App.DeveloperController = Ember.ObjectController.extend({ + isAvailableForHire: function() { + return !this.get('content.isEmployed') && this.get('content.isSeekingWork'); + }.property('isEmployed', 'isSeekingWork') + }) ``` - This helper could be used in a template as follows: - ```handlebars - {{repeat text count=3}} + {{#each person in developers itemController="developer"}} + {{person.name}} {{#if person.isAvailableForHire}}Hire me!{{/if}} + {{/each}} ``` - ## Example with bound options + Each itemController will receive a reference to the current controller as + a `parentController` property. + + ### (Experimental) Grouped Each - Bound hash options are also supported. Example: + When used in conjunction with the experimental [group helper](https://github.com/emberjs/group-helper), + you can inform Handlebars to re-render an entire group of items instead of + re-rendering them one at a time (in the event that they are changed en masse + or an item is added/removed). ```handlebars - {{repeat text countBinding="numRepeats"}} + {{#group}} + {{#each people}} + {{firstName}} {{lastName}} + {{/each}} + {{/group}} ``` - In this example, count will be bound to the value of - the `numRepeats` property on the context. If that property - changes, the helper will be re-rendered. - - ## Example with extra dependencies + This can be faster than the normal way that Handlebars re-renders items + in some cases. - The `Ember.Handlebars.registerBoundHelper` method takes a variable length - third parameter which indicates extra dependencies on the passed in value. - This allows the handlebars helper to update when these dependencies change. + If for some reason you have a group with more than one `#each`, you can make + one of the collections be updated in normal (non-grouped) fashion by setting + the option `groupedRows=true` (counter-intuitive, I know). - ```javascript - Ember.Handlebars.registerBoundHelper('capitalizeName', function(value) { - return value.get('name').toUpperCase(); - }, 'name'); - ``` + For example, - ## Example with multiple bound properties + ```handlebars + {{dealershipName}} - `Ember.Handlebars.registerBoundHelper` supports binding to - multiple properties, e.g.: + {{#group}} + {{#each dealers}} + {{firstName}} {{lastName}} + {{/each}} - ```javascript - Ember.Handlebars.registerBoundHelper('concatenate', function() { - var values = Array.prototype.slice.call(arguments, 0, -1); - return values.join('||'); - }); + {{#each car in cars groupedRows=true}} + {{car.make}} {{car.model}} {{car.color}} + {{/each}} + {{/group}} ``` + Any change to `dealershipName` or the `dealers` collection will cause the + entire group to be re-rendered. However, changes to the `cars` collection + will be re-rendered individually (as normal). - Which allows for template syntax such as `{{concatenate prop1 prop2}}` or - `{{concatenate prop1 prop2 prop3}}`. If any of the properties change, - the helpr will re-render. Note that dependency keys cannot be - using in conjunction with multi-property helpers, since it is ambiguous - which property the dependent keys would belong to. - - ## Use with unbound helper + Note that `group` behavior is also disabled by specifying an `itemViewClass`. - The `{{unbound}}` helper can be used with bound helper invocations - to render them in their unbound form, e.g. + @method each + @for Ember.Handlebars.helpers + @param [name] {String} name for item (used with `in`) + @param [path] {String} path + @param [options] {Object} Handlebars key/value pairs of options + @param [options.itemViewClass] {String} a path to a view class used for each item + @param [options.itemController] {String} name of a controller to be created for each item + @param [options.groupedRows] {boolean} enable normal item-by-item rendering when inside a `#group` helper +*/ +Ember.Handlebars.registerHelper('each', function eachHelper(path, options) { + if (arguments.length === 4) { + Ember.assert("If you pass more than one argument to the each helper, it must be in the form #each foo in bar", arguments[1] === "in"); - ```handlebars - {{unbound capitalize name}} - ``` + var keywordName = arguments[0]; - In this example, if the name property changes, the helper - will not re-render. + options = arguments[3]; + path = arguments[2]; + if (path === '') { path = "this"; } - ## Use with blocks not supported + options.hash.keyword = keywordName; + } - Bound helpers do not support use with Handlebars blocks or - the addition of child views of any kind. + if (arguments.length === 1) { + options = path; + path = 'this'; + } - @method registerBoundHelper - @for Ember.Handlebars - @param {String} name - @param {Function} function - @param {String} dependentKeys* -*/ -Ember.Handlebars.registerBoundHelper = function(name, fn) { - var dependentKeys = slice.call(arguments, 2); + options.hash.dataSourceBinding = path; + // Set up emptyView as a metamorph with no tag + //options.hash.emptyViewClass = Ember._MetamorphView; - function helper() { - var properties = slice.call(arguments, 0, -1), - numProperties = properties.length, - options = arguments[arguments.length - 1], - normalizedProperties = [], - types = options.types, - data = options.data, - hash = options.hash, - view = data.view, - contexts = options.contexts, - currentContext = (contexts && contexts.length) ? contexts[0] : this, - prefixPathForDependentKeys = '', - loc, len, hashOption, - boundOption, property, - normalizedValue = Ember._SimpleHandlebarsView.prototype.normalizedValue; + if (options.data.insideGroup && !options.hash.groupedRows && !options.hash.itemViewClass) { + new Ember.Handlebars.GroupedEach(this, path, options).render(); + } else { + return Ember.Handlebars.helpers.collection.call(this, 'Ember.Handlebars.EachView', options); + } +}); - Ember.assert("registerBoundHelper-generated helpers do not support use with Handlebars blocks.", !options.fn); +})(); - // Detect bound options (e.g. countBinding="otherCount") - var boundOptions = hash.boundOptions = {}; - for (hashOption in hash) { - if (Ember.IS_BINDING.test(hashOption)) { - // Lop off 'Binding' suffix. - boundOptions[hashOption.slice(0, -7)] = hash[hashOption]; - } - } - // Expose property names on data.properties object. - var watchedProperties = []; - data.properties = []; - for (loc = 0; loc < numProperties; ++loc) { - data.properties.push(properties[loc]); - if (types[loc] === 'ID') { - var normalizedProp = normalizePath(currentContext, properties[loc], data); - normalizedProperties.push(normalizedProp); - watchedProperties.push(normalizedProp); - } else { - normalizedProperties.push(null); - } - } - // Handle case when helper invocation is preceded by `unbound`, e.g. - // {{unbound myHelper foo}} - if (data.isUnbound) { - return evaluateUnboundHelper(this, fn, normalizedProperties, options); - } +(function() { +/** +@module ember +@submodule ember-handlebars +*/ - var bindView = new Ember._SimpleHandlebarsView(null, null, !options.hash.unescaped, options.data); +/** + `template` allows you to render a template from inside another template. + This allows you to re-use the same template in multiple places. For example: - // Override SimpleHandlebarsView's method for generating the view's content. - bindView.normalizedValue = function() { - var args = [], boundOption; + ```html + + ``` - // Copy over bound hash options. - for (boundOption in boundOptions) { - if (!boundOptions.hasOwnProperty(boundOption)) { continue; } - property = normalizePath(currentContext, boundOptions[boundOption], data); - bindView.path = property.path; - bindView.pathRoot = property.root; - hash[boundOption] = normalizedValue.call(bindView); - } + ```html + + ``` - for (loc = 0; loc < numProperties; ++loc) { - property = normalizedProperties[loc]; - if (property) { - bindView.path = property.path; - bindView.pathRoot = property.root; - args.push(normalizedValue.call(bindView)); - } else { - args.push(properties[loc]); - } - } - args.push(options); + ```handlebars + {{#if isUser}} + {{template "user_info"}} + {{else}} + {{template "unlogged_user_info"}} + {{/if}} + ``` - // Run the supplied helper function. - return fn.apply(currentContext, args); - }; + This helper looks for templates in the global `Ember.TEMPLATES` hash. If you + add ` + ``` + + Take note that `"welcome"` is a string and not an object + reference. + + @method loc + @for Ember.Handlebars.helpers + @param {String} str The string to format */ -Ember._SimpleMetamorphView = Ember.CoreView.extend(Ember._Metamorph); +Ember.Handlebars.registerHelper('loc', function locHelper(str) { + return Ember.String.loc(str); +}); })(); (function() { -/*globals Handlebars */ -/*jshint newcap:false*/ -/** -@module ember -@submodule ember-handlebars -*/ -var get = Ember.get, set = Ember.set, handlebarsGet = Ember.Handlebars.get; -var Metamorph = requireModule('metamorph'); -function SimpleHandlebarsView(path, pathRoot, isEscaped, templateData) { - this.path = path; - this.pathRoot = pathRoot; - this.isEscaped = isEscaped; - this.templateData = templateData; +})(); - this.morph = Metamorph(); - this.state = 'preRender'; - this.updateId = null; - this._parentView = null; - this.buffer = null; -} -Ember._SimpleHandlebarsView = SimpleHandlebarsView; -SimpleHandlebarsView.prototype = { - isVirtual: true, - isView: true, +(function() { - destroy: function () { - if (this.updateId) { - Ember.run.cancel(this.updateId); - this.updateId = null; - } - if (this._parentView) { - this._parentView.removeChild(this); - } - this.morph = null; - this.state = 'destroyed'; - }, +})(); - propertyWillChange: Ember.K, - propertyDidChange: Ember.K, - normalizedValue: function() { - var path = this.path, - pathRoot = this.pathRoot, - result, templateData; +(function() { +/** +@module ember +@submodule ember-handlebars +*/ - // Use the pathRoot as the result if no path is provided. This - // happens if the path is `this`, which gets normalized into - // a `pathRoot` of the current Handlebars context and a path - // of `''`. - if (path === '') { - result = pathRoot; - } else { - templateData = this.templateData; - result = handlebarsGet(pathRoot, path, { data: templateData }); - } +var set = Ember.set, get = Ember.get; - return result; - }, +/** + The internal class used to create text inputs when the `{{input}}` + helper is used with `type` of `checkbox`. - renderToBuffer: function(buffer) { - var string = ''; + See [handlebars.helpers.input](/api/classes/Ember.Handlebars.helpers.html#method_input) for usage details. - string += this.morph.startTag(); - string += this.render(); - string += this.morph.endTag(); + ## Direct manipulation of `checked` - buffer.push(string); - }, + The `checked` attribute of an `Ember.Checkbox` object should always be set + through the Ember object or by interacting with its rendered element + representation via the mouse, keyboard, or touch. Updating the value of the + checkbox via jQuery will result in the checked value of the object and its + element losing synchronization. - render: function() { - // If not invoked via a triple-mustache ({{{foo}}}), escape - // the content of the template. - var escape = this.isEscaped; - var result = this.normalizedValue(); + ## Layout and LayoutName properties - if (result === null || result === undefined) { - result = ""; - } else if (!(result instanceof Handlebars.SafeString)) { - result = String(result); - } + Because HTML `input` elements are self closing `layout` and `layoutName` + properties will not be applied. See [Ember.View](/api/classes/Ember.View.html)'s + layout section for more information. - if (escape) { result = Handlebars.Utils.escapeExpression(result); } - return result; - }, + @class Checkbox + @namespace Ember + @extends Ember.View +*/ +Ember.Checkbox = Ember.View.extend({ + classNames: ['ember-checkbox'], - rerender: function() { - switch(this.state) { - case 'preRender': - case 'destroyed': - break; - case 'inBuffer': - throw new Ember.Error("Something you did tried to replace an {{expression}} before it was inserted into the DOM."); - case 'hasElement': - case 'inDOM': - this.updateId = Ember.run.scheduleOnce('render', this, 'update'); - break; - } + tagName: 'input', - return this; + attributeBindings: ['type', 'checked', 'indeterminate', 'disabled', 'tabindex', 'name', + 'autofocus', 'form'], + + type: "checkbox", + checked: false, + disabled: false, + indeterminate: false, + + init: function() { + this._super(); + this.on("change", this, this._updateElementValue); }, - update: function () { - this.updateId = null; - this.morph.html(this.render()); + didInsertElement: function() { + this._super(); + this.get('element').indeterminate = !!this.get('indeterminate'); }, - transitionTo: function(state) { - this.state = state; + _updateElementValue: function() { + set(this, 'checked', this.$().prop('checked')); } -}; +}); -var states = Ember.View.cloneStates(Ember.View.states), merge = Ember.merge; +})(); -merge(states._default, { - rerenderIfNeeded: Ember.K -}); -merge(states.inDOM, { - rerenderIfNeeded: function(view) { - if (view.normalizedValue() !== view._lastNormalizedValue) { - view.rerender(); - } - } -}); +(function() { /** - `Ember._HandlebarsBoundView` is a private view created by the Handlebars - `{{bind}}` helpers that is used to keep track of bound properties. +@module ember +@submodule ember-handlebars +*/ - Every time a property is bound using a `{{mustache}}`, an anonymous subclass - of `Ember._HandlebarsBoundView` is created with the appropriate sub-template - and context set up. When the associated property changes, just the template - for this view will re-render. +var get = Ember.get, set = Ember.set; - @class _HandlebarsBoundView +/** + Shared mixin used by `Ember.TextField` and `Ember.TextArea`. + + @class TextSupport @namespace Ember - @extends Ember._MetamorphView + @uses Ember.TargetActionSupport + @extends Ember.Mixin @private */ -Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ - instrumentName: 'boundHandlebars', - states: states, +Ember.TextSupport = Ember.Mixin.create(Ember.TargetActionSupport, { + value: "", + + attributeBindings: ['placeholder', 'disabled', 'maxlength', 'tabindex', 'readonly', + 'autofocus', 'form', 'selectionDirection', 'spellcheck', 'required'], + placeholder: null, + disabled: false, + maxlength: null, + + init: function() { + this._super(); + this.on("focusOut", this, this._elementValueDidChange); + this.on("change", this, this._elementValueDidChange); + this.on("paste", this, this._elementValueDidChange); + this.on("cut", this, this._elementValueDidChange); + this.on("input", this, this._elementValueDidChange); + this.on("keyUp", this, this.interpretKeyEvents); + }, /** - The function used to determine if the `displayTemplate` or - `inverseTemplate` should be rendered. This should be a function that takes - a value and returns a Boolean. + The action to be sent when the user presses the return key. - @property shouldDisplayFunc - @type Function + This is similar to the `{{action}}` helper, but is fired when + the user presses the return key when editing a text field, and sends + the value of the field as the context. + + @property action + @type String @default null */ - shouldDisplayFunc: null, + action: null, /** - Whether the template rendered by this view gets passed the context object - of its parent template, or gets passed the value of retrieving `path` - from the `pathRoot`. + The event that should send the action. - For example, this is true when using the `{{#if}}` helper, because the - template inside the helper should look up properties relative to the same - object as outside the block. This would be `false` when used with `{{#with - foo}}` because the template should receive the object found by evaluating - `foo`. + Options are: - @property preserveContext + * `enter`: the user pressed enter + * `keyPress`: the user pressed a key + + @property onEvent + @type String + @default enter + */ + onEvent: 'enter', + + /** + Whether they `keyUp` event that triggers an `action` to be sent continues + propagating to other views. + + By default, when the user presses the return key on their keyboard and + the text field has an `action` set, the action will be sent to the view's + controller and the key event will stop propagating. + + If you would like parent views to receive the `keyUp` event even after an + action has been dispatched, set `bubbles` to true. + + @property bubbles @type Boolean @default false */ - preserveContext: false, + bubbles: false, + + interpretKeyEvents: function(event) { + var map = Ember.TextSupport.KEY_EVENTS; + var method = map[event.keyCode]; + + this._elementValueDidChange(); + if (method) { return this[method](event); } + }, + + _elementValueDidChange: function() { + set(this, 'value', this.$().val()); + }, /** - If `preserveContext` is true, this is the object that will be used - to render the template. + The action to be sent when the user inserts a new line. - @property previousContext - @type Object + Called by the `Ember.TextSupport` mixin on keyUp if keycode matches 13. + Uses sendAction to send the `enter` action to the controller. + + @method insertNewline + @param {Event} event */ - previousContext: null, + insertNewline: function(event) { + sendAction('enter', this, event); + sendAction('insert-newline', this, event); + }, /** - The template to render when `shouldDisplayFunc` evaluates to `true`. + Called when the user hits escape. - @property displayTemplate - @type Function - @default null + Called by the `Ember.TextSupport` mixin on keyUp if keycode matches 27. + Uses sendAction to send the `escape-press` action to the controller. + + @method cancel + @param {Event} event */ - displayTemplate: null, + cancel: function(event) { + sendAction('escape-press', this, event); + }, /** - The template to render when `shouldDisplayFunc` evaluates to `false`. + Called when the text area is focused. - @property inverseTemplate - @type Function - @default null + @method focusIn + @param {Event} event */ - inverseTemplate: null, - + focusIn: function(event) { + sendAction('focus-in', this, event); + }, /** - The path to look up on `pathRoot` that is passed to - `shouldDisplayFunc` to determine which template to render. - - In addition, if `preserveContext` is `false,` the object at this path will - be passed to the template when rendering. + Called when the text area is blurred. - @property path - @type String - @default null + @method focusOut + @param {Event} event */ - path: null, + focusOut: function(event) { + sendAction('focus-out', this, event); + }, /** - The object from which the `path` will be looked up. Sometimes this is the - same as the `previousContext`, but in cases where this view has been - generated for paths that start with a keyword such as `view` or - `controller`, the path root will be that resolved object. + The action to be sent when the user presses a key. Enabled by setting + the `onEvent` property to `keyPress`. - @property pathRoot - @type Object + Uses sendAction to send the `keyPress` action to the controller. + + @method keyPress + @param {Event} event */ - pathRoot: null, + keyPress: function(event) { + sendAction('key-press', this, event); + } - normalizedValue: function() { - var path = get(this, 'path'), - pathRoot = get(this, 'pathRoot'), - valueNormalizer = get(this, 'valueNormalizerFunc'), - result, templateData; +}); - // Use the pathRoot as the result if no path is provided. This - // happens if the path is `this`, which gets normalized into - // a `pathRoot` of the current Handlebars context and a path - // of `''`. - if (path === '') { - result = pathRoot; - } else { - templateData = get(this, 'templateData'); - result = handlebarsGet(pathRoot, path, { data: templateData }); +Ember.TextSupport.KEY_EVENTS = { + 13: 'insertNewline', + 27: 'cancel' +}; + +// In principle, this shouldn't be necessary, but the legacy +// sectionAction semantics for TextField are different from +// the component semantics so this method normalizes them. +function sendAction(eventName, view, event) { + var action = get(view, eventName), + on = get(view, 'onEvent'), + value = get(view, 'value'); + + // back-compat support for keyPress as an event name even though + // it's also a method name that consumes the event (and therefore + // incompatible with sendAction semantics). + if (on === eventName || (on === 'keyPress' && eventName === 'key-press')) { + view.sendAction('action', value); + } + + view.sendAction(eventName, value); + + if (action || on === eventName) { + if(!get(view, 'bubbles')) { + event.stopPropagation(); } + } +} - return valueNormalizer ? valueNormalizer(result) : result; - }, +})(); - rerenderIfNeeded: function() { - this.currentState.rerenderIfNeeded(this); - }, - /** - Determines which template to invoke, sets up the correct state based on - that logic, then invokes the default `Ember.View` `render` implementation. - This method will first look up the `path` key on `pathRoot`, - then pass that value to the `shouldDisplayFunc` function. If that returns - `true,` the `displayTemplate` function will be rendered to DOM. Otherwise, - `inverseTemplate`, if specified, will be rendered. +(function() { +/** +@module ember +@submodule ember-handlebars +*/ - For example, if this `Ember._HandlebarsBoundView` represented the `{{#with - foo}}` helper, it would look up the `foo` property of its context, and - `shouldDisplayFunc` would always return true. The object found by looking - up `foo` would be passed to `displayTemplate`. +var get = Ember.get, set = Ember.set; - @method render - @param {Ember.RenderBuffer} buffer +/** + + The internal class used to create text inputs when the `{{input}}` + helper is used with `type` of `text`. + + See [Handlebars.helpers.input](/api/classes/Ember.Handlebars.helpers.html#method_input) for usage details. + + ## Layout and LayoutName properties + + Because HTML `input` elements are self closing `layout` and `layoutName` + properties will not be applied. See [Ember.View](/api/classes/Ember.View.html)'s + layout section for more information. + + @class TextField + @namespace Ember + @extends Ember.Component + @uses Ember.TextSupport +*/ +Ember.TextField = Ember.Component.extend(Ember.TextSupport, { + + classNames: ['ember-text-field'], + tagName: "input", + attributeBindings: ['type', 'value', 'size', 'pattern', 'name', 'min', 'max', + 'accept', 'autocomplete', 'autosave', 'formaction', + 'formenctype', 'formmethod', 'formnovalidate', 'formtarget', + 'height', 'inputmode', 'list', 'multiple', 'pattern', 'step', + 'width'], + + /** + The `value` attribute of the input element. As the user inputs text, this + property is updated live. + + @property value + @type String + @default "" */ - render: function(buffer) { - // If not invoked via a triple-mustache ({{{foo}}}), escape - // the content of the template. - var escape = get(this, 'isEscaped'); + value: "", - var shouldDisplay = get(this, 'shouldDisplayFunc'), - preserveContext = get(this, 'preserveContext'), - context = get(this, 'previousContext'); + /** + The `type` attribute of the input element. - var inverseTemplate = get(this, 'inverseTemplate'), - displayTemplate = get(this, 'displayTemplate'); + @property type + @type String + @default "text" + */ + type: "text", - var result = this.normalizedValue(); - this._lastNormalizedValue = result; + /** + The `size` of the text field in characters. - // First, test the conditional to see if we should - // render the template or not. - if (shouldDisplay(result)) { - set(this, 'template', displayTemplate); + @property size + @type String + @default null + */ + size: null, - // If we are preserving the context (for example, if this - // is an #if block, call the template with the same object. - if (preserveContext) { - set(this, '_context', context); - } else { - // Otherwise, determine if this is a block bind or not. - // If so, pass the specified object to the template - if (displayTemplate) { - set(this, '_context', result); - } else { - // This is not a bind block, just push the result of the - // expression to the render context and return. - if (result === null || result === undefined) { - result = ""; - } else if (!(result instanceof Handlebars.SafeString)) { - result = String(result); - } + /** + The `pattern` attribute of input element. - if (escape) { result = Handlebars.Utils.escapeExpression(result); } - buffer.push(result); - return; - } - } - } else if (inverseTemplate) { - set(this, 'template', inverseTemplate); + @property pattern + @type String + @default null + */ + pattern: null, - if (preserveContext) { - set(this, '_context', context); - } else { - set(this, '_context', result); - } - } else { - set(this, 'template', function() { return ''; }); - } + /** + The `min` attribute of input element used with `type="number"` or `type="range"`. + + @property min + @type String + @default null + */ + min: null, - return this._super(buffer); - } + /** + The `max` attribute of input element used with `type="number"` or `type="range"`. + + @property max + @type String + @default null + */ + max: null }); })(); @@ -25142,1252 +31053,1092 @@ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ @submodule ember-handlebars */ -var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; -var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.normalizePath; -var forEach = Ember.ArrayPolyfills.forEach; - -var EmberHandlebars = Ember.Handlebars, helpers = EmberHandlebars.helpers; - -function exists(value) { - return !Ember.isNone(value); -} - -// Binds a property into the DOM. This will create a hook in DOM that the -// KVO system will look for and update if the property changes. -function bind(property, options, preserveContext, shouldDisplay, valueNormalizer, childProperties) { - var data = options.data, - fn = options.fn, - inverse = options.inverse, - view = data.view, - currentContext = this, - normalized, observer, i; - - normalized = normalizePath(currentContext, property, data); +var get = Ember.get, set = Ember.set; - // Set up observers for observable objects - if ('object' === typeof this) { - if (data.insideGroup) { - observer = function() { - Ember.run.once(view, 'rerender'); - }; +/** + The internal class used to create textarea element when the `{{textarea}}` + helper is used. - var template, context, result = handlebarsGet(currentContext, property, options); + See [handlebars.helpers.textarea](/api/classes/Ember.Handlebars.helpers.html#method_textarea) for usage details. - result = valueNormalizer ? valueNormalizer(result) : result; + ## Layout and LayoutName properties - context = preserveContext ? currentContext : result; - if (shouldDisplay(result)) { - template = fn; - } else if (inverse) { - template = inverse; - } + Because HTML `textarea` elements do not contain inner HTML the `layout` and + `layoutName` properties will not be applied. See [Ember.View](/api/classes/Ember.View.html)'s + layout section for more information. - template(context, { data: options.data }); - } else { - // Create the view that will wrap the output of this template/property - // and add it to the nearest view's childViews array. - // See the documentation of Ember._HandlebarsBoundView for more. - var bindView = view.createChildView(Ember._HandlebarsBoundView, { - preserveContext: preserveContext, - shouldDisplayFunc: shouldDisplay, - valueNormalizerFunc: valueNormalizer, - displayTemplate: fn, - inverseTemplate: inverse, - path: property, - pathRoot: currentContext, - previousContext: currentContext, - isEscaped: !options.hash.unescaped, - templateData: options.data - }); + @class TextArea + @namespace Ember + @extends Ember.Component + @uses Ember.TextSupport +*/ +Ember.TextArea = Ember.Component.extend(Ember.TextSupport, { + classNames: ['ember-text-area'], - view.appendChild(bindView); + tagName: "textarea", + attributeBindings: ['rows', 'cols', 'name', 'selectionEnd', 'selectionStart', 'wrap'], + rows: null, + cols: null, - observer = function() { - Ember.run.scheduleOnce('render', bindView, 'rerenderIfNeeded'); - }; + _updateElementValue: Ember.observer('value', function() { + // We do this check so cursor position doesn't get affected in IE + var value = get(this, 'value'), + $el = this.$(); + if ($el && value !== $el.val()) { + $el.val(value); } + }), - // Observes the given property on the context and - // tells the Ember._HandlebarsBoundView to re-render. If property - // is an empty string, we are printing the current context - // object ({{this}}) so updating it is not our responsibility. - if (normalized.path !== '') { - view.registerObserver(normalized.root, normalized.path, observer); - if (childProperties) { - for (i=0; i -1; + } else { + // Primitives get passed through bindings as objects... since + // `new Number(4) !== 4`, we use `==` below + return content == selection; + } + }).property('content', 'parentView.selection'), - if (!options.fn) { - return simpleBind(context, property, options); - } + labelPathDidChange: Ember.observer('parentView.optionLabelPath', function() { + var labelPath = get(this, 'parentView.optionLabelPath'); - return bind.call(context, property, options, false, exists); -}); + if (!labelPath) { return; } -/** - @private + Ember.defineProperty(this, 'label', Ember.computed(function() { + return get(this, labelPath); + }).property(labelPath)); + }), - Use the `boundIf` helper to create a conditional that re-evaluates - whenever the truthiness of the bound value changes. + valuePathDidChange: Ember.observer('parentView.optionValuePath', function() { + var valuePath = get(this, 'parentView.optionValuePath'); - ```handlebars - {{#boundIf "content.shouldDisplayTitle"}} - {{content.title}} - {{/boundIf}} - ``` + if (!valuePath) { return; } - @method boundIf - @for Ember.Handlebars.helpers - @param {String} property Property to bind - @param {Function} fn Context to provide for rendering - @return {String} HTML string -*/ -EmberHandlebars.registerHelper('boundIf', function(property, fn) { - var context = (fn.contexts && fn.contexts.length) ? fn.contexts[0] : this; - var func = function(result) { - var truthy = result && get(result, 'isTruthy'); - if (typeof truthy === 'boolean') { return truthy; } + Ember.defineProperty(this, 'value', Ember.computed(function() { + return get(this, valuePath); + }).property(valuePath)); + }) +}); - if (Ember.isArray(result)) { - return get(result, 'length') !== 0; - } else { - return !!result; - } - }; +Ember.SelectOptgroup = Ember.CollectionView.extend({ + tagName: 'optgroup', + attributeBindings: ['label'], + + selectionBinding: 'parentView.selection', + multipleBinding: 'parentView.multiple', + optionLabelPathBinding: 'parentView.optionLabelPath', + optionValuePathBinding: 'parentView.optionValuePath', - return bind.call(context, property, fn, true, func, func, ['isTruthy', 'length']); + itemViewClassBinding: 'parentView.optionView' }); /** - @method with - @for Ember.Handlebars.helpers - @param {Function} context - @param {Hash} options - @return {String} HTML string -*/ -EmberHandlebars.registerHelper('with', function(context, options) { - if (arguments.length === 4) { - var keywordName, path, rootPath, normalized; + The `Ember.Select` view class renders a + [select](https://developer.mozilla.org/en/HTML/Element/select) HTML element, + allowing the user to choose from a list of options. - Ember.assert("If you pass more than one argument to the with helper, it must be in the form #with foo as bar", arguments[1] === "as"); - options = arguments[3]; - keywordName = arguments[2]; - path = arguments[0]; + The text and `value` property of each ` + + ``` - `bind-attr` cannot redeclare existing DOM element attributes. The use of `src` - in the following `bind-attr` example will be ignored and the hard coded value - of `src="/failwhale.gif"` will take precedence: + The `value` attribute of the selected `"); + return buffer; } - var attrKeys = Ember.keys(attrs); +function program3(depth0,data) { + + var stack1; + stack1 = helpers.each.call(depth0, "view.groupedContent", {hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(4, program4, data),contexts:[depth0],types:["ID"],data:data}); + if(stack1 || stack1 === 0) { data.buffer.push(stack1); } + else { data.buffer.push(''); } + } +function program4(depth0,data) { + + + data.buffer.push(escapeExpression(helpers.view.call(depth0, "view.groupView", {hash:{ + 'content': ("content"), + 'label': ("label") + },hashTypes:{'content': "ID",'label': "ID"},hashContexts:{'content': depth0,'label': depth0},contexts:[depth0],types:["ID"],data:data}))); + } - // For each attribute passed, create an observer and emit the - // current value of the property as an attribute. - forEach.call(attrKeys, function(attr) { - var path = attrs[attr], - normalized; +function program6(depth0,data) { + + var stack1; + stack1 = helpers.each.call(depth0, "view.content", {hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(7, program7, data),contexts:[depth0],types:["ID"],data:data}); + if(stack1 || stack1 === 0) { data.buffer.push(stack1); } + else { data.buffer.push(''); } + } +function program7(depth0,data) { + + + data.buffer.push(escapeExpression(helpers.view.call(depth0, "view.optionView", {hash:{ + 'content': ("") + },hashTypes:{'content': "ID"},hashContexts:{'content': depth0},contexts:[depth0],types:["ID"],data:data}))); + } - Ember.assert(fmt("You must provide an expression as the value of bound attribute. You specified: %@=%@", [attr, path]), typeof path === 'string'); + stack1 = helpers['if'].call(depth0, "view.prompt", {hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(1, program1, data),contexts:[depth0],types:["ID"],data:data}); + if(stack1 || stack1 === 0) { data.buffer.push(stack1); } + stack1 = helpers['if'].call(depth0, "view.optionGroupPath", {hash:{},hashTypes:{},hashContexts:{},inverse:self.program(6, program6, data),fn:self.program(3, program3, data),contexts:[depth0],types:["ID"],data:data}); + if(stack1 || stack1 === 0) { data.buffer.push(stack1); } + return buffer; + +}), + attributeBindings: ['multiple', 'disabled', 'tabindex', 'name', 'required', 'autofocus', + 'form', 'size'], - normalized = normalizePath(ctx, path, options.data); + /** + The `multiple` attribute of the select element. Indicates whether multiple + options can be selected. - var value = (path === 'this') ? normalized.root : handlebarsGet(ctx, path, options), - type = Ember.typeOf(value); + @property multiple + @type Boolean + @default false + */ + multiple: false, - Ember.assert(fmt("Attributes must be numbers, strings or booleans, not %@", [value]), value === null || value === undefined || type === 'number' || type === 'string' || type === 'boolean'); + /** + The `disabled` attribute of the select element. Indicates whether + the element is disabled from interactions. - var observer, invoker; + @property disabled + @type Boolean + @default false + */ + disabled: false, - observer = function observer() { - var result = handlebarsGet(ctx, path, options); + /** + The `required` attribute of the select element. Indicates whether + a selected option is required for form validation. - Ember.assert(fmt("Attributes must be numbers, strings or booleans, not %@", [result]), result === null || result === undefined || typeof result === 'number' || typeof result === 'string' || typeof result === 'boolean'); + @property required + @type Boolean + @default false + */ + required: false, - var elem = view.$("[data-bindattr-" + dataId + "='" + dataId + "']"); + /** + The list of options. - // If we aren't able to find the element, it means the element - // to which we were bound has been removed from the view. - // In that case, we can assume the template has been re-rendered - // and we need to clean up the observer. - if (!elem || elem.length === 0) { - Ember.removeObserver(normalized.root, normalized.path, invoker); - return; - } + If `optionLabelPath` and `optionValuePath` are not overridden, this should + be a list of strings, which will serve simultaneously as labels and values. - Ember.View.applyAttributeBindings(elem, attr, result); - }; + Otherwise, this should be a list of objects. For instance: - // Add an observer to the view for when the property changes. - // When the observer fires, find the element using the - // unique data id and update the attribute to the new value. - // Note: don't add observer when path is 'this' or path - // is whole keyword e.g. {{#each x in list}} ... {{bind-attr attr="x"}} - if (path !== 'this' && !(normalized.isKeyword && normalized.path === '' )) { - view.registerObserver(normalized.root, normalized.path, observer); - } + ```javascript + Ember.Select.create({ + content: Ember.A([ + { id: 1, firstName: 'Yehuda' }, + { id: 2, firstName: 'Tom' } + ]), + optionLabelPath: 'content.firstName', + optionValuePath: 'content.id' + }); + ``` - // if this changes, also change the logic in ember-views/lib/views/view.js - if ((type === 'string' || (type === 'number' && !isNaN(value)))) { - ret.push(attr + '="' + Handlebars.Utils.escapeExpression(value) + '"'); - } else if (value && type === 'boolean') { - // The developer controls the attr name, so it should always be safe - ret.push(attr + '="' + attr + '"'); - } - }, this); + @property content + @type Array + @default null + */ + content: null, - // Add the unique identifier - // NOTE: We use all lower-case since Firefox has problems with mixed case in SVG - ret.push('data-bindattr-' + dataId + '="' + dataId + '"'); - return new EmberHandlebars.SafeString(ret.join(' ')); -}); + /** + When `multiple` is `false`, the element of `content` that is currently + selected, if any. -/** - See `bind-attr` + When `multiple` is `true`, an array of such elements. - @method bindAttr - @for Ember.Handlebars.helpers - @deprecated - @param {Function} context - @param {Hash} options - @return {String} HTML string -*/ -EmberHandlebars.registerHelper('bindAttr', EmberHandlebars.helpers['bind-attr']); + @property selection + @type Object or Array + @default null + */ + selection: null, -/** - @private + /** + In single selection mode (when `multiple` is `false`), value can be used to + get the current selection's value or set the selection by it's value. - Helper that, given a space-separated string of property paths and a context, - returns an array of class names. Calling this method also has the side - effect of setting up observers at those property paths, such that if they - change, the correct class name will be reapplied to the DOM element. + It is not currently supported in multiple selection mode. - For example, if you pass the string "fooBar", it will first look up the - "fooBar" value of the context. If that value is true, it will add the - "foo-bar" class to the current element (i.e., the dasherized form of - "fooBar"). If the value is a string, it will add that string as the class. - Otherwise, it will not add any new class name. + @property value + @type String + @default null + */ + value: Ember.computed(function(key, value) { + if (arguments.length === 2) { return value; } + var valuePath = get(this, 'optionValuePath').replace(/^content\.?/, ''); + return valuePath ? get(this, 'selection.' + valuePath) : get(this, 'selection'); + }).property('selection'), - @method bindClasses - @for Ember.Handlebars - @param {Ember.Object} context The context from which to lookup properties - @param {String} classBindings A string, space-separated, of class bindings - to use - @param {Ember.View} view The view in which observers should look for the - element to update - @param {Srting} bindAttrId Optional bindAttr id used to lookup elements - @return {Array} An array of class names to add -*/ -EmberHandlebars.bindClasses = function(context, classBindings, view, bindAttrId, options) { - var ret = [], newClass, value, elem; + /** + If given, a top-most dummy option will be rendered to serve as a user + prompt. - // Helper method to retrieve the property from the context and - // determine which class string to return, based on whether it is - // a Boolean or not. - var classStringForPath = function(root, parsedPath, options) { - var val, - path = parsedPath.path; + @property prompt + @type String + @default null + */ + prompt: null, - if (path === 'this') { - val = root; - } else if (path === '') { - val = true; - } else { - val = handlebarsGet(root, path, options); - } + /** + The path of the option labels. See [content](/api/classes/Ember.Select.html#property_content). - return Ember.View._classStringForValue(path, val, parsedPath.className, parsedPath.falsyClassName); - }; + @property optionLabelPath + @type String + @default 'content' + */ + optionLabelPath: 'content', - // For each property passed, loop through and setup - // an observer. - forEach.call(classBindings.split(' '), function(binding) { + /** + The path of the option values. See [content](/api/classes/Ember.Select.html#property_content). - // Variable in which the old class value is saved. The observer function - // closes over this variable, so it knows which string to remove when - // the property changes. - var oldClass; + @property optionValuePath + @type String + @default 'content' + */ + optionValuePath: 'content', - var observer, invoker; + /** + The path of the option group. + When this property is used, `content` should be sorted by `optionGroupPath`. - var parsedPath = Ember.View._parsePropertyPath(binding), - path = parsedPath.path, - pathRoot = context, - normalized; + @property optionGroupPath + @type String + @default null + */ + optionGroupPath: null, - if (path !== '' && path !== 'this') { - normalized = normalizePath(context, path, options.data); + /** + The view class for optgroup. - pathRoot = normalized.root; - path = normalized.path; - } + @property groupView + @type Ember.View + @default Ember.SelectOptgroup + */ + groupView: Ember.SelectOptgroup, - // Set up an observer on the context. If the property changes, toggle the - // class name. - observer = function() { - // Get the current value of the property - newClass = classStringForPath(context, parsedPath, options); - elem = bindAttrId ? view.$("[data-bindattr-" + bindAttrId + "='" + bindAttrId + "']") : view.$(); + groupedContent: Ember.computed(function() { + var groupPath = get(this, 'optionGroupPath'); + var groupedContent = Ember.A(); + var content = get(this, 'content') || []; - // If we can't find the element anymore, a parent template has been - // re-rendered and we've been nuked. Remove the observer. - if (!elem || elem.length === 0) { - Ember.removeObserver(pathRoot, path, invoker); - } else { - // If we had previously added a class to the element, remove it. - if (oldClass) { - elem.removeClass(oldClass); - } + forEach(content, function(item) { + var label = get(item, groupPath); - // If necessary, add a new class. Make sure we keep track of it so - // it can be removed in the future. - if (newClass) { - elem.addClass(newClass); - oldClass = newClass; - } else { - oldClass = null; - } + if (get(groupedContent, 'lastObject.label') !== label) { + groupedContent.pushObject({ + label: label, + content: Ember.A() + }); } - }; - - if (path !== '' && path !== 'this') { - view.registerObserver(pathRoot, path, observer); - } - - // We've already setup the observer; now we just need to figure out the - // correct behavior right now on the first pass through. - value = classStringForPath(context, parsedPath, options); - - if (value) { - ret.push(value); - - // Make sure we save the current value so that it can be removed if the - // observer fires. - oldClass = value; - } - }); - - return ret; -}; - - -})(); - - - -(function() { -/*globals Handlebars */ -// TODO: Don't require the entire module -/** -@module ember -@submodule ember-handlebars -*/ + get(groupedContent, 'lastObject.content').push(item); + }); -var get = Ember.get, set = Ember.set; -var EmberHandlebars = Ember.Handlebars; -var LOWERCASE_A_Z = /^[a-z]/; -var VIEW_PREFIX = /^view\./; + return groupedContent; + }).property('optionGroupPath', 'content.@each'), -function makeBindings(thisContext, options) { - var hash = options.hash, - hashType = options.hashTypes; + /** + The view class for option. - for (var prop in hash) { - if (hashType[prop] === 'ID') { + @property optionView + @type Ember.View + @default Ember.SelectOption + */ + optionView: Ember.SelectOption, - var value = hash[prop]; + _change: function() { + if (get(this, 'multiple')) { + this._changeMultiple(); + } else { + this._changeSingle(); + } + }, - if (Ember.IS_BINDING.test(prop)) { - Ember.warn("You're attempting to render a view by passing " + prop + "=" + value + " to a view helper, but this syntax is ambiguous. You should either surround " + value + " in quotes or remove `Binding` from " + prop + "."); - } else { - hash[prop + 'Binding'] = value; - hashType[prop + 'Binding'] = 'STRING'; - delete hash[prop]; - delete hashType[prop]; + selectionDidChange: Ember.observer('selection.@each', function() { + var selection = get(this, 'selection'); + if (get(this, 'multiple')) { + if (!isArray(selection)) { + set(this, 'selection', Ember.A([selection])); + return; } + this._selectionDidChangeMultiple(); + } else { + this._selectionDidChangeSingle(); } - } - - if (hash.hasOwnProperty('idBinding')) { - // id can't be bound, so just perform one-time lookup. - hash.id = EmberHandlebars.get(thisContext, hash.idBinding, options); - hashType.id = 'STRING'; - delete hash.idBinding; - delete hashType.idBinding; - } -} + }), -EmberHandlebars.ViewHelper = Ember.Object.create({ + valueDidChange: Ember.observer('value', function() { + var content = get(this, 'content'), + value = get(this, 'value'), + valuePath = get(this, 'optionValuePath').replace(/^content\.?/, ''), + selectedValue = (valuePath ? get(this, 'selection.' + valuePath) : get(this, 'selection')), + selection; - propertiesFromHTMLOptions: function(options, thisContext) { - var hash = options.hash, data = options.data; - var extensions = {}, - classes = hash['class'], - dup = false; + if (value !== selectedValue) { + selection = content ? content.find(function(obj) { + return value === (valuePath ? get(obj, valuePath) : obj); + }) : null; - if (hash.id) { - extensions.elementId = hash.id; - dup = true; + this.set('selection', selection); } + }), - if (hash.tag) { - extensions.tagName = hash.tag; - dup = true; - } - if (classes) { - classes = classes.split(' '); - extensions.classNames = classes; - dup = true; - } + _triggerChange: function() { + var selection = get(this, 'selection'); + var value = get(this, 'value'); - if (hash.classBinding) { - extensions.classNameBindings = hash.classBinding.split(' '); - dup = true; - } + if (!Ember.isNone(selection)) { this.selectionDidChange(); } + if (!Ember.isNone(value)) { this.valueDidChange(); } - if (hash.classNameBindings) { - if (extensions.classNameBindings === undefined) extensions.classNameBindings = []; - extensions.classNameBindings = extensions.classNameBindings.concat(hash.classNameBindings.split(' ')); - dup = true; - } + this._change(); + }, - if (hash.attributeBindings) { - Ember.assert("Setting 'attributeBindings' via Handlebars is not allowed. Please subclass Ember.View and set it there instead."); - extensions.attributeBindings = null; - dup = true; - } + _changeSingle: function() { + var selectedIndex = this.$()[0].selectedIndex, + content = get(this, 'content'), + prompt = get(this, 'prompt'); + + if (!content || !get(content, 'length')) { return; } + if (prompt && selectedIndex === 0) { set(this, 'selection', null); return; } + + if (prompt) { selectedIndex -= 1; } + set(this, 'selection', content.objectAt(selectedIndex)); + }, - if (dup) { - hash = Ember.$.extend({}, hash); - delete hash.id; - delete hash.tag; - delete hash['class']; - delete hash.classBinding; - } - // Set the proper context for all bindings passed to the helper. This applies to regular attribute bindings - // as well as class name bindings. If the bindings are local, make them relative to the current context - // instead of the view. - var path; + _changeMultiple: function() { + var options = this.$('option:selected'), + prompt = get(this, 'prompt'), + offset = prompt ? 1 : 0, + content = get(this, 'content'), + selection = get(this, 'selection'); - // Evaluate the context of regular attribute bindings: - for (var prop in hash) { - if (!hash.hasOwnProperty(prop)) { continue; } + if (!content) { return; } + if (options) { + var selectedIndexes = options.map(function() { + return this.index - offset; + }).toArray(); + var newSelection = content.objectsAt(selectedIndexes); - // Test if the property ends in "Binding" - if (Ember.IS_BINDING.test(prop) && typeof hash[prop] === 'string') { - path = this.contextualizeBindingPath(hash[prop], data); - if (path) { hash[prop] = path; } + if (isArray(selection)) { + replace(selection, 0, get(selection, 'length'), newSelection); + } else { + set(this, 'selection', newSelection); } } + }, - // Evaluate the context of class name bindings: - if (extensions.classNameBindings) { - for (var b in extensions.classNameBindings) { - var full = extensions.classNameBindings[b]; - if (typeof full === 'string') { - // Contextualize the path of classNameBinding so this: - // - // classNameBinding="isGreen:green" - // - // is converted to this: - // - // classNameBinding="_parentView.context.isGreen:green" - var parsedPath = Ember.View._parsePropertyPath(full); - path = this.contextualizeBindingPath(parsedPath.path, data); - if (path) { extensions.classNameBindings[b] = path + parsedPath.classNames; } - } - } - } + _selectionDidChangeSingle: function() { + var el = this.get('element'); + if (!el) { return; } - return Ember.$.extend(hash, extensions); + var content = get(this, 'content'), + selection = get(this, 'selection'), + selectionIndex = content ? indexOf(content, selection) : -1, + prompt = get(this, 'prompt'); + + if (prompt) { selectionIndex += 1; } + if (el) { el.selectedIndex = selectionIndex; } }, - // Transform bindings from the current context to a context that can be evaluated within the view. - // Returns null if the path shouldn't be changed. - // - // TODO: consider the addition of a prefix that would allow this method to return `path`. - contextualizeBindingPath: function(path, data) { - var normalized = Ember.Handlebars.normalizePath(null, path, data); - if (normalized.isKeyword) { - return 'templateData.keywords.' + path; - } else if (Ember.isGlobalPath(path)) { - return null; - } else if (path === 'this') { - return '_parentView.context'; - } else { - return '_parentView.context.' + path; + _selectionDidChangeMultiple: function() { + var content = get(this, 'content'), + selection = get(this, 'selection'), + selectedIndexes = content ? indexesOf(content, selection) : [-1], + prompt = get(this, 'prompt'), + offset = prompt ? 1 : 0, + options = this.$('option'), + adjusted; + + if (options) { + options.each(function() { + adjusted = this.index > -1 ? this.index - offset : -1; + this.selected = indexOf(selectedIndexes, adjusted) > -1; + }); } }, - helper: function(thisContext, path, options) { - var data = options.data, - fn = options.fn, - newView; + init: function() { + this._super(); + this.on("didInsertElement", this, this._triggerChange); + this.on("change", this, this._change); + } +}); - makeBindings(thisContext, options); +})(); - if ('string' === typeof path) { - // TODO: this is a lame conditional, this should likely change - // but something along these lines will likely need to be added - // as deprecation warnings - // - if (options.types[0] === 'STRING' && LOWERCASE_A_Z.test(path) && !VIEW_PREFIX.test(path)) { - Ember.assert("View requires a container", !!data.view.container); - newView = data.view.container.lookupFactory('view:' + path); - } else { - newView = EmberHandlebars.get(thisContext, path, options); - } - Ember.assert("Unable to find view at path '" + path + "'", !!newView); - } else { - newView = path; - } +(function() { +/** +@module ember +@submodule ember-handlebars-compiler +*/ - Ember.assert(Ember.String.fmt('You must pass a view to the #view helper, not %@ (%@)', [path, newView]), Ember.View.detect(newView) || Ember.View.detectInstance(newView)); +/** - var viewOptions = this.propertiesFromHTMLOptions(options, thisContext); - var currentView = data.view; - viewOptions.templateData = data; - var newViewProto = newView.proto ? newView.proto() : newView; + The `{{input}}` helper inserts an HTML `` tag into the template, + with a `type` value of either `text` or `checkbox`. If no `type` is provided, + `text` will be the default value applied. The attributes of `{{input}}` + match those of the native HTML tag as closely as possible for these two types. - if (fn) { - Ember.assert("You cannot provide a template block if you also specified a templateName", !get(viewOptions, 'templateName') && !get(newViewProto, 'templateName')); - viewOptions.template = fn; - } + ## Use as text field + An `{{input}}` with no `type` or a `type` of `text` will render an HTML text input. + The following HTML attributes can be set via the helper: - // We only want to override the `_context` computed property if there is - // no specified controller. See View#_context for more information. - if (!newViewProto.controller && !newViewProto.controllerBinding && !viewOptions.controller && !viewOptions.controllerBinding) { - viewOptions._context = thisContext; - } + + + + + + + + + + + +
    `readonly``required``autofocus`
    `value``placeholder``disabled`
    `size``tabindex``maxlength`
    `name``min``max`
    `pattern``accept``autocomplete`
    `autosave``formaction``formenctype`
    `formmethod``formnovalidate``formtarget`
    `height``inputmode``multiple`
    `step``width``form`
    `selectionDirection``spellcheck` 
    - currentView.appendChild(newView, viewOptions); - } -}); -/** - `{{view}}` inserts a new instance of `Ember.View` into a template passing its - options to the `Ember.View`'s `create` method and using the supplied block as - the view's own template. + When set to a quoted string, these values will be directly applied to the HTML + element. When left unquoted, these values will be bound to a property on the + template's current rendering context (most typically a controller instance). - An empty `` and the following template: + ## Unbound: ```handlebars - A span: - {{#view tagName="span"}} - hello. - {{/view}} + {{input value="http://www.facebook.com"}} ``` - Will result in HTML structure: ```html - - - -
    - A span: - - Hello. - -
    - + ``` - ### `parentView` setting - - The `parentView` property of the new `Ember.View` instance created through - `{{view}}` will be set to the `Ember.View` instance of the template where - `{{view}}` was called. + ## Bound: ```javascript - aView = Ember.View.create({ - template: Ember.Handlebars.compile("{{#view}} my parent: {{parentView.elementId}} {{/view}}") + App.ApplicationController = Ember.Controller.extend({ + firstName: "Stanley", + entryNotAllowed: true }); + ``` - aView.appendTo('body'); + + ```handlebars + {{input type="text" value=firstName disabled=entryNotAllowed size="50"}} ``` - Will result in HTML structure: ```html -
    -
    - my parent: ember1 -
    -
    + ``` - ### Setting CSS id and class attributes + ## Extension - The HTML `id` attribute can be set on the `{{view}}`'s resulting element with - the `id` option. This option will _not_ be passed to `Ember.View.create`. + Internally, `{{input type="text"}}` creates an instance of `Ember.TextField`, passing + arguments from the helper to `Ember.TextField`'s `create` method. You can extend the + capablilties of text inputs in your applications by reopening this class. For example, + if you are deploying to browsers where the `required` attribute is used, you + can add this to the `TextField`'s `attributeBindings` property: - ```handlebars - {{#view tagName="span" id="a-custom-id"}} - hello. - {{/view}} + + ```javascript + Ember.TextField.reopen({ + attributeBindings: ['required'] + }); ``` - Results in the following HTML structure: + Keep in mind when writing `Ember.TextField` subclasses that `Ember.TextField` + itself extends `Ember.Component`, meaning that it does NOT inherit + the `controller` of the parent view. - ```html -
    - - hello. - -
    - ``` + See more about [Ember components](api/classes/Ember.Component.html) - The HTML `class` attribute can be set on the `{{view}}`'s resulting element - with the `class` or `classNameBindings` options. The `class` option will - directly set the CSS `class` attribute and will not be passed to - `Ember.View.create`. `classNameBindings` will be passed to `create` and use - `Ember.View`'s class name binding functionality: - ```handlebars - {{#view tagName="span" class="a-custom-class"}} - hello. - {{/view}} - ``` + ## Use as checkbox - Results in the following HTML structure: + An `{{input}}` with a `type` of `checkbox` will render an HTML checkbox input. + The following HTML attributes can be set via the helper: - ```html -
    - - hello. - -
    - ``` +* `checked` +* `disabled` +* `tabindex` +* `indeterminate` +* `name` +* `autofocus` +* `form` - ### Supplying a different view class - `{{view}}` can take an optional first argument before its supplied options to - specify a path to a custom view class. + When set to a quoted string, these values will be directly applied to the HTML + element. When left unquoted, these values will be bound to a property on the + template's current rendering context (most typically a controller instance). + + ## Unbound: ```handlebars - {{#view "MyApp.CustomView"}} - hello. - {{/view}} + {{input type="checkbox" name="isAdmin"}} ``` - The first argument can also be a relative path accessible from the current - context. + ```html + + ``` + + ## Bound: ```javascript - MyApp = Ember.Application.create({}); - MyApp.OuterView = Ember.View.extend({ - innerViewClass: Ember.View.extend({ - classNames: ['a-custom-view-class-as-property'] - }), - template: Ember.Handlebars.compile('{{#view "view.innerViewClass"}} hi {{/view}}') + App.ApplicationController = Ember.Controller.extend({ + isAdmin: true }); - - MyApp.OuterView.create().appendTo('body'); ``` - Will result in the following HTML: - ```html -
    -
    - hi -
    -
    + ```handlebars + {{input type="checkbox" checked=isAdmin }} ``` - ### Blockless use - - If you supply a custom `Ember.View` subclass that specifies its own template - or provide a `templateName` option to `{{view}}` it can be used without - supplying a block. Attempts to use both a `templateName` option and supply a - block will throw an error. - ```handlebars - {{view "MyApp.ViewWithATemplateDefined"}} + ```html + ``` - ### `viewName` property + ## Extension + + Internally, `{{input type="checkbox"}}` creates an instance of `Ember.Checkbox`, passing + arguments from the helper to `Ember.Checkbox`'s `create` method. You can extend the + capablilties of checkbox inputs in your applications by reopening this class. For example, + if you wanted to add a css class to all checkboxes in your application: - You can supply a `viewName` option to `{{view}}`. The `Ember.View` instance - will be referenced as a property of its parent view by this name. ```javascript - aView = Ember.View.create({ - template: Ember.Handlebars.compile('{{#view viewName="aChildByName"}} hi {{/view}}') + Ember.Checkbox.reopen({ + classNames: ['my-app-checkbox'] }); - - aView.appendTo('body'); - aView.get('aChildByName') // the instance of Ember.View created by {{view}} helper ``` - @method view + + @method input @for Ember.Handlebars.helpers - @param {String} path @param {Hash} options - @return {String} HTML string */ -EmberHandlebars.registerHelper('view', function(path, options) { - Ember.assert("The view helper only takes a single argument", arguments.length <= 2); +Ember.Handlebars.registerHelper('input', function(options) { + Ember.assert('You can only pass attributes to the `input` helper, not arguments', arguments.length < 2); - // If no path is provided, treat path param as options. - if (path && path.data && path.data.isRenderData) { - options = path; - path = "Ember.View"; - } + var hash = options.hash, + types = options.hashTypes, + inputType = hash.type, + onEvent = hash.on; - return EmberHandlebars.ViewHelper.helper(this, path, options); + delete hash.type; + delete hash.on; + + if (inputType === 'checkbox') { + Ember.assert("{{input type='checkbox'}} does not support setting `value=someBooleanValue`; you must use `checked=someBooleanValue` instead.", options.hashTypes.value !== 'ID'); + return Ember.Handlebars.helpers.view.call(this, Ember.Checkbox, options); + } else { + if (inputType) { hash.type = inputType; } + hash.onEvent = onEvent || 'enter'; + return Ember.Handlebars.helpers.view.call(this, Ember.TextField, options); + } }); +/** + `{{textarea}}` inserts a new instance of ` + ``` - `{{collection}}`'s primary use is as a block helper with a `contentBinding` - option pointing towards an `Ember.Array`-compatible object. An `Ember.View` - instance will be created for each item in its `content` property. Each view - will have its own `content` property set to the appropriate item in the - collection. + Bound: - The provided block will be applied as the template for each item's view. + In the following example, the `writtenWords` property on `App.ApplicationController` + will be updated live as the user types 'Lots of text that IS bound' into + the text area of their browser's window. - Given an empty `` the following template: + ```javascript + App.ApplicationController = Ember.Controller.extend({ + writtenWords: "Lots of text that IS bound" + }); + ``` ```handlebars - {{#collection contentBinding="App.items"}} - Hi {{view.content.name}} - {{/collection}} + {{textarea value=writtenWords}} ``` - And the following application code + Would result in the following HTML: + + ```html + + ``` + + If you wanted a one way binding between the text area and a div tag + somewhere else on your screen, you could use `Ember.computed.oneWay`: ```javascript - App = Ember.Application.create() - App.items = [ - Ember.Object.create({name: 'Dave'}), - Ember.Object.create({name: 'Mary'}), - Ember.Object.create({name: 'Sara'}) - ] + App.ApplicationController = Ember.Controller.extend({ + writtenWords: "Lots of text that IS bound", + outputWrittenWords: Ember.computed.oneWay("writtenWords") + }); ``` - Will result in the HTML structure below + ```handlebars + {{textarea value=writtenWords}} - ```html -
    -
    Hi Dave
    -
    Hi Mary
    -
    Hi Sara
    +
    + {{outputWrittenWords}}
    ``` - ### Blockless Use + Would result in the following HTML: - If you provide an `itemViewClass` option that has its own `template` you can - omit the block. + ```html + - The following template: + <-- the following div will be updated in real time as you type --> - ```handlebars - {{collection contentBinding="App.items" itemViewClass="App.AnItemView"}} +
    + Lots of text that IS bound +
    ``` - And application code + Finally, this example really shows the power and ease of Ember when two + properties are bound to eachother via `Ember.computed.alias`. Type into + either text area box and they'll both stay in sync. Note that + `Ember.computed.alias` costs more in terms of performance, so only use it when + your really binding in both directions: ```javascript - App = Ember.Application.create(); - App.items = [ - Ember.Object.create({name: 'Dave'}), - Ember.Object.create({name: 'Mary'}), - Ember.Object.create({name: 'Sara'}) - ]; - - App.AnItemView = Ember.View.extend({ - template: Ember.Handlebars.compile("Greetings {{view.content.name}}") + App.ApplicationController = Ember.Controller.extend({ + writtenWords: "Lots of text that IS bound", + twoWayWrittenWords: Ember.computed.alias("writtenWords") }); ``` - Will result in the HTML structure below - - ```html -
    -
    Greetings Dave
    -
    Greetings Mary
    -
    Greetings Sara
    -
    + ```handlebars + {{textarea value=writtenWords}} + {{textarea value=twoWayWrittenWords}} ``` - ### Specifying a CollectionView subclass + ```html + - By default the `{{collection}}` helper will create an instance of - `Ember.CollectionView`. You can supply a `Ember.CollectionView` subclass to - the helper by passing it as the first argument: + <-- both updated in real time --> - ```handlebars - {{#collection App.MyCustomCollectionClass contentBinding="App.items"}} - Hi {{view.content.name}} - {{/collection}} + ``` - ### Forwarded `item.*`-named Options - - As with the `{{view}}`, helper options passed to the `{{collection}}` will be - set on the resulting `Ember.CollectionView` as properties. Additionally, - options prefixed with `item` will be applied to the views rendered for each - item (note the camelcasing): + ## Extension - ```handlebars - {{#collection contentBinding="App.items" - itemTagName="p" - itemClassNames="greeting"}} - Howdy {{view.content.name}} - {{/collection}} + Internally, `{{textarea}}` creates an instance of `Ember.TextArea`, passing + arguments from the helper to `Ember.TextArea`'s `create` method. You can + extend the capabilities of text areas in your application by reopening this + class. For example, if you are deploying to browsers where the `required` + attribute is used, you can globally add support for the `required` attribute + on all `{{textarea}}`s' in your app by reopening `Ember.TextArea` or + `Ember.TextSupport` and adding it to the `attributeBindings` concatenated + property: + + ```javascript + Ember.TextArea.reopen({ + attributeBindings: ['required'] + }); ``` - Will result in the following HTML structure: + Keep in mind when writing `Ember.TextArea` subclasses that `Ember.TextArea` + itself extends `Ember.Component`, meaning that it does NOT inherit + the `controller` of the parent view. - ```html -
    -

    Howdy Dave

    -

    Howdy Mary

    -

    Howdy Sara

    -
    - ``` + See more about [Ember components](api/classes/Ember.Component.html) - @method collection + @method textarea @for Ember.Handlebars.helpers - @param {String} path @param {Hash} options - @return {String} HTML string - @deprecated Use `{{each}}` helper instead. */ -Ember.Handlebars.registerHelper('collection', function(path, options) { - Ember.deprecate("Using the {{collection}} helper without specifying a class has been deprecated as the {{each}} helper now supports the same functionality.", path !== 'collection'); - - // If no path is provided, treat path param as options. - if (path && path.data && path.data.isRenderData) { - options = path; - path = undefined; - Ember.assert("You cannot pass more than one argument to the collection helper", arguments.length === 1); - } else { - Ember.assert("You cannot pass more than one argument to the collection helper", arguments.length === 2); - } +Ember.Handlebars.registerHelper('textarea', function(options) { + Ember.assert('You can only pass attributes to the `textarea` helper, not arguments', arguments.length < 2); - var fn = options.fn; - var data = options.data; - var inverse = options.inverse; - var view = options.data.view; + var hash = options.hash, + types = options.hashTypes; - // If passed a path string, convert that into an object. - // Otherwise, just default to the standard class. - var collectionClass; - collectionClass = path ? handlebarsGet(this, path, options) : Ember.CollectionView; - Ember.assert(fmt("%@ #collection: Could not find collection class %@", [data.view, path]), !!collectionClass); + return Ember.Handlebars.helpers.view.call(this, Ember.TextArea, options); +}); - var hash = options.hash, itemHash = {}, match; +})(); - // Extract item view class if provided else default to the standard class - var collectionPrototype = collectionClass.proto(), - itemViewClass; - if (hash.itemView) { - var controller = data.keywords.controller; - Ember.assert('You specified an itemView, but the current context has no container to look the itemView up in. This probably means that you created a view manually, instead of through the container. Instead, use container.lookup("view:viewName"), which will properly instantiate your view.', controller && controller.container); - var container = controller.container; - itemViewClass = container.resolve('view:' + Ember.String.camelize(hash.itemView)); - Ember.assert('You specified the itemView ' + hash.itemView + ", but it was not found at " + container.describe("view:" + hash.itemView) + " (and it was not registered in the container)", !!itemViewClass); - } else if (hash.itemViewClass) { - itemViewClass = handlebarsGet(collectionPrototype, hash.itemViewClass, options); - } else { - itemViewClass = collectionPrototype.itemViewClass; - } - Ember.assert(fmt("%@ #collection: Could not find itemViewClass %@", [data.view, itemViewClass]), !!itemViewClass); +(function() { +Ember.ComponentLookup = Ember.Object.extend({ + lookupFactory: function(name, container) { - delete hash.itemViewClass; - delete hash.itemView; + container = container || this.container; - // Go through options passed to the {{collection}} helper and extract options - // that configure item views instead of the collection itself. - for (var prop in hash) { - if (hash.hasOwnProperty(prop)) { - match = prop.match(/^item(.)(.*)$/); + var fullName = 'component:' + name, + templateFullName = 'template:components/' + name, + templateRegistered = container && container.has(templateFullName); - if (match && prop !== 'itemController') { - // Convert itemShouldFoo -> shouldFoo - itemHash[match[1].toLowerCase() + match[2]] = hash[prop]; - // Delete from hash as this will end up getting passed to the - // {{view}} helper method. - delete hash[prop]; - } + if (templateRegistered) { + container.injection(fullName, 'layout', templateFullName); } - } - - if (fn) { - itemHash.template = fn; - delete options.fn; - } - var emptyViewClass; - if (inverse && inverse !== Handlebars.VM.noop) { - emptyViewClass = get(collectionPrototype, 'emptyViewClass'); - emptyViewClass = emptyViewClass.extend({ - template: inverse, - tagName: itemHash.tagName - }); - } else if (hash.emptyViewClass) { - emptyViewClass = handlebarsGet(this, hash.emptyViewClass, options); - } - if (emptyViewClass) { hash.emptyView = emptyViewClass; } + var Component = container.lookupFactory(fullName); - if (!hash.keyword) { - itemHash._context = Ember.computed.alias('content'); + // Only treat as a component if either the component + // or a template has been registered. + if (templateRegistered || Component) { + if (!Component) { + container.register(fullName, Ember.Component); + Component = container.lookupFactory(fullName); + } + return Component; + } } - - var viewOptions = Ember.Handlebars.ViewHelper.propertiesFromHTMLOptions({ data: data, hash: itemHash }, this); - hash.itemViewClass = itemViewClass.extend(viewOptions); - - return Ember.Handlebars.helpers.view.call(this, collectionClass, options); }); - })(); @@ -26399,42 +32150,82 @@ Ember.Handlebars.registerHelper('collection', function(path, options) { @submodule ember-handlebars */ -var handlebarsGet = Ember.Handlebars.get; - /** - `unbound` allows you to output a property without binding. *Important:* The - output will not be updated if the property changes. Use with caution. + Find templates stored in the head tag as script tags and make them available + to `Ember.CoreView` in the global `Ember.TEMPLATES` object. This will be run + as as jQuery DOM-ready callback. - ```handlebars -
    {{unbound somePropertyThatDoesntChange}}
    - ``` + Script tags with `text/x-handlebars` will be compiled + with Ember's Handlebars and are suitable for use as a view's template. + Those with type `text/x-raw-handlebars` will be compiled with regular + Handlebars and are suitable for use in views' computed properties. - `unbound` can also be used in conjunction with a bound helper to - render it in its unbound form: + @private + @method bootstrap + @for Ember.Handlebars + @static + @param ctx +*/ +Ember.Handlebars.bootstrap = function(ctx) { + var selectors = 'script[type="text/x-handlebars"], script[type="text/x-raw-handlebars"]'; - ```handlebars -
    {{unbound helperName somePropertyThatDoesntChange}}
    - ``` + Ember.$(selectors, ctx) + .each(function() { + // Get a reference to the script tag + var script = Ember.$(this); - @method unbound - @for Ember.Handlebars.helpers - @param {String} property - @return {String} HTML string + var compile = (script.attr('type') === 'text/x-raw-handlebars') ? + Ember.$.proxy(Handlebars.compile, Handlebars) : + Ember.$.proxy(Ember.Handlebars.compile, Ember.Handlebars), + // Get the name of the script, used by Ember.View's templateName property. + // First look for data-template-name attribute, then fall back to its + // id if no name is found. + templateName = script.attr('data-template-name') || script.attr('id') || 'application', + template = compile(script.html()); + + // Check if template of same name already exists + if (Ember.TEMPLATES[templateName] !== undefined) { + throw new Ember.Error('Template named "' + templateName + '" already exists.'); + } + + // For templates which have a name, we save them and then remove them from the DOM + Ember.TEMPLATES[templateName] = template; + + // Remove script tag from DOM + script.remove(); + }); +}; + +function bootstrap() { + Ember.Handlebars.bootstrap( Ember.$(document) ); +} + +function registerComponentLookup(container) { + container.register('component-lookup:main', Ember.ComponentLookup); +} + +/* + We tie this to application.load to ensure that we've at least + attempted to bootstrap at the point that the application is loaded. + + We also tie this to document ready since we're guaranteed that all + the inline templates are present at this point. + + There's no harm to running this twice, since we remove the templates + from the DOM after processing. */ -Ember.Handlebars.registerHelper('unbound', function(property, fn) { - var options = arguments[arguments.length - 1], helper, context, out; - if (arguments.length > 2) { - // Unbound helper call. - options.data.isUnbound = true; - helper = Ember.Handlebars.helpers[arguments[0]] || Ember.Handlebars.helperMissing; - out = helper.apply(this, Array.prototype.slice.call(arguments, 1)); - delete options.data.isUnbound; - return out; - } +Ember.onLoad('Ember.Application', function(Application) { + Application.initializer({ + name: 'domTemplates', + initialize: bootstrap + }); - context = (fn.contexts && fn.contexts.length) ? fn.contexts[0] : this; - return handlebarsGet(context, property, fn); + Application.initializer({ + name: 'registerComponentLookup', + after: 'domTemplates', + initialize: registerComponentLookup + }); }); })(); @@ -26442,1900 +32233,2497 @@ Ember.Handlebars.registerHelper('unbound', function(property, fn) { (function() { -/*jshint debug:true*/ /** +Ember Handlebars + @module ember @submodule ember-handlebars +@requires ember-views */ -var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.normalizePath; +Ember.runLoadHooks('Ember.Handlebars', Ember.Handlebars); -/** - `log` allows you to output the value of a variable in the current rendering - context. +})(); - ```handlebars - {{log myVariable}} - ``` +(function() { +define("route-recognizer", + ["exports"], + function(__exports__) { + "use strict"; + var specials = [ + '/', '.', '*', '+', '?', '|', + '(', ')', '[', ']', '{', '}', '\\' + ]; - @method log - @for Ember.Handlebars.helpers - @param {String} property -*/ -Ember.Handlebars.registerHelper('log', function(property, options) { - var context = (options.contexts && options.contexts.length) ? options.contexts[0] : this, - normalized = normalizePath(context, property, options.data), - pathRoot = normalized.root, - path = normalized.path, - value = (path === 'this') ? pathRoot : handlebarsGet(pathRoot, path, options); - Ember.Logger.log(value); -}); + var escapeRegex = new RegExp('(\\' + specials.join('|\\') + ')', 'g'); -/** - Execute the `debugger` statement in the current context. + function isArray(test) { + return Object.prototype.toString.call(test) === "[object Array]"; + } - ```handlebars - {{debugger}} - ``` + // A Segment represents a segment in the original route description. + // Each Segment type provides an `eachChar` and `regex` method. + // + // The `eachChar` method invokes the callback with one or more character + // specifications. A character specification consumes one or more input + // characters. + // + // The `regex` method returns a regex fragment for the segment. If the + // segment is a dynamic of star segment, the regex fragment also includes + // a capture. + // + // A character specification contains: + // + // * `validChars`: a String with a list of all valid characters, or + // * `invalidChars`: a String with a list of all invalid characters + // * `repeat`: true if the character specification can repeat - @method debugger - @for Ember.Handlebars.helpers - @param {String} property -*/ -Ember.Handlebars.registerHelper('debugger', function(options) { - debugger; -}); + function StaticSegment(string) { this.string = string; } + StaticSegment.prototype = { + eachChar: function(callback) { + var string = this.string, ch; -})(); + for (var i=0, l=string.length; i " + n.nextStates.map(function(s) { return s.debug() }).join(" or ") + " )"; + }).join(", ") } + END IF **/ - return view; - }, + // This is a somewhat naive strategy, but should work in a lot of cases + // A better strategy would properly resolve /posts/:id/new and /posts/edit/:id. + // + // This strategy generally prefers more static and less dynamic matching. + // Specifically, it + // + // * prefers fewer stars to more, then + // * prefers using stars for less of the match to more, then + // * prefers fewer dynamic segments to more, then + // * prefers more static segments to more + function sortSolutions(states) { + return states.sort(function(a, b) { + if (a.types.stars !== b.types.stars) { return a.types.stars - b.types.stars; } - destroy: function() { - if (!this._super()) { return; } + if (a.types.stars) { + if (a.types.statics !== b.types.statics) { return b.types.statics - a.types.statics; } + if (a.types.dynamics !== b.types.dynamics) { return b.types.dynamics - a.types.dynamics; } + } - var arrayController = get(this, '_arrayController'); + if (a.types.dynamics !== b.types.dynamics) { return a.types.dynamics - b.types.dynamics; } + if (a.types.statics !== b.types.statics) { return b.types.statics - a.types.statics; } - if (arrayController) { - arrayController.destroy(); + return 0; + }); + } + + function recognizeChar(states, ch) { + var nextStates = []; + + for (var i=0, l=states.length; iSorry, nobody is available for this task.

    - {{/each}} - ``` - ### Specifying a View class for items - If you provide an `itemViewClass` option that references a view class - with its own `template` you can omit the block. + if (segment instanceof EpsilonSegment) { continue; } - The following template: + output += "/"; + output += segment.generate(params); + } - ```handlebars - {{#view App.MyView }} - {{each view.items itemViewClass="App.AnItemView"}} - {{/view}} - ``` + if (output.charAt(0) !== '/') { output = '/' + output; } - And application code + if (params && params.queryParams) { + output += this.generateQueryString(params.queryParams, route.handlers); + } - ```javascript - App = Ember.Application.create({ - MyView: Ember.View.extend({ - items: [ - Ember.Object.create({name: 'Dave'}), - Ember.Object.create({name: 'Mary'}), - Ember.Object.create({name: 'Sara'}) - ] - }) - }); + return output; + }, - App.AnItemView = Ember.View.extend({ - template: Ember.Handlebars.compile("Greetings {{name}}") - }); - ``` + generateQueryString: function(params, handlers) { + var pairs = []; + var keys = []; + for(var key in params) { + if (params.hasOwnProperty(key)) { + keys.push(key); + } + } + keys.sort(); + for (var i = 0, len = keys.length; i < len; i++) { + key = keys[i]; + var value = params[key]; + if (value === false || value == null) { + continue; + } + var pair = key; + if (isArray(value)) { + for (var j = 0, l = value.length; j < l; j++) { + var arrayPair = key + '[]' + '=' + encodeURIComponent(value[j]); + pairs.push(arrayPair); + } + } + else if (value !== true) { + pair += "=" + encodeURIComponent(value); + pairs.push(pair); + } else { + pairs.push(pair); + } + } - Will result in the HTML structure below + if (pairs.length === 0) { return ''; } - ```html -
    -
    Greetings Dave
    -
    Greetings Mary
    -
    Greetings Sara
    -
    - ``` + return "?" + pairs.join("&"); + }, - If an `itemViewClass` is defined on the helper, and therefore the helper is not - being used as a block, an `emptyViewClass` can also be provided optionally. - The `emptyViewClass` will match the behavior of the `{{else}}` condition - described above. That is, the `emptyViewClass` will render if the collection - is empty. + parseQueryString: function(queryString) { + var pairs = queryString.split("&"), queryParams = {}; + for(var i=0; i < pairs.length; i++) { + var pair = pairs[i].split('='), + key = decodeURIComponent(pair[0]), + keyLength = key.length, + isArray = false, + value; + if (pair.length === 1) { + value = true; + } else { + //Handle arrays + if (keyLength > 2 && key.slice(keyLength -2) === '[]') { + isArray = true; + key = key.slice(0, keyLength - 2); + if(!queryParams[key]) { + queryParams[key] = []; + } + } + value = pair[1] ? decodeURIComponent(pair[1]) : ''; + } + if (isArray) { + queryParams[key].push(value); + } else { + queryParams[key] = value; + } - ### Representing each item with a Controller. - By default the controller lookup within an `{{#each}}` block will be - the controller of the template where the `{{#each}}` was used. If each - item needs to be presented by a custom controller you can provide a - `itemController` option which references a controller by lookup name. - Each item in the loop will be wrapped in an instance of this controller - and the item itself will be set to the `content` property of that controller. + } + return queryParams; + }, - This is useful in cases where properties of model objects need transformation - or synthesis for display: + recognize: function(path) { + var states = [ this.rootState ], + pathLen, i, l, queryStart, queryParams = {}, + isSlashDropped = false; - ```javascript - App.DeveloperController = Ember.ObjectController.extend({ - isAvailableForHire: function() { - return !this.get('content.isEmployed') && this.get('content.isSeekingWork'); - }.property('isEmployed', 'isSeekingWork') - }) - ``` + path = decodeURI(path); - ```handlebars - {{#each person in developers itemController="developer"}} - {{person.name}} {{#if person.isAvailableForHire}}Hire me!{{/if}} - {{/each}} - ``` + queryStart = path.indexOf('?'); + if (queryStart !== -1) { + var queryString = path.substr(queryStart + 1, path.length); + path = path.substr(0, queryStart); + queryParams = this.parseQueryString(queryString); + } - Each itemController will receive a reference to the current controller as - a `parentController` property. + // DEBUG GROUP path - ### (Experimental) Grouped Each + if (path.charAt(0) !== "/") { path = "/" + path; } - When used in conjunction with the experimental [group helper](https://github.com/emberjs/group-helper), - you can inform Handlebars to re-render an entire group of items instead of - re-rendering them one at a time (in the event that they are changed en masse - or an item is added/removed). + pathLen = path.length; + if (pathLen > 1 && path.charAt(pathLen - 1) === "/") { + path = path.substr(0, pathLen - 1); + isSlashDropped = true; + } - ```handlebars - {{#group}} - {{#each people}} - {{firstName}} {{lastName}} - {{/each}} - {{/group}} - ``` + for (i=0, l=path.length; i - {{#with loggedInUser}} - Last Login: {{lastLogin}} - User Info: {{template "user_info"}} - {{/with}} - - ``` + function addRoute(routeArray, path, handler) { + var len = 0; + for (var i=0, l=routeArray.length; i - Name: {{name}} - Karma: {{karma}} - - ``` + path = path.substr(len); + var route = { path: path, handler: handler }; + routeArray.push(route); + } - ```handlebars - {{#if isUser}} - {{template "user_info"}} - {{else}} - {{template "unlogged_user_info"}} - {{/if}} - ``` + function eachRoute(baseRoute, matcher, callback, binding) { + var routes = matcher.routes; - This helper looks for templates in the global `Ember.TEMPLATES` hash. If you - add ` - ``` +define("router/handler-info", + ["./utils","rsvp","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var bind = __dependency1__.bind; + var merge = __dependency1__.merge; + var oCreate = __dependency1__.oCreate; + var serialize = __dependency1__.serialize; + var promiseLabel = __dependency1__.promiseLabel; + var resolve = __dependency2__.resolve; - The `data-template-name` attribute of a partial template - is prefixed with an underscore. + function HandlerInfo(props) { + if (props) { + merge(this, props); + } + } - ```html - - ``` + HandlerInfo.prototype = { + name: null, + handler: null, + params: null, + context: null, - @method partial - @for Ember.Handlebars.helpers - @param {String} partialName the name of the template to render minus the leading underscore -*/ + log: function(payload, message) { + if (payload.log) { + payload.log(this.name + ': ' + message); + } + }, -Ember.Handlebars.registerHelper('partial', function(name, options) { - var nameParts = name.split("/"), - lastPart = nameParts[nameParts.length - 1]; + promiseLabel: function(label) { + return promiseLabel("'" + this.name + "' " + label); + }, - nameParts[nameParts.length - 1] = "_" + lastPart; + resolve: function(async, shouldContinue, payload) { + var checkForAbort = bind(this.checkForAbort, this, shouldContinue), + beforeModel = bind(this.runBeforeModelHook, this, async, payload), + model = bind(this.getModel, this, async, payload), + afterModel = bind(this.runAfterModelHook, this, async, payload), + becomeResolved = bind(this.becomeResolved, this, payload); + + return resolve(undefined, this.promiseLabel("Start handler")) + .then(checkForAbort, null, this.promiseLabel("Check for abort")) + .then(beforeModel, null, this.promiseLabel("Before model")) + .then(checkForAbort, null, this.promiseLabel("Check if aborted during 'beforeModel' hook")) + .then(model, null, this.promiseLabel("Model")) + .then(checkForAbort, null, this.promiseLabel("Check if aborted in 'model' hook")) + .then(afterModel, null, this.promiseLabel("After model")) + .then(checkForAbort, null, this.promiseLabel("Check if aborted in 'afterModel' hook")) + .then(becomeResolved, null, this.promiseLabel("Become resolved")); + }, - var view = options.data.view, - underscoredName = nameParts.join("/"), - template = view.templateForName(underscoredName), - deprecatedTemplate = !template && view.templateForName(name); + runBeforeModelHook: function(async, payload) { + if (payload.trigger) { + payload.trigger(true, 'willResolveModel', payload, this.handler); + } + return this.runSharedModelHook(async, payload, 'beforeModel', []); + }, - Ember.assert("Unable to find partial with name '"+name+"'.", template || deprecatedTemplate); + runAfterModelHook: function(async, payload, resolvedModel) { + // Stash the resolved model on the payload. + // This makes it possible for users to swap out + // the resolved model in afterModel. + var name = this.name; + this.stashResolvedModel(payload, resolvedModel); + + return this.runSharedModelHook(async, payload, 'afterModel', [resolvedModel]) + .then(function() { + // Ignore the fulfilled value returned from afterModel. + // Return the value stashed in resolvedModels, which + // might have been swapped out in afterModel. + return payload.resolvedModels[name]; + }, null, this.promiseLabel("Ignore fulfillment value and return model value")); + }, - template = template || deprecatedTemplate; + runSharedModelHook: function(async, payload, hookName, args) { + this.log(payload, "calling " + hookName + " hook"); - template(this, { data: options.data }); -}); + if (this.queryParams) { + args.push(this.queryParams); + } + args.push(payload); -})(); + var handler = this.handler; + return async(function() { + return handler[hookName] && handler[hookName].apply(handler, args); + }, this.promiseLabel("Handle " + hookName)); + }, + getModel: function(payload) { + throw new Error("This should be overridden by a subclass of HandlerInfo"); + }, + checkForAbort: function(shouldContinue, promiseValue) { + return resolve(shouldContinue(), this.promiseLabel("Check for abort")).then(function() { + // We don't care about shouldContinue's resolve value; + // pass along the original value passed to this fn. + return promiseValue; + }, null, this.promiseLabel("Ignore fulfillment value and continue")); + }, -(function() { -/** -@module ember -@submodule ember-handlebars -*/ + stashResolvedModel: function(payload, resolvedModel) { + payload.resolvedModels = payload.resolvedModels || {}; + payload.resolvedModels[this.name] = resolvedModel; + }, -var get = Ember.get, set = Ember.set; + becomeResolved: function(payload, resolvedContext) { + var params = this.params || serialize(this.handler, resolvedContext, this.names); -/** - `{{yield}}` denotes an area of a template that will be rendered inside - of another template. It has two main uses: + if (payload) { + this.stashResolvedModel(payload, resolvedContext); + payload.params = payload.params || {}; + payload.params[this.name] = params; + } - ### Use with `layout` - When used in a Handlebars template that is assigned to an `Ember.View` - instance's `layout` property Ember will render the layout template first, - inserting the view's own rendered output at the `{{yield}}` location. + return new ResolvedHandlerInfo({ + context: resolvedContext, + name: this.name, + handler: this.handler, + params: params + }); + }, - An empty `` and the following application code: + shouldSupercede: function(other) { + // Prefer this newer handlerInfo over `other` if: + // 1) The other one doesn't exist + // 2) The names don't match + // 3) This handler has a context that doesn't match + // the other one (or the other one doesn't have one). + // 4) This handler has parameters that don't match the other. + if (!other) { return true; } - ```javascript - AView = Ember.View.extend({ - classNames: ['a-view-with-layout'], - layout: Ember.Handlebars.compile('
    {{yield}}
    '), - template: Ember.Handlebars.compile('I am wrapped') - }); + var contextsMatch = (other.context === this.context); + return other.name !== this.name || + (this.hasOwnProperty('context') && !contextsMatch) || + (this.hasOwnProperty('params') && !paramsMatch(this.params, other.params)); + } + }; - aView = AView.create(); - aView.appendTo('body'); - ``` + function ResolvedHandlerInfo(props) { + HandlerInfo.call(this, props); + } - Will result in the following HTML output: + ResolvedHandlerInfo.prototype = oCreate(HandlerInfo.prototype); + ResolvedHandlerInfo.prototype.resolve = function(async, shouldContinue, payload) { + // A ResolvedHandlerInfo just resolved with itself. + if (payload && payload.resolvedModels) { + payload.resolvedModels[this.name] = this.context; + } + return resolve(this, this.promiseLabel("Resolve")); + }; - ```html - -
    -
    - I am wrapped -
    -
    - - ``` + // These are generated by URL transitions and + // named transitions for non-dynamic route segments. + function UnresolvedHandlerInfoByParam(props) { + HandlerInfo.call(this, props); + this.params = this.params || {}; + } - The `yield` helper cannot be used outside of a template assigned to an - `Ember.View`'s `layout` property and will throw an error if attempted. + UnresolvedHandlerInfoByParam.prototype = oCreate(HandlerInfo.prototype); + UnresolvedHandlerInfoByParam.prototype.getModel = function(async, payload) { + var fullParams = this.params; + if (payload && payload.queryParams) { + fullParams = {}; + merge(fullParams, this.params); + fullParams.queryParams = payload.queryParams; + } - ```javascript - BView = Ember.View.extend({ - classNames: ['a-view-with-layout'], - template: Ember.Handlebars.compile('{{yield}}') - }); + var hookName = typeof this.handler.deserialize === 'function' ? + 'deserialize' : 'model'; - bView = BView.create(); - bView.appendTo('body'); + return this.runSharedModelHook(async, payload, hookName, [fullParams]); + }; - // throws - // Uncaught Error: assertion failed: - // You called yield in a template that was not a layout - ``` - ### Use with Ember.Component - When designing components `{{yield}}` is used to denote where, inside the component's - template, an optional block passed to the component should render: + // These are generated only for named transitions + // with dynamic route segments. + function UnresolvedHandlerInfoByObject(props) { + HandlerInfo.call(this, props); + } - ```handlebars - - {{#labeled-textfield value=someProperty}} - First name: - {{/labeled-textfield}} - ``` + UnresolvedHandlerInfoByObject.prototype = oCreate(HandlerInfo.prototype); + UnresolvedHandlerInfoByObject.prototype.getModel = function(async, payload) { + this.log(payload, this.name + ": resolving provided model"); + return resolve(this.context); + }; - ```handlebars - - - ``` + function paramsMatch(a, b) { + if ((!a) ^ (!b)) { + // Only one is null. + return false; + } - Result: + if (!a) { + // Both must be null. + return true; + } - ```html -
    - @property loadingClass - @type String - @default loading - **/ - loadingClass: 'loading', + var FooView = Ember.View.extend({ + template: Ember.Handlebars.compile('

    Foo

    ') + }); + var fooView = FooView.create(); + myView.connectOutlet('main', fooView); + // The html for myView now looks like: + //
    Child view: + //

    Foo

    + //
    + ``` + @method connectOutlet + @param {String} outletName A unique name for the outlet + @param {Object} view An Ember.View + */ + connectOutlet: function(outletName, view) { + if (this._pendingDisconnections) { + delete this._pendingDisconnections[outletName]; + } - /** - The CSS class to apply to a `LinkView`'s element when its `disabled` - property is `true`. + if (this._hasEquivalentView(outletName, view)) { + view.destroy(); + return; + } - @property disabledClass - @type String - @default disabled - **/ - disabledClass: 'disabled', - _isDisabled: false, + var outlets = get(this, '_outlets'), + container = get(this, 'container'), + router = container && container.lookup('router:main'), + renderedName = get(view, 'renderedName'); - /** - Determines whether the `LinkView` will trigger routing via - the `replaceWith` routing strategy. + set(outlets, outletName, view); - @property replace - @type Boolean - @default false - **/ - replace: false, + if (router && renderedName) { + router._connectActiveView(renderedName, view); + } + }, - /** - By default the `{{link-to}}` helper will bind to the `href` and - `title` attributes. It's discourage that you override these defaults, - however you can push onto the array if needed. + /** + Determines if the view has already been created by checking if + the view has the same constructor, template, and context as the + view in the `_outlets` object. - @property attributeBindings - @type Array | String - @default ['href', 'title', 'rel'] - **/ - attributeBindings: ['href', 'title', 'rel'], + @private + @method _hasEquivalentView + @param {String} outletName The name of the outlet we are checking + @param {Object} view An Ember.View + @return {Boolean} + */ + _hasEquivalentView: function(outletName, view) { + var existingView = get(this, '_outlets.'+outletName); + return existingView && + existingView.constructor === view.constructor && + existingView.get('template') === view.get('template') && + existingView.get('context') === view.get('context'); + }, - /** - By default the `{{link-to}}` helper will bind to the `active`, `loading`, and - `disabled` classes. It is discouraged to override these directly. + /** + Removes an outlet from the view. - @property classNameBindings - @type Array - @default ['active', 'loading', 'disabled'] - **/ - classNameBindings: ['active', 'loading', 'disabled'], + Example - /** - By default the `{{link-to}}` helper responds to the `click` event. You - can override this globally by setting this property to your custom - event name. + ```javascript + var MyView = Ember.View.extend({ + template: Ember.Handlebars.compile('Child view: {{outlet "main"}} ') + }); + var myView = MyView.create(); + myView.appendTo('body'); + // myView's html: + //
    Child view:
    - This is particularly useful on mobile when one wants to avoid the 300ms - click delay using some sort of custom `tap` event. + var FooView = Ember.View.extend({ + template: Ember.Handlebars.compile('

    Foo

    ') + }); + var fooView = FooView.create(); + myView.connectOutlet('main', fooView); + // myView's html: + //
    Child view: + //

    Foo

    + //
    - @property eventName - @type String - @default click - */ - eventName: 'click', + myView.disconnectOutlet('main'); + // myView's html: + //
    Child view:
    + ``` - // this is doc'ed here so it shows up in the events - // section of the API documentation, which is where - // people will likely go looking for it. - /** - Triggers the `LinkView`'s routing behavior. If - `eventName` is changed to a value other than `click` - the routing behavior will trigger on that custom event - instead. + @method disconnectOutlet + @param {String} outletName The name of the outlet to be removed + */ + disconnectOutlet: function(outletName) { + if (!this._pendingDisconnections) { + this._pendingDisconnections = {}; + } + this._pendingDisconnections[outletName] = true; + Ember.run.once(this, '_finishDisconnections'); + }, - @event click - **/ + /** + Gets an outlet that is pending disconnection and then + nullifys the object on the `_outlet` object. - /** - An overridable method called when LinkView objects are instantiated. + @private + @method _finishDisconnections + */ + _finishDisconnections: function() { + if (this.isDestroyed) return; // _outlets will be gone anyway + var outlets = get(this, '_outlets'); + var pendingDisconnections = this._pendingDisconnections; + this._pendingDisconnections = null; - Example: + for (var outletName in pendingDisconnections) { + set(outlets, outletName, null); + } + } +}); - ```javascript - App.MyLinkView = Ember.LinkView.extend({ - init: function() { - this._super(); - Ember.Logger.log('Event is ' + this.get('eventName')); - } - }); - ``` +})(); - NOTE: If you do override `init` for a framework class like `Ember.View` or - `Ember.ArrayController`, be sure to call `this._super()` in your - `init` declaration! If you don't, Ember may not have an opportunity to - do important setup work, and you'll see strange behavior in your - application. - @method init - */ - init: function() { - this._super.apply(this, arguments); - // Map desired event name to invoke function - var eventName = get(this, 'eventName'); - this.on(eventName, this, this._invoke); +(function() { +/** +@module ember +@submodule ember-views +*/ - var helperParameters = this.parameters, - templateContext = helperParameters.context, - paths = getResolvedPaths(helperParameters), - length = paths.length, - path, i; +// Add a new named queue after the 'actions' queue (where RSVP promises +// resolve), which is used in router transitions to prevent unnecessary +// loading state entry if all context promises resolve on the +// 'actions' queue first. - for(i=0; i < length; i++) { - path = paths[i]; - if (null === path) { - // A literal value was provided, not a path, so nothing to observe. - continue; - } +var queues = Ember.run.queues, + indexOf = Ember.ArrayPolyfills.indexOf; +queues.splice(indexOf.call(queues, 'actions') + 1, 0, 'routerTransitions'); - var normalizedPath = - Ember.Handlebars.normalizePath(templateContext, path, helperParameters.options.data); - this.registerObserver(normalizedPath.root, normalizedPath.path, this, this._paramsChanged); - } - }, +})(); - /** - @private - This method is invoked by observers installed during `init` that fire - whenever the helpers - @method _paramsChanged - */ - _paramsChanged: function() { - this.notifyPropertyChange('resolvedParams'); - }, - /** - @private +(function() { +/** +@module ember +@submodule ember-routing +*/ - Even though this isn't a virtual view, we want to treat it as if it is - so that you can access the parent with {{view.prop}} +var get = Ember.get, set = Ember.set; - @method concreteView - **/ - concreteView: Ember.computed(function() { - return get(this, 'parentView'); - }).property('parentView'), +/** + Ember.Location returns an instance of the correct implementation of + the `location` API. - /** + ## Implementations - Accessed as a classname binding to apply the `LinkView`'s `disabledClass` - CSS `class` to the element when the link is disabled. + You can pass an implementation name (`hash`, `history`, `none`) to force a + particular implementation to be used in your application. - When `true` interactions with the element will not trigger route changes. - @property disabled - */ - disabled: Ember.computed(function(key, value) { - if (value !== undefined) { this.set('_isDisabled', value); } + ### HashLocation - return value ? get(this, 'disabledClass') : false; - }), + Using `HashLocation` results in URLs with a `#` (hash sign) separating the + server side URL portion of the URL from the portion that is used by Ember. + This relies upon the `hashchange` event existing in the browser. - /** - Accessed as a classname binding to apply the `LinkView`'s `activeClass` - CSS `class` to the element when the link is active. + Example: - A `LinkView` is considered active when its `currentWhen` property is `true` - or the application's current route is the route the `LinkView` would trigger - transitions into. + ```javascript + App.Router.map(function() { + this.resource('posts', function() { + this.route('new'); + }); + }); - @property active - **/ - active: Ember.computed(function() { - if (get(this, 'loading')) { return false; } + App.Router.reopen({ + location: 'hash' + }); + ``` - var router = get(this, 'router'), - routeArgs = get(this, 'routeArgs'), - contexts = routeArgs.slice(1), - resolvedParams = get(this, 'resolvedParams'), - currentWhen = this.currentWhen || resolvedParams[0], - currentWithIndex = currentWhen + '.index', - isActive = router.isActive.apply(router, [currentWhen].concat(contexts)) || - router.isActive.apply(router, [currentWithIndex].concat(contexts)); + This will result in a posts.new url of `/#/posts/new`. - if (isActive) { return get(this, 'activeClass'); } - }).property('resolvedParams', 'routeArgs', 'router.url'), + ### HistoryLocation - /** - Accessed as a classname binding to apply the `LinkView`'s `loadingClass` - CSS `class` to the element when the link is loading. + Using `HistoryLocation` results in URLs that are indistinguishable from a + standard URL. This relies upon the browser's `history` API. - A `LinkView` is considered loading when it has at least one - parameter whose value is currently null or undefined. During - this time, clicking the link will perform no transition and - emit a warning that the link is still in a loading state. + Example: - @property loading - **/ - loading: Ember.computed(function() { - if (!get(this, 'routeArgs')) { return get(this, 'loadingClass'); } - }).property('routeArgs'), + ```javascript + App.Router.map(function() { + this.resource('posts', function() { + this.route('new'); + }); + }); - /** - @private + App.Router.reopen({ + location: 'history' + }); + ``` - Returns the application's main router from the container. + This will result in a posts.new url of `/posts/new`. - @property router - **/ - router: Ember.computed(function() { - return get(this, 'controller').container.lookup('router:main'); - }), + Keep in mind that your server must serve the Ember app at all the routes you + define. - /** - @private + ### AutoLocation - Event handler that invokes the link, activating the associated route. + Using `AutoLocation`, the router will use the best Location class supported by + the browser it is running in. - @method _invoke - @param {Event} event - */ - _invoke: function(event) { - if (!isSimpleClick(event)) { return true; } + Browsers that support the `history` API will use `HistoryLocation`, those that + do not, but still support the `hashchange` event will use `HashLocation`, and + in the rare case neither is supported will use `NoneLocation`. - event.preventDefault(); - if (this.bubbles === false) { event.stopPropagation(); } + Example: - if (get(this, '_isDisabled')) { return false; } + ```javascript + App.Router.map(function() { + this.resource('posts', function() { + this.route('new'); + }); + }); - if (get(this, 'loading')) { - Ember.Logger.warn("This link-to is in an inactive loading state because at least one of its parameters presently has a null/undefined value, or the provided route name is invalid."); - return false; - } + App.Router.reopen({ + location: 'auto' + }); + ``` - var router = get(this, 'router'), - routeArgs = get(this, 'routeArgs'); + This will result in a posts.new url of `/posts/new` for modern browsers that + support the `history` api or `/#/posts/new` for older ones, like Internet + Explorer 9 and below. - if (get(this, 'replace')) { - router.replaceWith.apply(router, routeArgs); - } else { - router.transitionTo.apply(router, routeArgs); - } - }, + When a user visits a link to your application, they will be automatically + upgraded or downgraded to the appropriate `Location` class, with the URL + transformed accordingly, if needed. - /** - @private + Keep in mind that since some of your users will use `HistoryLocation`, your + server must serve the Ember app at all the routes you define. - Computed property that returns the resolved parameters. + ### NoneLocation - @property - @return {Array} - */ - resolvedParams: Ember.computed(function() { - var parameters = this.parameters, - options = parameters.options, - types = options.types, - data = options.data; + Using `NoneLocation` causes Ember to not store the applications URL state + in the actual URL. This is generally used for testing purposes, and is one + of the changes made when calling `App.setupForTesting()`. - return resolveParams(parameters.context, parameters.params, { types: types, data: data }); - }).property(), + ## Location API - /** - @private + Each location implementation must provide the following methods: - Computed property that returns the current route name and - any dynamic segments. + * implementation: returns the string name used to reference the implementation. + * getURL: returns the current URL. + * setURL(path): sets the current URL. + * replaceURL(path): replace the current URL (optional). + * onUpdateURL(callback): triggers the callback when the URL changes. + * formatURL(url): formats `url` to be placed into `href` attribute. - @property - @return {Array} An array with the route name and any dynamic segments - */ - routeArgs: Ember.computed(function() { + Calling setURL or replaceURL will not trigger onUpdateURL callbacks. - var resolvedParams = get(this, 'resolvedParams').slice(0), - router = get(this, 'router'), - namedRoute = resolvedParams[0]; + @class Location + @namespace Ember + @static +*/ +Ember.Location = { + /** + This is deprecated in favor of using the container to lookup the location + implementation as desired. - if (!namedRoute) { return; } + For example: - namedRoute = fullRouteName(router, namedRoute); - resolvedParams[0] = namedRoute; + ```javascript + // Given a location registered as follows: + container.register('location:history-test', HistoryTestLocation); - Ember.assert(fmt("The attempt to link-to route '%@' failed. The router did not find '%@' in its possible routes: '%@'", [namedRoute, namedRoute, Ember.keys(router.router.recognizer.names).join("', '")]), router.hasRoute(namedRoute)); + // You could create a new instance via: + container.lookup('location:history-test'); + ``` - for (var i = 1, len = resolvedParams.length; i < len; ++i) { - var param = resolvedParams[i]; - if (param === null || typeof param === 'undefined') { - // If contexts aren't present, consider the linkView unloaded. - return; - } - } + @method create + @param {Object} options + @return {Object} an instance of an implementation of the `location` API + @deprecated Use the container to lookup the location implementation that you + need. + */ + create: function(options) { + var implementation = options && options.implementation; + Ember.assert("Ember.Location.create: you must specify a 'implementation' option", !!implementation); - return resolvedParams; - }).property('resolvedParams'), + var implementationClass = this.implementations[implementation]; + Ember.assert("Ember.Location.create: " + implementation + " is not a valid implementation", !!implementationClass); - /** - Sets the element's `href` attribute to the url for - the `LinkView`'s targeted route. + return implementationClass.create.apply(implementationClass, arguments); + }, - If the `LinkView`'s `tagName` is changed to a value other - than `a`, this property will be ignored. + /** + This is deprecated in favor of using the container to register the + location implementation as desired. - @property href - **/ - href: Ember.computed(function() { - if (get(this, 'tagName') !== 'a') { return; } + Example: - var router = get(this, 'router'), - routeArgs = get(this, 'routeArgs'); + ```javascript + Application.initializer({ + name: "history-test-location", - return routeArgs ? router.generate.apply(router, routeArgs) : get(this, 'loadingHref'); - }).property('routeArgs'), + initialize: function(container, application) { + application.register('location:history-test', HistoryTestLocation); + } + }); + ``` - /** - The default href value to use while a link-to is loading. - Only applies when tagName is 'a' + @method registerImplementation + @param {String} name + @param {Object} implementation of the `location` API + @deprecated Register your custom location implementation with the + container directly. + */ + registerImplementation: function(name, implementation) { + Ember.deprecate('Using the Ember.Location.registerImplementation is no longer supported. Register your custom location implementation with the container instead.', false); - @property loadingHref - @type String - @default # - */ - loadingHref: '#' - }); + this.implementations[name] = implementation; + }, - LinkView.toString = function() { return "LinkView"; }; + implementations: {}, + _location: window.location, /** - The `{{link-to}}` helper renders a link to the supplied - `routeName` passing an optionally supplied model to the - route as its `model` context of the route. The block - for `{{link-to}}` becomes the innerHTML of the rendered - element: + Returns the current `location.hash` by parsing location.href since browsers + inconsistently URL-decode `location.hash`. + + https://bugzilla.mozilla.org/show_bug.cgi?id=483304 - ```handlebars - {{#link-to 'photoGallery'}} - Great Hamster Photos - {{/link-to}} - ``` + @private + @method getHash + */ + _getHash: function () { + // AutoLocation has it at _location, HashLocation at .location. + // Being nice and not changing + var href = (this._location || this.location).href, + hashIndex = href.indexOf('#'); - ```html - - Great Hamster Photos - - ``` + if (hashIndex === -1) { + return ''; + } else { + return href.substr(hashIndex); + } + } +}; - ### Supplying a tagName - By default `{{link-to}}` renders an `` element. This can - be overridden for a single use of `{{link-to}}` by supplying - a `tagName` option: +})(); - ```handlebars - {{#link-to 'photoGallery' tagName="li"}} - Great Hamster Photos - {{/link-to}} - ``` - ```html -
  • - Great Hamster Photos -
  • - ``` - To override this option for your entire application, see - "Overriding Application-wide Defaults". - - ### Disabling the `link-to` helper - By default `{{link-to}}` is enabled. - any passed value to `disabled` helper property will disable the `link-to` helper. - - static use: the `disabled` option: - - ```handlebars - {{#link-to 'photoGallery' disabled=true}} - Great Hamster Photos - {{/link-to}} - ``` - - dynamic use: the `disabledWhen` option: - - ```handlebars - {{#link-to 'photoGallery' disabledWhen=controller.someProperty}} - Great Hamster Photos - {{/link-to}} - ``` - - any passed value to `disabled` will disable it except `undefined`. - to ensure that only `true` disable the `link-to` helper you can - override the global behaviour of `Ember.LinkView`. - - ```javascript - Ember.LinkView.reopen({ - disabled: Ember.computed(function(key, value) { - if (value !== undefined) { - this.set('_isDisabled', value === true); - } - return value === true ? get(this, 'disabledClass') : false; - }) - }); - ``` - - see "Overriding Application-wide Defaults" for more. - - ### Handling `href` - `{{link-to}}` will use your application's Router to - fill the element's `href` property with a url that - matches the path to the supplied `routeName` for your - routers's configured `Location` scheme, which defaults - to Ember.HashLocation. +(function() { +/** +@module ember +@submodule ember-routing +*/ - ### Handling current route - `{{link-to}}` will apply a CSS class name of 'active' - when the application's current route matches - the supplied routeName. For example, if the application's - current route is 'photoGallery.recent' the following - use of `{{link-to}}`: +var get = Ember.get, set = Ember.set; - ```handlebars - {{#link-to 'photoGallery.recent'}} - Great Hamster Photos from the last week - {{/link-to}} - ``` +/** + Ember.NoneLocation does not interact with the browser. It is useful for + testing, or when you need to manage state with your Router, but temporarily + don't want it to muck with the URL (for example when you embed your + application in a larger page). - will result in + @class NoneLocation + @namespace Ember + @extends Ember.Object +*/ +Ember.NoneLocation = Ember.Object.extend({ + implementation: 'none', + path: '', - ```html -
    - Great Hamster Photos - - ``` + /** + Returns the current path. - The CSS class name used for active classes can be customized - for a single use of `{{link-to}}` by passing an `activeClass` - option: + @private + @method getURL + @return {String} path + */ + getURL: function() { + return get(this, 'path'); + }, - ```handlebars - {{#link-to 'photoGallery.recent' activeClass="current-url"}} - Great Hamster Photos from the last week - {{/link-to}} - ``` + /** + Set the path and remembers what was set. Using this method + to change the path will not invoke the `updateURL` callback. - ```html - - Great Hamster Photos - - ``` + @private + @method setURL + @param path {String} + */ + setURL: function(path) { + set(this, 'path', path); + }, - To override this option for your entire application, see - "Overriding Application-wide Defaults". + /** + Register a callback to be invoked when the path changes. These + callbacks will execute when the user presses the back or forward + button, but not after `setURL` is invoked. - ### Supplying a model - An optional model argument can be used for routes whose - paths contain dynamic segments. This argument will become - the model context of the linked route: + @private + @method onUpdateURL + @param callback {Function} + */ + onUpdateURL: function(callback) { + this.updateCallback = callback; + }, - ```javascript - App.Router.map(function() { - this.resource("photoGallery", {path: "hamster-photos/:photo_id"}); - }); - ``` + /** + Sets the path and calls the `updateURL` callback. - ```handlebars - {{#link-to 'photoGallery' aPhoto}} - {{aPhoto.title}} - {{/link-to}} - ``` + @private + @method handleURL + @param callback {Function} + */ + handleURL: function(url) { + set(this, 'path', url); + this.updateCallback(url); + }, - ```html - - Tomster - - ``` + /** + Given a URL, formats it to be placed into the page as part + of an element's `href` attribute. - ### Supplying multiple models - For deep-linking to route paths that contain multiple - dynamic segments, multiple model arguments can be used. - As the router transitions through the route path, each - supplied model argument will become the context for the - route with the dynamic segments: + This is used, for example, when using the {{action}} helper + to generate a URL based on an event. - ```javascript - App.Router.map(function() { - this.resource("photoGallery", {path: "hamster-photos/:photo_id"}, function() { - this.route("comment", {path: "comments/:comment_id"}); - }); - }); - ``` - This argument will become the model context of the linked route: + @private + @method formatURL + @param url {String} + @return {String} url + */ + formatURL: function(url) { + // The return value is not overly meaningful, but we do not want to throw + // errors when test code renders templates containing {{action href=true}} + // helpers. + return url; + } +}); - ```handlebars - {{#link-to 'photoGallery.comment' aPhoto comment}} - {{comment.body}} - {{/link-to}} - ``` +})(); - ```html - - A+++ would snuggle again. - - ``` - ### Supplying an explicit dynamic segment value - If you don't have a model object available to pass to `{{link-to}}`, - an optional string or integer argument can be passed for routes whose - paths contain dynamic segments. This argument will become the value - of the dynamic segment: - ```javascript - App.Router.map(function() { - this.resource("photoGallery", {path: "hamster-photos/:photo_id"}); - }); - ``` +(function() { +/** +@module ember +@submodule ember-routing +*/ - ```handlebars - {{#link-to 'photoGallery' aPhotoId}} - {{aPhoto.title}} - {{/link-to}} - ``` +var get = Ember.get, set = Ember.set; - ```html - - Tomster - - ``` +/** + `Ember.HashLocation` implements the location API using the browser's + hash. At present, it relies on a `hashchange` event existing in the + browser. + + @class HashLocation + @namespace Ember + @extends Ember.Object +*/ +Ember.HashLocation = Ember.Object.extend({ + implementation: 'hash', + + init: function() { + set(this, 'location', get(this, '_location') || window.location); + }, + + /** + @private - When transitioning into the linked route, the `model` hook will - be triggered with parameters including this passed identifier. + Returns normalized location.hash - ### Overriding attributes - You can override any given property of the Ember.LinkView - that is generated by the `{{link-to}}` helper by passing - key/value pairs, like so: + @method getHash + */ + getHash: Ember.Location._getHash, - ```handlebars - {{#link-to aPhoto tagName='li' title='Following this link will change your life' classNames=['pic', 'sweet']}} - Uh-mazing! - {{/link-to}} - ``` + /** + Returns the current `location.hash`, minus the '#' at the front. - See [Ember.LinkView](/api/classes/Ember.LinkView.html) for a - complete list of overrideable properties. Be sure to also - check out inherited properties of `LinkView`. + @private + @method getURL + */ + getURL: function() { + return this.getHash().substr(1); + }, - ### Overriding Application-wide Defaults - ``{{link-to}}`` creates an instance of Ember.LinkView - for rendering. To override options for your entire - application, reopen Ember.LinkView and supply the - desired values: + /** + Set the `location.hash` and remembers what was set. This prevents + `onUpdateURL` callbacks from triggering when the hash was set by + `HashLocation`. - ``` javascript - Ember.LinkView.reopen({ - activeClass: "is-active", - tagName: 'li' - }) - ``` + @private + @method setURL + @param path {String} + */ + setURL: function(path) { + get(this, 'location').hash = path; + set(this, 'lastSetURL', path); + }, - It is also possible to override the default event in - this manner: + /** + Uses location.replace to update the url without a page reload + or history modification. - ``` javascript - Ember.LinkView.reopen({ - eventName: 'customEventName' - }); - ``` + @private + @method replaceURL + @param path {String} + */ + replaceURL: function(path) { + get(this, 'location').replace('#' + path); + set(this, 'lastSetURL', path); + }, - @method link-to - @for Ember.Handlebars.helpers - @param {String} routeName - @param {Object} [context]* - @param [options] {Object} Handlebars key/value pairs of options, you can override any property of Ember.LinkView - @return {String} HTML string - @see {Ember.LinkView} + /** + Register a callback to be invoked when the hash changes. These + callbacks will execute when the user presses the back or forward + button, but not after `setURL` is invoked. + + @private + @method onUpdateURL + @param callback {Function} */ - Ember.Handlebars.registerHelper('link-to', function(name) { - var options = [].slice.call(arguments, -1)[0], - params = [].slice.call(arguments, 0, -1), - hash = options.hash; + onUpdateURL: function(callback) { + var self = this; + var guid = Ember.guidFor(this); - hash.disabledBinding = hash.disabledWhen; + Ember.$(window).on('hashchange.ember-location-'+guid, function() { + Ember.run(function() { + var path = self.getURL(); + if (get(self, 'lastSetURL') === path) { return; } - hash.parameters = { - context: this, - options: options, - params: params - }; + set(self, 'lastSetURL', null); - return Ember.Handlebars.helpers.view.call(this, LinkView, options); - }); + callback(path); + }); + }); + }, /** - See [link-to](/api/classes/Ember.Handlebars.helpers.html#method_link-to) + Given a URL, formats it to be placed into the page as part + of an element's `href` attribute. - @method linkTo - @for Ember.Handlebars.helpers - @deprecated - @param {String} routeName - @param {Object} [context]* - @return {String} HTML string + This is used, for example, when using the {{action}} helper + to generate a URL based on an event. + + @private + @method formatURL + @param url {String} */ - Ember.Handlebars.registerHelper('linkTo', Ember.Handlebars.helpers['link-to']); -}); + formatURL: function(url) { + return '#'+url; + }, + + /** + Cleans up the HashLocation event listener. + @private + @method willDestroy + */ + willDestroy: function() { + var guid = Ember.guidFor(this); + Ember.$(window).off('hashchange.ember-location-'+guid); + } +}); })(); @@ -32914,538 +39510,712 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { */ var get = Ember.get, set = Ember.set; -Ember.onLoad('Ember.Handlebars', function(Handlebars) { +var popstateFired = false; +var supportsHistoryState = window.history && 'state' in window.history; + +/** + Ember.HistoryLocation implements the location API using the browser's + history.pushState API. + + @class HistoryLocation + @namespace Ember + @extends Ember.Object +*/ +Ember.HistoryLocation = Ember.Object.extend({ + implementation: 'history', + + init: function() { + set(this, 'location', get(this, 'location') || window.location); + set(this, 'baseURL', Ember.$('base').attr('href') || ''); + }, + /** - @module ember - @submodule ember-routing + Used to set state on first call to setURL + + @private + @method initState */ + initState: function() { + set(this, 'history', get(this, 'history') || window.history); + this.replaceState(this.formatURL(this.getURL())); + }, - Handlebars.OutletView = Ember.ContainerView.extend(Ember._Metamorph); + /** + Will be pre-pended to path upon state change + + @property rootURL + @default '/' + */ + rootURL: '/', /** - The `outlet` helper is a placeholder that the router will fill in with - the appropriate template based on the current state of the application. + Returns the current `location.pathname` without `rootURL` or `baseURL` - ``` handlebars - {{outlet}} - ``` + @private + @method getURL + @return url {String} + */ + getURL: function() { + var rootURL = get(this, 'rootURL'), + location = get(this, 'location'), + path = location.pathname, + baseURL = get(this, 'baseURL'); - By default, a template based on Ember's naming conventions will be rendered - into the `outlet` (e.g. `App.PostsRoute` will render the `posts` template). + rootURL = rootURL.replace(/\/$/, ''); + baseURL = baseURL.replace(/\/$/, ''); + var url = path.replace(baseURL, '').replace(rootURL, ''); - You can render a different template by using the `render()` method in the - route's `renderTemplate` hook. The following will render the `favoritePost` - template into the `outlet`. + + return url; + }, - ``` javascript - App.PostsRoute = Ember.Route.extend({ - renderTemplate: function() { - this.render('favoritePost'); - } - }); - ``` + /** + Uses `history.pushState` to update the url without a page reload. - You can create custom named outlets for more control. + @private + @method setURL + @param path {String} + */ + setURL: function(path) { + var state = this.getState(); + path = this.formatURL(path); - ``` handlebars - {{outlet 'favoritePost'}} - {{outlet 'posts'}} - ``` + if (!state || state.path !== path) { + this.pushState(path); + } + }, - Then you can define what template is rendered into each outlet in your - route. + /** + Uses `history.replaceState` to update the url without a page reload + or history modification. + @private + @method replaceURL + @param path {String} + */ + replaceURL: function(path) { + var state = this.getState(); + path = this.formatURL(path); - ``` javascript - App.PostsRoute = Ember.Route.extend({ - renderTemplate: function() { - this.render('favoritePost', { outlet: 'favoritePost' }); - this.render('posts', { outlet: 'posts' }); - } - }); - ``` + if (!state || state.path !== path) { + this.replaceState(path); + } + }, - You can specify the view class that the outlet uses to contain and manage the - templates rendered into it. + /** + Get the current `history.state`. Checks for if a polyfill is + required and if so fetches this._historyState. The state returned + from getState may be null if an iframe has changed a window's + history. - ``` handlebars - {{outlet viewClass=App.SectionContainer}} - ``` + @private + @method getState + @return state {Object} + */ + getState: function() { + return supportsHistoryState ? get(this, 'history').state : this._historyState; + }, - ``` javascript - App.SectionContainer = Ember.ContainerView.extend({ - tagName: 'section', - classNames: ['special'] - }); - ``` + /** + Pushes a new state. - @method outlet - @for Ember.Handlebars.helpers - @param {String} property the property on the controller - that holds the view for this outlet - @return {String} HTML string + @private + @method pushState + @param path {String} */ - Handlebars.registerHelper('outlet', function(property, options) { - var outletSource, outletContainerClass; + pushState: function(path) { + var state = { path: path }; - if (property && property.data && property.data.isRenderData) { - options = property; - property = 'main'; + get(this, 'history').pushState(state, null, path); + + // store state if browser doesn't support `history.state` + if (!supportsHistoryState) { + this._historyState = state; } - outletSource = options.data.view; - while (!outletSource.get('template.isTop')) { - outletSource = outletSource.get('_parentView'); + // used for webkit workaround + this._previousURL = this.getURL(); + }, + + /** + Replaces the current state. + + @private + @method replaceState + @param path {String} + */ + replaceState: function(path) { + var state = { path: path }; + + get(this, 'history').replaceState(state, null, path); + + // store state if browser doesn't support `history.state` + if (!supportsHistoryState) { + this._historyState = state; } - outletContainerClass = options.hash.viewClass || Handlebars.OutletView; + // used for webkit workaround + this._previousURL = this.getURL(); + }, - options.data.view.set('outletSource', outletSource); - options.hash.currentViewBinding = '_view.outletSource._outlets.' + property; + /** + Register a callback to be invoked whenever the browser + history changes, including using forward and back buttons. - return Handlebars.helpers.view.call(this, outletContainerClass, options); - }); -}); + @private + @method onUpdateURL + @param callback {Function} + */ + onUpdateURL: function(callback) { + var guid = Ember.guidFor(this), + self = this; -})(); + Ember.$(window).on('popstate.ember-location-'+guid, function(e) { + // Ignore initial page load popstate event in Chrome + if (!popstateFired) { + popstateFired = true; + if (self.getURL() === self._previousURL) { return; } + } + callback(self.getURL()); + }); + }, + /** + Used when using `{{action}}` helper. The url is always appended to the rootURL. + @private + @method formatURL + @param url {String} + @return formatted url {String} + */ + formatURL: function(url) { + var rootURL = get(this, 'rootURL'), + baseURL = get(this, 'baseURL'); -(function() { -/** -@module ember -@submodule ember-routing -*/ + if (url !== '') { + rootURL = rootURL.replace(/\/$/, ''); + baseURL = baseURL.replace(/\/$/, ''); + } else if(baseURL.match(/^\//) && rootURL.match(/^\//)) { + baseURL = baseURL.replace(/\/$/, ''); + } -var get = Ember.get, set = Ember.set; -Ember.onLoad('Ember.Handlebars', function(Handlebars) { + return baseURL + rootURL + url; + }, /** - Calling ``{{render}}`` from within a template will insert another - template that matches the provided name. The inserted template will - access its properties on its own controller (rather than the controller - of the parent template). + Cleans up the HistoryLocation event listener. - If a view class with the same name exists, the view class also will be used. - - Note: A given controller may only be used *once* in your app in this manner. - A singleton instance of the controller will be created for you. + @private + @method willDestroy + */ + willDestroy: function() { + var guid = Ember.guidFor(this); - Example: + Ember.$(window).off('popstate.ember-location-'+guid); + } +}); - ```javascript - App.NavigationController = Ember.Controller.extend({ - who: "world" - }); - ``` +})(); - ```handelbars - - Hello, {{who}}. - ``` - ```handelbars - -

    My great app

    - {{render navigaton}} - ``` - - ```html -

    My great app

    -
    - Hello, world. -
    - ``` - Optionally you may provide a second argument: a property path - that will be bound to the `model` property of the controller. +(function() { - If a `model` property path is specified, then a new instance of the - controller will be created and `{{render}}` can be used multiple times - with the same name. + /** + @module ember + @submodule ember-routing + */ - For example if you had this `author` template. + var get = Ember.get, set = Ember.set, + HistoryLocation = Ember.HistoryLocation, + HashLocation = Ember.HashLocation, + NoneLocation = Ember.NoneLocation, + EmberLocation = Ember.Location; - ```handlebars -
    - Written by {{firstName}} {{lastName}}. - Total Posts: {{postCount}} -
    - ``` + /** + Ember.AutoLocation will select the best location option based off browser + support with the priority order: history, hash, none. - You could render it inside the `post` template using the `render` helper. + Clean pushState paths accessed by hashchange-only browsers will be redirected + to the hash-equivalent and vice versa so future transitions are consistent. - ```handlebars -
    -

    {{title}}

    -
    {{body}}
    - {{render "author" author}} -
    - ``` + Keep in mind that since some of your users will use `HistoryLocation`, your + server must serve the Ember app at all the routes you define. - @method render - @for Ember.Handlebars.helpers - @param {String} name - @param {Object?} contextString - @param {Hash} options - @return {String} HTML string + @class AutoLocation + @namespace Ember + @static */ - Ember.Handlebars.registerHelper('render', function(name, contextString, options) { - Ember.assert("You must pass a template to render", arguments.length >= 2); - var contextProvided = arguments.length === 3, - container, router, controller, view, context, lookupOptions; + var AutoLocation = Ember.AutoLocation = { - if (arguments.length === 2) { - options = contextString; - contextString = undefined; - } + /** + @private - if (typeof contextString === 'string') { - context = Ember.Handlebars.get(options.contexts[1], contextString, options); - lookupOptions = { singleton: false }; - } + This property is used by router:main to know whether to cancel the routing + setup process, which is needed while we redirect the browser. - name = name.replace(/\//g, '.'); - container = options.data.keywords.controller.container; - router = container.lookup('router:main'); + @property cancelRouterSetup + @default false + */ + cancelRouterSetup: false, - Ember.assert("You can only use the {{render}} helper once without a model object as its second argument, as in {{render \"post\" post}}.", contextProvided || !router || !router._lookupActiveView(name)); + /** + @private - view = container.lookup('view:' + name) || container.lookup('view:default'); + Will be pre-pended to path upon state change. - var controllerName = options.hash.controller; + @property rootURL + @default '/' + */ + rootURL: '/', - // Look up the controller by name, if provided. - if (controllerName) { - controller = container.lookup('controller:' + controllerName, lookupOptions); - Ember.assert("The controller name you supplied '" + controllerName + "' did not resolve to a controller.", !!controller); - } else { - controller = container.lookup('controller:' + name, lookupOptions) || - Ember.generateController(container, name, context); - } + /** + @private - if (controller && contextProvided) { - controller.set('model', context); - } + Attached for mocking in tests - var root = options.contexts[1]; + @property _window + @default window + */ + _window: window, - if (root) { - view.registerObserver(root, contextString, function() { - controller.set('model', Ember.Handlebars.get(root, contextString, options)); - }); - } + /** + @private - controller.set('target', options.data.keywords.controller); + Attached for mocking in tests - options.hash.viewName = Ember.String.camelize(name); - options.hash.template = container.lookup('template:' + name); - options.hash.controller = controller; + @property location + @default window.location + */ + _location: window.location, - if (router && !context) { - router._connectActiveView(name, view); - } + /** + @private - Ember.Handlebars.helpers.view.call(this, view, options); - }); + Attached for mocking in tests -}); + @property _history + @default window.history + */ + _history: window.history, -})(); + /** + @private + Attached for mocking in tests + @property _HistoryLocation + @default Ember.HistoryLocation + */ + _HistoryLocation: HistoryLocation, -(function() { -/** -@module ember -@submodule ember-routing -*/ -Ember.onLoad('Ember.Handlebars', function(Handlebars) { + /** + @private - var resolveParams = Ember.Router.resolveParams, - isSimpleClick = Ember.ViewUtils.isSimpleClick; + Attached for mocking in tests - var EmberHandlebars = Ember.Handlebars, - handlebarsGet = EmberHandlebars.get, - SafeString = EmberHandlebars.SafeString, - forEach = Ember.ArrayPolyfills.forEach, - get = Ember.get, - a_slice = Array.prototype.slice; + @property _HashLocation + @default Ember.HashLocation + */ + _HashLocation: HashLocation, - function args(options, actionName) { - var ret = []; - if (actionName) { ret.push(actionName); } + /** + @private - var types = options.options.types.slice(1), - data = options.options.data; + Attached for mocking in tests - return ret.concat(resolveParams(options.context, options.params, { types: types, data: data })); - } + @property _NoneLocation + @default Ember.NoneLocation + */ + _NoneLocation: NoneLocation, - var ActionHelper = EmberHandlebars.ActionHelper = { - registeredActions: {} - }; + /** + @private - var keys = ["alt", "shift", "meta", "ctrl"]; + Returns location.origin or builds it if device doesn't support it. - var isAllowedClick = function(event, allowedKeys) { - if (typeof allowedKeys === "undefined") { - return isSimpleClick(event); - } + @method _getOrigin + */ + _getOrigin: function () { + var location = this._location, + origin = location.origin; - if (allowedKeys.indexOf("any") >= 0) { - return true; - } + // Older browsers, especially IE, don't have origin + if (!origin) { + origin = location.protocol + '//' + location.hostname; - var allowed = true; + if (location.port) { + origin += ':' + location.port; + } + } - forEach.call(keys, function(key) { - if (event[key + "Key"] && allowedKeys.indexOf(key) === -1) { - allowed = false; + return origin; + }, + + /** + @private + + We assume that if the history object has a pushState method, the host should + support HistoryLocation. + + @method _getSupportsHistory + */ + _getSupportsHistory: function () { + // Boosted from Modernizr: https://github.com/Modernizr/Modernizr/blob/master/feature-detects/history.js + // The stock browser on Android 2.2 & 2.3 returns positive on history support + // Unfortunately support is really buggy and there is no clean way to detect + // these bugs, so we fall back to a user agent sniff :( + var userAgent = this._window.navigator.userAgent; + + // We only want Android 2, stock browser, and not Chrome which identifies + // itself as 'Mobile Safari' as well + if (userAgent.indexOf('Android 2') !== -1 && + userAgent.indexOf('Mobile Safari') !== -1 && + userAgent.indexOf('Chrome') === -1) { + return false; } - }); - return allowed; - }; + return !!(this._history && 'pushState' in this._history); + }, + + /** + @private - ActionHelper.registerAction = function(actionName, options, allowedKeys) { - var actionId = (++Ember.uuid).toString(); + IE8 running in IE7 compatibility mode gives false positive, so we must also + check documentMode. - ActionHelper.registeredActions[actionId] = { - eventName: options.eventName, - handler: function(event) { - if (!isAllowedClick(event, allowedKeys)) { return true; } + @method _getSupportsHashChange + */ + _getSupportsHashChange: function () { + var window = this._window, + documentMode = window.document.documentMode; - event.preventDefault(); + return ('onhashchange' in window && (documentMode === undefined || documentMode > 7 )); + }, - if (options.bubbles === false) { - event.stopPropagation(); - } + /** + @private - var target = options.target; + Redirects the browser using location.replace, prepending the locatin.origin + to prevent phishing attempts - if (target.target) { - target = handlebarsGet(target.root, target.target, target.options); - } else { - target = target.root; - } + @method _replacePath + */ + _replacePath: function (path) { + this._location.replace(this._getOrigin() + path); + }, - Ember.run(function() { - if (target.send) { - target.send.apply(target, args(options.parameters, actionName)); - } else { - Ember.assert("The action '" + actionName + "' did not exist on " + target, typeof target[actionName] === 'function'); - target[actionName].apply(target, args(options.parameters)); - } - }); - } - }; + /** + @private + @method _getRootURL + */ + _getRootURL: function () { + return this.rootURL; + }, - options.view.on('willClearRender', function() { - delete ActionHelper.registeredActions[actionId]; - }); + /** + @private - return actionId; - }; + Returns the current `location.pathname`, normalized for IE inconsistencies. - /** - The `{{action}}` helper registers an HTML element within a template for DOM - event handling and forwards that interaction to the templates's controller - or supplied `target` option (see 'Specifying a Target'). + @method _getPath + */ + _getPath: function () { + var pathname = this._location.pathname; + // Various versions of IE/Opera don't always return a leading slash + if (pathname.charAt(0) !== '/') { + pathname = '/' + pathname; + } - If the controller does not implement the event, the event is sent - to the current route, and it bubbles up the route hierarchy from there. + return pathname; + }, - User interaction with that element will invoke the supplied action name on - the appropriate target. + /** + @private - Given the following application Handlebars template on the page + Returns normalized location.hash as an alias to Ember.Location._getHash - ```handlebars -
    - click me -
    - ``` + @method _getHash + */ + _getHash: EmberLocation._getHash, - And application code + /** + @private - ```javascript - App.ApplicationController = Ember.Controller.extend({ - actions: { - anActionName: function() { - - } - } - }); - ``` + Returns location.search - Will result in the following rendered HTML + @method _getQuery + */ + _getQuery: function () { + return this._location.search; + }, - ```html -
    -
    - click me -
    -
    - ``` + /** + @private - Clicking "click me" will trigger the `anActionName` action of the - `App.ApplicationController`. In this case, no additional parameters will be passed. + Returns the full pathname including query and hash - If you provide additional parameters to the helper: + @method _getFullPath + */ + _getFullPath: function () { + return this._getPath() + this._getQuery() + this._getHash(); + }, - ```handlebars - - ``` + /** + @private - Those parameters will be passed along as arguments to the JavaScript - function implementing the action. + Returns the current path as it should appear for HistoryLocation supported + browsers. This may very well differ from the real current path (e.g. if it + starts off as a hashed URL) - ### Event Propagation + @method _getHistoryPath + */ + _getHistoryPath: function () { + var rootURL = this._getRootURL(), + path = this._getPath(), + hash = this._getHash(), + query = this._getQuery(), + rootURLIndex = path.indexOf(rootURL), + routeHash, hashParts; + + Ember.assert('Path ' + path + ' does not start with the provided rootURL ' + rootURL, rootURLIndex === 0); + + // By convention, Ember.js routes using HashLocation are required to start + // with `#/`. Anything else should NOT be considered a route and should + // be passed straight through, without transformation. + if (hash.substr(0, 2) === '#/') { + // There could be extra hash segments after the route + hashParts = hash.substr(1).split('#'); + // The first one is always the route url + routeHash = hashParts.shift(); + + // If the path already has a trailing slash, remove the one + // from the hashed route so we don't double up. + if (path.slice(-1) === '/') { + routeHash = routeHash.substr(1); + } - Events triggered through the action helper will automatically have - `.preventDefault()` called on them. You do not need to do so in your event - handlers. + // This is the "expected" final order + path += routeHash; + path += query; - To also disable bubbling, pass `bubbles=false` to the helper: + if (hashParts.length) { + path += '#' + hashParts.join('#'); + } + } else { + path += query; + path += hash; + } - ```handlebars - - ``` + return path; + }, - If you need the default handler to trigger you should either register your - own event handler, or use event methods on your view class. See [Ember.View](/api/classes/Ember.View.html) - 'Responding to Browser Events' for more information. + /** + @private - ### Specifying DOM event type + Returns the current path as it should appear for HashLocation supported + browsers. This may very well differ from the real current path. - By default the `{{action}}` helper registers for DOM `click` events. You can - supply an `on` option to the helper to specify a different DOM event name: + @method _getHashPath + */ + _getHashPath: function () { + var rootURL = this._getRootURL(), + path = rootURL, + historyPath = this._getHistoryPath(), + routePath = historyPath.substr(rootURL.length); + + if (routePath !== '') { + if (routePath.charAt(0) !== '/') { + routePath = '/' + routePath; + } - ```handlebars -
    - click me -
    - ``` + path += '#' + routePath; + } - See `Ember.View` 'Responding to Browser Events' for a list of - acceptable DOM event names. + return path; + }, - NOTE: Because `{{action}}` depends on Ember's event dispatch system it will - only function if an `Ember.EventDispatcher` instance is available. An - `Ember.EventDispatcher` instance will be created when a new `Ember.Application` - is created. Having an instance of `Ember.Application` will satisfy this - requirement. + /** + Selects the best location option based off browser support and returns an + instance of that Location class. - ### Specifying whitelisted modifier keys + @see Ember.AutoLocation + @method create + */ + create: function (options) { + if (options && options.rootURL) { + Ember.assert('rootURL must end with a trailing forward slash e.g. "/app/"', options.rootURL.charAt(options.rootURL.length-1) === '/'); + this.rootURL = options.rootURL; + } - By default the `{{action}}` helper will ignore click event with pressed modifier - keys. You can supply an `allowedKeys` option to specify which keys should not be ignored. + var historyPath, hashPath, + cancelRouterSetup = false, + implementationClass = this._NoneLocation, + currentPath = this._getFullPath(); - ```handlebars -
    - click me -
    - ``` + if (this._getSupportsHistory()) { + historyPath = this._getHistoryPath(); - This way the `{{action}}` will fire when clicking with the alt key pressed down. + // Since we support history paths, let's be sure we're using them else + // switch the location over to it. + if (currentPath === historyPath) { + implementationClass = this._HistoryLocation; + } else { + cancelRouterSetup = true; + this._replacePath(historyPath); + } - Alternatively, supply "any" to the `allowedKeys` option to accept any combination of modifier keys. + } else if (this._getSupportsHashChange()) { + hashPath = this._getHashPath(); - ```handlebars -
    - click me with any key pressed -
    - ``` + // Be sure we're using a hashed path, otherwise let's switch over it to so + // we start off clean and consistent. We'll count an index path with no + // hash as "good enough" as well. + if (currentPath === hashPath || (currentPath === '/' && hashPath === '/#/')) { + implementationClass = this._HashLocation; + } else { + // Our URL isn't in the expected hash-supported format, so we want to + // cancel the router setup and replace the URL to start off clean + cancelRouterSetup = true; + this._replacePath(hashPath); + } + } - ### Specifying a Target + var implementation = implementationClass.create.apply(implementationClass, arguments); - There are several possible target objects for `{{action}}` helpers: + if (cancelRouterSetup) { + set(implementation, 'cancelRouterSetup', true); + } - In a typical Ember application, where views are managed through use of the - `{{outlet}}` helper, actions will bubble to the current controller, then - to the current route, and then up the route hierarchy. + return implementation; + } + }; - Alternatively, a `target` option can be provided to the helper to change - which object will receive the method call. This option must be a path - to an object, accessible in the current context: +})(); - ```handlebars - {{! the application template }} -
    - click me -
    - ``` - ```javascript - App.ApplicationView = Ember.View.extend({ - actions: { - anActionName: function(){} - } - }); - ``` +(function() { - ### Additional Parameters +})(); - You may specify additional parameters to the `{{action}}` helper. These - parameters are passed along as the arguments to the JavaScript function - implementing the action. - ```handlebars - {{#each person in people}} -
    - click me -
    - {{/each}} - ``` - Clicking "click me" will trigger the `edit` method on the current controller - with the value of `person` as a parameter. +(function() { +/** +Ember Routing - @method action - @for Ember.Handlebars.helpers - @param {String} actionName - @param {Object} [context]* - @param {Hash} options - */ - EmberHandlebars.registerHelper('action', function(actionName) { - var options = arguments[arguments.length - 1], - contexts = a_slice.call(arguments, 1, -1); +@module ember +@submodule ember-routing +@requires ember-views +*/ - var hash = options.hash, - controller; +})(); - // create a hash to pass along to registerAction - var action = { - eventName: hash.on || "click" - }; +(function() { +function visit(vertex, fn, visited, path) { + var name = vertex.name, + vertices = vertex.incoming, + names = vertex.incomingNames, + len = names.length, + i; + if (!visited) { + visited = {}; + } + if (!path) { + path = []; + } + if (visited.hasOwnProperty(name)) { + return; + } + path.push(name); + visited[name] = true; + for (i = 0; i < len; i++) { + visit(vertices[names[i]], fn, visited, path); + } + fn(vertex, path); + path.pop(); +} - action.parameters = { - context: this, - options: options, - params: contexts - }; +function DAG() { + this.names = []; + this.vertices = {}; +} - action.view = options.data.view; +DAG.prototype.add = function(name) { + if (!name) { return; } + if (this.vertices.hasOwnProperty(name)) { + return this.vertices[name]; + } + var vertex = { + name: name, incoming: {}, incomingNames: [], hasOutgoing: false, value: null + }; + this.vertices[name] = vertex; + this.names.push(name); + return vertex; +}; - var root, target; +DAG.prototype.map = function(name, value) { + this.add(name).value = value; +}; - if (hash.target) { - root = this; - target = hash.target; - } else if (controller = options.data.keywords.controller) { - root = controller; +DAG.prototype.addEdge = function(fromName, toName) { + if (!fromName || !toName || fromName === toName) { + return; + } + var from = this.add(fromName), to = this.add(toName); + if (to.incoming.hasOwnProperty(fromName)) { + return; + } + function checkCycle(vertex, path) { + if (vertex.name === toName) { + throw new Ember.Error("cycle detected: " + toName + " <- " + path.join(" <- ")); } + } + visit(from, checkCycle); + from.hasOutgoing = true; + to.incoming[fromName] = from; + to.incomingNames.push(fromName); +}; - action.target = { root: root, target: target, options: options }; - action.bubbles = hash.bubbles; +DAG.prototype.topsort = function(fn) { + var visited = {}, + vertices = this.vertices, + names = this.names, + len = names.length, + i, vertex; + for (i = 0; i < len; i++) { + vertex = vertices[names[i]]; + if (!vertex.hasOutgoing) { + visit(vertex, fn, visited); + } + } +}; - var actionId = ActionHelper.registerAction(actionName, action, hash.allowedKeys); - return new SafeString('data-ember-action="' + actionId + '"'); - }); +DAG.prototype.addEdges = function(name, value, before, after) { + var i; + this.map(name, value); + if (before) { + if (typeof before === 'string') { + this.addEdge(name, before); + } else { + for (i = 0; i < before.length; i++) { + this.addEdge(name, before[i]); + } + } + } + if (after) { + if (typeof after === 'string') { + this.addEdge(after, name); + } else { + for (i = 0; i < after.length; i++) { + this.addEdge(after[i], name); + } + } + } +}; -}); +Ember.DAG = DAG; })(); @@ -33454,226 +40224,343 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { (function() { /** @module ember -@submodule ember-routing +@submodule ember-application */ -if (Ember.ENV.EXPERIMENTAL_CONTROL_HELPER) { - var get = Ember.get, set = Ember.set; +var get = Ember.get, + classify = Ember.String.classify, + capitalize = Ember.String.capitalize, + decamelize = Ember.String.decamelize; +Ember.Resolver = Ember.Object.extend({ /** - `{{control}}` works like render, except it uses a new controller instance for every call, instead of reusing the singleton controller. + This will be set to the Application instance when it is + created. - The control helper is currently under development and is considered experimental. - To enable it, set `ENV.EXPERIMENTAL_CONTROL_HELPER = true` before requiring Ember. + @property namespace + */ + namespace: null, + normalize: function(fullName) { + throw new Error("Invalid call to `resolver.normalize(fullName)`. Please override the 'normalize' method in subclass of `Ember.AbstractResolver` to prevent falling through to this error."); + }, + resolve: function(fullName) { + throw new Error("Invalid call to `resolver.resolve(parsedName)`. Please override the 'resolve' method in subclass of `Ember.AbstractResolver` to prevent falling through to this error."); + }, + parseName: function(parsedName) { + throw new Error("Invalid call to `resolver.resolveByType(parsedName)`. Please override the 'resolveByType' method in subclass of `Ember.AbstractResolver` to prevent falling through to this error."); + }, + lookupDescription: function(fullName) { + throw new Error("Invalid call to `resolver.lookupDescription(fullName)`. Please override the 'lookupDescription' method in subclass of `Ember.AbstractResolver` to prevent falling through to this error."); + }, + makeToString: function(factory, fullName) { + throw new Error("Invalid call to `resolver.makeToString(factory, fullName)`. Please override the 'makeToString' method in subclass of `Ember.AbstractResolver` to prevent falling through to this error."); + }, + resolveOther: function(parsedName) { + throw new Error("Invalid call to `resolver.resolveDefault(parsedName)`. Please override the 'resolveDefault' method in subclass of `Ember.AbstractResolver` to prevent falling through to this error."); + } +}); - For example if you had this `author` template. - ```handlebars -
    - Written by {{firstName}} {{lastName}}. - Total Posts: {{postCount}} -
    - ``` - You could render it inside the `post` template using the `control` helper. +/** + The DefaultResolver defines the default lookup rules to resolve + container lookups before consulting the container for registered + items: - ```handlebars -
    -

    {{title}}

    -
    {{body}}
    - {{control "author" author}} -
    - ``` + * templates are looked up on `Ember.TEMPLATES` + * other names are looked up on the application after converting + the name. For example, `controller:post` looks up + `App.PostController` by default. + * there are some nuances (see examples below) - @method control - @for Ember.Handlebars.helpers - @param {String} path - @param {String} modelPath - @param {Hash} options - @return {String} HTML string - */ - Ember.Handlebars.registerHelper('control', function(path, modelPath, options) { - if (arguments.length === 2) { - options = modelPath; - modelPath = undefined; - } + ### How Resolving Works + + The container calls this object's `resolve` method with the + `fullName` argument. - var model; + It first parses the fullName into an object using `parseName`. - if (modelPath) { - model = Ember.Handlebars.get(this, modelPath, options); - } + Then it checks for the presence of a type-specific instance + method of the form `resolve[Type]` and calls it if it exists. + For example if it was resolving 'template:post', it would call + the `resolveTemplate` method. - var controller = options.data.keywords.controller, - view = options.data.keywords.view, - children = get(controller, '_childContainers'), - controlID = options.hash.controlID, - container, subContainer; + Its last resort is to call the `resolveOther` method. - if (children.hasOwnProperty(controlID)) { - subContainer = children[controlID]; - } else { - container = get(controller, 'container'), - subContainer = container.child(); - children[controlID] = subContainer; - } + The methods of this object are designed to be easy to override + in a subclass. For example, you could enhance how a template + is resolved like so: + + ```javascript + App = Ember.Application.create({ + Resolver: Ember.DefaultResolver.extend({ + resolveTemplate: function(parsedName) { + var resolvedTemplate = this._super(parsedName); + if (resolvedTemplate) { return resolvedTemplate; } + return Ember.TEMPLATES['not_found']; + } + }) + }); + ``` + + Some examples of how names are resolved: + + ``` + 'template:post' //=> Ember.TEMPLATES['post'] + 'template:posts/byline' //=> Ember.TEMPLATES['posts/byline'] + 'template:posts.byline' //=> Ember.TEMPLATES['posts/byline'] + 'template:blogPost' //=> Ember.TEMPLATES['blogPost'] + // OR + // Ember.TEMPLATES['blog_post'] + 'controller:post' //=> App.PostController + 'controller:posts.index' //=> App.PostsIndexController + 'controller:blog/post' //=> Blog.PostController + 'controller:basic' //=> Ember.Controller + 'route:post' //=> App.PostRoute + 'route:posts.index' //=> App.PostsIndexRoute + 'route:blog/post' //=> Blog.PostRoute + 'route:basic' //=> Ember.Route + 'view:post' //=> App.PostView + 'view:posts.index' //=> App.PostsIndexView + 'view:blog/post' //=> Blog.PostView + 'view:basic' //=> Ember.View + 'foo:post' //=> App.PostFoo + 'model:post' //=> App.Post + ``` + + @class DefaultResolver + @namespace Ember + @extends Ember.Object +*/ +Ember.DefaultResolver = Ember.Object.extend({ + /** + This will be set to the Application instance when it is + created. - var normalizedPath = path.replace(/\//g, '.'); + @property namespace + */ + namespace: null, - var childView = subContainer.lookup('view:' + normalizedPath) || subContainer.lookup('view:default'), - childController = subContainer.lookup('controller:' + normalizedPath), - childTemplate = subContainer.lookup('template:' + path); + normalize: function(fullName) { + var split = fullName.split(':', 2), + type = split[0], + name = split[1]; - Ember.assert("Could not find controller for path: " + normalizedPath, childController); - Ember.assert("Could not find view for path: " + normalizedPath, childView); + Ember.assert("Tried to normalize a container name without a colon (:) in it. You probably tried to lookup a name that did not contain a type, a colon, and a name. A proper lookup name would be `view:post`.", split.length === 2); - set(childController, 'target', controller); - set(childController, 'model', model); + if (type !== 'template') { + var result = name; - options.hash.template = childTemplate; - options.hash.controller = childController; + if (result.indexOf('.') > -1) { + result = result.replace(/\.(.)/g, function(m) { return m.charAt(1).toUpperCase(); }); + } - function observer() { - var model = Ember.Handlebars.get(this, modelPath, options); - set(childController, 'model', model); - childView.rerender(); - } + if (name.indexOf('_') > -1) { + result = result.replace(/_(.)/g, function(m) { return m.charAt(1).toUpperCase(); }); + } - if (modelPath) { - Ember.addObserver(this, modelPath, observer); - childView.one('willDestroyElement', this, function() { - Ember.removeObserver(this, modelPath, observer); - }); + return type + ':' + result; + } else { + return fullName; } + }, - Ember.Handlebars.helpers.view.call(this, childView, options); - }); -} -})(); + /** + This method is called via the container's resolver method. + It parses the provided `fullName` and then looks up and + returns the appropriate template or class. + @method resolve + @param {String} fullName the lookup string + @return {Object} the resolved factory + */ + resolve: function(fullName) { + var parsedName = this.parseName(fullName), + resolveMethodName = parsedName.resolveMethodName; + if (!(parsedName.name && parsedName.type)) { + throw new TypeError("Invalid fullName: `" + fullName + "`, must be of the form `type:name` "); + } -(function() { + if (this[resolveMethodName]) { + var resolved = this[resolveMethodName](parsedName); + if (resolved) { return resolved; } + } + return this.resolveOther(parsedName); + }, + /** + Convert the string name of the form "type:name" to + a Javascript object with the parsed aspects of the name + broken out. -})(); + @protected + @param {String} fullName the lookup string + @method parseName + */ + parseName: function(fullName) { + var nameParts = fullName.split(":"), + type = nameParts[0], fullNameWithoutType = nameParts[1], + name = fullNameWithoutType, + namespace = get(this, 'namespace'), + root = namespace; + if (type !== 'template' && name.indexOf('/') !== -1) { + var parts = name.split('/'); + name = parts[parts.length - 1]; + var namespaceName = capitalize(parts.slice(0, -1).join('.')); + root = Ember.Namespace.byName(namespaceName); + Ember.assert('You are looking for a ' + name + ' ' + type + ' in the ' + namespaceName + ' namespace, but the namespace could not be found', root); + } -(function() { -/** -@module ember -@submodule ember-routing -*/ + return { + fullName: fullName, + type: type, + fullNameWithoutType: fullNameWithoutType, + name: name, + root: root, + resolveMethodName: "resolve" + classify(type) + }; + }, -var get = Ember.get, set = Ember.set; + /** + Returns a human-readable description for a fullName. Used by the + Application namespace in assertions to describe the + precise name of the class that Ember is looking for, rather than + container keys. + + @protected + @param {String} fullName the lookup string + @method lookupDescription + */ + lookupDescription: function(fullName) { + var parsedName = this.parseName(fullName); -Ember.ControllerMixin.reopen({ - /** - Transition the application into another route. The route may - be either a single route or route path: + if (parsedName.type === 'template') { + return "template at " + parsedName.fullNameWithoutType.replace(/\./g, '/'); + } - ```javascript - aController.transitionToRoute('blogPosts'); - aController.transitionToRoute('blogPosts.recentEntries'); - ``` + var description = parsedName.root + "." + classify(parsedName.name); + if (parsedName.type !== 'model') { description += classify(parsedName.type); } - Optionally supply a model for the route in question. The model - will be serialized into the URL using the `serialize` hook of - the route: + return description; + }, - ```javascript - aController.transitionToRoute('blogPost', aPost); - ``` + makeToString: function(factory, fullName) { + return factory.toString(); + }, + /** + Given a parseName object (output from `parseName`), apply + the conventions expected by `Ember.Router` - Multiple models will be applied last to first recursively up the - resource tree. + @protected + @param {Object} parsedName a parseName object with the parsed + fullName lookup string + @method useRouterNaming + */ + useRouterNaming: function(parsedName) { + parsedName.name = parsedName.name.replace(/\./g, '_'); + if (parsedName.name === 'basic') { + parsedName.name = ''; + } + }, + /** + Look up the template in Ember.TEMPLATES - ```javascript + @protected + @param {Object} parsedName a parseName object with the parsed + fullName lookup string + @method resolveTemplate + */ + resolveTemplate: function(parsedName) { + var templateName = parsedName.fullNameWithoutType.replace(/\./g, '/'); - this.resource('blogPost', {path:':blogPostId'}, function(){ - this.resource('blogComment', {path: ':blogCommentId'}); - }); - - aController.transitionToRoute('blogComment', aPost, aComment); - ``` + if (Ember.TEMPLATES[templateName]) { + return Ember.TEMPLATES[templateName]; + } - See also 'replaceRoute'. + templateName = decamelize(templateName); + if (Ember.TEMPLATES[templateName]) { + return Ember.TEMPLATES[templateName]; + } + }, + /** + Lookup the view using `resolveOther` - @param {String} name the name of the route - @param {...Object} models the model(s) to be used while transitioning - to the route. - @for Ember.ControllerMixin - @method transitionToRoute + @protected + @param {Object} parsedName a parseName object with the parsed + fullName lookup string + @method resolveView */ - transitionToRoute: function() { - // target may be either another controller or a router - var target = get(this, 'target'), - method = target.transitionToRoute || target.transitionTo; - return method.apply(target, arguments); + resolveView: function(parsedName) { + this.useRouterNaming(parsedName); + return this.resolveOther(parsedName); }, - /** - @deprecated - @for Ember.ControllerMixin - @method transitionTo + Lookup the controller using `resolveOther` + + @protected + @param {Object} parsedName a parseName object with the parsed + fullName lookup string + @method resolveController */ - transitionTo: function() { - Ember.deprecate("transitionTo is deprecated. Please use transitionToRoute."); - return this.transitionToRoute.apply(this, arguments); + resolveController: function(parsedName) { + this.useRouterNaming(parsedName); + return this.resolveOther(parsedName); }, - /** - Transition into another route while replacing the current URL, if possible. - This will replace the current history entry instead of adding a new one. - Beside that, it is identical to `transitionToRoute` in all other respects. - - ```javascript - aController.replaceRoute('blogPosts'); - aController.replaceRoute('blogPosts.recentEntries'); - ``` - - Optionally supply a model for the route in question. The model - will be serialized into the URL using the `serialize` hook of - the route: + Lookup the route using `resolveOther` - ```javascript - aController.replaceRoute('blogPost', aPost); - ``` + @protected + @param {Object} parsedName a parseName object with the parsed + fullName lookup string + @method resolveRoute + */ + resolveRoute: function(parsedName) { + this.useRouterNaming(parsedName); + return this.resolveOther(parsedName); + }, - Multiple models will be applied last to first recursively up the - resource tree. + /** + Lookup the model on the Application namespace - ```javascript + @protected + @param {Object} parsedName a parseName object with the parsed + fullName lookup string + @method resolveModel + */ + resolveModel: function(parsedName) { + var className = classify(parsedName.name), + factory = get(parsedName.root, className); - this.resource('blogPost', {path:':blogPostId'}, function(){ - this.resource('blogComment', {path: ':blogCommentId'}); - }); - - aController.replaceRoute('blogComment', aPost, aComment); - ``` + if (factory) { return factory; } + }, + /** + Look up the specified object (from parsedName) on the appropriate + namespace (usually on the Application) - @param {String} name the name of the route - @param {...Object} models the model(s) to be used while transitioning - to the route. - @for Ember.ControllerMixin - @method replaceRoute + @protected + @param {Object} parsedName a parseName object with the parsed + fullName lookup string + @method resolveHelper */ - replaceRoute: function() { - // target may be either another controller or a router - var target = get(this, 'target'), - method = target.replaceRoute || target.replaceWith; - return method.apply(target, arguments); + resolveHelper: function(parsedName) { + return this.resolveOther(parsedName) || Ember.Handlebars.helpers[parsedName.fullNameWithoutType]; }, - /** - @deprecated - @for Ember.ControllerMixin - @method replaceWith + Look up the specified object (from parsedName) on the appropriate + namespace (usually on the Application) + + @protected + @param {Object} parsedName a parseName object with the parsed + fullName lookup string + @method resolveOther */ - replaceWith: function() { - Ember.deprecate("replaceWith is deprecated. Please use replaceRoute."); - return this.replaceRoute.apply(this, arguments); + resolveOther: function(parsedName) { + var className = classify(parsedName.name) + classify(parsedName.type), + factory = get(parsedName.root, className); + if (factory) { return factory; } } }); @@ -33684,1132 +40571,1081 @@ Ember.ControllerMixin.reopen({ (function() { /** @module ember -@submodule ember-routing +@submodule ember-application */ var get = Ember.get, set = Ember.set; -Ember.View.reopen({ +function DeprecatedContainer(container) { + this._container = container; +} - /** - Sets the private `_outlets` object on the view. +DeprecatedContainer.deprecate = function(method) { + return function() { + var container = this._container; - @method init - */ - init: function() { - set(this, '_outlets', {}); - this._super(); - }, + Ember.deprecate('Using the defaultContainer is no longer supported. [defaultContainer#' + method + '] see: http://git.io/EKPpnA', false); + return container[method].apply(container, arguments); + }; +}; - /** - Manually fill any of a view's `{{outlet}}` areas with the - supplied view. +DeprecatedContainer.prototype = { + _container: null, + lookup: DeprecatedContainer.deprecate('lookup'), + resolve: DeprecatedContainer.deprecate('resolve'), + register: DeprecatedContainer.deprecate('register') +}; - Example +/** + An instance of `Ember.Application` is the starting point for every Ember + application. It helps to instantiate, initialize and coordinate the many + objects that make up your app. - ```javascript - var MyView = Ember.View.extend({ - template: Ember.Handlebars.compile('Child view: {{outlet "main"}} ') - }); - var myView = MyView.create(); - myView.appendTo('body'); - // The html for myView now looks like: - //
    Child view:
    + Each Ember app has one and only one `Ember.Application` object. In fact, the + very first thing you should do in your application is create the instance: - myView.connectOutlet('main', Ember.View.extend({ - template: Ember.Handlebars.compile('

    Foo

    ') - })); - // The html for myView now looks like: - //
    Child view: - //

    Foo

    - //
    - ``` - @method connectOutlet - @param {String} outletName A unique name for the outlet - @param {Object} view An Ember.View - */ - connectOutlet: function(outletName, view) { - if (this._pendingDisconnections) { - delete this._pendingDisconnections[outletName]; - } + ```javascript + window.App = Ember.Application.create(); + ``` - if (this._hasEquivalentView(outletName, view)) { - view.destroy(); - return; - } + Typically, the application object is the only global variable. All other + classes in your app should be properties on the `Ember.Application` instance, + which highlights its first role: a global namespace. - var outlets = get(this, '_outlets'), - container = get(this, 'container'), - router = container && container.lookup('router:main'), - renderedName = get(view, 'renderedName'); + For example, if you define a view class, it might look like this: - set(outlets, outletName, view); + ```javascript + App.MyView = Ember.View.extend(); + ``` - if (router && renderedName) { - router._connectActiveView(renderedName, view); - } - }, + By default, calling `Ember.Application.create()` will automatically initialize + your application by calling the `Ember.Application.initialize()` method. If + you need to delay initialization, you can call your app's `deferReadiness()` + method. When you are ready for your app to be initialized, call its + `advanceReadiness()` method. - /** - @private + You can define a `ready` method on the `Ember.Application` instance, which + will be run by Ember when the application is initialized. - Determines if the view has already been created by checking if - the view has the same constructor, template, and context as the - view in the `_outlets` object. + Because `Ember.Application` inherits from `Ember.Namespace`, any classes + you create will have useful string representations when calling `toString()`. + See the `Ember.Namespace` documentation for more information. - @method _hasEquivalentView - @param {String} outletName The name of the outlet we are checking - @param {Object} view An Ember.View - @return {Boolean} - */ - _hasEquivalentView: function(outletName, view) { - var existingView = get(this, '_outlets.'+outletName); - return existingView && - existingView.constructor === view.constructor && - existingView.get('template') === view.get('template') && - existingView.get('context') === view.get('context'); - }, + While you can think of your `Ember.Application` as a container that holds the + other classes in your application, there are several other responsibilities + going on under-the-hood that you may want to understand. - /** - Removes an outlet from the view. + ### Event Delegation - Example + Ember uses a technique called _event delegation_. This allows the framework + to set up a global, shared event listener instead of requiring each view to + do it manually. For example, instead of each view registering its own + `mousedown` listener on its associated element, Ember sets up a `mousedown` + listener on the `body`. - ```javascript - var MyView = Ember.View.extend({ - template: Ember.Handlebars.compile('Child view: {{outlet "main"}} ') - }); - var myView = MyView.create(); - myView.appendTo('body'); - // myView's html: - //
    Child view:
    + If a `mousedown` event occurs, Ember will look at the target of the event and + start walking up the DOM node tree, finding corresponding views and invoking + their `mouseDown` method as it goes. - myView.connectOutlet('main', Ember.View.extend({ - template: Ember.Handlebars.compile('

    Foo

    ') - })); - // myView's html: - //
    Child view: - //

    Foo

    - //
    + `Ember.Application` has a number of default events that it listens for, as + well as a mapping from lowercase events to camel-cased view method names. For + example, the `keypress` event causes the `keyPress` method on the view to be + called, the `dblclick` event causes `doubleClick` to be called, and so on. - myView.disconnectOutlet('main'); - // myView's html: - //
    Child view:
    - ``` + If there is a bubbling browser event that Ember does not listen for by + default, you can specify custom events and their corresponding view method + names by setting the application's `customEvents` property: - @method disconnectOutlet - @param {String} outletName The name of the outlet to be removed - */ - disconnectOutlet: function(outletName) { - if (!this._pendingDisconnections) { - this._pendingDisconnections = {}; + ```javascript + App = Ember.Application.create({ + customEvents: { + // add support for the paste event + paste: "paste" } - this._pendingDisconnections[outletName] = true; - Ember.run.once(this, '_finishDisconnections'); - }, + }); + ``` - /** - @private + By default, the application sets up these event listeners on the document + body. However, in cases where you are embedding an Ember application inside + an existing page, you may want it to set up the listeners on an element + inside the body. - Gets an outlet that is pending disconnection and then - nullifys the object on the `_outlet` object. + For example, if only events inside a DOM element with the ID of `ember-app` + should be delegated, set your application's `rootElement` property: - @method _finishDisconnections - */ - _finishDisconnections: function() { - var outlets = get(this, '_outlets'); - var pendingDisconnections = this._pendingDisconnections; - this._pendingDisconnections = null; + ```javascript + window.App = Ember.Application.create({ + rootElement: '#ember-app' + }); + ``` - for (var outletName in pendingDisconnections) { - set(outlets, outletName, null); - } - } -}); + The `rootElement` can be either a DOM element or a jQuery-compatible selector + string. Note that *views appended to the DOM outside the root element will + not receive events.* If you specify a custom root element, make sure you only + append views inside it! -})(); + To learn more about the advantages of event delegation and the Ember view + layer, and a list of the event listeners that are setup by default, visit the + [Ember View Layer guide](http://emberjs.com/guides/understanding-ember/the-view-layer/#toc_event-delegation). + ### Initializers + Libraries on top of Ember can add initializers, like so: -(function() { -/** -@module ember -@submodule ember-views -*/ + ```javascript + Ember.Application.initializer({ + name: 'api-adapter', -// Add a new named queue after the 'actions' queue (where RSVP promises -// resolve), which is used in router transitions to prevent unnecessary -// loading state entry if all context promises resolve on the -// 'actions' queue first. + initialize: function(container, application) { + application.register('api-adapter:main', ApiAdapter); + } + }); + ``` -var queues = Ember.run.queues, - indexOf = Ember.ArrayPolyfills.indexOf; -queues.splice(indexOf.call(queues, 'actions') + 1, 0, 'routerTransitions'); + Initializers provide an opportunity to access the container, which + organizes the different components of an Ember application. Additionally + they provide a chance to access the instantiated application. Beyond + being used for libraries, initializers are also a great way to organize + dependency injection or setup in your own application. -})(); + ### Routing + + In addition to creating your application's router, `Ember.Application` is + also responsible for telling the router when to start routing. Transitions + between routes can be logged with the `LOG_TRANSITIONS` flag, and more + detailed intra-transition logging can be logged with + the `LOG_TRANSITIONS_INTERNAL` flag: + ```javascript + window.App = Ember.Application.create({ + LOG_TRANSITIONS: true, // basic logging of successful transitions + LOG_TRANSITIONS_INTERNAL: true // detailed logging of all routing steps + }); + ``` + By default, the router will begin trying to translate the current URL into + application state once the browser emits the `DOMContentReady` event. If you + need to defer routing, you can call the application's `deferReadiness()` + method. Once routing can begin, call the `advanceReadiness()` method. -(function() { -/** -@module ember -@submodule ember-routing + If there is any setup required before routing begins, you can implement a + `ready()` method on your app that will be invoked immediately before routing + begins. + ``` + + @class Application + @namespace Ember + @extends Ember.Namespace */ -var get = Ember.get, set = Ember.set; +var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin, { -/* - This file implements the `location` API used by Ember's router. + /** + The root DOM element of the Application. This can be specified as an + element or a + [jQuery-compatible selector string](http://api.jquery.com/category/selectors/). - That API is: + This is the element that will be passed to the Application's, + `eventDispatcher`, which sets up the listeners for event delegation. Every + view in your application should be a child of the element you specify here. - getURL: returns the current URL - setURL(path): sets the current URL - replaceURL(path): replace the current URL (optional) - onUpdateURL(callback): triggers the callback when the URL changes - formatURL(url): formats `url` to be placed into `href` attribute + @property rootElement + @type DOMElement + @default 'body' + */ + rootElement: 'body', - Calling setURL or replaceURL will not trigger onUpdateURL callbacks. + /** + The `Ember.EventDispatcher` responsible for delegating events to this + application's views. - TODO: This should perhaps be moved so that it's visible in the doc output. -*/ + The event dispatcher is created by the application at initialization time + and sets up event listeners on the DOM element described by the + application's `rootElement` property. -/** - Ember.Location returns an instance of the correct implementation of - the `location` API. + See the documentation for `Ember.EventDispatcher` for more information. - You can pass it a `implementation` ('hash', 'history', 'none') to force a - particular implementation. + @property eventDispatcher + @type Ember.EventDispatcher + @default null + */ + eventDispatcher: null, - @class Location - @namespace Ember - @static -*/ -Ember.Location = { /** - Create an instance of a an implementation of the `location` API. Requires - an options object with an `implementation` property. + The DOM events for which the event dispatcher should listen. - Example + By default, the application's `Ember.EventDispatcher` listens + for a set of standard DOM events, such as `mousedown` and + `keyup`, and delegates them to your application's `Ember.View` + instances. - ```javascript - var hashLocation = Ember.Location.create({implementation: 'hash'}); - var historyLocation = Ember.Location.create({implementation: 'history'}); - var noneLocation = Ember.Location.create({implementation: 'none'}); - ``` + If you would like additional bubbling events to be delegated to your + views, set your `Ember.Application`'s `customEvents` property + to a hash containing the DOM event name as the key and the + corresponding view method name as the value. For example: - @method create - @param {Object} options - @return {Object} an instance of an implementation of the `location` API + ```javascript + App = Ember.Application.create({ + customEvents: { + // add support for the paste event + paste: "paste" + } + }); + ``` + + @property customEvents + @type Object + @default null */ - create: function(options) { - var implementation = options && options.implementation; - Ember.assert("Ember.Location.create: you must specify a 'implementation' option", !!implementation); + customEvents: null, - var implementationClass = this.implementations[implementation]; - Ember.assert("Ember.Location.create: " + implementation + " is not a valid implementation", !!implementationClass); + // Start off the number of deferrals at 1. This will be + // decremented by the Application's own `initialize` method. + _readinessDeferrals: 1, - return implementationClass.create.apply(implementationClass, arguments); + init: function() { + if (!this.$) { this.$ = Ember.$; } + this.__container__ = this.buildContainer(); + + this.Router = this.defaultRouter(); + + this._super(); + + this.scheduleInitialize(); + + Ember.libraries.registerCoreLibrary('Handlebars', Ember.Handlebars.VERSION); + Ember.libraries.registerCoreLibrary('jQuery', Ember.$().jquery); + + if ( Ember.LOG_VERSION ) { + Ember.LOG_VERSION = false; // we only need to see this once per Application#init + var maxNameLength = Math.max.apply(this, Ember.A(Ember.libraries).mapBy("name.length")); + + Ember.debug('-------------------------------'); + Ember.libraries.each(function(name, version) { + var spaces = new Array(maxNameLength - name.length + 1).join(" "); + Ember.debug([name, spaces, ' : ', version].join("")); + }); + Ember.debug('-------------------------------'); + } }, /** - Registers a class that implements the `location` API with an implementation - name. This implementation name can then be specified by the location property on - the application's router class. - - Example - - ```javascript - Ember.Location.registerImplementation('history', Ember.HistoryLocation); + Build the container for the current application. - App.Router.reopen({ - location: 'history' - }); - ``` + Also register a default application view in case the application + itself does not. - @method registerImplementation - @param {String} name - @param {Object} implementation of the `location` API + @private + @method buildContainer + @return {Ember.Container} the configured container */ - registerImplementation: function(name, implementation) { - this.implementations[name] = implementation; + buildContainer: function() { + var container = this.__container__ = Application.buildContainer(this); + + return container; }, - implementations: {} -}; + /** + If the application has not opted out of routing and has not explicitly + defined a router, supply a default router for the application author + to configure. -})(); + This allows application developers to do: + ```javascript + var App = Ember.Application.create(); + App.Router.map(function() { + this.resource('posts'); + }); + ``` -(function() { -/** -@module ember -@submodule ember-routing -*/ + @private + @method defaultRouter + @return {Ember.Router} the default router + */ -var get = Ember.get, set = Ember.set; + defaultRouter: function() { + if (this.Router === false) { return; } + var container = this.__container__; -/** - Ember.NoneLocation does not interact with the browser. It is useful for - testing, or when you need to manage state with your Router, but temporarily - don't want it to muck with the URL (for example when you embed your - application in a larger page). + if (this.Router) { + container.unregister('router:main'); + container.register('router:main', this.Router); + } - @class NoneLocation - @namespace Ember - @extends Ember.Object -*/ -Ember.NoneLocation = Ember.Object.extend({ - path: '', + return container.lookupFactory('router:main'); + }, /** - @private + Automatically initialize the application once the DOM has + become ready. - Returns the current path. + The initialization itself is scheduled on the actions queue + which ensures that application loading finishes before + booting. - @method getURL - @return {String} path + If you are asynchronously loading code, you should call + `deferReadiness()` to defer booting, and then call + `advanceReadiness()` once all of your code has finished + loading. + + @private + @method scheduleInitialize */ - getURL: function() { - return get(this, 'path'); + scheduleInitialize: function() { + var self = this; + + if (!this.$ || this.$.isReady) { + Ember.run.schedule('actions', self, '_initialize'); + } else { + this.$().ready(function runInitialize() { + Ember.run(self, '_initialize'); + }); + } }, /** - @private + Use this to defer readiness until some condition is true. - Set the path and remembers what was set. Using this method - to change the path will not invoke the `updateURL` callback. + Example: - @method setURL - @param path {String} - */ - setURL: function(path) { - set(this, 'path', path); - }, + ```javascript + App = Ember.Application.create(); + App.deferReadiness(); - /** - @private + jQuery.getJSON("/auth-token", function(token) { + App.token = token; + App.advanceReadiness(); + }); + ``` - Register a callback to be invoked when the path changes. These - callbacks will execute when the user presses the back or forward - button, but not after `setURL` is invoked. + This allows you to perform asynchronous setup logic and defer + booting your application until the setup has finished. - @method onUpdateURL - @param callback {Function} + However, if the setup requires a loading UI, it might be better + to use the router for this purpose. + + @method deferReadiness */ - onUpdateURL: function(callback) { - this.updateCallback = callback; + deferReadiness: function() { + Ember.assert("You must call deferReadiness on an instance of Ember.Application", this instanceof Ember.Application); + Ember.assert("You cannot defer readiness since the `ready()` hook has already been called.", this._readinessDeferrals > 0); + this._readinessDeferrals++; }, /** - @private - - Sets the path and calls the `updateURL` callback. + Call `advanceReadiness` after any asynchronous setup logic has completed. + Each call to `deferReadiness` must be matched by a call to `advanceReadiness` + or the application will never become ready and routing will not begin. - @method handleURL - @param callback {Function} + @method advanceReadiness + @see {Ember.Application#deferReadiness} */ - handleURL: function(url) { - set(this, 'path', url); - this.updateCallback(url); + advanceReadiness: function() { + Ember.assert("You must call advanceReadiness on an instance of Ember.Application", this instanceof Ember.Application); + this._readinessDeferrals--; + + if (this._readinessDeferrals === 0) { + Ember.run.once(this, this.didBecomeReady); + } }, /** - @private - - Given a URL, formats it to be placed into the page as part - of an element's `href` attribute. - - This is used, for example, when using the {{action}} helper - to generate a URL based on an event. + Registers a factory that can be used for dependency injection (with + `App.inject`) or for service lookup. Each factory is registered with + a full name including two parts: `type:name`. - @method formatURL - @param url {String} - @return {String} url - */ - formatURL: function(url) { - // The return value is not overly meaningful, but we do not want to throw - // errors when test code renders templates containing {{action href=true}} - // helpers. - return url; - } -}); + A simple example: -Ember.Location.registerImplementation('none', Ember.NoneLocation); + ```javascript + var App = Ember.Application.create(); + App.Orange = Ember.Object.extend(); + App.register('fruit:favorite', App.Orange); + ``` -})(); + Ember will resolve factories from the `App` namespace automatically. + For example `App.CarsController` will be discovered and returned if + an application requests `controller:cars`. + An example of registering a controller with a non-standard name: + ```javascript + var App = Ember.Application.create(), + Session = Ember.Controller.extend(); -(function() { -/** -@module ember -@submodule ember-routing -*/ + App.register('controller:session', Session); -var get = Ember.get, set = Ember.set; + // The Session controller can now be treated like a normal controller, + // despite its non-standard name. + App.ApplicationController = Ember.Controller.extend({ + needs: ['session'] + }); + ``` -/** - Ember.HashLocation implements the location API using the browser's - hash. At present, it relies on a hashchange event existing in the - browser. + Registered factories are **instantiated** by having `create` + called on them. Additionally they are **singletons**, each time + they are looked up they return the same instance. - @class HashLocation - @namespace Ember - @extends Ember.Object -*/ -Ember.HashLocation = Ember.Object.extend({ + Some examples modifying that default behavior: - init: function() { - set(this, 'location', get(this, 'location') || window.location); - }, + ```javascript + var App = Ember.Application.create(); - /** - @private + App.Person = Ember.Object.extend(); + App.Orange = Ember.Object.extend(); + App.Email = Ember.Object.extend(); + App.session = Ember.Object.create(); - Returns the current `location.hash`, minus the '#' at the front. + App.register('model:user', App.Person, {singleton: false }); + App.register('fruit:favorite', App.Orange); + App.register('communication:main', App.Email, {singleton: false}); + App.register('session', App.session, {instantiate: false}); + ``` - @method getURL - */ - getURL: function() { - return get(this, 'location').hash.substr(1); + @method register + @param fullName {String} type:name (e.g., 'model:user') + @param factory {Function} (e.g., App.Person) + @param options {Object} (optional) disable instantiation or singleton usage + **/ + register: function() { + var container = this.__container__; + container.register.apply(container, arguments); }, /** - @private + Define a dependency injection onto a specific factory or all factories + of a type. - Set the `location.hash` and remembers what was set. This prevents - `onUpdateURL` callbacks from triggering when the hash was set by - `HashLocation`. + When Ember instantiates a controller, view, or other framework component + it can attach a dependency to that component. This is often used to + provide services to a set of framework components. - @method setURL - @param path {String} - */ - setURL: function(path) { - get(this, 'location').hash = path; - set(this, 'lastSetURL', path); - }, + An example of providing a session object to all controllers: - /** - @private + ```javascript + var App = Ember.Application.create(), + Session = Ember.Object.extend({ isAuthenticated: false }); - Uses location.replace to update the url without a page reload - or history modification. + // A factory must be registered before it can be injected + App.register('session:main', Session); - @method replaceURL - @param path {String} - */ - replaceURL: function(path) { - get(this, 'location').replace('#' + path); - }, + // Inject 'session:main' onto all factories of the type 'controller' + // with the name 'session' + App.inject('controller', 'session', 'session:main'); - /** - @private + App.IndexController = Ember.Controller.extend({ + isLoggedIn: Ember.computed.alias('session.isAuthenticated') + }); + ``` - Register a callback to be invoked when the hash changes. These - callbacks will execute when the user presses the back or forward - button, but not after `setURL` is invoked. + Injections can also be performed on specific factories. - @method onUpdateURL - @param callback {Function} - */ - onUpdateURL: function(callback) { - var self = this; - var guid = Ember.guidFor(this); + ```javascript + App.inject(, , ) + App.inject('route', 'source', 'source:main') + App.inject('route:application', 'email', 'model:email') + ``` - Ember.$(window).on('hashchange.ember-location-'+guid, function() { - Ember.run(function() { - var path = location.hash.substr(1); - if (get(self, 'lastSetURL') === path) { return; } + It is important to note that injections can only be performed on + classes that are instantiated by Ember itself. Instantiating a class + directly (via `create` or `new`) bypasses the dependency injection + system. - set(self, 'lastSetURL', null); + Ember-Data instantiates its models in a unique manner, and consequently + injections onto models (or all models) will not work as expected. Injections + on models can be enabled by setting `Ember.MODEL_FACTORY_INJECTIONS` + to `true`. - callback(path); - }); - }); + @method inject + @param factoryNameOrType {String} + @param property {String} + @param injectionName {String} + **/ + inject: function() { + var container = this.__container__; + container.injection.apply(container, arguments); }, /** - @private - - Given a URL, formats it to be placed into the page as part - of an element's `href` attribute. + Calling initialize manually is not supported. - This is used, for example, when using the {{action}} helper - to generate a URL based on an event. + Please see Ember.Application#advanceReadiness and + Ember.Application#deferReadiness. - @method formatURL - @param url {String} - */ - formatURL: function(url) { - return '#'+url; + @private + @deprecated + @method initialize + **/ + initialize: function() { + Ember.deprecate('Calling initialize manually is not supported. Please see Ember.Application#advanceReadiness and Ember.Application#deferReadiness'); }, /** - @private + Initialize the application. This happens automatically. - Cleans up the HashLocation event listener. + Run any initializers and run the application load hook. These hooks may + choose to defer readiness. For example, an authentication hook might want + to defer readiness until the auth token has been retrieved. - @method willDestroy + @private + @method _initialize */ - willDestroy: function() { - var guid = Ember.guidFor(this); + _initialize: function() { + if (this.isDestroyed) { return; } - Ember.$(window).off('hashchange.ember-location-'+guid); - } -}); + // At this point, the App.Router must already be assigned + if (this.Router) { + var container = this.__container__; + container.unregister('router:main'); + container.register('router:main', this.Router); + } -Ember.Location.registerImplementation('hash', Ember.HashLocation); + this.runInitializers(); + Ember.runLoadHooks('application', this); -})(); + // At this point, any initializers or load hooks that would have wanted + // to defer readiness have fired. In general, advancing readiness here + // will proceed to didBecomeReady. + this.advanceReadiness(); + return this; + }, + /** + Reset the application. This is typically used only in tests. It cleans up + the application in the following order: -(function() { -/** -@module ember -@submodule ember-routing -*/ + 1. Deactivate existing routes + 2. Destroy all objects in the container + 3. Create a new application container + 4. Re-route to the existing url -var get = Ember.get, set = Ember.set; -var popstateFired = false; -var supportsHistoryState = window.history && 'state' in window.history; + Typical Example: -/** - Ember.HistoryLocation implements the location API using the browser's - history.pushState API. + ```javascript - @class HistoryLocation - @namespace Ember - @extends Ember.Object -*/ -Ember.HistoryLocation = Ember.Object.extend({ + var App; - init: function() { - set(this, 'location', get(this, 'location') || window.location); - this.initState(); - }, + Ember.run(function() { + App = Ember.Application.create(); + }); - /** - @private + module("acceptance test", { + setup: function() { + App.reset(); + } + }); - Used to set state on first call to setURL + test("first test", function() { + // App is freshly reset + }); - @method initState - */ - initState: function() { - set(this, 'history', get(this, 'history') || window.history); - this.replaceState(this.formatURL(this.getURL())); - }, + test("first test", function() { + // App is again freshly reset + }); + ``` - /** - Will be pre-pended to path upon state change + Advanced Example: - @property rootURL - @default '/' - */ - rootURL: '/', + Occasionally you may want to prevent the app from initializing during + setup. This could enable extra configuration, or enable asserting prior + to the app becoming ready. - /** - @private + ```javascript - Returns the current `location.pathname` without rootURL + var App; - @method getURL - @return url {String} - */ - getURL: function() { - var rootURL = get(this, 'rootURL'), - url = get(this, 'location').pathname; + Ember.run(function() { + App = Ember.Application.create(); + }); - rootURL = rootURL.replace(/\/$/, ''); - url = url.replace(rootURL, ''); + module("acceptance test", { + setup: function() { + Ember.run(function() { + App.reset(); + App.deferReadiness(); + }); + } + }); - return url; - }, + test("first test", function() { + ok(true, 'something before app is initialized'); - /** - @private + Ember.run(function() { + App.advanceReadiness(); + }); + ok(true, 'something after app is initialized'); + }); + ``` - Uses `history.pushState` to update the url without a page reload. + @method reset + **/ + reset: function() { + this._readinessDeferrals = 1; - @method setURL - @param path {String} - */ - setURL: function(path) { - var state = this.getState(); - path = this.formatURL(path); + function handleReset() { + var router = this.__container__.lookup('router:main'); + router.reset(); - if (state && state.path !== path) { - this.pushState(path); + Ember.run(this.__container__, 'destroy'); + + this.buildContainer(); + + Ember.run.schedule('actions', this, function() { + this._initialize(); + }); } + + Ember.run.join(this, handleReset); }, /** @private - - Uses `history.replaceState` to update the url without a page reload - or history modification. - - @method replaceURL - @param path {String} + @method runInitializers */ - replaceURL: function(path) { - var state = this.getState(); - path = this.formatURL(path); + runInitializers: function() { + var initializers = get(this.constructor, 'initializers'), + container = this.__container__, + graph = new Ember.DAG(), + namespace = this, + name, initializer; - if (state && state.path !== path) { - this.replaceState(path); + for (name in initializers) { + initializer = initializers[name]; + graph.addEdges(initializer.name, initializer.initialize, initializer.before, initializer.after); } - }, - - /** - @private - Get the current `history.state` - Polyfill checks for native browser support and falls back to retrieving - from a private _historyState variable - - @method getState - @return state {Object} - */ - getState: function() { - return supportsHistoryState ? get(this, 'history').state : this._historyState; + graph.topsort(function (vertex) { + var initializer = vertex.value; + Ember.assert("No application initializer named '"+vertex.name+"'", initializer); + initializer(container, namespace); + }); }, /** - @private - - Pushes a new state - - @method pushState - @param path {String} + @private + @method didBecomeReady */ - pushState: function(path) { - var state = { path: path }; - - get(this, 'history').pushState(state, null, path); + didBecomeReady: function() { + this.setupEventDispatcher(); + this.ready(); // user hook + this.startRouting(); - // store state if browser doesn't support `history.state` - if (!supportsHistoryState) { - this._historyState = state; + if (!Ember.testing) { + // Eagerly name all classes that are already loaded + Ember.Namespace.processAll(); + Ember.BOOTED = true; } - // used for webkit workaround - this._previousURL = this.getURL(); + this.resolve(this); }, /** - @private - - Replaces the current state + Setup up the event dispatcher to receive events on the + application's `rootElement` with any registered + `customEvents`. - @method replaceState - @param path {String} + @private + @method setupEventDispatcher */ - replaceState: function(path) { - var state = { path: path }; - - get(this, 'history').replaceState(state, null, path); - - // store state if browser doesn't support `history.state` - if (!supportsHistoryState) { - this._historyState = state; - } + setupEventDispatcher: function() { + var customEvents = get(this, 'customEvents'), + rootElement = get(this, 'rootElement'), + dispatcher = this.__container__.lookup('event_dispatcher:main'); - // used for webkit workaround - this._previousURL = this.getURL(); + set(this, 'eventDispatcher', dispatcher); + dispatcher.setup(customEvents, rootElement); }, /** + trigger a new call to `route` whenever the URL changes. + If the application has a router, use it to route to the current URL, and + @private + @method startRouting + @property router {Ember.Router} + */ + startRouting: function() { + var router = this.__container__.lookup('router:main'); + if (!router) { return; } - Register a callback to be invoked whenever the browser - history changes, including using forward and back buttons. + router.startRouting(); + }, - @method onUpdateURL - @param callback {Function} - */ - onUpdateURL: function(callback) { - var guid = Ember.guidFor(this), - self = this; + handleURL: function(url) { + var router = this.__container__.lookup('router:main'); - Ember.$(window).on('popstate.ember-location-'+guid, function(e) { - // Ignore initial page load popstate event in Chrome - if (!popstateFired) { - popstateFired = true; - if (self.getURL() === self._previousURL) { return; } - } - callback(self.getURL()); - }); + router.handleURL(url); }, /** - @private - - Used when using `{{action}}` helper. The url is always appended to the rootURL. + Called when the Application has become ready. + The call will be delayed until the DOM has become ready. - @method formatURL - @param url {String} - @return formatted url {String} + @event ready */ - formatURL: function(url) { - var rootURL = get(this, 'rootURL'); + ready: Ember.K, - if (url !== '') { - rootURL = rootURL.replace(/\/$/, ''); - } + /** + @deprecated Use 'Resolver' instead + Set this to provide an alternate class to `Ember.DefaultResolver` - return rootURL + url; - }, - /** - @private + @property resolver + */ + resolver: null, - Cleans up the HistoryLocation event listener. + /** + Set this to provide an alternate class to `Ember.DefaultResolver` - @method willDestroy + @property resolver */ + Resolver: null, + willDestroy: function() { - var guid = Ember.guidFor(this); + Ember.BOOTED = false; + // Ensure deactivation of routes before objects are destroyed + this.__container__.lookup('router:main').reset(); - Ember.$(window).off('popstate.ember-location-'+guid); + this.__container__.destroy(); + }, + + initializer: function(options) { + this.constructor.initializer(options); } }); -Ember.Location.registerImplementation('history', Ember.HistoryLocation); - -})(); - - +Ember.Application.reopenClass({ + initializers: {}, + initializer: function(initializer) { + // If this is the first initializer being added to a subclass, we are going to reopen the class + // to make sure we have a new `initializers` object, which extends from the parent class' using + // prototypal inheritance. Without this, attempting to add initializers to the subclass would + // pollute the parent class as well as other subclasses. + if (this.superclass.initializers !== undefined && this.superclass.initializers === this.initializers) { + this.reopenClass({ + initializers: Ember.create(this.initializers) + }); + } -(function() { + Ember.assert("The initializer '" + initializer.name + "' has already been registered", !this.initializers[initializer.name]); + Ember.assert("An initializer cannot be registered with both a before and an after", !(initializer.before && initializer.after)); + Ember.assert("An initializer cannot be registered without an initialize function", Ember.canInvoke(initializer, 'initialize')); -})(); + this.initializers[initializer.name] = initializer; + }, + /** + This creates a container with the default Ember naming conventions. + It also configures the container: -(function() { -/** -Ember Routing + * registered views are created every time they are looked up (they are + not singletons) + * registered templates are not factories; the registered value is + returned directly. + * the router receives the application as its `namespace` property + * all controllers receive the router as their `target` and `controllers` + properties + * all controllers receive the application as their `namespace` property + * the application view receives the application controller as its + `controller` property + * the application view receives the application template as its + `defaultTemplate` property -@module ember -@submodule ember-routing -@requires ember-views -*/ + @private + @method buildContainer + @static + @param {Ember.Application} namespace the application to build the + container for. + @return {Ember.Container} the built container + */ + buildContainer: function(namespace) { + var container = new Ember.Container(); -})(); + Ember.Container.defaultContainer = new DeprecatedContainer(container); -(function() { -function visit(vertex, fn, visited, path) { - var name = vertex.name, - vertices = vertex.incoming, - names = vertex.incomingNames, - len = names.length, - i; - if (!visited) { - visited = {}; - } - if (!path) { - path = []; - } - if (visited.hasOwnProperty(name)) { - return; - } - path.push(name); - visited[name] = true; - for (i = 0; i < len; i++) { - visit(vertices[names[i]], fn, visited, path); - } - fn(vertex, path); - path.pop(); -} + container.set = Ember.set; + container.resolver = resolverFor(namespace); + container.normalize = container.resolver.normalize; + container.describe = container.resolver.describe; + container.makeToString = container.resolver.makeToString; -function DAG() { - this.names = []; - this.vertices = {}; -} + container.optionsForType('component', { singleton: false }); + container.optionsForType('view', { singleton: false }); + container.optionsForType('template', { instantiate: false }); + container.optionsForType('helper', { instantiate: false }); -DAG.prototype.add = function(name) { - if (!name) { return; } - if (this.vertices.hasOwnProperty(name)) { - return this.vertices[name]; - } - var vertex = { - name: name, incoming: {}, incomingNames: [], hasOutgoing: false, value: null - }; - this.vertices[name] = vertex; - this.names.push(name); - return vertex; -}; + container.register('application:main', namespace, { instantiate: false }); -DAG.prototype.map = function(name, value) { - this.add(name).value = value; -}; + container.register('controller:basic', Ember.Controller, { instantiate: false }); + container.register('controller:object', Ember.ObjectController, { instantiate: false }); + container.register('controller:array', Ember.ArrayController, { instantiate: false }); + container.register('route:basic', Ember.Route, { instantiate: false }); + container.register('event_dispatcher:main', Ember.EventDispatcher); -DAG.prototype.addEdge = function(fromName, toName) { - if (!fromName || !toName || fromName === toName) { - return; - } - var from = this.add(fromName), to = this.add(toName); - if (to.incoming.hasOwnProperty(fromName)) { - return; - } - function checkCycle(vertex, path) { - if (vertex.name === toName) { - throw new Ember.Error("cycle detected: " + toName + " <- " + path.join(" <- ")); - } - } - visit(from, checkCycle); - from.hasOutgoing = true; - to.incoming[fromName] = from; - to.incomingNames.push(fromName); -}; + container.register('router:main', Ember.Router); + container.injection('router:main', 'namespace', 'application:main'); -DAG.prototype.topsort = function(fn) { - var visited = {}, - vertices = this.vertices, - names = this.names, - len = names.length, - i, vertex; - for (i = 0; i < len; i++) { - vertex = vertices[names[i]]; - if (!vertex.hasOutgoing) { - visit(vertex, fn, visited); - } - } -}; + + container.register('location:auto', Ember.AutoLocation); + -DAG.prototype.addEdges = function(name, value, before, after) { - var i; - this.map(name, value); - if (before) { - if (typeof before === 'string') { - this.addEdge(name, before); - } else { - for (i = 0; i < before.length; i++) { - this.addEdge(name, before[i]); - } - } - } - if (after) { - if (typeof after === 'string') { - this.addEdge(after, name); - } else { - for (i = 0; i < after.length; i++) { - this.addEdge(after[i], name); - } - } - } -}; + container.register('location:hash', Ember.HashLocation); + container.register('location:history', Ember.HistoryLocation); + container.register('location:none', Ember.NoneLocation); -Ember.DAG = DAG; + container.injection('controller', 'target', 'router:main'); + container.injection('controller', 'namespace', 'application:main'); -})(); + container.injection('route', 'router', 'router:main'); + container.injection('location', 'rootURL', '-location-setting:root-url'); + // DEBUGGING + container.register('resolver-for-debugging:main', container.resolver.__resolver__, { instantiate: false }); + container.injection('container-debug-adapter:main', 'resolver', 'resolver-for-debugging:main'); + container.injection('data-adapter:main', 'containerDebugAdapter', 'container-debug-adapter:main'); + // Custom resolver authors may want to register their own ContainerDebugAdapter with this key + container.register('container-debug-adapter:main', Ember.ContainerDebugAdapter); + return container; + } +}); -(function() { /** -@module ember -@submodule ember-application -*/ + This function defines the default lookup rules for container lookups: -var get = Ember.get, - classify = Ember.String.classify, - capitalize = Ember.String.capitalize, - decamelize = Ember.String.decamelize; + * templates are looked up on `Ember.TEMPLATES` + * other names are looked up on the application after classifying the name. + For example, `controller:post` looks up `App.PostController` by default. + * if the default lookup fails, look for registered classes on the container -/** - The DefaultResolver defines the default lookup rules to resolve - container lookups before consulting the container for registered - items: + This allows the application to register default injections in the container + that could be overridden by the normal naming convention. -* templates are looked up on `Ember.TEMPLATES` -* other names are looked up on the application after converting - the name. For example, `controller:post` looks up - `App.PostController` by default. -* there are some nuances (see examples below) + @private + @method resolverFor + @param {Ember.Namespace} namespace the namespace to look for classes + @return {*} the resolved value for a given lookup +*/ +function resolverFor(namespace) { + if (namespace.get('resolver')) { + Ember.deprecate('Application.resolver is deprecated in favor of Application.Resolver', false); + } - ### How Resolving Works + var ResolverClass = namespace.get('resolver') || namespace.get('Resolver') || Ember.DefaultResolver; + var resolver = ResolverClass.create({ + namespace: namespace + }); - The container calls this object's `resolve` method with the - `fullName` argument. + function resolve(fullName) { + return resolver.resolve(fullName); + } - It first parses the fullName into an object using `parseName`. + resolve.describe = function(fullName) { + return resolver.lookupDescription(fullName); + }; - Then it checks for the presence of a type-specific instance - method of the form `resolve[Type]` and calls it if it exists. - For example if it was resolving 'template:post', it would call - the `resolveTemplate` method. + resolve.makeToString = function(factory, fullName) { + return resolver.makeToString(factory, fullName); + }; - Its last resort is to call the `resolveOther` method. + resolve.normalize = function(fullName) { + if (resolver.normalize) { + return resolver.normalize(fullName); + } else { + Ember.deprecate('The Resolver should now provide a \'normalize\' function', false); + return fullName; + } + }; - The methods of this object are designed to be easy to override - in a subclass. For example, you could enhance how a template - is resolved like so: + resolve.__resolver__ = resolver; - ```javascript - App = Ember.Application.create({ - Resolver: Ember.DefaultResolver.extend({ - resolveTemplate: function(parsedName) { - var resolvedTemplate = this._super(parsedName); - if (resolvedTemplate) { return resolvedTemplate; } - return Ember.TEMPLATES['not_found']; - } - }) - }); - ``` + return resolve; +} - Some examples of how names are resolved: +Ember.runLoadHooks('Ember.Application', Ember.Application); - ``` - 'template:post' //=> Ember.TEMPLATES['post'] - 'template:posts/byline' //=> Ember.TEMPLATES['posts/byline'] - 'template:posts.byline' //=> Ember.TEMPLATES['posts/byline'] - 'template:blogPost' //=> Ember.TEMPLATES['blogPost'] - // OR - // Ember.TEMPLATES['blog_post'] - 'controller:post' //=> App.PostController - 'controller:posts.index' //=> App.PostsIndexController - 'controller:blog/post' //=> Blog.PostController - 'controller:basic' //=> Ember.Controller - 'route:post' //=> App.PostRoute - 'route:posts.index' //=> App.PostsIndexRoute - 'route:blog/post' //=> Blog.PostRoute - 'route:basic' //=> Ember.Route - 'view:post' //=> App.PostView - 'view:posts.index' //=> App.PostsIndexView - 'view:blog/post' //=> Blog.PostView - 'view:basic' //=> Ember.View - 'foo:post' //=> App.PostFoo - 'model:post' //=> App.Post - ``` +})(); - @class DefaultResolver - @namespace Ember - @extends Ember.Object -*/ -Ember.DefaultResolver = Ember.Object.extend({ - /** - This will be set to the Application instance when it is - created. - @property namespace - */ - namespace: null, - normalize: function(fullName) { - var split = fullName.split(':', 2), - type = split[0], - name = split[1]; +(function() { - Ember.assert("Tried to normalize a container name without a colon (:) in it. You probably tried to lookup a name that did not contain a type, a colon, and a name. A proper lookup name would be `view:post`.", split.length === 2); +})(); - if (type !== 'template') { - var result = name; - if (result.indexOf('.') > -1) { - result = result.replace(/\.(.)/g, function(m) { return m.charAt(1).toUpperCase(); }); - } - if (name.indexOf('_') > -1) { - result = result.replace(/_(.)/g, function(m) { return m.charAt(1).toUpperCase(); }); - } +(function() { +/** +@module ember +@submodule ember-application +*/ - return type + ':' + result; - } else { - return fullName; - } - }, +var get = Ember.get, set = Ember.set; +function verifyNeedsDependencies(controller, container, needs) { + var dependency, i, l, missing = []; - /** - This method is called via the container's resolver method. - It parses the provided `fullName` and then looks up and - returns the appropriate template or class. + for (i=0, l=needs.length; i 1 ? 'they' : 'it') + " could not be found"); + } +} - @protected - @param {String} fullName the lookup string - @method parseName - */ - parseName: function(fullName) { - var nameParts = fullName.split(":"), - type = nameParts[0], fullNameWithoutType = nameParts[1], - name = fullNameWithoutType, - namespace = get(this, 'namespace'), - root = namespace; +var defaultControllersComputedProperty = Ember.computed(function() { + var controller = this; - if (type !== 'template' && name.indexOf('/') !== -1) { - var parts = name.split('/'); - name = parts[parts.length - 1]; - var namespaceName = capitalize(parts.slice(0, -1).join('.')); - root = Ember.Namespace.byName(namespaceName); + return { + needs: get(controller, 'needs'), + container: get(controller, 'container'), + unknownProperty: function(controllerName) { + var needs = this.needs, + dependency, i, l; + for (i=0, l=needs.length; i 0) { + Ember.assert(' `' + Ember.inspect(this) + ' specifies `needs`, but does ' + + "not have a container. Please ensure this controller was " + + "instantiated with a container.", + this.container || Ember.meta(this, false).descs.controllers !== defaultControllersComputedProperty); + + if (this.container) { + verifyNeedsDependencies(this, this.container, needs); + } + + // if needs then initialize controllers proxy + get(this, 'controllers'); + } + + this._super.apply(this, arguments); }, - /** - Look up the specified object (from parsedName) on the appropriate - namespace (usually on the Application) - @protected - @param {Object} parsedName a parseName object with the parsed - fullName lookup string - @method resolveOther + /** + @method controllerFor + @see {Ember.Route#controllerFor} + @deprecated Use `needs` instead */ - resolveOther: function(parsedName) { - var className = classify(parsedName.name) + classify(parsedName.type), - factory = get(parsedName.root, className); - if (factory) { return factory; } + controllerFor: function(controllerName) { + Ember.deprecate("Controller#controllerFor is deprecated, please use Controller#needs instead"); + return Ember.controllerFor(get(this, 'container'), controllerName); }, /** - Returns a human-readable description for a fullName. Used by the - Application namespace in assertions to describe the - precise name of the class that Ember is looking for, rather than - container keys. + Stores the instances of other controllers available from within + this controller. Any controller listed by name in the `needs` + property will be accessible by name through this property. - @protected - @param {String} fullName the lookup string - @method lookupDescription + ```javascript + App.CommentsController = Ember.ArrayController.extend({ + needs: ['post'], + postTitle: function(){ + var currentPost = this.get('controllers.post'); // instance of App.PostController + return currentPost.get('title'); + }.property('controllers.post.title') + }); + ``` + + @see {Ember.ControllerMixin#needs} + @property {Object} controllers + @default null */ - lookupDescription: function(fullName) { - var parsedName = this.parseName(fullName); + controllers: defaultControllersComputedProperty +}); - if (parsedName.type === 'template') { - return "template at " + parsedName.fullNameWithoutType.replace(/\./g, '/'); - } +})(); - var description = parsedName.root + "." + classify(parsedName.name); - if (parsedName.type !== 'model') { description += classify(parsedName.type); } - return description; - }, - makeToString: function(factory, fullName) { - return factory.toString(); - } -}); +(function() { })(); @@ -34817,1407 +41653,1475 @@ Ember.DefaultResolver = Ember.Object.extend({ (function() { /** +Ember Application + @module ember @submodule ember-application +@requires ember-views, ember-routing */ -var get = Ember.get, set = Ember.set; +})(); -function DeprecatedContainer(container) { - this._container = container; -} +(function() { +/** +@module ember +@submodule ember-extension-support +*/ +/** + The `ContainerDebugAdapter` helps the container and resolver interface + with tools that debug Ember such as the + [Ember Extension](https://github.com/tildeio/ember-extension) + for Chrome and Firefox. -DeprecatedContainer.deprecate = function(method) { - return function() { - var container = this._container; + This class can be extended by a custom resolver implementer + to override some of the methods with library-specific code. - Ember.deprecate('Using the defaultContainer is no longer supported. [defaultContainer#' + method + '] see: http://git.io/EKPpnA', false); - return container[method].apply(container, arguments); - }; -}; + The methods likely to be overridden are: -DeprecatedContainer.prototype = { - _container: null, - lookup: DeprecatedContainer.deprecate('lookup'), - resolve: DeprecatedContainer.deprecate('resolve'), - register: DeprecatedContainer.deprecate('register') -}; + * `canCatalogEntriesByType` + * `catalogEntriesByType` -/** - An instance of `Ember.Application` is the starting point for every Ember - application. It helps to instantiate, initialize and coordinate the many - objects that make up your app. + The adapter will need to be registered + in the application's container as `container-debug-adapter:main` - Each Ember app has one and only one `Ember.Application` object. In fact, the - very first thing you should do in your application is create the instance: + Example: ```javascript - window.App = Ember.Application.create(); - ``` + Application.initializer({ + name: "containerDebugAdapter", - Typically, the application object is the only global variable. All other - classes in your app should be properties on the `Ember.Application` instance, - which highlights its first role: a global namespace. + initialize: function(container, application) { + application.register('container-debug-adapter:main', require('app/container-debug-adapter')); + } + }); + ``` - For example, if you define a view class, it might look like this: + @class ContainerDebugAdapter + @namespace Ember + @extends Ember.Object +*/ +Ember.ContainerDebugAdapter = Ember.Object.extend({ + /** + The container of the application being debugged. + This property will be injected + on creation. - ```javascript - App.MyView = Ember.View.extend(); - ``` + @property container + @default null + */ + container: null, - By default, calling `Ember.Application.create()` will automatically initialize - your application by calling the `Ember.Application.initialize()` method. If - you need to delay initialization, you can call your app's `deferReadiness()` - method. When you are ready for your app to be initialized, call its - `advanceReadiness()` method. + /** + The resolver instance of the application + being debugged. This property will be injected + on creation. - You can define a `ready` method on the `Ember.Application` instance, which - will be run by Ember when the application is initialized. + @property resolver + @default null + */ + resolver: null, - Because `Ember.Application` inherits from `Ember.Namespace`, any classes - you create will have useful string representations when calling `toString()`. - See the `Ember.Namespace` documentation for more information. + /** + Returns true if it is possible to catalog a list of available + classes in the resolver for a given type. - While you can think of your `Ember.Application` as a container that holds the - other classes in your application, there are several other responsibilities - going on under-the-hood that you may want to understand. + @method canCatalogEntriesByType + @param {string} type The type. e.g. "model", "controller", "route" + @return {boolean} whether a list is available for this type. + */ + canCatalogEntriesByType: function(type) { + if (type === 'model' || type === 'template') return false; + return true; + }, - ### Event Delegation + /** + Returns the available classes a given type. - Ember uses a technique called _event delegation_. This allows the framework - to set up a global, shared event listener instead of requiring each view to - do it manually. For example, instead of each view registering its own - `mousedown` listener on its associated element, Ember sets up a `mousedown` - listener on the `body`. + @method catalogEntriesByType + @param {string} type The type. e.g. "model", "controller", "route" + @return {Array} An array of strings. + */ + catalogEntriesByType: function(type) { + var namespaces = Ember.A(Ember.Namespace.NAMESPACES), types = Ember.A(), self = this; + var typeSuffixRegex = new RegExp(Ember.String.classify(type) + "$"); - If a `mousedown` event occurs, Ember will look at the target of the event and - start walking up the DOM node tree, finding corresponding views and invoking - their `mouseDown` method as it goes. + namespaces.forEach(function(namespace) { + if (namespace !== Ember) { + for (var key in namespace) { + if (!namespace.hasOwnProperty(key)) { continue; } + if (typeSuffixRegex.test(key)) { + var klass = namespace[key]; + if (Ember.typeOf(klass) === 'class') { + types.push(Ember.String.dasherize(key.replace(typeSuffixRegex, ''))); + } + } + } + } + }); + return types; + } +}); - `Ember.Application` has a number of default events that it listens for, as - well as a mapping from lowercase events to camel-cased view method names. For - example, the `keypress` event causes the `keyPress` method on the view to be - called, the `dblclick` event causes `doubleClick` to be called, and so on. - If there is a bubbling browser event that Ember does not listen for by - default, you can specify custom events and their corresponding view method - names by setting the application's `customEvents` property: +})(); - ```javascript - App = Ember.Application.create({ - customEvents: { - // add support for the paste event - 'paste: "paste" - } - }); - ``` - By default, the application sets up these event listeners on the document - body. However, in cases where you are embedding an Ember application inside - an existing page, you may want it to set up the listeners on an element - inside the body. - For example, if only events inside a DOM element with the ID of `ember-app` - should be delegated, set your application's `rootElement` property: +(function() { +/** +@module ember +@submodule ember-extension-support +*/ +/** + The `DataAdapter` helps a data persistence library + interface with tools that debug Ember such + as the [Ember Extension](https://github.com/tildeio/ember-extension) + for Chrome and Firefox. - ```javascript - window.App = Ember.Application.create({ - rootElement: '#ember-app' - }); - ``` + This class will be extended by a persistence library + which will override some of the methods with + library-specific code. - The `rootElement` can be either a DOM element or a jQuery-compatible selector - string. Note that *views appended to the DOM outside the root element will - not receive events.* If you specify a custom root element, make sure you only - append views inside it! + The methods likely to be overridden are: - To learn more about the advantages of event delegation and the Ember view - layer, and a list of the event listeners that are setup by default, visit the - [Ember View Layer guide](http://emberjs.com/guides/understanding-ember/the-view-layer/#toc_event-delegation). + * `getFilters` + * `detect` + * `columnsForType` + * `getRecords` + * `getRecordColumnValues` + * `getRecordKeywords` + * `getRecordFilterValues` + * `getRecordColor` + * `observeRecord` - ### Initializers + The adapter will need to be registered + in the application's container as `dataAdapter:main` - Libraries on top of Ember can register additional initializers, like so: + Example: ```javascript - Ember.Application.initializer({ - name: "store", + Application.initializer({ + name: "data-adapter", initialize: function(container, application) { - container.register('store:main', application.Store); + application.register('data-adapter:main', DS.DataAdapter); } }); ``` - ### Routing - - In addition to creating your application's router, `Ember.Application` is - also responsible for telling the router when to start routing. Transitions - between routes can be logged with the `LOG_TRANSITIONS` flag, and more - detailed intra-transition logging can be logged with - the `LOG_TRANSITIONS_INTERNAL` flag: - - ```javascript - window.App = Ember.Application.create({ - LOG_TRANSITIONS: true, // basic logging of successful transitions - LOG_TRANSITIONS_INTERNAL: true // detailed logging of all routing steps - }); - ``` - - By default, the router will begin trying to translate the current URL into - application state once the browser emits the `DOMContentReady` event. If you - need to defer routing, you can call the application's `deferReadiness()` - method. Once routing can begin, call the `advanceReadiness()` method. - - If there is any setup required before routing begins, you can implement a - `ready()` method on your app that will be invoked immediately before routing - begins. - ``` - - @class Application + @class DataAdapter @namespace Ember - @extends Ember.Namespace + @extends Ember.Object */ - -var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin, { +Ember.DataAdapter = Ember.Object.extend({ + init: function() { + this._super(); + this.releaseMethods = Ember.A(); + }, /** - The root DOM element of the Application. This can be specified as an - element or a - [jQuery-compatible selector string](http://api.jquery.com/category/selectors/). - - This is the element that will be passed to the Application's, - `eventDispatcher`, which sets up the listeners for event delegation. Every - view in your application should be a child of the element you specify here. + The container of the application being debugged. + This property will be injected + on creation. - @property rootElement - @type DOMElement - @default 'body' + @property container + @default null */ - rootElement: 'body', + container: null, + /** - The `Ember.EventDispatcher` responsible for delegating events to this - application's views. + The container-debug-adapter which is used + to list all models. - The event dispatcher is created by the application at initialization time - and sets up event listeners on the DOM element described by the - application's `rootElement` property. + @property containerDebugAdapter + @default undefined + **/ + containerDebugAdapter: undefined, - See the documentation for `Ember.EventDispatcher` for more information. + /** + Number of attributes to send + as columns. (Enough to make the record + identifiable). - @property eventDispatcher - @type Ember.EventDispatcher - @default null + @private + @property attributeLimit + @default 3 */ - eventDispatcher: null, + attributeLimit: 3, /** - The DOM events for which the event dispatcher should listen. - - By default, the application's `Ember.EventDispatcher` listens - for a set of standard DOM events, such as `mousedown` and - `keyup`, and delegates them to your application's `Ember.View` - instances. + Stores all methods that clear observers. + These methods will be called on destruction. - If you would like additional bubbling events to be delegated to your - views, set your `Ember.Application`'s `customEvents` property - to a hash containing the DOM event name as the key and the - corresponding view method name as the value. For example: + @private + @property releaseMethods + */ + releaseMethods: Ember.A(), - ```javascript - App = Ember.Application.create({ - customEvents: { - // add support for the paste event - 'paste: "paste" - } - }); - ``` + /** + Specifies how records can be filtered. + Records returned will need to have a `filterValues` + property with a key for every name in the returned array. - @property customEvents - @type Object - @default null + @public + @method getFilters + @return {Array} List of objects defining filters. + The object should have a `name` and `desc` property. */ - customEvents: null, + getFilters: function() { + return Ember.A(); + }, - // Start off the number of deferrals at 1. This will be - // decremented by the Application's own `initialize` method. - _readinessDeferrals: 1, + /** + Fetch the model types and observe them for changes. - init: function() { - if (!this.$) { this.$ = Ember.$; } - this.__container__ = this.buildContainer(); + @public + @method watchModelTypes - this.Router = this.Router || this.defaultRouter(); - if (this.Router) { this.Router.namespace = this; } + @param {Function} typesAdded Callback to call to add types. + Takes an array of objects containing wrapped types (returned from `wrapModelType`). - this._super(); + @param {Function} typesUpdated Callback to call when a type has changed. + Takes an array of objects containing wrapped types. - this.scheduleInitialize(); + @return {Function} Method to call to remove all observers + */ + watchModelTypes: function(typesAdded, typesUpdated) { + var modelTypes = this.getModelTypes(), + self = this, typesToSend, releaseMethods = Ember.A(); - Ember.libraries.registerCoreLibrary('Handlebars', Ember.Handlebars.VERSION); - Ember.libraries.registerCoreLibrary('jQuery', Ember.$().jquery); + typesToSend = modelTypes.map(function(type) { + var klass = type.klass; + var wrapped = self.wrapModelType(klass, type.name); + releaseMethods.push(self.observeModelType(klass, typesUpdated)); + return wrapped; + }); - if ( Ember.LOG_VERSION ) { - Ember.LOG_VERSION = false; // we only need to see this once per Application#init - var maxNameLength = Math.max.apply(this, Ember.A(Ember.libraries).mapBy("name.length")); + typesAdded(typesToSend); - Ember.debug('-------------------------------'); - Ember.libraries.each(function(name, version) { - var spaces = new Array(maxNameLength - name.length + 1).join(" "); - Ember.debug([name, spaces, ' : ', version].join("")); - }); - Ember.debug('-------------------------------'); - } + var release = function() { + releaseMethods.forEach(function(fn) { fn(); }); + self.releaseMethods.removeObject(release); + }; + this.releaseMethods.pushObject(release); + return release; }, - /** - @private - - Build the container for the current application. - - Also register a default application view in case the application - itself does not. - - @method buildContainer - @return {Ember.Container} the configured container - */ - buildContainer: function() { - var container = this.__container__ = Application.buildContainer(this); - - return container; + _nameToClass: function(type) { + if (typeof type === 'string') { + type = this.container.lookupFactory('model:' + type); + } + return type; }, /** - @private + Fetch the records of a given type and observe them for changes. - If the application has not opted out of routing and has not explicitly - defined a router, supply a default router for the application author - to configure. + @public + @method watchRecords - This allows application developers to do: + @param {Function} recordsAdded Callback to call to add records. + Takes an array of objects containing wrapped records. + The object should have the following properties: + columnValues: {Object} key and value of a table cell + object: {Object} the actual record object - ```javascript - var App = Ember.Application.create(); + @param {Function} recordsUpdated Callback to call when a record has changed. + Takes an array of objects containing wrapped records. - App.Router.map(function() { - this.resource('posts'); - }); - ``` + @param {Function} recordsRemoved Callback to call when a record has removed. + Takes the following parameters: + index: the array index where the records were removed + count: the number of records removed - @method defaultRouter - @return {Ember.Router} the default router + @return {Function} Method to call to remove all observers */ - defaultRouter: function() { - // Create a default App.Router if one was not supplied to make - // it possible to do App.Router.map(...) without explicitly - // creating a router first. - if (this.router === undefined) { - return Ember.Router.extend(); - } - }, - - /** - @private - - Automatically initialize the application once the DOM has - become ready. - - The initialization itself is scheduled on the actions queue - which ensures that application loading finishes before - booting. + watchRecords: function(type, recordsAdded, recordsUpdated, recordsRemoved) { + var self = this, releaseMethods = Ember.A(), records = this.getRecords(type), release; - If you are asynchronously loading code, you should call - `deferReadiness()` to defer booting, and then call - `advanceReadiness()` once all of your code has finished - loading. + var recordUpdated = function(updatedRecord) { + recordsUpdated([updatedRecord]); + }; - @method scheduleInitialize - */ - scheduleInitialize: function() { - var self = this; + var recordsToSend = records.map(function(record) { + releaseMethods.push(self.observeRecord(record, recordUpdated)); + return self.wrapRecord(record); + }); - if (!this.$ || this.$.isReady) { - Ember.run.schedule('actions', self, '_initialize'); - } else { - this.$().ready(function() { - Ember.run(self, '_initialize'); - }); - } - }, - /** - Use this to defer readiness until some condition is true. + var contentDidChange = function(array, idx, removedCount, addedCount) { + for (var i = idx; i < idx + addedCount; i++) { + var record = array.objectAt(i); + var wrapped = self.wrapRecord(record); + releaseMethods.push(self.observeRecord(record, recordUpdated)); + recordsAdded([wrapped]); + } - Example: + if (removedCount) { + recordsRemoved(idx, removedCount); + } + }; - ```javascript - App = Ember.Application.create(); - App.deferReadiness(); + var observer = { didChange: contentDidChange, willChange: Ember.K }; + records.addArrayObserver(self, observer); - jQuery.getJSON("/auth-token", function(token) { - App.token = token; - App.advanceReadiness(); - }); - ``` + release = function() { + releaseMethods.forEach(function(fn) { fn(); }); + records.removeArrayObserver(self, observer); + self.releaseMethods.removeObject(release); + }; - This allows you to perform asynchronous setup logic and defer - booting your application until the setup has finished. + recordsAdded(recordsToSend); - However, if the setup requires a loading UI, it might be better - to use the router for this purpose. + this.releaseMethods.pushObject(release); + return release; + }, - @method deferReadiness + /** + Clear all observers before destruction + @private */ - deferReadiness: function() { - Ember.assert("You must call deferReadiness on an instance of Ember.Application", this instanceof Ember.Application); - Ember.assert("You cannot defer readiness since the `ready()` hook has already been called.", this._readinessDeferrals > 0); - this._readinessDeferrals++; + willDestroy: function() { + this._super(); + this.releaseMethods.forEach(function(fn) { + fn(); + }); }, /** - Call `advanceReadiness` after any asynchronous setup logic has completed. - Each call to `deferReadiness` must be matched by a call to `advanceReadiness` - or the application will never become ready and routing will not begin. - - @method advanceReadiness - @see {Ember.Application#deferReadiness} - */ - advanceReadiness: function() { - Ember.assert("You must call advanceReadiness on an instance of Ember.Application", this instanceof Ember.Application); - this._readinessDeferrals--; + Detect whether a class is a model. - if (this._readinessDeferrals === 0) { - Ember.run.once(this, this.didBecomeReady); - } + Test that against the model class + of your persistence library + + @private + @method detect + @param {Class} klass The class to test + @return boolean Whether the class is a model class or not + */ + detect: function(klass) { + return false; }, /** - registers a factory for later injection + Get the columns for a given model type. - Example: + @private + @method columnsForType + @param {Class} type The model type + @return {Array} An array of columns of the following format: + name: {String} name of the column + desc: {String} Humanized description (what would show in a table column name) + */ + columnsForType: function(type) { + return Ember.A(); + }, - ```javascript - App = Ember.Application.create(); + /** + Adds observers to a model type class. - App.Person = Ember.Object.extend({}); - App.Orange = Ember.Object.extend({}); - App.Email = Ember.Object.extend({}); - App.Session = Ember.Object.create({}); + @private + @method observeModelType + @param {Class} type The model type class + @param {Function} typesUpdated Called when a type is modified. + @return {Function} The function to call to remove observers + */ - App.register('model:user', App.Person, {singleton: false }); - App.register('fruit:favorite', App.Orange); - App.register('communication:main', App.Email, {singleton: false}); - App.register('session', App.Session, {instantiate: false}); - ``` + observeModelType: function(type, typesUpdated) { + var self = this, records = this.getRecords(type); - @method register - @param fullName {String} type:name (e.g., 'model:user') - @param factory {Function} (e.g., App.Person) - @param options {String} (optional) - **/ - register: function() { - var container = this.__container__; - container.register.apply(container, arguments); - }, - /** - defines an injection or typeInjection + var onChange = function() { + typesUpdated([self.wrapModelType(type)]); + }; + var observer = { + didChange: function() { + Ember.run.scheduleOnce('actions', this, onChange); + }, + willChange: Ember.K + }; - Example: + records.addArrayObserver(this, observer); - ```javascript - App.inject(, , ) - App.inject('model:user', 'email', 'model:email') - App.inject('model', 'source', 'source:main') - ``` + var release = function() { + records.removeArrayObserver(self, observer); + }; - @method inject - @param factoryNameOrType {String} - @param property {String} - @param injectionName {String} - **/ - inject: function() { - var container = this.__container__; - container.injection.apply(container, arguments); + return release; }, + /** + Wraps a given model type and observes changes to it. + @private - @deprecated + @method wrapModelType + @param {Class} type A model class + @param {String} Optional name of the class + @return {Object} contains the wrapped type and the function to remove observers + Format: + type: {Object} the wrapped type + The wrapped type has the following format: + name: {String} name of the type + count: {Integer} number of records available + columns: {Columns} array of columns to describe the record + object: {Class} the actual Model type class + release: {Function} The function to remove observers + */ + wrapModelType: function(type, name) { + var release, records = this.getRecords(type), + typeToSend, self = this; - Calling initialize manually is not supported. + typeToSend = { + name: name || type.toString(), + count: Ember.get(records, 'length'), + columns: this.columnsForType(type), + object: type + }; - Please see Ember.Application#advanceReadiness and - Ember.Application#deferReadiness. - @method initialize - **/ - initialize: function() { - Ember.deprecate('Calling initialize manually is not supported. Please see Ember.Application#advanceReadiness and Ember.Application#deferReadiness'); + return typeToSend; }, - /** - @private - Initialize the application. This happens automatically. - Run any initializers and run the application load hook. These hooks may - choose to defer readiness. For example, an authentication hook might want - to defer readiness until the auth token has been retrieved. + /** + Fetches all models defined in the application. - @method _initialize + @private + @method getModelTypes + @return {Array} Array of model types */ - _initialize: function() { - if (this.isDestroyed) { return; } + getModelTypes: function() { + var types, self = this, + containerDebugAdapter = this.get('containerDebugAdapter'); - // At this point, the App.Router must already be assigned - if (this.Router) { - this.register('router:main', this.Router); + if (containerDebugAdapter.canCatalogEntriesByType('model')) { + types = containerDebugAdapter.catalogEntriesByType('model'); + } else { + types = this._getObjectsOnNamespaces(); } + // New adapters return strings instead of classes + return types.map(function(name) { + return { + klass: self._nameToClass(name), + name: name + }; + }).filter(function(type) { + return self.detect(type.klass); + }); + }, - this.runInitializers(); - Ember.runLoadHooks('application', this); + /** + Loops over all namespaces and all objects + attached to them - // At this point, any initializers or load hooks that would have wanted - // to defer readiness have fired. In general, advancing readiness here - // will proceed to didBecomeReady. - this.advanceReadiness(); + @private + @method _getObjectsOnNamespaces + @return {Array} Array of model type strings + */ + _getObjectsOnNamespaces: function() { + var namespaces = Ember.A(Ember.Namespace.NAMESPACES), types = Ember.A(); - return this; + namespaces.forEach(function(namespace) { + for (var key in namespace) { + if (!namespace.hasOwnProperty(key)) { continue; } + var name = Ember.String.dasherize(key); + if (!(namespace instanceof Ember.Application) && namespace.toString()) { + name = namespace + '/' + name; + } + types.push(name); + } + }); + return types; }, /** - Reset the application. This is typically used only in tests. It cleans up - the application in the following order: + Fetches all loaded records for a given type. - 1. Deactivate existing routes - 2. Destroy all objects in the container - 3. Create a new application container - 4. Re-route to the existing url + @private + @method getRecords + @return {Array} An array of records. + This array will be observed for changes, + so it should update when new records are added/removed. + */ + getRecords: function(type) { + return Ember.A(); + }, - Typical Example: + /** + Wraps a record and observers changes to it. - ```javascript + @private + @method wrapRecord + @param {Object} record The record instance. + @return {Object} The wrapped record. Format: + columnValues: {Array} + searchKeywords: {Array} + */ + wrapRecord: function(record) { + var recordToSend = { object: record }, columnValues = {}, self = this; - var App; + recordToSend.columnValues = this.getRecordColumnValues(record); + recordToSend.searchKeywords = this.getRecordKeywords(record); + recordToSend.filterValues = this.getRecordFilterValues(record); + recordToSend.color = this.getRecordColor(record); - Ember.run(function() { - App = Ember.Application.create(); - }); + return recordToSend; + }, - module("acceptance test", { - setup: function() { - App.reset(); - } - }); + /** + Gets the values for each column. - test("first test", function() { - // App is freshly reset - }); + @private + @method getRecordColumnValues + @return {Object} Keys should match column names defined + by the model type. + */ + getRecordColumnValues: function(record) { + return {}; + }, - test("first test", function() { - // App is again freshly reset - }); - ``` + /** + Returns keywords to match when searching records. - Advanced Example: + @private + @method getRecordKeywords + @return {Array} Relevant keywords for search. + */ + getRecordKeywords: function(record) { + return Ember.A(); + }, - Occasionally you may want to prevent the app from initializing during - setup. This could enable extra configuration, or enable asserting prior - to the app becoming ready. + /** + Returns the values of filters defined by `getFilters`. - ```javascript + @private + @method getRecordFilterValues + @param {Object} record The record instance + @return {Object} The filter values + */ + getRecordFilterValues: function(record) { + return {}; + }, - var App; + /** + Each record can have a color that represents its state. - Ember.run(function() { - App = Ember.Application.create(); - }); + @private + @method getRecordColor + @param {Object} record The record instance + @return {String} The record's color + Possible options: black, red, blue, green + */ + getRecordColor: function(record) { + return null; + }, - module("acceptance test", { - setup: function() { - Ember.run(function() { - App.reset(); - App.deferReadiness(); - }); - } - }); + /** + Observes all relevant properties and re-sends the wrapped record + when a change occurs. - test("first test", function() { - ok(true, 'something before app is initialized'); + @private + @method observerRecord + @param {Object} record The record instance + @param {Function} recordUpdated The callback to call when a record is updated. + @return {Function} The function to call to remove all observers. + */ + observeRecord: function(record, recordUpdated) { + return function(){}; + } - Ember.run(function() { - App.advanceReadiness(); - }); - ok(true, 'something after app is initialized'); - }); - ``` +}); - @method reset - **/ - reset: function() { - this._readinessDeferrals = 1; - function handleReset() { - var router = this.__container__.lookup('router:main'); - router.reset(); +})(); - Ember.run(this.__container__, 'destroy'); - this.buildContainer(); - Ember.run.schedule('actions', this, function() { - this._initialize(); - }); - } +(function() { +/** +Ember Extension Support - Ember.run.join(this, handleReset); - }, +@module ember +@submodule ember-extension-support +@requires ember-application +*/ - /** - @private - @method runInitializers - */ - runInitializers: function() { - var initializers = get(this.constructor, 'initializers'), - container = this.__container__, - graph = new Ember.DAG(), - namespace = this, - i, initializer; +})(); - for (i=0; i true - Ember.Container.defaultContainer = new DeprecatedContainer(container); + container.unregister('model:user') + container.lookup('model:user') === undefined //=> true + ``` - container.set = Ember.set; - container.resolver = resolverFor(namespace); - container.normalize = container.resolver.normalize; - container.describe = container.resolver.describe; - container.makeToString = container.resolver.makeToString; + @method unregister + @param {String} fullName + */ + unregister: function(fullName) { + validateFullName(fullName); - container.optionsForType('component', { singleton: false }); - container.optionsForType('view', { singleton: false }); - container.optionsForType('template', { instantiate: false }); - container.register('application:main', namespace, { instantiate: false }); + var normalizedName = this.normalize(fullName); - container.register('controller:basic', Ember.Controller, { instantiate: false }); - container.register('controller:object', Ember.ObjectController, { instantiate: false }); - container.register('controller:array', Ember.ArrayController, { instantiate: false }); - container.register('route:basic', Ember.Route, { instantiate: false }); - container.register('event_dispatcher:main', Ember.EventDispatcher); + this.registry.remove(normalizedName); + this.cache.remove(normalizedName); + this.factoryCache.remove(normalizedName); + this.resolveCache.remove(normalizedName); + this._options.remove(normalizedName); + }, - container.injection('router:main', 'namespace', 'application:main'); + /** + Given a fullName return the corresponding factory. - container.injection('controller', 'target', 'router:main'); - container.injection('controller', 'namespace', 'application:main'); + By default `resolve` will retrieve the factory from + its container's registry. - container.injection('route', 'router', 'router:main'); + ```javascript + var container = new Container(); + container.register('api:twitter', Twitter); - return container; - } -}); + container.resolve('api:twitter') // => Twitter + ``` -/** - @private + Optionally the container can be provided with a custom resolver. + If provided, `resolve` will first provide the custom resolver + the oppertunity to resolve the fullName, otherwise it will fallback + to the registry. - This function defines the default lookup rules for container lookups: + ```javascript + var container = new Container(); + container.resolver = function(fullName) { + // lookup via the module system of choice + }; - * templates are looked up on `Ember.TEMPLATES` - * other names are looked up on the application after classifying the name. - For example, `controller:post` looks up `App.PostController` by default. - * if the default lookup fails, look for registered classes on the container + // the twitter factory is added to the module system + container.resolve('api:twitter') // => Twitter + ``` - This allows the application to register default injections in the container - that could be overridden by the normal naming convention. + @method resolve + @param {String} fullName + @return {Function} fullName's factory + */ + resolve: function(fullName) { + validateFullName(fullName); - @method resolverFor - @param {Ember.Namespace} namespace the namespace to look for classes - @return {*} the resolved value for a given lookup -*/ -function resolverFor(namespace) { - if (namespace.get('resolver')) { - Ember.deprecate('Application.resolver is deprecated in favor of Application.Resolver', false); - } + var normalizedName = this.normalize(fullName); + var cached = this.resolveCache.get(normalizedName); - var ResolverClass = namespace.get('resolver') || namespace.get('Resolver') || Ember.DefaultResolver; - var resolver = ResolverClass.create({ - namespace: namespace - }); + if (cached) { return cached; } - function resolve(fullName) { - return resolver.resolve(fullName); - } + var resolved = this.resolver(normalizedName) || this.registry.get(normalizedName); - resolve.describe = function(fullName) { - return resolver.lookupDescription(fullName); - }; + this.resolveCache.set(normalizedName, resolved); - resolve.makeToString = function(factory, fullName) { - return resolver.makeToString(factory, fullName); - }; + return resolved; + }, - resolve.normalize = function(fullName) { - if (resolver.normalize) { - return resolver.normalize(fullName); - } else { - Ember.deprecate('The Resolver should now provide a \'normalize\' function', false); - return fullName; - } - }; + /** + A hook that can be used to describe how the resolver will + attempt to find the factory. - return resolve; -} + For example, the default Ember `.describe` returns the full + class name (including namespace) where Ember's resolver expects + to find the `fullName`. -Ember.runLoadHooks('Ember.Application', Ember.Application); + @method describe + @param {String} fullName + @return {string} described fullName + */ + describe: function(fullName) { + return fullName; + }, -})(); + /** + A hook to enable custom fullName normalization behaviour + @method normalize + @param {String} fullName + @return {string} normalized fullName + */ + normalize: function(fullName) { + return fullName; + }, + /** + @method makeToString -(function() { + @param {any} factory + @param {string} fullName + @return {function} toString function + */ + makeToString: function(factory, fullName) { + return factory.toString(); + }, -})(); + /** + Given a fullName return a corresponding instance. + The default behaviour is for lookup to return a singleton instance. + The singleton is scoped to the container, allowing multiple containers + to all have their own locally scoped singletons. + ```javascript + var container = new Container(); + container.register('api:twitter', Twitter); -(function() { -/** -@module ember -@submodule ember-application -*/ + var twitter = container.lookup('api:twitter'); -var get = Ember.get, set = Ember.set; + twitter instanceof Twitter; // => true -function verifyNeedsDependencies(controller, container, needs) { - var dependency, i, l; + // by default the container will return singletons + var twitter2 = container.lookup('api:twitter'); + twitter instanceof Twitter; // => true - for (i=0, l=needs.length; i true + ``` - // Structure assert to still do verification but not string concat in production - if (!container.has(dependency)) { - Ember.assert(Ember.inspect(controller) + " needs " + dependency + " but it does not exist", false); - } - } -} + If singletons are not wanted an optional flag can be provided at lookup. -/** - @class ControllerMixin - @namespace Ember -*/ -Ember.ControllerMixin.reopen({ - concatenatedProperties: ['needs'], + ```javascript + var container = new Container(); + container.register('api:twitter', Twitter); - /** - An array of other controller objects available inside - instances of this controller via the `controllers` - property: + var twitter = container.lookup('api:twitter', { singleton: false }); + var twitter2 = container.lookup('api:twitter', { singleton: false }); - For example, when you define a controller: + twitter === twitter2; //=> false + ``` - ```javascript - App.CommentsController = Ember.ArrayController.extend({ - needs: ['post'] - }); - ``` + @method lookup + @param {String} fullName + @param {Object} options + @return {any} + */ + lookup: function(fullName, options) { + validateFullName(fullName); + return lookup(this, this.normalize(fullName), options); + }, - The application's single instance of these other - controllers are accessible by name through the - `controllers` property: + /** + Given a fullName return the corresponding factory. - ```javascript - this.get('controllers.post'); // instance of App.PostController - ``` + @method lookupFactory + @param {String} fullName + @return {any} + */ + lookupFactory: function(fullName) { + validateFullName(fullName); + return factoryFor(this, this.normalize(fullName)); + }, - This is only available for singleton controllers. + /** + Given a fullName check if the container is aware of its factory + or singleton instance. - @property {Array} needs - @default [] - */ - needs: [], + @method has + @param {String} fullName + @return {Boolean} + */ + has: function(fullName) { + validateFullName(fullName); + return has(this, this.normalize(fullName)); + }, - init: function() { - var needs = get(this, 'needs'), - length = get(needs, 'length'); + /** + Allow registering options for all factories of a type. - if (length > 0) { - Ember.assert(' `' + Ember.inspect(this) + ' specifies `needs`, but does not have a container. Please ensure this controller was instantiated with a container.', this.container); + ```javascript + var container = new Container(); - verifyNeedsDependencies(this, this.container, needs); + // if all of type `connection` must not be singletons + container.optionsForType('connection', { singleton: false }); - // if needs then initialize controllers proxy - get(this, 'controllers'); - } + container.register('connection:twitter', TwitterConnection); + container.register('connection:facebook', FacebookConnection); - this._super.apply(this, arguments); - }, + var twitter = container.lookup('connection:twitter'); + var twitter2 = container.lookup('connection:twitter'); - /** - @method controllerFor - @see {Ember.Route#controllerFor} - @deprecated Use `needs` instead - */ - controllerFor: function(controllerName) { - Ember.deprecate("Controller#controllerFor is deprecated, please use Controller#needs instead"); - return Ember.controllerFor(get(this, 'container'), controllerName); - }, + twitter === twitter2; // => false - /** - Stores the instances of other controllers available from within - this controller. Any controller listed by name in the `needs` - property will be accessible by name through this property. + var facebook = container.lookup('connection:facebook'); + var facebook2 = container.lookup('connection:facebook'); - ```javascript - App.CommentsController = Ember.ArrayController.extend({ - needs: ['post'], - postTitle: function(){ - var currentPost = this.get('controllers.post'); // instance of App.PostController - return currentPost.get('title'); - }.property('controllers.post.title') - }); - ``` + facebook === facebook2; // => false + ``` - @see {Ember.ControllerMixin#needs} - @property {Object} controllers - @default null - */ - controllers: Ember.computed(function() { - var controller = this; + @method optionsForType + @param {String} type + @param {Object} options + */ + optionsForType: function(type, options) { + if (this.parent) { illegalChildOperation('optionsForType'); } - return { - needs: get(controller, 'needs'), - container: get(controller, 'container'), - unknownProperty: function(controllerName) { - var needs = this.needs, - dependency, i, l; - for (i=0, l=needs.length; i true + post.router instanceof Router; //=> true -@module ember -@submodule ember-application -@requires ember-views, ember-routing -*/ + // both controllers share the same router + user.router === post.router; //=> true + ``` -})(); + @private + @method typeInjection + @param {String} type + @param {String} property + @param {String} fullName + */ + typeInjection: function(type, property, fullName) { + validateFullName(fullName); + if (this.parent) { illegalChildOperation('typeInjection'); } -(function() { -/** -@module ember -@submodule ember-extension-support -*/ -/** - The `DataAdapter` helps a data persistence library - interface with tools that debug Ember such - as the Chrome Ember Extension. + var fullNameType = fullName.split(':')[0]; + if(fullNameType === type) { + throw new Error('Cannot inject a `' + fullName + '` on other ' + type + '(s). Register the `' + fullName + '` as a different type and perform the typeInjection.'); + } + addTypeInjection(this.typeInjections, type, property, fullName); + }, - This class will be extended by a persistence library - which will override some of the methods with - library-specific code. + /** + Defines injection rules. - The methods likely to be overriden are - `getFilters`, `detect`, `columnsForType`, - `getRecords`, `getRecordColumnValues`, - `getRecordKeywords`, `getRecordFilterValues`, - `getRecordColor`, `observeRecord` + These rules are used to inject dependencies onto objects when they + are instantiated. - The adapter will need to be registered - in the application's container as `dataAdapter:main` + Two forms of injections are possible: - Example: - ```javascript - Application.initializer({ - name: "dataAdapter", + * Injecting one fullName on another fullName + * Injecting one fullName on a type - initialize: function(container, application) { - application.register('dataAdapter:main', DS.DataAdapter); - } - }); - ``` + Example: - @class DataAdapter - @namespace Ember - @extends Ember.Object -*/ -Ember.DataAdapter = Ember.Object.extend({ - init: function() { - this._super(); - this.releaseMethods = Ember.A(); - }, + ```javascript + var container = new Container(); - /** - The container of the application being debugged. - This property will be injected - on creation. - */ - container: null, + container.register('source:main', Source); + container.register('model:user', User); + container.register('model:post', Post); - /** - @private + // injecting one fullName on another fullName + // eg. each user model gets a post model + container.injection('model:user', 'post', 'model:post'); - Number of attributes to send - as columns. (Enough to make the record - identifiable). - */ - attributeLimit: 3, + // injecting one fullName on another type + container.injection('model', 'source', 'source:main'); - /** - @private + var user = container.lookup('model:user'); + var post = container.lookup('model:post'); - Stores all methods that clear observers. - These methods will be called on destruction. - */ - releaseMethods: Ember.A(), + user.source instanceof Source; //=> true + post.source instanceof Source; //=> true - /** - @public + user.post instanceof Post; //=> true - Specifies how records can be filtered. - Records returned will need to have a `filterValues` - property with a key for every name in the returned array. + // and both models share the same source + user.source === post.source; //=> true + ``` - @method getFilters - @return {Array} List of objects defining filters. - The object should have a `name` and `desc` property. - */ - getFilters: function() { - return Ember.A(); - }, + @method injection + @param {String} factoryName + @param {String} property + @param {String} injectionName + */ + injection: function(fullName, property, injectionName) { + if (this.parent) { illegalChildOperation('injection'); } - /** - @public + validateFullName(injectionName); + var normalizedInjectionName = this.normalize(injectionName); - Fetch the model types and observe them for changes. + if (fullName.indexOf(':') === -1) { + return this.typeInjection(fullName, property, normalizedInjectionName); + } - @method watchModelTypes + validateFullName(fullName); + var normalizedName = this.normalize(fullName); - @param {Function} typesAdded Callback to call to add types. - Takes an array of objects containing wrapped types (returned from `wrapModelType`). + if (this.cache.has(normalizedName)) { + throw new Error("Attempted to register an injection for a type that has already been looked up. ('" + normalizedName + "', '" + property + "', '" + injectionName + "')"); + } + addInjection(this.injections, normalizedName, property, normalizedInjectionName); + }, - @param {Function} typesUpdated Callback to call when a type has changed. - Takes an array of objects containing wrapped types. - @return {Function} Method to call to remove all observers - */ - watchModelTypes: function(typesAdded, typesUpdated) { - var modelTypes = this.getModelTypes(), - self = this, typesToSend, releaseMethods = Ember.A(); + /** + Used only via `factoryInjection`. - typesToSend = modelTypes.map(function(type) { - var wrapped = self.wrapModelType(type); - releaseMethods.push(self.observeModelType(type, typesUpdated)); - return wrapped; - }); + Provides a specialized form of injection, specifically enabling + all factory of one type to be injected with a reference to another + object. + + For example, provided each factory of type `model` needed a `store`. + one would do the following: - typesAdded(typesToSend); + ```javascript + var container = new Container(); - var release = function() { - releaseMethods.forEach(function(fn) { fn(); }); - self.releaseMethods.removeObject(release); - }; - this.releaseMethods.pushObject(release); - return release; - }, + container.register('store:main', SomeStore); - /** - @public + container.factoryTypeInjection('model', 'store', 'store:main'); - Fetch the records of a given type and observe them for changes. + var store = container.lookup('store:main'); + var UserFactory = container.lookupFactory('model:user'); - @method watchRecords + UserFactory.store instanceof SomeStore; //=> true + ``` - @param {Function} recordsAdded Callback to call to add records. - Takes an array of objects containing wrapped records. - The object should have the following properties: - columnValues: {Object} key and value of a table cell - object: {Object} the actual record object + @private + @method factoryTypeInjection + @param {String} type + @param {String} property + @param {String} fullName + */ + factoryTypeInjection: function(type, property, fullName) { + if (this.parent) { illegalChildOperation('factoryTypeInjection'); } - @param {Function} recordsUpdated Callback to call when a record has changed. - Takes an array of objects containing wrapped records. + addTypeInjection(this.factoryTypeInjections, type, property, this.normalize(fullName)); + }, - @param {Function} recordsRemoved Callback to call when a record has removed. - Takes the following parameters: - index: the array index where the records were removed - count: the number of records removed + /** + Defines factory injection rules. - @return {Function} Method to call to remove all observers - */ - watchRecords: function(type, recordsAdded, recordsUpdated, recordsRemoved) { - var self = this, releaseMethods = Ember.A(), records = this.getRecords(type), release; + Similar to regular injection rules, but are run against factories, via + `Container#lookupFactory`. - var recordUpdated = function(updatedRecord) { - recordsUpdated([updatedRecord]); - }; + These rules are used to inject objects onto factories when they + are looked up. - var recordsToSend = records.map(function(record) { - releaseMethods.push(self.observeRecord(record, recordUpdated)); - return self.wrapRecord(record); - }); + Two forms of injections are possible: + * Injecting one fullName on another fullName + * Injecting one fullName on a type - var contentDidChange = function(array, idx, removedCount, addedCount) { - for (var i = idx; i < idx + addedCount; i++) { - var record = array.objectAt(i); - var wrapped = self.wrapRecord(record); - releaseMethods.push(self.observeRecord(record, recordUpdated)); - recordsAdded([wrapped]); - } + Example: - if (removedCount) { - recordsRemoved(idx, removedCount); - } - }; + ```javascript + var container = new Container(); - var observer = { didChange: contentDidChange, willChange: Ember.K }; - records.addArrayObserver(self, observer); + container.register('store:main', Store); + container.register('store:secondary', OtherStore); + container.register('model:user', User); + container.register('model:post', Post); - release = function() { - releaseMethods.forEach(function(fn) { fn(); }); - records.removeArrayObserver(self, observer); - self.releaseMethods.removeObject(release); - }; + // injecting one fullName on another type + container.factoryInjection('model', 'store', 'store:main'); - recordsAdded(recordsToSend); + // injecting one fullName on another fullName + container.factoryInjection('model:post', 'secondaryStore', 'store:secondary'); - this.releaseMethods.pushObject(release); - return release; - }, + var UserFactory = container.lookupFactory('model:user'); + var PostFactory = container.lookupFactory('model:post'); + var store = container.lookup('store:main'); - /** - @private + UserFactory.store instanceof Store; //=> true + UserFactory.secondaryStore instanceof OtherStore; //=> false - Clear all observers before destruction - */ - willDestroy: function() { - this._super(); - this.releaseMethods.forEach(function(fn) { - fn(); - }); - }, + PostFactory.store instanceof Store; //=> true + PostFactory.secondaryStore instanceof OtherStore; //=> true - /** - @private + // and both models share the same source instance + UserFactory.store === PostFactory.store; //=> true + ``` - Detect whether a class is a model. + @method factoryInjection + @param {String} factoryName + @param {String} property + @param {String} injectionName + */ + factoryInjection: function(fullName, property, injectionName) { + if (this.parent) { illegalChildOperation('injection'); } - Test that against the model class - of your persistence library + var normalizedName = this.normalize(fullName); + var normalizedInjectionName = this.normalize(injectionName); - @method detect - @param {Class} klass The class to test - @return boolean Whether the class is a model class or not - */ - detect: function(klass) { - return false; - }, + validateFullName(injectionName); - /** - @private + if (fullName.indexOf(':') === -1) { + return this.factoryTypeInjection(normalizedName, property, normalizedInjectionName); + } - Get the columns for a given model type. + validateFullName(fullName); - @method columnsForType - @param {Class} type The model type - @return {Array} An array of columns of the following format: - name: {String} name of the column - desc: {String} Humanized description (what would show in a table column name) - */ - columnsForType: function(type) { - return Ember.A(); - }, + if (this.factoryCache.has(normalizedName)) { + throw new Error("Attempted to register a factoryInjection for a type that has already been looked up. ('" + normalizedName + "', '" + property + "', '" + injectionName + "')"); + } + addInjection(this.factoryInjections, normalizedName, property, normalizedInjectionName); + }, - /** - @private + /** + A depth first traversal, destroying the container, its descendant containers and all + their managed objects. - Adds observers to a model type class. + @method destroy + */ + destroy: function() { + for (var i=0, l=this.children.length; i 0) { + router['initialURL'] = url; + Ember.run(app, 'advanceReadiness'); + delete router['initialURL']; + } else { + Ember.run(app, app.handleURL, url); + } + return wait(app); } @@ -36672,17 +43889,33 @@ function click(app, selector, context) { return wait(app); } +function triggerEvent(app, selector, context, type, options){ + if (arguments.length === 3) { + type = context; + context = null; + } + + if (typeof options === 'undefined') { + options = {}; + } + + var $el = findWithAssert(app, selector, context); + + var event = Ember.$.Event(type, options); + + Ember.run($el, 'trigger', event); + + return wait(app); +} + function keyEvent(app, selector, context, type, keyCode) { - var $el; if (typeof keyCode === 'undefined') { keyCode = type; type = context; context = null; } - $el = findWithAssert(app, selector, context); - var event = Ember.$.Event(type, { keyCode: keyCode }); - Ember.run($el, 'trigger', event); - return wait(app); + + return triggerEvent(app, selector, context, type, { keyCode: keyCode, which: keyCode }); } function fillIn(app, selector, context, text) { @@ -36714,89 +43947,48 @@ function find(app, selector, context) { return $el; } -function wait(app, value) { - var promise; +function andThen(app, callback) { + return wait(app, callback(app)); +} - promise = Test.promise(function(resolve) { +function wait(app, value) { + return Test.promise(function(resolve) { + // If this is the first async promise, kick off the async test if (++countAsync === 1) { Test.adapter.asyncStart(); } + + // Every 10ms, poll for the async thing to have finished var watcher = setInterval(function() { - var routerIsLoading = app.__container__.lookup('router:main').router.isLoading; + // 1. If the router is loading, keep polling + var routerIsLoading = !!app.__container__.lookup('router:main').router.activeTransition; if (routerIsLoading) { return; } + + // 2. If there are pending Ajax requests, keep polling if (Test.pendingAjaxRequests) { return; } - if (Ember.run.hasScheduledTimers() || Ember.run.currentRunLoop) { return; } + // 3. If there are scheduled timers or we are inside of a run loop, keep polling + if (Ember.run.hasScheduledTimers() || Ember.run.currentRunLoop) { return; } + if (Test.waiters && Test.waiters.any(function(waiter) { + var context = waiter[0]; + var callback = waiter[1]; + return !callback.call(context); + })) { return; } + // Stop polling clearInterval(watcher); + // If this is the last async promise, end the async test if (--countAsync === 0) { Test.adapter.asyncEnd(); } + // Synchronously resolve the promise Ember.run(null, resolve, value); }, 10); }); - return buildChainObject(app, promise); -} - -/* - Builds an object that contains all helper methods. This object will be - returned by helpers and then-promises. - - This allows us to chain helpers: - - ```javascript - visit('posts/new') - .click('.add-btn') - .fillIn('.title', 'Post') - .click('.submit') - .then(function() { - equal('.post-title', 'Post'); - }) - .visit('comments') - .then(function() { - equal(find('.comments'),length, 0); - }); - ``` - - @method buildChainObject - @param {Ember.Application} app - @param {Ember.RSVP.Promise} promise - @return {Object} A new object with properties for each - of app's helpers to be used for continued - method chaining (using promises). -*/ -function buildChainObject(app, promise) { - var helperName, obj = {}; - for(helperName in app.testHelpers) { - obj[helperName] = chain(app, promise, app.testHelpers[helperName]); - } - obj.then = function(fn) { - var thenPromise = promise.then(fn); - return buildChainObject(app, thenPromise); - }; - return obj; } -/* - Used in conjunction with buildChainObject to setup a - continued chain of method calls (with promises) - - @method chain - @param {Ember.Application} app - @param {Ember.RSVP.Promise} promise - @param {Function} fn -*/ -function chain(app, promise, fn) { - return function() { - var args = arguments, chainedPromise; - chainedPromise = promise.then(function() { - return fn.apply(null, args); - }); - return buildChainObject(app, chainedPromise); - }; -} /** * Loads a route, sets up any controllers, and renders any templates associated @@ -36805,7 +43997,7 @@ function chain(app, promise, fn) { * * Example: * -* ``` +* ```javascript * visit('posts/index').then(function() { * // assert something * }); @@ -36815,7 +44007,7 @@ function chain(app, promise, fn) { * @param {String} url the name of the route * @return {RSVP.Promise} */ -helper('visit', visit); +asyncHelper('visit', visit); /** * Clicks an element and triggers any actions triggered by the element's `click` @@ -36823,9 +44015,9 @@ helper('visit', visit); * * Example: * -* ``` +* ```javascript * click('.some-jQuery-selector').then(function() { -* // assert something +* // assert something * }); * ``` * @@ -36833,14 +44025,14 @@ helper('visit', visit); * @param {String} selector jQuery selector for finding element on the DOM * @return {RSVP.Promise} */ -helper('click', click); +asyncHelper('click', click); /** * Simulates a key event, e.g. `keypress`, `keydown`, `keyup` with the desired keyCode * * Example: * -* ``` +* ```javascript * keyEvent('.some-jQuery-selector', 'keypress', 13).then(function() { * // assert something * }); @@ -36848,18 +44040,18 @@ helper('click', click); * * @method keyEvent * @param {String} selector jQuery selector for finding element on the DOM -* @param {String} the type of key event, e.g. `keypress`, `keydown`, `keyup` -* @param {Number} the keyCode of the simulated key event +* @param {String} type the type of key event, e.g. `keypress`, `keydown`, `keyup` +* @param {Number} keyCode the keyCode of the simulated key event * @return {RSVP.Promise} */ -helper('keyEvent', keyEvent); +asyncHelper('keyEvent', keyEvent); /** * Fills in an input element with some text. * * Example: * -* ``` +* ```javascript * fillIn('#email', 'you@example.com').then(function() { * // assert something * }); @@ -36871,7 +44063,7 @@ helper('keyEvent', keyEvent); * @param {String} text text to place inside the input element * @return {RSVP.Promise} */ -helper('fillIn', fillIn); +asyncHelper('fillIn', fillIn); /** * Finds an element in the context of the app's container element. A simple alias @@ -36879,8 +44071,8 @@ helper('fillIn', fillIn); * * Example: * -* ``` -* var $el = find('.my-selector); +* ```javascript +* var $el = find('.my-selector'); * ``` * * @method find @@ -36890,12 +44082,11 @@ helper('fillIn', fillIn); helper('find', find); /** -* -* Like `find`, but throws an error if the element selector returns no results +* Like `find`, but throws an error if the element selector returns no results. * * Example: * -* ``` +* ```javascript * var $el = findWithAssert('.doesnt-exist'); // throws error * ``` * @@ -36916,22 +44107,104 @@ helper('findWithAssert', findWithAssert); Example: - ``` - Ember.Test.registerHelper('loginUser', function(app, username, password) { + ```javascript + Ember.Test.registerAsyncHelper('loginUser', function(app, username, password) { visit('secured/path/here') .fillIn('#username', username) .fillIn('#password', username) .click('.submit') - return wait(app); + return wait(); }); @method wait @param {Object} value The value to be returned. @return {RSVP.Promise} - ``` */ -helper('wait', wait); +asyncHelper('wait', wait); +asyncHelper('andThen', andThen); + + + + /** + Returns the currently active route name. + + Example: + + ```javascript + function validateRouteName(){ + equal(currentRouteName(), 'some.path', "correct route was transitioned into."); + } + + visit('/some/path').then(validateRouteName) + ``` + + @method currentRouteName + @return {Object} The name of the currently active route. + */ + helper('currentRouteName', currentRouteName); + + /** + Returns the current path. + + Example: + + ```javascript + function validateURL(){ + equal(currentPath(), 'some.path.index', "correct path was transitioned into."); + } + + click('#some-link-id').then(validateURL); + ``` + + @method currentPath + @return {Object} The currently active path. + */ + helper('currentPath', currentPath); + + /** + Returns the current URL. + + Example: + + ```javascript + function validateURL(){ + equal(currentURL(), '/some/path', "correct URL was transitioned into."); + } + + click('#some-link-id').then(validateURL); + ``` + + @method currentURL + @return {Object} The currently active URL. + */ + helper('currentURL', currentURL); + + + + /** + Triggers the given event on the element identified by the provided selector. + + Example: + + ```javascript + triggerEvent('#some-elem-id', 'blur'); + ``` + + This is actually used internally by the `keyEvent` helper like so: + + ```javascript + triggerEvent('#some-elem-id', 'keypress', { keyCode: 13 }); + ``` + + @method triggerEvent + @param {String} selector jQuery selector for finding element on the DOM + @param {String} type The event type to be triggered. + @param {String} options The options to be passed to jQuery.Event. + @return {RSVP.Promise} + */ + asyncHelper('triggerEvent', triggerEvent); + })(); diff --git a/ember_app/js/libs/ember-data.js b/ember_app/js/libs/ember-data.js new file mode 100644 index 0000000..ffb8f45 --- /dev/null +++ b/ember_app/js/libs/ember-data.js @@ -0,0 +1,12053 @@ +/*! + * @overview Ember Data + * @copyright Copyright 2011-2014 Tilde Inc. and contributors. + * Portions Copyright 2011 LivingSocial Inc. + * @license Licensed under MIT license (see license.js) + * @version 1.0.0-beta.8.2a68c63a + */ +(function(global) { +var define, requireModule, require, requirejs; + +(function() { + var registry = {}, seen = {}; + + define = function(name, deps, callback) { + registry[name] = { deps: deps, callback: callback }; + }; + + requirejs = require = requireModule = function(name) { + requirejs._eak_seen = registry; + + if (seen[name]) { return seen[name]; } + seen[name] = {}; + + if (!registry[name]) { + throw new Error("Could not find module " + name); + } + + var mod = registry[name], + deps = mod.deps, + callback = mod.callback, + reified = [], + exports; + + for (var i=0, l=deps.length; i "famous_people" + ``` + + @method pathForType + @param {String} type + @return String + */ + pathForType: function(type) { + var decamelized = decamelize(type); + var underscored = underscore(decamelized); + return pluralize(underscored); + }, + + /** + The ActiveModelAdapter overrides the `ajaxError` method + to return a DS.InvalidError for all 422 Unprocessable Entity + responses. + + A 422 HTTP response from the server generally implies that the request + was well formed but the API was unable to process it because the + content was not semantically correct or meaningful per the API. + + For more information on 422 HTTP Error code see 11.2 WebDAV RFC 4918 + https://tools.ietf.org/html/rfc4918#section-11.2 + + @method ajaxError + @param jqXHR + @return error + */ + ajaxError: function(jqXHR) { + var error = this._super(jqXHR); + + if (jqXHR && jqXHR.status === 422) { + var response = Ember.$.parseJSON(jqXHR.responseText), + errors = {}; + + if (response.errors !== undefined) { + var jsonErrors = response.errors; + + forEach(Ember.keys(jsonErrors), function(key) { + errors[Ember.String.camelize(key)] = jsonErrors[key]; + }); + } + + return new InvalidError(errors); + } else { + return error; + } + } + }); + + __exports__["default"] = ActiveModelAdapter; + }); +define("activemodel-adapter/lib/system/active_model_serializer", + ["../../../ember-inflector/lib/main","../../../ember-data/lib/serializers/rest_serializer","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var singularize = __dependency1__.singularize; + var RESTSerializer = __dependency2__["default"]; + /** + @module ember-data + */ + + var get = Ember.get, + forEach = Ember.EnumerableUtils.forEach, + camelize = Ember.String.camelize, + capitalize = Ember.String.capitalize, + decamelize = Ember.String.decamelize, + underscore = Ember.String.underscore; + /** + The ActiveModelSerializer is a subclass of the RESTSerializer designed to integrate + with a JSON API that uses an underscored naming convention instead of camelCasing. + It has been designed to work out of the box with the + [active_model_serializers](http://github.com/rails-api/active_model_serializers) + Ruby gem. This Serializer expects specific settings using ActiveModel::Serializers, + `embed :ids, include: true` which sideloads the records. + + This serializer extends the DS.RESTSerializer by making consistent + use of the camelization, decamelization and pluralization methods to + normalize the serialized JSON into a format that is compatible with + a conventional Rails backend and Ember Data. + + ## JSON Structure + + The ActiveModelSerializer expects the JSON returned from your server + to follow the REST adapter conventions substituting underscored keys + for camelcased ones. + + ### Conventional Names + + Attribute names in your JSON payload should be the underscored versions of + the attributes in your Ember.js models. + + For example, if you have a `Person` model: + + ```js + App.FamousPerson = DS.Model.extend({ + firstName: DS.attr('string'), + lastName: DS.attr('string'), + occupation: DS.attr('string') + }); + ``` + + The JSON returned should look like this: + + ```js + { + "famous_person": { + "first_name": "Barack", + "last_name": "Obama", + "occupation": "President" + } + } + ``` + + @class ActiveModelSerializer + @namespace DS + @extends DS.RESTSerializer + */ + var ActiveModelSerializer = RESTSerializer.extend({ + // SERIALIZE + + /** + Converts camelCased attributes to underscored when serializing. + + @method keyForAttribute + @param {String} attribute + @return String + */ + keyForAttribute: function(attr) { + return decamelize(attr); + }, + + /** + Underscores relationship names and appends "_id" or "_ids" when serializing + relationship keys. + + @method keyForRelationship + @param {String} key + @param {String} kind + @return String + */ + keyForRelationship: function(key, kind) { + key = decamelize(key); + if (kind === "belongsTo") { + return key + "_id"; + } else if (kind === "hasMany") { + return singularize(key) + "_ids"; + } else { + return key; + } + }, + + /* + Does not serialize hasMany relationships by default. + */ + serializeHasMany: Ember.K, + + /** + Underscores the JSON root keys when serializing. + + @method serializeIntoHash + @param {Object} hash + @param {subclass of DS.Model} type + @param {DS.Model} record + @param {Object} options + */ + serializeIntoHash: function(data, type, record, options) { + var root = underscore(decamelize(type.typeKey)); + data[root] = this.serialize(record, options); + }, + + /** + Serializes a polymorphic type as a fully capitalized model name. + + @method serializePolymorphicType + @param {DS.Model} record + @param {Object} json + @param relationship + */ + serializePolymorphicType: function(record, json, relationship) { + var key = relationship.key, + belongsTo = get(record, key); + + if (belongsTo) { + key = this.keyForAttribute(key); + json[key + "_type"] = capitalize(belongsTo.constructor.typeKey); + } + }, + + // EXTRACT + + /** + Add extra step to `DS.RESTSerializer.normalize` so links are normalized. + + If your payload looks like: + + ```js + { + "post": { + "id": 1, + "title": "Rails is omakase", + "links": { "flagged_comments": "api/comments/flagged" } + } + } + ``` + + The normalized version would look like this + + ```js + { + "post": { + "id": 1, + "title": "Rails is omakase", + "links": { "flaggedComments": "api/comments/flagged" } + } + } + ``` + + @method normalize + @param {subclass of DS.Model} type + @param {Object} hash + @param {String} prop + @return Object + */ + + normalize: function(type, hash, prop) { + this.normalizeLinks(hash); + + return this._super(type, hash, prop); + }, + + /** + Convert `snake_cased` links to `camelCase` + + @method normalizeLinks + @param {Object} data + */ + + normalizeLinks: function(data){ + if (data.links) { + var links = data.links; + + for (var link in links) { + var camelizedLink = camelize(link); + + if (camelizedLink !== link) { + links[camelizedLink] = links[link]; + delete links[link]; + } + } + } + }, + + /** + Normalize the polymorphic type from the JSON. + + Normalize: + ```js + { + id: "1" + minion: { type: "evil_minion", id: "12"} + } + ``` + + To: + ```js + { + id: "1" + minion: { type: "evilMinion", id: "12"} + } + ``` + + @method normalizeRelationships + @private + */ + normalizeRelationships: function(type, hash) { + var payloadKey, payload; + + if (this.keyForRelationship) { + type.eachRelationship(function(key, relationship) { + if (relationship.options.polymorphic) { + payloadKey = this.keyForAttribute(key); + payload = hash[payloadKey]; + if (payload && payload.type) { + payload.type = this.typeForRoot(payload.type); + } else if (payload && relationship.kind === "hasMany") { + var self = this; + forEach(payload, function(single) { + single.type = self.typeForRoot(single.type); + }); + } + } else { + payloadKey = this.keyForRelationship(key, relationship.kind); + payload = hash[payloadKey]; + } + + hash[key] = payload; + + if (key !== payloadKey) { + delete hash[payloadKey]; + } + }, this); + } + } + }); + + __exports__["default"] = ActiveModelSerializer; + }); +define("activemodel-adapter/lib/system/embedded_records_mixin", + ["../../../ember-inflector/lib/main","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var get = Ember.get; + var forEach = Ember.EnumerableUtils.forEach; + var camelize = Ember.String.camelize; + + var pluralize = __dependency1__.pluralize; + + /** + ## Using Embedded Records + + `DS.EmbeddedRecordsMixin` supports serializing embedded records. + + To set up embedded records, include the mixin when extending a serializer + then define and configure embedded (model) relationships. + + Below is an example of a per-type serializer ('post' type). + + ```js + App.PostSerializer = DS.ActiveModelSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + author: {embedded: 'always'}, + comments: {serialize: 'ids'} + } + }) + ``` + + The `attrs` option for a resource `{embedded: 'always'}` is shorthand for: + + ```js + {serialize: 'records', deserialize: 'records'} + ``` + + ### Configuring Attrs + + A resource's `attrs` option may be set to use `ids`, `records` or `no` for the + `serialize` and `deserialize` settings. + + The `attrs` property can be set on the ApplicationSerializer or a per-type + serializer. + + In the case where embedded JSON is expected while extracting a payoad (reading) + the setting is `deserialize: 'records'`, there is no need to use `ids` when + extracting as that is the default behavior without this mixin if you are using + the vanilla ActiveModelAdapter. Likewise, to embed JSON in the payload while + serializing `serialize: 'records'` is the setting to use. There is an option of + not embedding JSON in the serialized payload by using `serialize: 'ids'`. If you + do not want the relationship sent at all, you can use `serialize: 'no'`. + + + ### ActiveModelSerializer defaults + If you do not overwrite `attrs` for a specific relationship, the `ActiveModelSerializer` + will behave in the following way: + + BelongsTo: `{serialize:'id', deserialize:'id'}` + HasMany: `{serialize:no, deserialize:'ids'}` + + ### Model Relationships + + Embedded records must have a model defined to be extracted and serialized. + + To successfully extract and serialize embedded records the model relationships + must be setup correcty See the + [defining relationships](/guides/models/defining-models/#toc_defining-relationships) + section of the **Defining Models** guide page. + + Records without an `id` property are not considered embedded records, model + instances must have an `id` property to be used with Ember Data. + + ### Example JSON payloads, Models and Serializers + + **When customizing a serializer it is imporant to grok what the cusomizations + are, please read the docs for the methods this mixin provides, in case you need + to modify to fit your specific needs.** + + For example review the docs for each method of this mixin: + + * [extractArray](/api/data/classes/DS.EmbeddedRecordsMixin.html#method_extractArray) + * [extractSingle](/api/data/classes/DS.EmbeddedRecordsMixin.html#method_extractSingle) + * [serializeBelongsTo](/api/data/classes/DS.EmbeddedRecordsMixin.html#method_serializeBelongsTo) + * [serializeHasMany](/api/data/classes/DS.EmbeddedRecordsMixin.html#method_serializeHasMany) + + @class EmbeddedRecordsMixin + @namespace DS + */ + var EmbeddedRecordsMixin = Ember.Mixin.create({ + + /** + Serialize `belongsTo` relationship when it is configured as an embedded object. + + This example of an author model belongs to a post model: + + ```js + Post = DS.Model.extend({ + title: DS.attr('string'), + body: DS.attr('string'), + author: DS.belongsTo('author') + }); + + Author = DS.Model.extend({ + name: DS.attr('string'), + post: DS.belongsTo('post') + }); + ``` + + Use a custom (type) serializer for the post model to configure embedded author + + ```js + App.PostSerializer = DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + author: {embedded: 'always'} + } + }) + ``` + + A payload with an attribute configured for embedded records can serialize + the records together under the root attribute's payload: + + ```js + { + "post": { + "id": "1" + "title": "Rails is omakase", + "author": { + "id": "2" + "name": "dhh" + } + } + } + ``` + + @method serializeBelongsTo + @param {DS.Model} record + @param {Object} json + @param {Object} relationship + */ + serializeBelongsTo: function(record, json, relationship) { + var attr = relationship.key; + var attrs = this.get('attrs'); + if (noSerializeOptionSpecified(attrs, attr)) { + this._super(record, json, relationship); + return; + } + var includeIds = hasSerializeIdsOption(attrs, attr); + var includeRecords = hasSerializeRecordsOption(attrs, attr); + var embeddedRecord = record.get(attr); + if (includeIds) { + key = this.keyForRelationship(attr, relationship.kind); + if (!embeddedRecord) { + json[key] = null; + } else { + json[key] = get(embeddedRecord, 'id'); + } + } else if (includeRecords) { + var key = this.keyForRelationship(attr); + if (!embeddedRecord) { + json[key] = null; + } else { + json[key] = embeddedRecord.serialize({includeId: true}); + this.removeEmbeddedForeignKey(record, embeddedRecord, relationship, json[key]); + } + } + }, + + /** + Serialize `hasMany` relationship when it is configured as embedded objects. + + This example of a post model has many comments: + + ```js + Post = DS.Model.extend({ + title: DS.attr('string'), + body: DS.attr('string'), + comments: DS.hasMany('comment') + }); + + Comment = DS.Model.extend({ + body: DS.attr('string'), + post: DS.belongsTo('post') + }); + ``` + + Use a custom (type) serializer for the post model to configure embedded comments + + ```js + App.PostSerializer = DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + comments: {embedded: 'always'} + } + }) + ``` + + A payload with an attribute configured for embedded records can serialize + the records together under the root attribute's payload: + + ```js + { + "post": { + "id": "1" + "title": "Rails is omakase", + "body": "I want this for my ORM, I want that for my template language..." + "comments": [{ + "id": "1", + "body": "Rails is unagi" + }, { + "id": "2", + "body": "Omakase O_o" + }] + } + } + ``` + + The attrs options object can use more specific instruction for extracting and + serializing. When serializing, an option to embed `ids` or `records` can be set. + When extracting the only option is `records`. + + So `{embedded: 'always'}` is shorthand for: + `{serialize: 'records', deserialize: 'records'}` + + To embed the `ids` for a related object (using a hasMany relationship): + + ```js + App.PostSerializer = DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + comments: {serialize: 'ids', deserialize: 'records'} + } + }) + ``` + + ```js + { + "post": { + "id": "1" + "title": "Rails is omakase", + "body": "I want this for my ORM, I want that for my template language..." + "comments": ["1", "2"] + } + } + ``` + + @method serializeHasMany + @param {DS.Model} record + @param {Object} json + @param {Object} relationship + */ + serializeHasMany: function(record, json, relationship) { + var attr = relationship.key; + var attrs = this.get('attrs'); + if (noSerializeOptionSpecified(attrs, attr)) { + this._super(record, json, relationship); + return; + } + var includeIds = hasSerializeIdsOption(attrs, attr); + var includeRecords = hasSerializeRecordsOption(attrs, attr); + var key; + if (includeIds) { + key = this.keyForRelationship(attr, relationship.kind); + json[key] = get(record, attr).mapBy('id'); + } else if (includeRecords) { + key = this.keyForAttribute(attr); + json[key] = get(record, attr).map(function(embeddedRecord) { + var serializedEmbeddedRecord = embeddedRecord.serialize({includeId: true}); + this.removeEmbeddedForeignKey(record, embeddedRecord, relationship, serializedEmbeddedRecord); + return serializedEmbeddedRecord; + }, this); + } + }, + + /** + When serializing an embedded record, modify the property (in the json payload) + that refers to the parent record (foreign key for relationship). + + Serializing a `belongsTo` relationship removes the property that refers to the + parent record + + Serializing a `hasMany` relationship does not remove the property that refers to + the parent record. + + @method removeEmbeddedForeignKey + @param {DS.Model} record + @param {DS.Model} embeddedRecord + @param {Object} relationship + @param {Object} json + */ + removeEmbeddedForeignKey: function (record, embeddedRecord, relationship, json) { + if (relationship.kind === 'hasMany') { + return; + } else if (relationship.kind === 'belongsTo') { + var parentRecord = record.constructor.inverseFor(relationship.key); + if (parentRecord) { + var name = parentRecord.name; + var embeddedSerializer = this.store.serializerFor(embeddedRecord.constructor); + var parentKey = embeddedSerializer.keyForRelationship(name, parentRecord.kind); + if (parentKey) { + delete json[parentKey]; + } + } + } + }, + + /** + Extract an embedded object from the payload for a single object + and add the object in the compound document (side-loaded) format instead. + + A payload with an attribute configured for embedded records needs to be extracted: + + ```js + { + "post": { + "id": 1 + "title": "Rails is omakase", + "author": { + "id": 2 + "name": "dhh" + } + "comments": [] + } + } + ``` + + Ember Data is expecting a payload with a compound document (side-loaded) like: + + ```js + { + "post": { + "id": "1" + "title": "Rails is omakase", + "author": "2" + "comments": [] + }, + "authors": [{ + "id": "2" + "post": "1" + "name": "dhh" + }] + "comments": [] + } + ``` + + The payload's `author` attribute represents an object with a `belongsTo` relationship. + The `post` attribute under `author` is the foreign key with the id for the post + + @method extractSingle + @param {DS.Store} store + @param {subclass of DS.Model} primaryType + @param {Object} payload + @param {String} recordId + @return Object the primary response to the original request + */ + extractSingle: function(store, primaryType, payload, recordId) { + var root = this.keyForAttribute(primaryType.typeKey), + partial = payload[root]; + + updatePayloadWithEmbedded(this, store, primaryType, payload, partial); + + return this._super(store, primaryType, payload, recordId); + }, + + /** + Extract embedded objects in an array when an attr is configured for embedded, + and add them as side-loaded objects instead. + + A payload with an attr configured for embedded records needs to be extracted: + + ```js + { + "post": { + "id": "1" + "title": "Rails is omakase", + "comments": [{ + "id": "1", + "body": "Rails is unagi" + }, { + "id": "2", + "body": "Omakase O_o" + }] + } + } + ``` + + Ember Data is expecting a payload with compound document (side-loaded) like: + + ```js + { + "post": { + "id": "1" + "title": "Rails is omakase", + "comments": ["1", "2"] + }, + "comments": [{ + "id": "1", + "body": "Rails is unagi" + }, { + "id": "2", + "body": "Omakase O_o" + }] + } + ``` + + The payload's `comments` attribute represents records in a `hasMany` relationship + + @method extractArray + @param {DS.Store} store + @param {subclass of DS.Model} primaryType + @param {Object} payload + @return {Array} The primary array that was returned in response + to the original query. + */ + extractArray: function(store, primaryType, payload) { + var root = this.keyForAttribute(primaryType.typeKey), + partials = payload[pluralize(root)]; + + forEach(partials, function(partial) { + updatePayloadWithEmbedded(this, store, primaryType, payload, partial); + }, this); + + return this._super(store, primaryType, payload); + } + }); + + // checks config for attrs option to embedded (always) - serialize and deserialize + function hasEmbeddedAlwaysOption(attrs, attr) { + var option = attrsOption(attrs, attr); + return option && option.embedded === 'always'; + } + + // checks config for attrs option to serialize ids + function hasSerializeRecordsOption(attrs, attr) { + var alwaysEmbed = hasEmbeddedAlwaysOption(attrs, attr); + var option = attrsOption(attrs, attr); + return alwaysEmbed || (option && (option.serialize === 'records')); + } + + // checks config for attrs option to serialize records + function hasSerializeIdsOption(attrs, attr) { + var option = attrsOption(attrs, attr); + return option && (option.serialize === 'ids' || option.serialize === 'id'); + } + + // checks config for attrs option to serialize records + function noSerializeOptionSpecified(attrs, attr) { + var option = attrsOption(attrs, attr); + var serializeRecords = hasSerializeRecordsOption(attrs, attr); + var serializeIds = hasSerializeIdsOption(attrs, attr); + return !(option && (option.serialize || option.embedded)); + } + + // checks config for attrs option to deserialize records + // a defined option object for a resource is treated the same as + // `deserialize: 'records'` + function hasDeserializeRecordsOption(attrs, attr) { + var alwaysEmbed = hasEmbeddedAlwaysOption(attrs, attr); + var option = attrsOption(attrs, attr); + var hasSerializingOption = option && (option.deserialize || option.serialize); + return alwaysEmbed || hasSerializingOption /* option.deserialize === 'records' */; + } + + function attrsOption(attrs, attr) { + return attrs && (attrs[Ember.String.camelize(attr)] || attrs[attr]); + } + + // chooses a relationship kind to branch which function is used to update payload + // does not change payload if attr is not embedded + function updatePayloadWithEmbedded(serializer, store, type, payload, partial) { + var attrs = get(serializer, 'attrs'); + + if (!attrs) { + return; + } + type.eachRelationship(function(key, relationship) { + if (hasDeserializeRecordsOption(attrs, key)) { + if (relationship.kind === "hasMany") { + updatePayloadWithEmbeddedHasMany(serializer, store, key, relationship, payload, partial); + } + if (relationship.kind === "belongsTo") { + updatePayloadWithEmbeddedBelongsTo(serializer, store, key, relationship, payload, partial); + } + } + }); + } + + // handles embedding for `hasMany` relationship + function updatePayloadWithEmbeddedHasMany(serializer, store, primaryType, relationship, payload, partial) { + var embeddedSerializer = store.serializerFor(relationship.type.typeKey); + var primaryKey = get(serializer, 'primaryKey'); + var attr = relationship.type.typeKey; + // underscore forces the embedded records to be side loaded. + // it is needed when main type === relationship.type + var embeddedTypeKey = '_' + serializer.typeForRoot(relationship.type.typeKey); + var expandedKey = serializer.keyForRelationship(primaryType, relationship.kind); + var attribute = serializer.keyForAttribute(primaryType); + var ids = []; + + if (!partial[attribute]) { + return; + } + + payload[embeddedTypeKey] = payload[embeddedTypeKey] || []; + + forEach(partial[attribute], function(data) { + var embeddedType = store.modelFor(attr); + updatePayloadWithEmbedded(embeddedSerializer, store, embeddedType, payload, data); + ids.push(data[primaryKey]); + payload[embeddedTypeKey].push(data); + }); + + partial[expandedKey] = ids; + delete partial[attribute]; + } + + // handles embedding for `belongsTo` relationship + function updatePayloadWithEmbeddedBelongsTo(serializer, store, primaryType, relationship, payload, partial) { + var attrs = serializer.get('attrs'); + + if (!attrs || + !(hasDeserializeRecordsOption(attrs, Ember.String.camelize(primaryType)) || + hasDeserializeRecordsOption(attrs, primaryType))) { + return; + } + var attr = relationship.type.typeKey; + var _serializer = store.serializerFor(relationship.type.typeKey); + var primaryKey = get(_serializer, 'primaryKey'); + var embeddedTypeKey = Ember.String.pluralize(attr); // TODO don't use pluralize + var expandedKey = _serializer.keyForRelationship(primaryType, relationship.kind); + var attribute = _serializer.keyForAttribute(primaryType); + + if (!partial[attribute]) { + return; + } + payload[embeddedTypeKey] = payload[embeddedTypeKey] || []; + var embeddedType = store.modelFor(relationship.type.typeKey); + // Recursive call for nested record + updatePayloadWithEmbedded(_serializer, store, embeddedType, payload, partial[attribute]); + partial[expandedKey] = partial[attribute].id; + // Need to move an embedded `belongsTo` object into a pluralized collection + payload[embeddedTypeKey].push(partial[attribute]); + // Need a reference to the parent so relationship works between both `belongsTo` records + partial[attribute][relationship.parentType.typeKey + '_id'] = partial.id; + delete partial[attribute]; + } + + __exports__["default"] = EmbeddedRecordsMixin; + }); +define("ember-data/lib/adapters", + ["./adapters/fixture_adapter","./adapters/rest_adapter","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + /** + @module ember-data + */ + + var FixtureAdapter = __dependency1__["default"]; + var RESTAdapter = __dependency2__["default"]; + + __exports__.RESTAdapter = RESTAdapter; + __exports__.FixtureAdapter = FixtureAdapter; + }); +define("ember-data/lib/adapters/fixture_adapter", + ["../system/adapter","exports"], + function(__dependency1__, __exports__) { + "use strict"; + /** + @module ember-data + */ + + var get = Ember.get, fmt = Ember.String.fmt, + indexOf = Ember.EnumerableUtils.indexOf; + + var counter = 0; + + var Adapter = __dependency1__["default"]; + + /** + `DS.FixtureAdapter` is an adapter that loads records from memory. + It's primarily used for development and testing. You can also use + `DS.FixtureAdapter` while working on the API but are not ready to + integrate yet. It is a fully functioning adapter. All CRUD methods + are implemented. You can also implement query logic that a remote + system would do. It's possible to develop your entire application + with `DS.FixtureAdapter`. + + For information on how to use the `FixtureAdapter` in your + application please see the [FixtureAdapter + guide](/guides/models/the-fixture-adapter/). + + @class FixtureAdapter + @namespace DS + @extends DS.Adapter + */ + var FixtureAdapter = Adapter.extend({ + // by default, fixtures are already in normalized form + serializer: null, + + /** + If `simulateRemoteResponse` is `true` the `FixtureAdapter` will + wait a number of milliseconds before resolving promises with the + fixture values. The wait time can be configured via the `latency` + property. + + @property simulateRemoteResponse + @type {Boolean} + @default true + */ + simulateRemoteResponse: true, + + /** + By default the `FixtureAdapter` will simulate a wait of the + `latency` milliseconds before resolving promises with the fixture + values. This behavior can be turned off via the + `simulateRemoteResponse` property. + + @property latency + @type {Number} + @default 50 + */ + latency: 50, + + /** + Implement this method in order to provide data associated with a type + + @method fixturesForType + @param {Subclass of DS.Model} type + @return {Array} + */ + fixturesForType: function(type) { + if (type.FIXTURES) { + var fixtures = Ember.A(type.FIXTURES); + return fixtures.map(function(fixture){ + var fixtureIdType = typeof fixture.id; + if(fixtureIdType !== "number" && fixtureIdType !== "string"){ + throw new Error(fmt('the id property must be defined as a number or string for fixture %@', [fixture])); + } + fixture.id = fixture.id + ''; + return fixture; + }); + } + return null; + }, + + /** + Implement this method in order to query fixtures data + + @method queryFixtures + @param {Array} fixture + @param {Object} query + @param {Subclass of DS.Model} type + @return {Promise|Array} + */ + queryFixtures: function(fixtures, query, type) { + Ember.assert('Not implemented: You must override the DS.FixtureAdapter::queryFixtures method to support querying the fixture store.'); + }, + + /** + @method updateFixtures + @param {Subclass of DS.Model} type + @param {Array} fixture + */ + updateFixtures: function(type, fixture) { + if(!type.FIXTURES) { + type.FIXTURES = []; + } + + var fixtures = type.FIXTURES; + + this.deleteLoadedFixture(type, fixture); + + fixtures.push(fixture); + }, + + /** + Implement this method in order to provide json for CRUD methods + + @method mockJSON + @param {Subclass of DS.Model} type + @param {DS.Model} record + */ + mockJSON: function(store, type, record) { + return store.serializerFor(type).serialize(record, { includeId: true }); + }, + + /** + @method generateIdForRecord + @param {DS.Store} store + @param {DS.Model} record + @return {String} id + */ + generateIdForRecord: function(store) { + return "fixture-" + counter++; + }, + + /** + @method find + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {String} id + @return {Promise} promise + */ + find: function(store, type, id) { + var fixtures = this.fixturesForType(type), + fixture; + + Ember.assert("Unable to find fixtures for model type "+type.toString(), fixtures); + + if (fixtures) { + fixture = Ember.A(fixtures).findProperty('id', id); + } + + if (fixture) { + return this.simulateRemoteCall(function() { + return fixture; + }, this); + } + }, + + /** + @method findMany + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Array} ids + @return {Promise} promise + */ + findMany: function(store, type, ids) { + var fixtures = this.fixturesForType(type); + + Ember.assert("Unable to find fixtures for model type "+type.toString(), fixtures); + + if (fixtures) { + fixtures = fixtures.filter(function(item) { + return indexOf(ids, item.id) !== -1; + }); + } + + if (fixtures) { + return this.simulateRemoteCall(function() { + return fixtures; + }, this); + } + }, + + /** + @private + @method findAll + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {String} sinceToken + @return {Promise} promise + */ + findAll: function(store, type) { + var fixtures = this.fixturesForType(type); + + Ember.assert("Unable to find fixtures for model type "+type.toString(), fixtures); + + return this.simulateRemoteCall(function() { + return fixtures; + }, this); + }, + + /** + @private + @method findQuery + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} query + @param {DS.AdapterPopulatedRecordArray} recordArray + @return {Promise} promise + */ + findQuery: function(store, type, query, array) { + var fixtures = this.fixturesForType(type); + + Ember.assert("Unable to find fixtures for model type " + type.toString(), fixtures); + + fixtures = this.queryFixtures(fixtures, query, type); + + if (fixtures) { + return this.simulateRemoteCall(function() { + return fixtures; + }, this); + } + }, + + /** + @method createRecord + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {DS.Model} record + @return {Promise} promise + */ + createRecord: function(store, type, record) { + var fixture = this.mockJSON(store, type, record); + + this.updateFixtures(type, fixture); + + return this.simulateRemoteCall(function() { + return fixture; + }, this); + }, + + /** + @method updateRecord + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {DS.Model} record + @return {Promise} promise + */ + updateRecord: function(store, type, record) { + var fixture = this.mockJSON(store, type, record); + + this.updateFixtures(type, fixture); + + return this.simulateRemoteCall(function() { + return fixture; + }, this); + }, + + /** + @method deleteRecord + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {DS.Model} record + @return {Promise} promise + */ + deleteRecord: function(store, type, record) { + var fixture = this.mockJSON(store, type, record); + + this.deleteLoadedFixture(type, fixture); + + return this.simulateRemoteCall(function() { + // no payload in a deletion + return null; + }); + }, + + /* + @method deleteLoadedFixture + @private + @param type + @param record + */ + deleteLoadedFixture: function(type, record) { + var existingFixture = this.findExistingFixture(type, record); + + if(existingFixture) { + var index = indexOf(type.FIXTURES, existingFixture); + type.FIXTURES.splice(index, 1); + return true; + } + }, + + /* + @method findExistingFixture + @private + @param type + @param record + */ + findExistingFixture: function(type, record) { + var fixtures = this.fixturesForType(type); + var id = get(record, 'id'); + + return this.findFixtureById(fixtures, id); + }, + + /* + @method findFixtureById + @private + @param fixtures + @param id + */ + findFixtureById: function(fixtures, id) { + return Ember.A(fixtures).find(function(r) { + if(''+get(r, 'id') === ''+id) { + return true; + } else { + return false; + } + }); + }, + + /* + @method simulateRemoteCall + @private + @param callback + @param context + */ + simulateRemoteCall: function(callback, context) { + var adapter = this; + + return new Ember.RSVP.Promise(function(resolve) { + if (get(adapter, 'simulateRemoteResponse')) { + // Schedule with setTimeout + Ember.run.later(function() { + resolve(callback.call(context)); + }, get(adapter, 'latency')); + } else { + // Asynchronous, but at the of the runloop with zero latency + Ember.run.schedule('actions', null, function() { + resolve(callback.call(context)); + }); + } + }, "DS: FixtureAdapter#simulateRemoteCall"); + } + }); + + __exports__["default"] = FixtureAdapter; + }); +define("ember-data/lib/adapters/rest_adapter", + ["../system/adapter","exports"], + function(__dependency1__, __exports__) { + "use strict"; + /** + @module ember-data + */ + + var Adapter = __dependency1__["default"]; + var get = Ember.get, set = Ember.set; + var forEach = Ember.ArrayPolyfills.forEach; + + /** + The REST adapter allows your store to communicate with an HTTP server by + transmitting JSON via XHR. Most Ember.js apps that consume a JSON API + should use the REST adapter. + + This adapter is designed around the idea that the JSON exchanged with + the server should be conventional. + + ## JSON Structure + + The REST adapter expects the JSON returned from your server to follow + these conventions. + + ### Object Root + + The JSON payload should be an object that contains the record inside a + root property. For example, in response to a `GET` request for + `/posts/1`, the JSON should look like this: + + ```js + { + "post": { + "title": "I'm Running to Reform the W3C's Tag", + "author": "Yehuda Katz" + } + } + ``` + + ### Conventional Names + + Attribute names in your JSON payload should be the camelCased versions of + the attributes in your Ember.js models. + + For example, if you have a `Person` model: + + ```js + App.Person = DS.Model.extend({ + firstName: DS.attr('string'), + lastName: DS.attr('string'), + occupation: DS.attr('string') + }); + ``` + + The JSON returned should look like this: + + ```js + { + "person": { + "firstName": "Barack", + "lastName": "Obama", + "occupation": "President" + } + } + ``` + + ## Customization + + ### Endpoint path customization + + Endpoint paths can be prefixed with a `namespace` by setting the namespace + property on the adapter: + + ```js + DS.RESTAdapter.reopen({ + namespace: 'api/1' + }); + ``` + Requests for `App.Person` would now target `/api/1/people/1`. + + ### Host customization + + An adapter can target other hosts by setting the `host` property. + + ```js + DS.RESTAdapter.reopen({ + host: 'https://api.example.com' + }); + ``` + + ### Headers customization + + Some APIs require HTTP headers, e.g. to provide an API key. Arbitrary + headers can be set as key/value pairs on the `RESTAdapter`'s `headers` + object and Ember Data will send them along with each ajax request. + + + ```js + App.ApplicationAdapter = DS.RESTAdapter.extend({ + headers: { + "API_KEY": "secret key", + "ANOTHER_HEADER": "Some header value" + } + }); + ``` + + `headers` can also be used as a computed property to support dynamic + headers. In the example below, the `session` object has been + injected into an adapter by Ember's container. + + ```js + App.ApplicationAdapter = DS.RESTAdapter.extend({ + headers: function() { + return { + "API_KEY": this.get("session.authToken"), + "ANOTHER_HEADER": "Some header value" + }; + }.property("session.authToken") + }); + ``` + + In some cases, your dynamic headers may require data from some + object outside of Ember's observer system (for example + `document.cookie`). You can use the + [volatile](/api/classes/Ember.ComputedProperty.html#method_volatile) + function to set the property into a non-chached mode causing the headers to + be recomputed with every request. + + ```js + App.ApplicationAdapter = DS.RESTAdapter.extend({ + headers: function() { + return { + "API_KEY": Ember.get(document.cookie.match(/apiKey\=([^;]*)/), "1"), + "ANOTHER_HEADER": "Some header value" + }; + }.property().volatile(); + }); + ``` + + @class RESTAdapter + @constructor + @namespace DS + @extends DS.Adapter + */ + var RESTAdapter = Adapter.extend({ + defaultSerializer: '-rest', + /** + Endpoint paths can be prefixed with a `namespace` by setting the namespace + property on the adapter: + + ```javascript + DS.RESTAdapter.reopen({ + namespace: 'api/1' + }); + ``` + + Requests for `App.Post` would now target `/api/1/post/`. + + @property namespace + @type {String} + */ + + /** + An adapter can target other hosts by setting the `host` property. + + ```javascript + DS.RESTAdapter.reopen({ + host: 'https://api.example.com' + }); + ``` + + Requests for `App.Post` would now target `https://api.example.com/post/`. + + @property host + @type {String} + */ + + /** + Some APIs require HTTP headers, e.g. to provide an API + key. Arbitrary headers can be set as key/value pairs on the + `RESTAdapter`'s `headers` object and Ember Data will send them + along with each ajax request. For dynamic headers see [headers + customization](/api/data/classes/DS.RESTAdapter.html#toc_headers-customization). + + ```javascript + App.ApplicationAdapter = DS.RESTAdapter.extend({ + headers: { + "API_KEY": "secret key", + "ANOTHER_HEADER": "Some header value" + } + }); + ``` + + @property headers + @type {Object} + */ + + /** + Called by the store in order to fetch the JSON for a given + type and ID. + + The `find` method makes an Ajax request to a URL computed by `buildURL`, and returns a + promise for the resulting payload. + + This method performs an HTTP `GET` request with the id provided as part of the query string. + + @method find + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {String} id + @return {Promise} promise + */ + find: function(store, type, id) { + return this.ajax(this.buildURL(type.typeKey, id), 'GET'); + }, + + /** + Called by the store in order to fetch a JSON array for all + of the records for a given type. + + The `findAll` method makes an Ajax (HTTP GET) request to a URL computed by `buildURL`, and returns a + promise for the resulting payload. + + @private + @method findAll + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {String} sinceToken + @return {Promise} promise + */ + findAll: function(store, type, sinceToken) { + var query; + + if (sinceToken) { + query = { since: sinceToken }; + } + + return this.ajax(this.buildURL(type.typeKey), 'GET', { data: query }); + }, + + /** + Called by the store in order to fetch a JSON array for + the records that match a particular query. + + The `findQuery` method makes an Ajax (HTTP GET) request to a URL computed by `buildURL`, and returns a + promise for the resulting payload. + + The `query` argument is a simple JavaScript object that will be passed directly + to the server as parameters. + + @private + @method findQuery + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} query + @return {Promise} promise + */ + findQuery: function(store, type, query) { + return this.ajax(this.buildURL(type.typeKey), 'GET', { data: query }); + }, + + /** + Called by the store in order to fetch a JSON array for + the unloaded records in a has-many relationship that were originally + specified as IDs. + + For example, if the original payload looks like: + + ```js + { + "id": 1, + "title": "Rails is omakase", + "comments": [ 1, 2, 3 ] + } + ``` + + The IDs will be passed as a URL-encoded Array of IDs, in this form: + + ``` + ids[]=1&ids[]=2&ids[]=3 + ``` + + Many servers, such as Rails and PHP, will automatically convert this URL-encoded array + into an Array for you on the server-side. If you want to encode the + IDs, differently, just override this (one-line) method. + + The `findMany` method makes an Ajax (HTTP GET) request to a URL computed by `buildURL`, and returns a + promise for the resulting payload. + + @method findMany + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Array} ids + @return {Promise} promise + */ + findMany: function(store, type, ids) { + return this.ajax(this.buildURL(type.typeKey), 'GET', { data: { ids: ids } }); + }, + + /** + Called by the store in order to fetch a JSON array for + the unloaded records in a has-many relationship that were originally + specified as a URL (inside of `links`). + + For example, if your original payload looks like this: + + ```js + { + "post": { + "id": 1, + "title": "Rails is omakase", + "links": { "comments": "/posts/1/comments" } + } + } + ``` + + This method will be called with the parent record and `/posts/1/comments`. + + The `findHasMany` method will make an Ajax (HTTP GET) request to the originally specified URL. + If the URL is host-relative (starting with a single slash), the + request will use the host specified on the adapter (if any). + + @method findHasMany + @param {DS.Store} store + @param {DS.Model} record + @param {String} url + @return {Promise} promise + */ + findHasMany: function(store, record, url) { + var host = get(this, 'host'), + id = get(record, 'id'), + type = record.constructor.typeKey; + + if (host && url.charAt(0) === '/' && url.charAt(1) !== '/') { + url = host + url; + } + + return this.ajax(this.urlPrefix(url, this.buildURL(type, id)), 'GET'); + }, + + /** + Called by the store in order to fetch a JSON array for + the unloaded records in a belongs-to relationship that were originally + specified as a URL (inside of `links`). + + For example, if your original payload looks like this: + + ```js + { + "person": { + "id": 1, + "name": "Tom Dale", + "links": { "group": "/people/1/group" } + } + } + ``` + + This method will be called with the parent record and `/people/1/group`. + + The `findBelongsTo` method will make an Ajax (HTTP GET) request to the originally specified URL. + + @method findBelongsTo + @param {DS.Store} store + @param {DS.Model} record + @param {String} url + @return {Promise} promise + */ + findBelongsTo: function(store, record, url) { + var id = get(record, 'id'), + type = record.constructor.typeKey; + + return this.ajax(this.urlPrefix(url, this.buildURL(type, id)), 'GET'); + }, + + /** + Called by the store when a newly created record is + saved via the `save` method on a model record instance. + + The `createRecord` method serializes the record and makes an Ajax (HTTP POST) request + to a URL computed by `buildURL`. + + See `serialize` for information on how to customize the serialized form + of a record. + + @method createRecord + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {DS.Model} record + @return {Promise} promise + */ + createRecord: function(store, type, record) { + var data = {}; + var serializer = store.serializerFor(type.typeKey); + + serializer.serializeIntoHash(data, type, record, { includeId: true }); + + return this.ajax(this.buildURL(type.typeKey), "POST", { data: data }); + }, + + /** + Called by the store when an existing record is saved + via the `save` method on a model record instance. + + The `updateRecord` method serializes the record and makes an Ajax (HTTP PUT) request + to a URL computed by `buildURL`. + + See `serialize` for information on how to customize the serialized form + of a record. + + @method updateRecord + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {DS.Model} record + @return {Promise} promise + */ + updateRecord: function(store, type, record) { + var data = {}; + var serializer = store.serializerFor(type.typeKey); + + serializer.serializeIntoHash(data, type, record); + + var id = get(record, 'id'); + + return this.ajax(this.buildURL(type.typeKey, id), "PUT", { data: data }); + }, + + /** + Called by the store when a record is deleted. + + The `deleteRecord` method makes an Ajax (HTTP DELETE) request to a URL computed by `buildURL`. + + @method deleteRecord + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {DS.Model} record + @return {Promise} promise + */ + deleteRecord: function(store, type, record) { + var id = get(record, 'id'); + + return this.ajax(this.buildURL(type.typeKey, id), "DELETE"); + }, + + /** + Builds a URL for a given type and optional ID. + + By default, it pluralizes the type's name (for example, 'post' + becomes 'posts' and 'person' becomes 'people'). To override the + pluralization see [pathForType](#method_pathForType). + + If an ID is specified, it adds the ID to the path generated + for the type, separated by a `/`. + + @method buildURL + @param {String} type + @param {String} id + @return {String} url + */ + buildURL: function(type, id) { + var url = [], + host = get(this, 'host'), + prefix = this.urlPrefix(); + + if (type) { url.push(this.pathForType(type)); } + if (id) { url.push(id); } + + if (prefix) { url.unshift(prefix); } + + url = url.join('/'); + if (!host && url) { url = '/' + url; } + + return url; + }, + + /** + @method urlPrefix + @private + @param {String} path + @param {String} parentUrl + @return {String} urlPrefix + */ + urlPrefix: function(path, parentURL) { + var host = get(this, 'host'), + namespace = get(this, 'namespace'), + url = []; + + if (path) { + // Absolute path + if (path.charAt(0) === '/') { + if (host) { + path = path.slice(1); + url.push(host); + } + // Relative path + } else if (!/^http(s)?:\/\//.test(path)) { + url.push(parentURL); + } + } else { + if (host) { url.push(host); } + if (namespace) { url.push(namespace); } + } + + if (path) { + url.push(path); + } + + return url.join('/'); + }, + + /** + Determines the pathname for a given type. + + By default, it pluralizes the type's name (for example, + 'post' becomes 'posts' and 'person' becomes 'people'). + + ### Pathname customization + + For example if you have an object LineItem with an + endpoint of "/line_items/". + + ```js + App.ApplicationAdapter = DS.RESTAdapter.extend({ + pathForType: function(type) { + var decamelized = Ember.String.decamelize(type); + return Ember.String.pluralize(decamelized); + } + }); + ``` + + @method pathForType + @param {String} type + @return {String} path + **/ + pathForType: function(type) { + var camelized = Ember.String.camelize(type); + return Ember.String.pluralize(camelized); + }, + + /** + Takes an ajax response, and returns a relevant error. + + Returning a `DS.InvalidError` from this method will cause the + record to transition into the `invalid` state and make the + `errors` object available on the record. + + ```javascript + App.ApplicationAdapter = DS.RESTAdapter.extend({ + ajaxError: function(jqXHR) { + var error = this._super(jqXHR); + + if (jqXHR && jqXHR.status === 422) { + var jsonErrors = Ember.$.parseJSON(jqXHR.responseText)["errors"]; + + return new DS.InvalidError(jsonErrors); + } else { + return error; + } + } + }); + ``` + + Note: As a correctness optimization, the default implementation of + the `ajaxError` method strips out the `then` method from jquery's + ajax response (jqXHR). This is important because the jqXHR's + `then` method fulfills the promise with itself resulting in a + circular "thenable" chain which may cause problems for some + promise libraries. + + @method ajaxError + @param {Object} jqXHR + @return {Object} jqXHR + */ + ajaxError: function(jqXHR) { + if (jqXHR && typeof jqXHR === 'object') { + jqXHR.then = null; + } + + return jqXHR; + }, + + /** + Takes a URL, an HTTP method and a hash of data, and makes an + HTTP request. + + When the server responds with a payload, Ember Data will call into `extractSingle` + or `extractArray` (depending on whether the original query was for one record or + many records). + + By default, `ajax` method has the following behavior: + + * It sets the response `dataType` to `"json"` + * If the HTTP method is not `"GET"`, it sets the `Content-Type` to be + `application/json; charset=utf-8` + * If the HTTP method is not `"GET"`, it stringifies the data passed in. The + data is the serialized record in the case of a save. + * Registers success and failure handlers. + + @method ajax + @private + @param {String} url + @param {String} type The request type GET, POST, PUT, DELETE etc. + @param {Object} hash + @return {Promise} promise + */ + ajax: function(url, type, hash) { + var adapter = this; + + return new Ember.RSVP.Promise(function(resolve, reject) { + hash = adapter.ajaxOptions(url, type, hash); + + hash.success = function(json) { + Ember.run(null, resolve, json); + }; + + hash.error = function(jqXHR, textStatus, errorThrown) { + Ember.run(null, reject, adapter.ajaxError(jqXHR)); + }; + + Ember.$.ajax(hash); + }, "DS: RestAdapter#ajax " + type + " to " + url); + }, + + /** + @method ajaxOptions + @private + @param {String} url + @param {String} type The request type GET, POST, PUT, DELETE etc. + @param {Object} hash + @return {Object} hash + */ + ajaxOptions: function(url, type, hash) { + hash = hash || {}; + hash.url = url; + hash.type = type; + hash.dataType = 'json'; + hash.context = this; + + if (hash.data && type !== 'GET') { + hash.contentType = 'application/json; charset=utf-8'; + hash.data = JSON.stringify(hash.data); + } + + var headers = get(this, 'headers'); + if (headers !== undefined) { + hash.beforeSend = function (xhr) { + forEach.call(Ember.keys(headers), function(key) { + xhr.setRequestHeader(key, headers[key]); + }); + }; + } + + + return hash; + } + + }); + + __exports__["default"] = RESTAdapter; + }); +define("ember-data/lib/core", + ["exports"], + function(__exports__) { + "use strict"; + /** + @module ember-data + */ + + /** + All Ember Data methods and functions are defined inside of this namespace. + + @class DS + @static + */ + var DS; + if ('undefined' === typeof DS) { + /** + @property VERSION + @type String + @default '1.0.0-beta.8.2a68c63a' + @static + */ + DS = Ember.Namespace.create({ + VERSION: '1.0.0-beta.8.2a68c63a' + }); + + if (Ember.libraries) { + Ember.libraries.registerCoreLibrary('Ember Data', DS.VERSION); + } + } + + __exports__["default"] = DS; + }); +define("ember-data/lib/ember-initializer", + ["./setup-container"], + function(__dependency1__) { + "use strict"; + var setupContainer = __dependency1__["default"]; + + var K = Ember.K; + + /** + @module ember-data + */ + + /** + + This code initializes Ember-Data onto an Ember application. + + If an Ember.js developer defines a subclass of DS.Store on their application, + as `App.ApplicationStore` (or via a module system that resolves to `store:application`) + this code will automatically instantiate it and make it available on the + router. + + Additionally, after an application's controllers have been injected, they will + each have the store made available to them. + + For example, imagine an Ember.js application with the following classes: + + App.ApplicationStore = DS.Store.extend({ + adapter: 'custom' + }); + + App.PostsController = Ember.ArrayController.extend({ + // ... + }); + + When the application is initialized, `App.ApplicationStore` will automatically be + instantiated, and the instance of `App.PostsController` will have its `store` + property set to that instance. + + Note that this code will only be run if the `ember-application` package is + loaded. If Ember Data is being used in an environment other than a + typical application (e.g., node.js where only `ember-runtime` is available), + this code will be ignored. + */ + + Ember.onLoad('Ember.Application', function(Application) { + + Application.initializer({ + name: "ember-data", + initialize: setupContainer + }); + + // Deprecated initializers to satisfy old code that depended on them + + Application.initializer({ + name: "store", + after: "ember-data", + initialize: K + }); + + Application.initializer({ + name: "activeModelAdapter", + before: "store", + initialize: K + }); + + Application.initializer({ + name: "transforms", + before: "store", + initialize: K + }); + + Application.initializer({ + name: "data-adapter", + before: "store", + initialize: K + }); + + Application.initializer({ + name: "injectStore", + before: "store", + initialize: K + }); + }); + }); +define("ember-data/lib/ext/date", + [], + function() { + "use strict"; + /** + @module ember-data + */ + + /** + Date.parse with progressive enhancement for ISO 8601 + + © 2011 Colin Snover + + Released under MIT license. + + @class Date + @namespace Ember + @static + */ + Ember.Date = Ember.Date || {}; + + var origParse = Date.parse, numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ]; + + /** + @method parse + @param date + */ + Ember.Date.parse = function (date) { + var timestamp, struct, minutesOffset = 0; + + // ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date Time String Format string + // before falling back to any implementation-specific date parsing, so that’s what we do, even if native + // implementations could be faster + // 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm + if ((struct = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec(date))) { + // avoid NaN timestamps caused by “undefined” values being passed to Date.UTC + for (var i = 0, k; (k = numericKeys[i]); ++i) { + struct[k] = +struct[k] || 0; + } + + // allow undefined days and months + struct[2] = (+struct[2] || 1) - 1; + struct[3] = +struct[3] || 1; + + if (struct[8] !== 'Z' && struct[9] !== undefined) { + minutesOffset = struct[10] * 60 + struct[11]; + + if (struct[9] === '+') { + minutesOffset = 0 - minutesOffset; + } + } + + timestamp = Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7]); + } + else { + timestamp = origParse ? origParse(date) : NaN; + } + + return timestamp; + }; + + if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Date) { + Date.parse = Ember.Date.parse; + } + }); +define("ember-data/lib/initializers/data_adapter", + ["../system/debug/debug_adapter","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var DebugAdapter = __dependency1__["default"]; + + /** + Configures a container with injections on Ember applications + for the Ember-Data store. Accepts an optional namespace argument. + + @method initializeStoreInjections + @param {Ember.Container} container + */ + __exports__["default"] = function initializeDebugAdapter(container){ + container.register('data-adapter:main', DebugAdapter); + }; + }); +define("ember-data/lib/initializers/store", + ["../serializers","../adapters","../system/container_proxy","../system/store","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + var JSONSerializer = __dependency1__.JSONSerializer; + var RESTSerializer = __dependency1__.RESTSerializer; + var RESTAdapter = __dependency2__.RESTAdapter; + var ContainerProxy = __dependency3__["default"]; + var Store = __dependency4__["default"]; + + /** + Configures a container for use with an Ember-Data + store. Accepts an optional namespace argument. + + @method initializeStore + @param {Ember.Container} container + @param {Object} [application] an application namespace + */ + __exports__["default"] = function initializeStore(container, application){ + Ember.deprecate('Specifying a custom Store for Ember Data on your global namespace as `App.Store` ' + + 'has been deprecated. Please use `App.ApplicationStore` instead.', !(application && application.Store)); + + container.register('store:main', container.lookupFactory('store:application') || (application && application.Store) || Store); + + // allow older names to be looked up + + var proxy = new ContainerProxy(container); + proxy.registerDeprecations([ + {deprecated: 'serializer:_default', valid: 'serializer:-default'}, + {deprecated: 'serializer:_rest', valid: 'serializer:-rest'}, + {deprecated: 'adapter:_rest', valid: 'adapter:-rest'} + ]); + + // new go forward paths + container.register('serializer:-default', JSONSerializer); + container.register('serializer:-rest', RESTSerializer); + container.register('adapter:-rest', RESTAdapter); + + // Eagerly generate the store so defaultStore is populated. + // TODO: Do this in a finisher hook + container.lookup('store:main'); + }; + }); +define("ember-data/lib/initializers/store_injections", + ["exports"], + function(__exports__) { + "use strict"; + /** + Configures a container with injections on Ember applications + for the Ember-Data store. Accepts an optional namespace argument. + + @method initializeStoreInjections + @param {Ember.Container} container + */ + __exports__["default"] = function initializeStoreInjections(container){ + container.injection('controller', 'store', 'store:main'); + container.injection('route', 'store', 'store:main'); + container.injection('serializer', 'store', 'store:main'); + container.injection('data-adapter', 'store', 'store:main'); + }; + }); +define("ember-data/lib/initializers/transforms", + ["../transforms","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var BooleanTransform = __dependency1__.BooleanTransform; + var DateTransform = __dependency1__.DateTransform; + var StringTransform = __dependency1__.StringTransform; + var NumberTransform = __dependency1__.NumberTransform; + + /** + Configures a container for use with Ember-Data + transforms. + + @method initializeTransforms + @param {Ember.Container} container + */ + __exports__["default"] = function initializeTransforms(container){ + container.register('transform:boolean', BooleanTransform); + container.register('transform:date', DateTransform); + container.register('transform:number', NumberTransform); + container.register('transform:string', StringTransform); + }; + }); +define("ember-data/lib/main", + ["./core","./ext/date","./system/store","./system/model","./system/changes","./system/adapter","./system/debug","./system/record_arrays","./system/record_array_manager","./adapters","./serializers/json_serializer","./serializers/rest_serializer","../../ember-inflector/lib/main","../../activemodel-adapter/lib/main","./transforms","./system/relationships","./ember-initializer","./setup-container","./system/container_proxy","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __dependency12__, __dependency13__, __dependency14__, __dependency15__, __dependency16__, __dependency17__, __dependency18__, __dependency19__, __exports__) { + "use strict"; + /** + Ember Data + + @module ember-data + @main ember-data + */ + + // support RSVP 2.x via resolve, but prefer RSVP 3.x's Promise.cast + Ember.RSVP.Promise.cast = Ember.RSVP.Promise.cast || Ember.RSVP.resolve; + + var DS = __dependency1__["default"]; + + var Store = __dependency3__.Store; + var PromiseArray = __dependency3__.PromiseArray; + var PromiseObject = __dependency3__.PromiseObject; + var Model = __dependency4__.Model; + var Errors = __dependency4__.Errors; + var RootState = __dependency4__.RootState; + var attr = __dependency4__.attr; + var AttributeChange = __dependency5__.AttributeChange; + var RelationshipChange = __dependency5__.RelationshipChange; + var RelationshipChangeAdd = __dependency5__.RelationshipChangeAdd; + var RelationshipChangeRemove = __dependency5__.RelationshipChangeRemove; + var OneToManyChange = __dependency5__.OneToManyChange; + var ManyToNoneChange = __dependency5__.ManyToNoneChange; + var OneToOneChange = __dependency5__.OneToOneChange; + var ManyToManyChange = __dependency5__.ManyToManyChange; + var InvalidError = __dependency6__.InvalidError; + var Adapter = __dependency6__.Adapter; + var DebugAdapter = __dependency7__["default"]; + var RecordArray = __dependency8__.RecordArray; + var FilteredRecordArray = __dependency8__.FilteredRecordArray; + var AdapterPopulatedRecordArray = __dependency8__.AdapterPopulatedRecordArray; + var ManyArray = __dependency8__.ManyArray; + var RecordArrayManager = __dependency9__["default"]; + var RESTAdapter = __dependency10__.RESTAdapter; + var FixtureAdapter = __dependency10__.FixtureAdapter; + var JSONSerializer = __dependency11__["default"]; + var RESTSerializer = __dependency12__["default"]; + var ActiveModelAdapter = __dependency14__.ActiveModelAdapter; + var ActiveModelSerializer = __dependency14__.ActiveModelSerializer; + var EmbeddedRecordsMixin = __dependency14__.EmbeddedRecordsMixin; + + var Transform = __dependency15__.Transform; + var DateTransform = __dependency15__.DateTransform; + var NumberTransform = __dependency15__.NumberTransform; + var StringTransform = __dependency15__.StringTransform; + var BooleanTransform = __dependency15__.BooleanTransform; + + var hasMany = __dependency16__.hasMany; + var belongsTo = __dependency16__.belongsTo; + var setupContainer = __dependency18__["default"]; + + var ContainerProxy = __dependency19__["default"]; + + DS.Store = Store; + DS.PromiseArray = PromiseArray; + DS.PromiseObject = PromiseObject; + + DS.Model = Model; + DS.RootState = RootState; + DS.attr = attr; + DS.Errors = Errors; + + DS.AttributeChange = AttributeChange; + DS.RelationshipChange = RelationshipChange; + DS.RelationshipChangeAdd = RelationshipChangeAdd; + DS.OneToManyChange = OneToManyChange; + DS.ManyToNoneChange = OneToManyChange; + DS.OneToOneChange = OneToOneChange; + DS.ManyToManyChange = ManyToManyChange; + + DS.Adapter = Adapter; + DS.InvalidError = InvalidError; + + DS.DebugAdapter = DebugAdapter; + + DS.RecordArray = RecordArray; + DS.FilteredRecordArray = FilteredRecordArray; + DS.AdapterPopulatedRecordArray = AdapterPopulatedRecordArray; + DS.ManyArray = ManyArray; + + DS.RecordArrayManager = RecordArrayManager; + + DS.RESTAdapter = RESTAdapter; + DS.FixtureAdapter = FixtureAdapter; + + DS.RESTSerializer = RESTSerializer; + DS.JSONSerializer = JSONSerializer; + + DS.Transform = Transform; + DS.DateTransform = DateTransform; + DS.StringTransform = StringTransform; + DS.NumberTransform = NumberTransform; + DS.BooleanTransform = BooleanTransform; + + DS.ActiveModelAdapter = ActiveModelAdapter; + DS.ActiveModelSerializer = ActiveModelSerializer; + DS.EmbeddedRecordsMixin = EmbeddedRecordsMixin; + + DS.belongsTo = belongsTo; + DS.hasMany = hasMany; + + DS.ContainerProxy = ContainerProxy; + + DS._setupContainer = setupContainer; + + Ember.lookup.DS = DS; + + __exports__["default"] = DS; + }); +define("ember-data/lib/serializers", + ["./serializers/json_serializer","./serializers/rest_serializer","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var JSONSerializer = __dependency1__["default"]; + var RESTSerializer = __dependency2__["default"]; + + __exports__.JSONSerializer = JSONSerializer; + __exports__.RESTSerializer = RESTSerializer; + }); +define("ember-data/lib/serializers/json_serializer", + ["../system/changes","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var RelationshipChange = __dependency1__.RelationshipChange; + var get = Ember.get, set = Ember.set, isNone = Ember.isNone, + map = Ember.ArrayPolyfills.map; + + /** + In Ember Data a Serializer is used to serialize and deserialize + records when they are transferred in and out of an external source. + This process involves normalizing property names, transforming + attribute values and serializing relationships. + + For maximum performance Ember Data recommends you use the + [RESTSerializer](DS.RESTSerializer.html) or one of its subclasses. + + `JSONSerializer` is useful for simpler or legacy backends that may + not support the http://jsonapi.org/ spec. + + @class JSONSerializer + @namespace DS + */ + var JSONSerializer = Ember.Object.extend({ + /** + The primaryKey is used when serializing and deserializing + data. Ember Data always uses the `id` property to store the id of + the record. The external source may not always follow this + convention. In these cases it is useful to override the + primaryKey property to match the primaryKey of your external + store. + + Example + + ```javascript + App.ApplicationSerializer = DS.JSONSerializer.extend({ + primaryKey: '_id' + }); + ``` + + @property primaryKey + @type {String} + @default 'id' + */ + primaryKey: 'id', + + /** + The `attrs` object can be used to declare a simple mapping between + property names on `DS.Model` records and payload keys in the + serialized JSON object representing the record. An object with the + propery `key` can also be used to designate the attribute's key on + the response payload. + + Example + + ```javascript + App.Person = DS.Model.extend({ + firstName: DS.attr('string'), + lastName: DS.attr('string'), + occupation: DS.attr('string'), + admin: DS.attr('boolean') + }); + + App.PersonSerializer = DS.JSONSerializer.extend({ + attrs: { + admin: 'is_admin', + occupation: {key: 'career'} + } + }); + ``` + + @property attrs + @type {Object} + */ + + /** + Given a subclass of `DS.Model` and a JSON object this method will + iterate through each attribute of the `DS.Model` and invoke the + `DS.Transform#deserialize` method on the matching property of the + JSON object. This method is typically called after the + serializer's `normalize` method. + + @method applyTransforms + @private + @param {subclass of DS.Model} type + @param {Object} data The data to transform + @return {Object} data The transformed data object + */ + applyTransforms: function(type, data) { + type.eachTransformedAttribute(function(key, type) { + var transform = this.transformFor(type); + data[key] = transform.deserialize(data[key]); + }, this); + + return data; + }, + + /** + Normalizes a part of the JSON payload returned by + the server. You should override this method, munge the hash + and call super if you have generic normalization to do. + + It takes the type of the record that is being normalized + (as a DS.Model class), the property where the hash was + originally found, and the hash to normalize. + + You can use this method, for example, to normalize underscored keys to camelized + or other general-purpose normalizations. + + Example + + ```javascript + App.ApplicationSerializer = DS.JSONSerializer.extend({ + normalize: function(type, hash) { + var fields = Ember.get(type, 'fields'); + fields.forEach(function(field) { + var payloadField = Ember.String.underscore(field); + if (field === payloadField) { return; } + + hash[field] = hash[payloadField]; + delete hash[payloadField]; + }); + return this._super.apply(this, arguments); + } + }); + ``` + + @method normalize + @param {subclass of DS.Model} type + @param {Object} hash + @return {Object} + */ + normalize: function(type, hash) { + if (!hash) { return hash; } + + this.normalizeId(hash); + this.normalizeUsingDeclaredMapping(type, hash); + this.applyTransforms(type, hash); + return hash; + }, + + /** + @method normalizeUsingDeclaredMapping + @private + */ + normalizeUsingDeclaredMapping: function(type, hash) { + var attrs = get(this, 'attrs'), payloadKey, key; + + if (attrs) { + for (key in attrs) { + payloadKey = attrs[key]; + if (payloadKey && payloadKey.key) { + payloadKey = payloadKey.key; + } + if (typeof payloadKey === 'string') { + hash[key] = hash[payloadKey]; + delete hash[payloadKey]; + } + } + } + }, + /** + @method normalizeId + @private + */ + normalizeId: function(hash) { + var primaryKey = get(this, 'primaryKey'); + + if (primaryKey === 'id') { return; } + + hash.id = hash[primaryKey]; + delete hash[primaryKey]; + }, + + // SERIALIZE + /** + Called when a record is saved in order to convert the + record into JSON. + + By default, it creates a JSON object with a key for + each attribute and belongsTo relationship. + + For example, consider this model: + + ```javascript + App.Comment = DS.Model.extend({ + title: DS.attr(), + body: DS.attr(), + + author: DS.belongsTo('user') + }); + ``` + + The default serialization would create a JSON object like: + + ```javascript + { + "title": "Rails is unagi", + "body": "Rails? Omakase? O_O", + "author": 12 + } + ``` + + By default, attributes are passed through as-is, unless + you specified an attribute type (`DS.attr('date')`). If + you specify a transform, the JavaScript value will be + serialized when inserted into the JSON hash. + + By default, belongs-to relationships are converted into + IDs when inserted into the JSON hash. + + ## IDs + + `serialize` takes an options hash with a single option: + `includeId`. If this option is `true`, `serialize` will, + by default include the ID in the JSON object it builds. + + The adapter passes in `includeId: true` when serializing + a record for `createRecord`, but not for `updateRecord`. + + ## Customization + + Your server may expect a different JSON format than the + built-in serialization format. + + In that case, you can implement `serialize` yourself and + return a JSON hash of your choosing. + + ```javascript + App.PostSerializer = DS.JSONSerializer.extend({ + serialize: function(post, options) { + var json = { + POST_TTL: post.get('title'), + POST_BDY: post.get('body'), + POST_CMS: post.get('comments').mapProperty('id') + } + + if (options.includeId) { + json.POST_ID_ = post.get('id'); + } + + return json; + } + }); + ``` + + ## Customizing an App-Wide Serializer + + If you want to define a serializer for your entire + application, you'll probably want to use `eachAttribute` + and `eachRelationship` on the record. + + ```javascript + App.ApplicationSerializer = DS.JSONSerializer.extend({ + serialize: function(record, options) { + var json = {}; + + record.eachAttribute(function(name) { + json[serverAttributeName(name)] = record.get(name); + }) + + record.eachRelationship(function(name, relationship) { + if (relationship.kind === 'hasMany') { + json[serverHasManyName(name)] = record.get(name).mapBy('id'); + } + }); + + if (options.includeId) { + json.ID_ = record.get('id'); + } + + return json; + } + }); + + function serverAttributeName(attribute) { + return attribute.underscore().toUpperCase(); + } + + function serverHasManyName(name) { + return serverAttributeName(name.singularize()) + "_IDS"; + } + ``` + + This serializer will generate JSON that looks like this: + + ```javascript + { + "TITLE": "Rails is omakase", + "BODY": "Yep. Omakase.", + "COMMENT_IDS": [ 1, 2, 3 ] + } + ``` + + ## Tweaking the Default JSON + + If you just want to do some small tweaks on the default JSON, + you can call super first and make the tweaks on the returned + JSON. + + ```javascript + App.PostSerializer = DS.JSONSerializer.extend({ + serialize: function(record, options) { + var json = this._super.apply(this, arguments); + + json.subject = json.title; + delete json.title; + + return json; + } + }); + ``` + + @method serialize + @param {subclass of DS.Model} record + @param {Object} options + @return {Object} json + */ + serialize: function(record, options) { + var json = {}; + + if (options && options.includeId) { + var id = get(record, 'id'); + + if (id) { + json[get(this, 'primaryKey')] = id; + } + } + + record.eachAttribute(function(key, attribute) { + this.serializeAttribute(record, json, key, attribute); + }, this); + + record.eachRelationship(function(key, relationship) { + if (relationship.kind === 'belongsTo') { + this.serializeBelongsTo(record, json, relationship); + } else if (relationship.kind === 'hasMany') { + this.serializeHasMany(record, json, relationship); + } + }, this); + + return json; + }, + + /** + `serializeAttribute` can be used to customize how `DS.attr` + properties are serialized + + For example if you wanted to ensure all your attributes were always + serialized as properties on an `attributes` object you could + write: + + ```javascript + App.ApplicationSerializer = DS.JSONSerializer.extend({ + serializeAttribute: function(record, json, key, attributes) { + json.attributes = json.attributes || {}; + this._super(record, json.attributes, key, attributes); + } + }); + ``` + + @method serializeAttribute + @param {DS.Model} record + @param {Object} json + @param {String} key + @param {Object} attribute + */ + serializeAttribute: function(record, json, key, attribute) { + var attrs = get(this, 'attrs'); + var value = get(record, key), type = attribute.type; + + if (type) { + var transform = this.transformFor(type); + value = transform.serialize(value); + } + + // if provided, use the mapping provided by `attrs` in + // the serializer + key = attrs && attrs[key] || (this.keyForAttribute ? this.keyForAttribute(key) : key); + + json[key] = value; + }, + + /** + `serializeBelongsTo` can be used to customize how `DS.belongsTo` + properties are serialized. + + Example + + ```javascript + App.PostSerializer = DS.JSONSerializer.extend({ + serializeBelongsTo: function(record, json, relationship) { + var key = relationship.key; + + var belongsTo = get(record, key); + + key = this.keyForRelationship ? this.keyForRelationship(key, "belongsTo") : key; + + json[key] = Ember.isNone(belongsTo) ? belongsTo : belongsTo.toJSON(); + } + }); + ``` + + @method serializeBelongsTo + @param {DS.Model} record + @param {Object} json + @param {Object} relationship + */ + serializeBelongsTo: function(record, json, relationship) { + var key = relationship.key; + + var belongsTo = get(record, key); + + key = this.keyForRelationship ? this.keyForRelationship(key, "belongsTo") : key; + + if (isNone(belongsTo)) { + json[key] = belongsTo; + } else { + json[key] = get(belongsTo, 'id'); + } + + if (relationship.options.polymorphic) { + this.serializePolymorphicType(record, json, relationship); + } + }, + + /** + `serializeHasMany` can be used to customize how `DS.hasMany` + properties are serialized. + + Example + + ```javascript + App.PostSerializer = DS.JSONSerializer.extend({ + serializeHasMany: function(record, json, relationship) { + var key = relationship.key; + if (key === 'comments') { + return; + } else { + this._super.apply(this, arguments); + } + } + }); + ``` + + @method serializeHasMany + @param {DS.Model} record + @param {Object} json + @param {Object} relationship + */ + serializeHasMany: function(record, json, relationship) { + var key = relationship.key; + var payloadKey = this.keyForRelationship ? this.keyForRelationship(key, "hasMany") : key; + var relationshipType = RelationshipChange.determineRelationshipType(record.constructor, relationship); + + if (relationshipType === 'manyToNone' || relationshipType === 'manyToMany') { + json[payloadKey] = get(record, key).mapBy('id'); + // TODO support for polymorphic manyToNone and manyToMany relationships + } + }, + + /** + You can use this method to customize how polymorphic objects are + serialized. Objects are considered to be polymorphic if + `{polymorphic: true}` is pass as the second argument to the + `DS.belongsTo` function. + + Example + + ```javascript + App.CommentSerializer = DS.JSONSerializer.extend({ + serializePolymorphicType: function(record, json, relationship) { + var key = relationship.key, + belongsTo = get(record, key); + key = this.keyForAttribute ? this.keyForAttribute(key) : key; + json[key + "_type"] = belongsTo.constructor.typeKey; + } + }); + ``` + + @method serializePolymorphicType + @param {DS.Model} record + @param {Object} json + @param {Object} relationship + */ + serializePolymorphicType: Ember.K, + + // EXTRACT + + /** + The `extract` method is used to deserialize payload data from the + server. By default the `JSONSerializer` does not push the records + into the store. However records that subclass `JSONSerializer` + such as the `RESTSerializer` may push records into the store as + part of the extract call. + + This method delegates to a more specific extract method based on + the `requestType`. + + Example + + ```javascript + var get = Ember.get; + socket.on('message', function(message) { + var modelName = message.model; + var data = message.data; + var type = store.modelFor(modelName); + var serializer = store.serializerFor(type.typeKey); + var record = serializer.extract(store, type, data, get(data, 'id'), 'single'); + store.push(modelName, record); + }); + ``` + + @method extract + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @param {String or Number} id + @param {String} requestType + @return {Object} json The deserialized payload + */ + extract: function(store, type, payload, id, requestType) { + this.extractMeta(store, type, payload); + + var specificExtract = "extract" + requestType.charAt(0).toUpperCase() + requestType.substr(1); + return this[specificExtract](store, type, payload, id, requestType); + }, + + /** + `extractFindAll` is a hook into the extract method used when a + call is made to `DS.Store#findAll`. By default this method is an + alias for [extractArray](#method_extractArray). + + @method extractFindAll + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Array} array An array of deserialized objects + */ + extractFindAll: function(store, type, payload){ + return this.extractArray(store, type, payload); + }, + /** + `extractFindQuery` is a hook into the extract method used when a + call is made to `DS.Store#findQuery`. By default this method is an + alias for [extractArray](#method_extractArray). + + @method extractFindQuery + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Array} array An array of deserialized objects + */ + extractFindQuery: function(store, type, payload){ + return this.extractArray(store, type, payload); + }, + /** + `extractFindMany` is a hook into the extract method used when a + call is made to `DS.Store#findMany`. By default this method is + alias for [extractArray](#method_extractArray). + + @method extractFindMany + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Array} array An array of deserialized objects + */ + extractFindMany: function(store, type, payload){ + return this.extractArray(store, type, payload); + }, + /** + `extractFindHasMany` is a hook into the extract method used when a + call is made to `DS.Store#findHasMany`. By default this method is + alias for [extractArray](#method_extractArray). + + @method extractFindHasMany + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Array} array An array of deserialized objects + */ + extractFindHasMany: function(store, type, payload){ + return this.extractArray(store, type, payload); + }, + + /** + `extractCreateRecord` is a hook into the extract method used when a + call is made to `DS.Store#createRecord`. By default this method is + alias for [extractSave](#method_extractSave). + + @method extractCreateRecord + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Object} json The deserialized payload + */ + extractCreateRecord: function(store, type, payload) { + return this.extractSave(store, type, payload); + }, + /** + `extractUpdateRecord` is a hook into the extract method used when + a call is made to `DS.Store#update`. By default this method is alias + for [extractSave](#method_extractSave). + + @method extractUpdateRecord + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Object} json The deserialized payload + */ + extractUpdateRecord: function(store, type, payload) { + return this.extractSave(store, type, payload); + }, + /** + `extractDeleteRecord` is a hook into the extract method used when + a call is made to `DS.Store#deleteRecord`. By default this method is + alias for [extractSave](#method_extractSave). + + @method extractDeleteRecord + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Object} json The deserialized payload + */ + extractDeleteRecord: function(store, type, payload) { + return this.extractSave(store, type, payload); + }, + + /** + `extractFind` is a hook into the extract method used when + a call is made to `DS.Store#find`. By default this method is + alias for [extractSingle](#method_extractSingle). + + @method extractFind + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Object} json The deserialized payload + */ + extractFind: function(store, type, payload) { + return this.extractSingle(store, type, payload); + }, + /** + `extractFindBelongsTo` is a hook into the extract method used when + a call is made to `DS.Store#findBelongsTo`. By default this method is + alias for [extractSingle](#method_extractSingle). + + @method extractFindBelongsTo + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Object} json The deserialized payload + */ + extractFindBelongsTo: function(store, type, payload) { + return this.extractSingle(store, type, payload); + }, + /** + `extractSave` is a hook into the extract method used when a call + is made to `DS.Model#save`. By default this method is alias + for [extractSingle](#method_extractSingle). + + @method extractSave + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Object} json The deserialized payload + */ + extractSave: function(store, type, payload) { + return this.extractSingle(store, type, payload); + }, + + /** + `extractSingle` is used to deserialize a single record returned + from the adapter. + + Example + + ```javascript + App.PostSerializer = DS.JSONSerializer.extend({ + extractSingle: function(store, type, payload) { + payload.comments = payload._embedded.comment; + delete payload._embedded; + + return this._super(store, type, payload); + }, + }); + ``` + + @method extractSingle + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Object} json The deserialized payload + */ + extractSingle: function(store, type, payload) { + return this.normalize(type, payload); + }, + + /** + `extractArray` is used to deserialize an array of records + returned from the adapter. + + Example + + ```javascript + App.PostSerializer = DS.JSONSerializer.extend({ + extractArray: function(store, type, payload) { + return payload.map(function(json) { + return this.extractSingle(store, type, json); + }, this); + } + }); + ``` + + @method extractArray + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + @return {Array} array An array of deserialized objects + */ + extractArray: function(store, type, arrayPayload) { + var serializer = this; + return map.call(arrayPayload, function(singlePayload) { + return serializer.normalize(type, singlePayload); + }); + }, + + /** + `extractMeta` is used to deserialize any meta information in the + adapter payload. By default Ember Data expects meta information to + be located on the `meta` property of the payload object. + + Example + + ```javascript + App.PostSerializer = DS.JSONSerializer.extend({ + extractMeta: function(store, type, payload) { + if (payload && payload._pagination) { + store.metaForType(type, payload._pagination); + delete payload._pagination; + } + } + }); + ``` + + @method extractMeta + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} payload + */ + extractMeta: function(store, type, payload) { + if (payload && payload.meta) { + store.metaForType(type, payload.meta); + delete payload.meta; + } + }, + + /** + `keyForAttribute` can be used to define rules for how to convert an + attribute name in your model to a key in your JSON. + + Example + + ```javascript + App.ApplicationSerializer = DS.RESTSerializer.extend({ + keyForAttribute: function(attr) { + return Ember.String.underscore(attr).toUpperCase(); + } + }); + ``` + + @method keyForAttribute + @param {String} key + @return {String} normalized key + */ + + + /** + `keyForRelationship` can be used to define a custom key when + serializing relationship properties. By default `JSONSerializer` + does not provide an implementation of this method. + + Example + + ```javascript + App.PostSerializer = DS.JSONSerializer.extend({ + keyForRelationship: function(key, relationship) { + return 'rel_' + Ember.String.underscore(key); + } + }); + ``` + + @method keyForRelationship + @param {String} key + @param {String} relationship type + @return {String} normalized key + */ + + // HELPERS + + /** + @method transformFor + @private + @param {String} attributeType + @param {Boolean} skipAssertion + @return {DS.Transform} transform + */ + transformFor: function(attributeType, skipAssertion) { + var transform = this.container.lookup('transform:' + attributeType); + Ember.assert("Unable to find transform for '" + attributeType + "'", skipAssertion || !!transform); + return transform; + } + }); + + __exports__["default"] = JSONSerializer; + }); +define("ember-data/lib/serializers/rest_serializer", + ["./json_serializer","ember-inflector/lib/system/string","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + /** + @module ember-data + */ + + var JSONSerializer = __dependency1__["default"]; + var get = Ember.get, set = Ember.set; + var forEach = Ember.ArrayPolyfills.forEach; + var map = Ember.ArrayPolyfills.map; + + var singularize = __dependency2__.singularize; + var camelize = Ember.String.camelize; + + function coerceId(id) { + return id == null ? null : id+''; + } + + /** + Normally, applications will use the `RESTSerializer` by implementing + the `normalize` method and individual normalizations under + `normalizeHash`. + + This allows you to do whatever kind of munging you need, and is + especially useful if your server is inconsistent and you need to + do munging differently for many different kinds of responses. + + See the `normalize` documentation for more information. + + ## Across the Board Normalization + + There are also a number of hooks that you might find useful to defined + across-the-board rules for your payload. These rules will be useful + if your server is consistent, or if you're building an adapter for + an infrastructure service, like Parse, and want to encode service + conventions. + + For example, if all of your keys are underscored and all-caps, but + otherwise consistent with the names you use in your models, you + can implement across-the-board rules for how to convert an attribute + name in your model to a key in your JSON. + + ```js + App.ApplicationSerializer = DS.RESTSerializer.extend({ + keyForAttribute: function(attr) { + return Ember.String.underscore(attr).toUpperCase(); + } + }); + ``` + + You can also implement `keyForRelationship`, which takes the name + of the relationship as the first parameter, and the kind of + relationship (`hasMany` or `belongsTo`) as the second parameter. + + @class RESTSerializer + @namespace DS + @extends DS.JSONSerializer + */ + var RESTSerializer = JSONSerializer.extend({ + /** + If you want to do normalizations specific to some part of the payload, you + can specify those under `normalizeHash`. + + For example, given the following json where the the `IDs` under + `"comments"` are provided as `_id` instead of `id`. + + ```javascript + { + "post": { + "id": 1, + "title": "Rails is omakase", + "comments": [ 1, 2 ] + }, + "comments": [{ + "_id": 1, + "body": "FIRST" + }, { + "_id": 2, + "body": "Rails is unagi" + }] + } + ``` + + You use `normalizeHash` to normalize just the comments: + + ```javascript + App.PostSerializer = DS.RESTSerializer.extend({ + normalizeHash: { + comments: function(hash) { + hash.id = hash._id; + delete hash._id; + return hash; + } + } + }); + ``` + + The key under `normalizeHash` is usually just the original key + that was in the original payload. However, key names will be + impacted by any modifications done in the `normalizePayload` + method. The `DS.RESTSerializer`'s default implementation makes no + changes to the payload keys. + + @property normalizeHash + @type {Object} + @default undefined + */ + + /** + Normalizes a part of the JSON payload returned by + the server. You should override this method, munge the hash + and call super if you have generic normalization to do. + + It takes the type of the record that is being normalized + (as a DS.Model class), the property where the hash was + originally found, and the hash to normalize. + + For example, if you have a payload that looks like this: + + ```js + { + "post": { + "id": 1, + "title": "Rails is omakase", + "comments": [ 1, 2 ] + }, + "comments": [{ + "id": 1, + "body": "FIRST" + }, { + "id": 2, + "body": "Rails is unagi" + }] + } + ``` + + The `normalize` method will be called three times: + + * With `App.Post`, `"posts"` and `{ id: 1, title: "Rails is omakase", ... }` + * With `App.Comment`, `"comments"` and `{ id: 1, body: "FIRST" }` + * With `App.Comment`, `"comments"` and `{ id: 2, body: "Rails is unagi" }` + + You can use this method, for example, to normalize underscored keys to camelized + or other general-purpose normalizations. + + If you want to do normalizations specific to some part of the payload, you + can specify those under `normalizeHash`. + + For example, if the `IDs` under `"comments"` are provided as `_id` instead of + `id`, you can specify how to normalize just the comments: + + ```js + App.PostSerializer = DS.RESTSerializer.extend({ + normalizeHash: { + comments: function(hash) { + hash.id = hash._id; + delete hash._id; + return hash; + } + } + }); + ``` + + The key under `normalizeHash` is just the original key that was in the original + payload. + + @method normalize + @param {subclass of DS.Model} type + @param {Object} hash + @param {String} prop + @return {Object} + */ + normalize: function(type, hash, prop) { + this.normalizeId(hash); + this.normalizeAttributes(type, hash); + this.normalizeRelationships(type, hash); + + this.normalizeUsingDeclaredMapping(type, hash); + + if (this.normalizeHash && this.normalizeHash[prop]) { + this.normalizeHash[prop](hash); + } + + this.applyTransforms(type, hash); + return hash; + }, + + /** + You can use this method to normalize all payloads, regardless of whether they + represent single records or an array. + + For example, you might want to remove some extraneous data from the payload: + + ```js + App.ApplicationSerializer = DS.RESTSerializer.extend({ + normalizePayload: function(payload) { + delete payload.version; + delete payload.status; + return payload; + } + }); + ``` + + @method normalizePayload + @param {Object} payload + @return {Object} the normalized payload + */ + normalizePayload: function(payload) { + return payload; + }, + + /** + @method normalizeAttributes + @private + */ + normalizeAttributes: function(type, hash) { + var payloadKey, key; + + if (this.keyForAttribute) { + type.eachAttribute(function(key) { + payloadKey = this.keyForAttribute(key); + if (key === payloadKey) { return; } + + hash[key] = hash[payloadKey]; + delete hash[payloadKey]; + }, this); + } + }, + + /** + @method normalizeRelationships + @private + */ + normalizeRelationships: function(type, hash) { + var payloadKey, key; + + if (this.keyForRelationship) { + type.eachRelationship(function(key, relationship) { + payloadKey = this.keyForRelationship(key, relationship.kind); + if (key === payloadKey) { return; } + + hash[key] = hash[payloadKey]; + delete hash[payloadKey]; + }, this); + } + }, + + /** + Called when the server has returned a payload representing + a single record, such as in response to a `find` or `save`. + + It is your opportunity to clean up the server's response into the normalized + form expected by Ember Data. + + If you want, you can just restructure the top-level of your payload, and + do more fine-grained normalization in the `normalize` method. + + For example, if you have a payload like this in response to a request for + post 1: + + ```js + { + "id": 1, + "title": "Rails is omakase", + + "_embedded": { + "comment": [{ + "_id": 1, + "comment_title": "FIRST" + }, { + "_id": 2, + "comment_title": "Rails is unagi" + }] + } + } + ``` + + You could implement a serializer that looks like this to get your payload + into shape: + + ```js + App.PostSerializer = DS.RESTSerializer.extend({ + // First, restructure the top-level so it's organized by type + extractSingle: function(store, type, payload, id) { + var comments = payload._embedded.comment; + delete payload._embedded; + + payload = { comments: comments, post: payload }; + return this._super(store, type, payload, id); + }, + + normalizeHash: { + // Next, normalize individual comments, which (after `extract`) + // are now located under `comments` + comments: function(hash) { + hash.id = hash._id; + hash.title = hash.comment_title; + delete hash._id; + delete hash.comment_title; + return hash; + } + } + }) + ``` + + When you call super from your own implementation of `extractSingle`, the + built-in implementation will find the primary record in your normalized + payload and push the remaining records into the store. + + The primary record is the single hash found under `post` or the first + element of the `posts` array. + + The primary record has special meaning when the record is being created + for the first time or updated (`createRecord` or `updateRecord`). In + particular, it will update the properties of the record that was saved. + + @method extractSingle + @param {DS.Store} store + @param {subclass of DS.Model} primaryType + @param {Object} payload + @param {String} recordId + @return {Object} the primary response to the original request + */ + extractSingle: function(store, primaryType, payload, recordId) { + payload = this.normalizePayload(payload); + var primaryTypeName = primaryType.typeKey, + primaryRecord; + + for (var prop in payload) { + var typeName = this.typeForRoot(prop), + type = store.modelFor(typeName), + isPrimary = type.typeKey === primaryTypeName; + + // legacy support for singular resources + if (isPrimary && Ember.typeOf(payload[prop]) !== "array" ) { + primaryRecord = this.normalize(primaryType, payload[prop], prop); + continue; + } + + /*jshint loopfunc:true*/ + forEach.call(payload[prop], function(hash) { + var typeName = this.typeForRoot(prop), + type = store.modelFor(typeName), + typeSerializer = store.serializerFor(type); + + hash = typeSerializer.normalize(type, hash, prop); + + var isFirstCreatedRecord = isPrimary && !recordId && !primaryRecord, + isUpdatedRecord = isPrimary && coerceId(hash.id) === recordId; + + // find the primary record. + // + // It's either: + // * the record with the same ID as the original request + // * in the case of a newly created record that didn't have an ID, the first + // record in the Array + if (isFirstCreatedRecord || isUpdatedRecord) { + primaryRecord = hash; + } else { + store.push(typeName, hash); + } + }, this); + } + + return primaryRecord; + }, + + /** + Called when the server has returned a payload representing + multiple records, such as in response to a `findAll` or `findQuery`. + + It is your opportunity to clean up the server's response into the normalized + form expected by Ember Data. + + If you want, you can just restructure the top-level of your payload, and + do more fine-grained normalization in the `normalize` method. + + For example, if you have a payload like this in response to a request for + all posts: + + ```js + { + "_embedded": { + "post": [{ + "id": 1, + "title": "Rails is omakase" + }, { + "id": 2, + "title": "The Parley Letter" + }], + "comment": [{ + "_id": 1, + "comment_title": "Rails is unagi" + "post_id": 1 + }, { + "_id": 2, + "comment_title": "Don't tread on me", + "post_id": 2 + }] + } + } + ``` + + You could implement a serializer that looks like this to get your payload + into shape: + + ```js + App.PostSerializer = DS.RESTSerializer.extend({ + // First, restructure the top-level so it's organized by type + // and the comments are listed under a post's `comments` key. + extractArray: function(store, type, payload) { + var posts = payload._embedded.post; + var comments = []; + var postCache = {}; + + posts.forEach(function(post) { + post.comments = []; + postCache[post.id] = post; + }); + + payload._embedded.comment.forEach(function(comment) { + comments.push(comment); + postCache[comment.post_id].comments.push(comment); + delete comment.post_id; + } + + payload = { comments: comments, posts: payload }; + + return this._super(store, type, payload); + }, + + normalizeHash: { + // Next, normalize individual comments, which (after `extract`) + // are now located under `comments` + comments: function(hash) { + hash.id = hash._id; + hash.title = hash.comment_title; + delete hash._id; + delete hash.comment_title; + return hash; + } + } + }) + ``` + + When you call super from your own implementation of `extractArray`, the + built-in implementation will find the primary array in your normalized + payload and push the remaining records into the store. + + The primary array is the array found under `posts`. + + The primary record has special meaning when responding to `findQuery` + or `findHasMany`. In particular, the primary array will become the + list of records in the record array that kicked off the request. + + If your primary array contains secondary (embedded) records of the same type, + you cannot place these into the primary array `posts`. Instead, place the + secondary items into an underscore prefixed property `_posts`, which will + push these items into the store and will not affect the resulting query. + + @method extractArray + @param {DS.Store} store + @param {subclass of DS.Model} primaryType + @param {Object} payload + @return {Array} The primary array that was returned in response + to the original query. + */ + extractArray: function(store, primaryType, payload) { + payload = this.normalizePayload(payload); + + var primaryTypeName = primaryType.typeKey, + primaryArray; + + for (var prop in payload) { + var typeKey = prop, + forcedSecondary = false; + + if (prop.charAt(0) === '_') { + forcedSecondary = true; + typeKey = prop.substr(1); + } + + var typeName = this.typeForRoot(typeKey), + type = store.modelFor(typeName), + typeSerializer = store.serializerFor(type), + isPrimary = (!forcedSecondary && (type.typeKey === primaryTypeName)); + + /*jshint loopfunc:true*/ + var normalizedArray = map.call(payload[prop], function(hash) { + return typeSerializer.normalize(type, hash, prop); + }, this); + + if (isPrimary) { + primaryArray = normalizedArray; + } else { + store.pushMany(typeName, normalizedArray); + } + } + + return primaryArray; + }, + + /** + This method allows you to push a payload containing top-level + collections of records organized per type. + + ```js + { + "posts": [{ + "id": "1", + "title": "Rails is omakase", + "author", "1", + "comments": [ "1" ] + }], + "comments": [{ + "id": "1", + "body": "FIRST" + }], + "users": [{ + "id": "1", + "name": "@d2h" + }] + } + ``` + + It will first normalize the payload, so you can use this to push + in data streaming in from your server structured the same way + that fetches and saves are structured. + + @method pushPayload + @param {DS.Store} store + @param {Object} payload + */ + pushPayload: function(store, payload) { + payload = this.normalizePayload(payload); + + for (var prop in payload) { + var typeName = this.typeForRoot(prop), + type = store.modelFor(typeName), + typeSerializer = store.serializerFor(type); + + /*jshint loopfunc:true*/ + var normalizedArray = map.call(Ember.makeArray(payload[prop]), function(hash) { + return typeSerializer.normalize(type, hash, prop); + }, this); + + store.pushMany(typeName, normalizedArray); + } + }, + + /** + This method is used to convert each JSON root key in the payload + into a typeKey that it can use to look up the appropriate model for + that part of the payload. By default the typeKey for a model is its + name in camelCase, so if your JSON root key is 'fast-car' you would + use typeForRoot to convert it to 'fastCar' so that Ember Data finds + the `FastCar` model. + + If you diverge from this norm you should also consider changes to + store._normalizeTypeKey as well. + + For example, your server may return prefixed root keys like so: + + ```js + { + "response-fast-car": { + "id": "1", + "name": "corvette" + } + } + ``` + + In order for Ember Data to know that the model corresponding to + the 'response-fast-car' hash is `FastCar` (typeKey: 'fastCar'), + you can override typeForRoot to convert 'response-fast-car' to + 'fastCar' like so: + + ```js + App.ApplicationSerializer = DS.RESTSerializer.extend({ + typeForRoot: function(root) { + // 'response-fast-car' should become 'fast-car' + var subRoot = root.substring(9); + + // _super normalizes 'fast-car' to 'fastCar' + return this._super(subRoot); + } + }); + ``` + + @method typeForRoot + @param {String} key + @return {String} the model's typeKey + */ + typeForRoot: function(key) { + return camelize(singularize(key)); + }, + + // SERIALIZE + + /** + Called when a record is saved in order to convert the + record into JSON. + + By default, it creates a JSON object with a key for + each attribute and belongsTo relationship. + + For example, consider this model: + + ```js + App.Comment = DS.Model.extend({ + title: DS.attr(), + body: DS.attr(), + + author: DS.belongsTo('user') + }); + ``` + + The default serialization would create a JSON object like: + + ```js + { + "title": "Rails is unagi", + "body": "Rails? Omakase? O_O", + "author": 12 + } + ``` + + By default, attributes are passed through as-is, unless + you specified an attribute type (`DS.attr('date')`). If + you specify a transform, the JavaScript value will be + serialized when inserted into the JSON hash. + + By default, belongs-to relationships are converted into + IDs when inserted into the JSON hash. + + ## IDs + + `serialize` takes an options hash with a single option: + `includeId`. If this option is `true`, `serialize` will, + by default include the ID in the JSON object it builds. + + The adapter passes in `includeId: true` when serializing + a record for `createRecord`, but not for `updateRecord`. + + ## Customization + + Your server may expect a different JSON format than the + built-in serialization format. + + In that case, you can implement `serialize` yourself and + return a JSON hash of your choosing. + + ```js + App.PostSerializer = DS.RESTSerializer.extend({ + serialize: function(post, options) { + var json = { + POST_TTL: post.get('title'), + POST_BDY: post.get('body'), + POST_CMS: post.get('comments').mapProperty('id') + } + + if (options.includeId) { + json.POST_ID_ = post.get('id'); + } + + return json; + } + }); + ``` + + ## Customizing an App-Wide Serializer + + If you want to define a serializer for your entire + application, you'll probably want to use `eachAttribute` + and `eachRelationship` on the record. + + ```js + App.ApplicationSerializer = DS.RESTSerializer.extend({ + serialize: function(record, options) { + var json = {}; + + record.eachAttribute(function(name) { + json[serverAttributeName(name)] = record.get(name); + }) + + record.eachRelationship(function(name, relationship) { + if (relationship.kind === 'hasMany') { + json[serverHasManyName(name)] = record.get(name).mapBy('id'); + } + }); + + if (options.includeId) { + json.ID_ = record.get('id'); + } + + return json; + } + }); + + function serverAttributeName(attribute) { + return attribute.underscore().toUpperCase(); + } + + function serverHasManyName(name) { + return serverAttributeName(name.singularize()) + "_IDS"; + } + ``` + + This serializer will generate JSON that looks like this: + + ```js + { + "TITLE": "Rails is omakase", + "BODY": "Yep. Omakase.", + "COMMENT_IDS": [ 1, 2, 3 ] + } + ``` + + ## Tweaking the Default JSON + + If you just want to do some small tweaks on the default JSON, + you can call super first and make the tweaks on the returned + JSON. + + ```js + App.PostSerializer = DS.RESTSerializer.extend({ + serialize: function(record, options) { + var json = this._super(record, options); + + json.subject = json.title; + delete json.title; + + return json; + } + }); + ``` + + @method serialize + @param record + @param options + */ + serialize: function(record, options) { + return this._super.apply(this, arguments); + }, + + /** + You can use this method to customize the root keys serialized into the JSON. + By default the REST Serializer sends the typeKey of a model, whih is a camelized + version of the name. + + For example, your server may expect underscored root objects. + + ```js + App.ApplicationSerializer = DS.RESTSerializer.extend({ + serializeIntoHash: function(data, type, record, options) { + var root = Ember.String.decamelize(type.typeKey); + data[root] = this.serialize(record, options); + } + }); + ``` + + @method serializeIntoHash + @param {Object} hash + @param {subclass of DS.Model} type + @param {DS.Model} record + @param {Object} options + */ + serializeIntoHash: function(hash, type, record, options) { + hash[type.typeKey] = this.serialize(record, options); + }, + + /** + You can use this method to customize how polymorphic objects are serialized. + By default the JSON Serializer creates the key by appending `Type` to + the attribute and value from the model's camelcased model name. + + @method serializePolymorphicType + @param {DS.Model} record + @param {Object} json + @param {Object} relationship + */ + serializePolymorphicType: function(record, json, relationship) { + var key = relationship.key, + belongsTo = get(record, key); + key = this.keyForAttribute ? this.keyForAttribute(key) : key; + json[key + "Type"] = belongsTo.constructor.typeKey; + } + }); + + __exports__["default"] = RESTSerializer; + }); +define("ember-data/lib/setup-container", + ["./initializers/store","./initializers/transforms","./initializers/store_injections","./initializers/data_adapter","../../../activemodel-adapter/lib/setup-container","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __exports__) { + "use strict"; + var initializeStore = __dependency1__["default"]; + var initializeTransforms = __dependency2__["default"]; + var initializeStoreInjections = __dependency3__["default"]; + var initializeDataAdapter = __dependency4__["default"]; + var setupActiveModelContainer = __dependency5__["default"]; + + __exports__["default"] = function setupContainer(container, application){ + // application is not a required argument. This ensures + // testing setups can setup a container without booting an + // entire ember application. + + initializeDataAdapter(container, application); + initializeTransforms(container, application); + initializeStoreInjections(container, application); + initializeStore(container, application); + setupActiveModelContainer(container, application); + }; + }); +define("ember-data/lib/system/adapter", + ["exports"], + function(__exports__) { + "use strict"; + /** + @module ember-data + */ + + var get = Ember.get, set = Ember.set; + var map = Ember.ArrayPolyfills.map; + + var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; + + /** + A `DS.InvalidError` is used by an adapter to signal the external API + was unable to process a request because the content was not + semantically correct or meaningful per the API. Usually this means a + record failed some form of server side validation. When a promise + from an adapter is rejected with a `DS.InvalidError` the record will + transition to the `invalid` state and the errors will be set to the + `errors` property on the record. + + Example + + ```javascript + App.ApplicationAdapter = DS.RESTAdapter.extend({ + ajaxError: function(jqXHR) { + var error = this._super(jqXHR); + + if (jqXHR && jqXHR.status === 422) { + var jsonErrors = Ember.$.parseJSON(jqXHR.responseText)["errors"]; + return new DS.InvalidError(jsonErrors); + } else { + return error; + } + } + }); + ``` + + The `DS.InvalidError` must be constructed with a single object whose + keys are the invalid model properties, and whose values are the + corresponding error messages. For example: + + ```javascript + return new DS.InvalidError({ + length: 'Must be less than 15', + name: 'Must not be blank + }); + ``` + + @class InvalidError + @namespace DS + */ + var InvalidError = function(errors) { + var tmp = Error.prototype.constructor.call(this, "The backend rejected the commit because it was invalid: " + Ember.inspect(errors)); + this.errors = errors; + + for (var i=0, l=errorProps.length; i 0; i--) { + var proxyPair = proxyPairs[i - 1], + deprecated = proxyPair['deprecated'], + valid = proxyPair['valid']; + + this.registerDeprecation(deprecated, valid); + } + }; + + __exports__["default"] = ContainerProxy; + }); +define("ember-data/lib/system/debug", + ["./debug/debug_info","./debug/debug_adapter","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + /** + @module ember-data + */ + + var DebugAdapter = __dependency2__["default"]; + + __exports__["default"] = DebugAdapter; + }); +define("ember-data/lib/system/debug/debug_adapter", + ["../model","exports"], + function(__dependency1__, __exports__) { + "use strict"; + /** + @module ember-data + */ + var Model = __dependency1__.Model; + var get = Ember.get, capitalize = Ember.String.capitalize, underscore = Ember.String.underscore; + + /** + Extend `Ember.DataAdapter` with ED specific code. + + @class DebugAdapter + @namespace DS + @extends Ember.DataAdapter + @private + */ + var DebugAdapter = Ember.DataAdapter.extend({ + getFilters: function() { + return [ + { name: 'isNew', desc: 'New' }, + { name: 'isModified', desc: 'Modified' }, + { name: 'isClean', desc: 'Clean' } + ]; + }, + + detect: function(klass) { + return klass !== Model && Model.detect(klass); + }, + + columnsForType: function(type) { + var columns = [{ name: 'id', desc: 'Id' }], count = 0, self = this; + get(type, 'attributes').forEach(function(name, meta) { + if (count++ > self.attributeLimit) { return false; } + var desc = capitalize(underscore(name).replace('_', ' ')); + columns.push({ name: name, desc: desc }); + }); + return columns; + }, + + getRecords: function(type) { + return this.get('store').all(type); + }, + + getRecordColumnValues: function(record) { + var self = this, count = 0, + columnValues = { id: get(record, 'id') }; + + record.eachAttribute(function(key) { + if (count++ > self.attributeLimit) { + return false; + } + var value = get(record, key); + columnValues[key] = value; + }); + return columnValues; + }, + + getRecordKeywords: function(record) { + var keywords = [], keys = Ember.A(['id']); + record.eachAttribute(function(key) { + keys.push(key); + }); + keys.forEach(function(key) { + keywords.push(get(record, key)); + }); + return keywords; + }, + + getRecordFilterValues: function(record) { + return { + isNew: record.get('isNew'), + isModified: record.get('isDirty') && !record.get('isNew'), + isClean: !record.get('isDirty') + }; + }, + + getRecordColor: function(record) { + var color = 'black'; + if (record.get('isNew')) { + color = 'green'; + } else if (record.get('isDirty')) { + color = 'blue'; + } + return color; + }, + + observeRecord: function(record, recordUpdated) { + var releaseMethods = Ember.A(), self = this, + keysToObserve = Ember.A(['id', 'isNew', 'isDirty']); + + record.eachAttribute(function(key) { + keysToObserve.push(key); + }); + + keysToObserve.forEach(function(key) { + var handler = function() { + recordUpdated(self.wrapRecord(record)); + }; + Ember.addObserver(record, key, handler); + releaseMethods.push(function() { + Ember.removeObserver(record, key, handler); + }); + }); + + var release = function() { + releaseMethods.forEach(function(fn) { fn(); } ); + }; + + return release; + } + + }); + + __exports__["default"] = DebugAdapter; + }); +define("ember-data/lib/system/debug/debug_info", + ["../model","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Model = __dependency1__.Model; + + Model.reopen({ + + /** + Provides info about the model for debugging purposes + by grouping the properties into more semantic groups. + + Meant to be used by debugging tools such as the Chrome Ember Extension. + + - Groups all attributes in "Attributes" group. + - Groups all belongsTo relationships in "Belongs To" group. + - Groups all hasMany relationships in "Has Many" group. + - Groups all flags in "Flags" group. + - Flags relationship CPs as expensive properties. + + @method _debugInfo + @for DS.Model + @private + */ + _debugInfo: function() { + var attributes = ['id'], + relationships = { belongsTo: [], hasMany: [] }, + expensiveProperties = []; + + this.eachAttribute(function(name, meta) { + attributes.push(name); + }, this); + + this.eachRelationship(function(name, relationship) { + relationships[relationship.kind].push(name); + expensiveProperties.push(name); + }); + + var groups = [ + { + name: 'Attributes', + properties: attributes, + expand: true + }, + { + name: 'Belongs To', + properties: relationships.belongsTo, + expand: true + }, + { + name: 'Has Many', + properties: relationships.hasMany, + expand: true + }, + { + name: 'Flags', + properties: ['isLoaded', 'isDirty', 'isSaving', 'isDeleted', 'isError', 'isNew', 'isValid'] + } + ]; + + return { + propertyInfo: { + // include all other mixins / properties (not just the grouped ones) + includeOtherProperties: true, + groups: groups, + // don't pre-calculate unless cached + expensiveProperties: expensiveProperties + } + }; + } + }); + + __exports__["default"] = Model; + }); +define("ember-data/lib/system/model", + ["./model/model","./model/attributes","./model/states","./model/errors","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + /** + @module ember-data + */ + + var Model = __dependency1__["default"]; + var attr = __dependency2__["default"]; + var RootState = __dependency3__["default"]; + var Errors = __dependency4__["default"]; + + __exports__.Model = Model; + __exports__.RootState = RootState; + __exports__.attr = attr; + __exports__.Errors = Errors; + }); +define("ember-data/lib/system/model/attributes", + ["./model","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Model = __dependency1__["default"]; + + /** + @module ember-data + */ + + var get = Ember.get; + + /** + @class Model + @namespace DS + */ + Model.reopenClass({ + /** + A map whose keys are the attributes of the model (properties + described by DS.attr) and whose values are the meta object for the + property. + + Example + + ```javascript + + App.Person = DS.Model.extend({ + firstName: attr('string'), + lastName: attr('string'), + birthday: attr('date') + }); + + var attributes = Ember.get(App.Person, 'attributes') + + attributes.forEach(function(name, meta) { + console.log(name, meta); + }); + + // prints: + // firstName {type: "string", isAttribute: true, options: Object, parentType: function, name: "firstName"} + // lastName {type: "string", isAttribute: true, options: Object, parentType: function, name: "lastName"} + // birthday {type: "date", isAttribute: true, options: Object, parentType: function, name: "birthday"} + ``` + + @property attributes + @static + @type {Ember.Map} + @readOnly + */ + attributes: Ember.computed(function() { + var map = Ember.Map.create(); + + this.eachComputedProperty(function(name, meta) { + if (meta.isAttribute) { + Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('')` from " + this.toString(), name !== 'id'); + + meta.name = name; + map.set(name, meta); + } + }); + + return map; + }), + + /** + A map whose keys are the attributes of the model (properties + described by DS.attr) and whose values are type of transformation + applied to each attribute. This map does not include any + attributes that do not have an transformation type. + + Example + + ```javascript + App.Person = DS.Model.extend({ + firstName: attr(), + lastName: attr('string'), + birthday: attr('date') + }); + + var transformedAttributes = Ember.get(App.Person, 'transformedAttributes') + + transformedAttributes.forEach(function(field, type) { + console.log(field, type); + }); + + // prints: + // lastName string + // birthday date + ``` + + @property transformedAttributes + @static + @type {Ember.Map} + @readOnly + */ + transformedAttributes: Ember.computed(function() { + var map = Ember.Map.create(); + + this.eachAttribute(function(key, meta) { + if (meta.type) { + map.set(key, meta.type); + } + }); + + return map; + }), + + /** + Iterates through the attributes of the model, calling the passed function on each + attribute. + + The callback method you provide should have the following signature (all + parameters are optional): + + ```javascript + function(name, meta); + ``` + + - `name` the name of the current property in the iteration + - `meta` the meta object for the attribute property in the iteration + + Note that in addition to a callback, you can also pass an optional target + object that will be set as `this` on the context. + + Example + + ```javascript + App.Person = DS.Model.extend({ + firstName: attr('string'), + lastName: attr('string'), + birthday: attr('date') + }); + + App.Person.eachAttribute(function(name, meta) { + console.log(name, meta); + }); + + // prints: + // firstName {type: "string", isAttribute: true, options: Object, parentType: function, name: "firstName"} + // lastName {type: "string", isAttribute: true, options: Object, parentType: function, name: "lastName"} + // birthday {type: "date", isAttribute: true, options: Object, parentType: function, name: "birthday"} + ``` + + @method eachAttribute + @param {Function} callback The callback to execute + @param {Object} [target] The target object to use + @static + */ + eachAttribute: function(callback, binding) { + get(this, 'attributes').forEach(function(name, meta) { + callback.call(binding, name, meta); + }, binding); + }, + + /** + Iterates through the transformedAttributes of the model, calling + the passed function on each attribute. Note the callback will not be + called for any attributes that do not have an transformation type. + + The callback method you provide should have the following signature (all + parameters are optional): + + ```javascript + function(name, type); + ``` + + - `name` the name of the current property in the iteration + - `type` a string containing the name of the type of transformed + applied to the attribute + + Note that in addition to a callback, you can also pass an optional target + object that will be set as `this` on the context. + + Example + + ```javascript + App.Person = DS.Model.extend({ + firstName: attr(), + lastName: attr('string'), + birthday: attr('date') + }); + + App.Person.eachTransformedAttribute(function(name, type) { + console.log(name, type); + }); + + // prints: + // lastName string + // birthday date + ``` + + @method eachTransformedAttribute + @param {Function} callback The callback to execute + @param {Object} [target] The target object to use + @static + */ + eachTransformedAttribute: function(callback, binding) { + get(this, 'transformedAttributes').forEach(function(name, type) { + callback.call(binding, name, type); + }); + } + }); + + + Model.reopen({ + eachAttribute: function(callback, binding) { + this.constructor.eachAttribute(callback, binding); + } + }); + + function getDefaultValue(record, options, key) { + if (typeof options.defaultValue === "function") { + return options.defaultValue.apply(null, arguments); + } else { + return options.defaultValue; + } + } + + function hasValue(record, key) { + return record._attributes.hasOwnProperty(key) || + record._inFlightAttributes.hasOwnProperty(key) || + record._data.hasOwnProperty(key); + } + + function getValue(record, key) { + if (record._attributes.hasOwnProperty(key)) { + return record._attributes[key]; + } else if (record._inFlightAttributes.hasOwnProperty(key)) { + return record._inFlightAttributes[key]; + } else { + return record._data[key]; + } + } + + /** + `DS.attr` defines an attribute on a [DS.Model](/api/data/classes/DS.Model.html). + By default, attributes are passed through as-is, however you can specify an + optional type to have the value automatically transformed. + Ember Data ships with four basic transform types: `string`, `number`, + `boolean` and `date`. You can define your own transforms by subclassing + [DS.Transform](/api/data/classes/DS.Transform.html). + + Note that you cannot use `attr` to define an attribute of `id`. + + `DS.attr` takes an optional hash as a second parameter, currently + supported options are: + + - `defaultValue`: Pass a string or a function to be called to set the attribute + to a default value if none is supplied. + + Example + + ```javascript + var attr = DS.attr; + + App.User = DS.Model.extend({ + username: attr('string'), + email: attr('string'), + verified: attr('boolean', {defaultValue: false}) + }); + ``` + + @namespace + @method attr + @for DS + @param {String} type the attribute type + @param {Object} options a hash of options + @return {Attribute} + */ + + function attr(type, options) { + options = options || {}; + + var meta = { + type: type, + isAttribute: true, + options: options + }; + + return Ember.computed('data', function(key, value) { + if (arguments.length > 1) { + Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('')` from " + this.constructor.toString(), key !== 'id'); + var oldValue = getValue(this, key); + + if (value !== oldValue) { + // Add the new value to the changed attributes hash; it will get deleted by + // the 'didSetProperty' handler if it is no different from the original value + this._attributes[key] = value; + + this.send('didSetProperty', { + name: key, + oldValue: oldValue, + originalValue: this._data[key], + value: value + }); + } + + return value; + } else if (hasValue(this, key)) { + return getValue(this, key); + } else { + return getDefaultValue(this, options, key); + } + + // `data` is never set directly. However, it may be + // invalidated from the state manager's setData + // event. + }).meta(meta); + } + + __exports__["default"] = attr; + }); +define("ember-data/lib/system/model/errors", + ["exports"], + function(__exports__) { + "use strict"; + var get = Ember.get, isEmpty = Ember.isEmpty; + var map = Ember.EnumerableUtils.map; + + /** + @module ember-data + */ + + /** + Holds validation errors for a given record organized by attribute names. + + Every DS.Model has an `errors` property that is an instance of + `DS.Errors`. This can be used to display validation error + messages returned from the server when a `record.save()` rejects. + + For Example, if you had an `User` model that looked like this: + + ```javascript + App.User = DS.Model.extend({ + username: attr('string'), + email: attr('string') + }); + ``` + And you attempted to save a record that did not validate on the backend. + + ```javascript + var user = store.createRecord('user', { + username: 'tomster', + email: 'invalidEmail' + }); + user.save(); + ``` + + Your backend data store might return a response that looks like + this. This response will be used to populate the error object. + + ```javascript + { + "errors": { + "username": ["This username is already taken!"], + "email": ["Doesn't look like a valid email."] + } + } + ``` + + Errors can be displayed to the user by accessing their property name + or using the `messages` property to get an array of all errors. + + ```handlebars + {{#each errors.messages}} +
    + {{message}} +
    + {{/each}} + + + {{#each errors.username}} +
    + {{message}} +
    + {{/each}} + + + {{#each errors.email}} +
    + {{message}} +
    + {{/each}} + ``` + + @class Errors + @namespace DS + @extends Ember.Object + @uses Ember.Enumerable + @uses Ember.Evented + */ + var Errors = Ember.Object.extend(Ember.Enumerable, Ember.Evented, { + /** + Register with target handler + + @method registerHandlers + @param {Object} target + @param {Function} becameInvalid + @param {Function} becameValid + */ + registerHandlers: function(target, becameInvalid, becameValid) { + this.on('becameInvalid', target, becameInvalid); + this.on('becameValid', target, becameValid); + }, + + /** + @property errorsByAttributeName + @type {Ember.MapWithDefault} + @private + */ + errorsByAttributeName: Ember.reduceComputed("content", { + initialValue: function() { + return Ember.MapWithDefault.create({ + defaultValue: function() { + return Ember.A(); + } + }); + }, + + addedItem: function(errors, error) { + errors.get(error.attribute).pushObject(error); + + return errors; + }, + + removedItem: function(errors, error) { + errors.get(error.attribute).removeObject(error); + + return errors; + } + }), + + /** + Returns errors for a given attribute + + ```javascript + var user = store.createRecord('user', { + username: 'tomster', + email: 'invalidEmail' + }); + user.save().catch(function(){ + user.get('errors').errorsFor('email'); // ["Doesn't look like a valid email."] + }); + ``` + + @method errorsFor + @param {String} attribute + @return {Array} + */ + errorsFor: function(attribute) { + return get(this, 'errorsByAttributeName').get(attribute); + }, + + /** + An array containing all of the error messages for this + record. This is useful for displaying all errors to the user. + + ```handlebars + {{#each errors.messages}} +
    + {{message}} +
    + {{/each}} + ``` + + @property messages + @type {Array} + */ + messages: Ember.computed.mapBy('content', 'message'), + + /** + @property content + @type {Array} + @private + */ + content: Ember.computed(function() { + return Ember.A(); + }), + + /** + @method unknownProperty + @private + */ + unknownProperty: function(attribute) { + var errors = this.errorsFor(attribute); + if (isEmpty(errors)) { return null; } + return errors; + }, + + /** + @method nextObject + @private + */ + nextObject: function(index, previousObject, context) { + return get(this, 'content').objectAt(index); + }, + + /** + Total number of errors. + + @property length + @type {Number} + @readOnly + */ + length: Ember.computed.oneWay('content.length').readOnly(), + + /** + @property isEmpty + @type {Boolean} + @readOnly + */ + isEmpty: Ember.computed.not('length').readOnly(), + + /** + Adds error messages to a given attribute and sends + `becameInvalid` event to the record. + + Example: + + ```javascript + if (!user.get('username') { + user.get('errors').add('username', 'This field is required'); + } + ``` + + @method add + @param {String} attribute + @param {Array|String} messages + */ + add: function(attribute, messages) { + var wasEmpty = get(this, 'isEmpty'); + + messages = this._findOrCreateMessages(attribute, messages); + get(this, 'content').addObjects(messages); + + this.notifyPropertyChange(attribute); + this.enumerableContentDidChange(); + + if (wasEmpty && !get(this, 'isEmpty')) { + this.trigger('becameInvalid'); + } + }, + + /** + @method _findOrCreateMessages + @private + */ + _findOrCreateMessages: function(attribute, messages) { + var errors = this.errorsFor(attribute); + + return map(Ember.makeArray(messages), function(message) { + return errors.findBy('message', message) || { + attribute: attribute, + message: message + }; + }); + }, + + /** + Removes all error messages from the given attribute and sends + `becameValid` event to the record if there no more errors left. + + Example: + + ```javascript + App.User = DS.Model.extend({ + email: DS.attr('string'), + twoFactorAuth: DS.attr('boolean'), + phone: DS.attr('string') + }); + + App.UserEditRoute = Ember.Route.extend({ + actions: { + save: function(user) { + if (!user.get('twoFactorAuth')) { + user.get('errors').remove('phone'); + } + user.save(); + } + } + }); + ``` + + @method remove + @param {String} attribute + */ + remove: function(attribute) { + if (get(this, 'isEmpty')) { return; } + + var content = get(this, 'content').rejectBy('attribute', attribute); + get(this, 'content').setObjects(content); + + this.notifyPropertyChange(attribute); + this.enumerableContentDidChange(); + + if (get(this, 'isEmpty')) { + this.trigger('becameValid'); + } + }, + + /** + Removes all error messages and sends `becameValid` event + to the record. + + Example: + + ```javascript + App.UserEditRoute = Ember.Route.extend({ + actions: { + retrySave: function(user) { + user.get('errors').clear(); + user.save(); + } + } + }); + ``` + + @method clear + */ + clear: function() { + if (get(this, 'isEmpty')) { return; } + + get(this, 'content').clear(); + this.enumerableContentDidChange(); + + this.trigger('becameValid'); + }, + + /** + Checks if there is error messages for the given attribute. + + ```javascript + App.UserEditRoute = Ember.Route.extend({ + actions: { + save: function(user) { + if (user.get('errors').has('email')) { + return alert('Please update your email before attempting to save.'); + } + user.save(); + } + } + }); + ``` + + @method has + @param {String} attribute + @return {Boolean} true if there some errors on given attribute + */ + has: function(attribute) { + return !isEmpty(this.errorsFor(attribute)); + } + }); + + __exports__["default"] = Errors; + }); +define("ember-data/lib/system/model/model", + ["./states","./errors","../store","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var RootState = __dependency1__["default"]; + var Errors = __dependency2__["default"]; + var PromiseObject = __dependency3__.PromiseObject; + /** + @module ember-data + */ + + var get = Ember.get, set = Ember.set, + merge = Ember.merge, + Promise = Ember.RSVP.Promise; + + var JSONSerializer; + var retrieveFromCurrentState = Ember.computed('currentState', function(key, value) { + return get(get(this, 'currentState'), key); + }).readOnly(); + + /** + + The model class that all Ember Data records descend from. + + @class Model + @namespace DS + @extends Ember.Object + @uses Ember.Evented + */ + var Model = Ember.Object.extend(Ember.Evented, { + _recordArrays: undefined, + _relationships: undefined, + _loadingRecordArrays: undefined, + /** + If this property is `true` the record is in the `empty` + state. Empty is the first state all records enter after they have + been created. Most records created by the store will quickly + transition to the `loading` state if data needs to be fetched from + the server or the `created` state if the record is created on the + client. A record can also enter the empty state if the adapter is + unable to locate the record. + + @property isEmpty + @type {Boolean} + @readOnly + */ + isEmpty: retrieveFromCurrentState, + /** + If this property is `true` the record is in the `loading` state. A + record enters this state when the store asks the adapter for its + data. It remains in this state until the adapter provides the + requested data. + + @property isLoading + @type {Boolean} + @readOnly + */ + isLoading: retrieveFromCurrentState, + /** + If this property is `true` the record is in the `loaded` state. A + record enters this state when its data is populated. Most of a + record's lifecycle is spent inside substates of the `loaded` + state. + + Example + + ```javascript + var record = store.createRecord('model'); + record.get('isLoaded'); // true + + store.find('model', 1).then(function(model) { + model.get('isLoaded'); // true + }); + ``` + + @property isLoaded + @type {Boolean} + @readOnly + */ + isLoaded: retrieveFromCurrentState, + /** + If this property is `true` the record is in the `dirty` state. The + record has local changes that have not yet been saved by the + adapter. This includes records that have been created (but not yet + saved) or deleted. + + Example + + ```javascript + var record = store.createRecord('model'); + record.get('isDirty'); // true + + store.find('model', 1).then(function(model) { + model.get('isDirty'); // false + model.set('foo', 'some value'); + model.get('isDirty'); // true + }); + ``` + + @property isDirty + @type {Boolean} + @readOnly + */ + isDirty: retrieveFromCurrentState, + /** + If this property is `true` the record is in the `saving` state. A + record enters the saving state when `save` is called, but the + adapter has not yet acknowledged that the changes have been + persisted to the backend. + + Example + + ```javascript + var record = store.createRecord('model'); + record.get('isSaving'); // false + var promise = record.save(); + record.get('isSaving'); // true + promise.then(function() { + record.get('isSaving'); // false + }); + ``` + + @property isSaving + @type {Boolean} + @readOnly + */ + isSaving: retrieveFromCurrentState, + /** + If this property is `true` the record is in the `deleted` state + and has been marked for deletion. When `isDeleted` is true and + `isDirty` is true, the record is deleted locally but the deletion + was not yet persisted. When `isSaving` is true, the change is + in-flight. When both `isDirty` and `isSaving` are false, the + change has persisted. + + Example + + ```javascript + var record = store.createRecord('model'); + record.get('isDeleted'); // false + record.deleteRecord(); + + // Locally deleted + record.get('isDeleted'); // true + record.get('isDirty'); // true + record.get('isSaving'); // false + + // Persisting the deletion + var promise = record.save(); + record.get('isDeleted'); // true + record.get('isSaving'); // true + + // Deletion Persisted + promise.then(function() { + record.get('isDeleted'); // true + record.get('isSaving'); // false + record.get('isDirty'); // false + }); + ``` + + @property isDeleted + @type {Boolean} + @readOnly + */ + isDeleted: retrieveFromCurrentState, + /** + If this property is `true` the record is in the `new` state. A + record will be in the `new` state when it has been created on the + client and the adapter has not yet report that it was successfully + saved. + + Example + + ```javascript + var record = store.createRecord('model'); + record.get('isNew'); // true + + record.save().then(function(model) { + model.get('isNew'); // false + }); + ``` + + @property isNew + @type {Boolean} + @readOnly + */ + isNew: retrieveFromCurrentState, + /** + If this property is `true` the record is in the `valid` state. + + A record will be in the `valid` state when the adapter did not report any + server-side validation failures. + + @property isValid + @type {Boolean} + @readOnly + */ + isValid: retrieveFromCurrentState, + /** + If the record is in the dirty state this property will report what + kind of change has caused it to move into the dirty + state. Possible values are: + + - `created` The record has been created by the client and not yet saved to the adapter. + - `updated` The record has been updated by the client and not yet saved to the adapter. + - `deleted` The record has been deleted by the client and not yet saved to the adapter. + + Example + + ```javascript + var record = store.createRecord('model'); + record.get('dirtyType'); // 'created' + ``` + + @property dirtyType + @type {String} + @readOnly + */ + dirtyType: retrieveFromCurrentState, + + /** + If `true` the adapter reported that it was unable to save local + changes to the backend for any reason other than a server-side + validation error. + + Example + + ```javascript + record.get('isError'); // false + record.set('foo', 'valid value'); + record.save().then(null, function() { + record.get('isError'); // true + }); + ``` + + @property isError + @type {Boolean} + @readOnly + */ + isError: false, + /** + If `true` the store is attempting to reload the record form the adapter. + + Example + + ```javascript + record.get('isReloading'); // false + record.reload(); + record.get('isReloading'); // true + ``` + + @property isReloading + @type {Boolean} + @readOnly + */ + isReloading: false, + + /** + The `clientId` property is a transient numerical identifier + generated at runtime by the data store. It is important + primarily because newly created objects may not yet have an + externally generated id. + + @property clientId + @private + @type {Number|String} + */ + clientId: null, + /** + All ember models have an id property. This is an identifier + managed by an external source. These are always coerced to be + strings before being used internally. Note when declaring the + attributes for a model it is an error to declare an id + attribute. + + ```javascript + var record = store.createRecord('model'); + record.get('id'); // null + + store.find('model', 1).then(function(model) { + model.get('id'); // '1' + }); + ``` + + @property id + @type {String} + */ + id: null, + + /** + @property currentState + @private + @type {Object} + */ + currentState: RootState.empty, + + /** + When the record is in the `invalid` state this object will contain + any errors returned by the adapter. When present the errors hash + typically contains keys corresponding to the invalid property names + and values which are an array of error messages. + + ```javascript + record.get('errors.length'); // 0 + record.set('foo', 'invalid value'); + record.save().then(null, function() { + record.get('errors').get('foo'); // ['foo should be a number.'] + }); + ``` + + @property errors + @type {DS.Errors} + */ + errors: Ember.computed(function() { + var errors = Errors.create(); + + errors.registerHandlers(this, function() { + this.send('becameInvalid'); + }, function() { + this.send('becameValid'); + }); + + return errors; + }).readOnly(), + + /** + Create a JSON representation of the record, using the serialization + strategy of the store's adapter. + + `serialize` takes an optional hash as a parameter, currently + supported options are: + + - `includeId`: `true` if the record's ID should be included in the + JSON representation. + + @method serialize + @param {Object} options + @return {Object} an object whose values are primitive JSON values only + */ + serialize: function(options) { + var store = get(this, 'store'); + return store.serialize(this, options); + }, + + /** + Use [DS.JSONSerializer](DS.JSONSerializer.html) to + get the JSON representation of a record. + + `toJSON` takes an optional hash as a parameter, currently + supported options are: + + - `includeId`: `true` if the record's ID should be included in the + JSON representation. + + @method toJSON + @param {Object} options + @return {Object} A JSON representation of the object. + */ + toJSON: function(options) { + if (!JSONSerializer) { JSONSerializer = requireModule("ember-data/lib/serializers/json_serializer")["default"]; } + // container is for lazy transform lookups + var serializer = JSONSerializer.create({ container: this.container }); + return serializer.serialize(this, options); + }, + + /** + Fired when the record is loaded from the server. + + @event didLoad + */ + didLoad: Ember.K, + + /** + Fired when the record is updated. + + @event didUpdate + */ + didUpdate: Ember.K, + + /** + Fired when the record is created. + + @event didCreate + */ + didCreate: Ember.K, + + /** + Fired when the record is deleted. + + @event didDelete + */ + didDelete: Ember.K, + + /** + Fired when the record becomes invalid. + + @event becameInvalid + */ + becameInvalid: Ember.K, + + /** + Fired when the record enters the error state. + + @event becameError + */ + becameError: Ember.K, + + /** + @property data + @private + @type {Object} + */ + data: Ember.computed(function() { + this._data = this._data || {}; + return this._data; + }).readOnly(), + + _data: null, + + init: function() { + this._super(); + this._setup(); + }, + + _setup: function() { + this._changesToSync = {}; + this._deferredTriggers = []; + this._data = {}; + this._attributes = {}; + this._inFlightAttributes = {}; + this._relationships = {}; + }, + + /** + @method send + @private + @param {String} name + @param {Object} context + */ + send: function(name, context) { + var currentState = get(this, 'currentState'); + + if (!currentState[name]) { + this._unhandledEvent(currentState, name, context); + } + + return currentState[name](this, context); + }, + + /** + @method transitionTo + @private + @param {String} name + */ + transitionTo: function(name) { + // POSSIBLE TODO: Remove this code and replace with + // always having direct references to state objects + + var pivotName = name.split(".", 1), + currentState = get(this, 'currentState'), + state = currentState; + + do { + if (state.exit) { state.exit(this); } + state = state.parentState; + } while (!state.hasOwnProperty(pivotName)); + + var path = name.split("."); + + var setups = [], enters = [], i, l; + + for (i=0, l=path.length; i "root.created.uncommitted" + ``` + + The hierarchy of valid states that ship with ember data looks like + this: + + ```text + * root + * deleted + * saved + * uncommitted + * inFlight + * empty + * loaded + * created + * uncommitted + * inFlight + * saved + * updated + * uncommitted + * inFlight + * loading + ``` + + The `DS.Model` states are themselves stateless. What we mean is + that, the hierarchical states that each of *those* points to is a + shared data structure. For performance reasons, instead of each + record getting its own copy of the hierarchy of states, each record + points to this global, immutable shared instance. How does a state + know which record it should be acting on? We pass the record + instance into the state's event handlers as the first argument. + + The record passed as the first parameter is where you should stash + state about the record if needed; you should never store data on the state + object itself. + + ### Events and Flags + + A state may implement zero or more events and flags. + + #### Events + + Events are named functions that are invoked when sent to a record. The + record will first look for a method with the given name on the + current state. If no method is found, it will search the current + state's parent, and then its grandparent, and so on until reaching + the top of the hierarchy. If the root is reached without an event + handler being found, an exception will be raised. This can be very + helpful when debugging new features. + + Here's an example implementation of a state with a `myEvent` event handler: + + ```javascript + aState: DS.State.create({ + myEvent: function(manager, param) { + console.log("Received myEvent with", param); + } + }) + ``` + + To trigger this event: + + ```javascript + record.send('myEvent', 'foo'); + //=> "Received myEvent with foo" + ``` + + Note that an optional parameter can be sent to a record's `send()` method, + which will be passed as the second parameter to the event handler. + + Events should transition to a different state if appropriate. This can be + done by calling the record's `transitionTo()` method with a path to the + desired state. The state manager will attempt to resolve the state path + relative to the current state. If no state is found at that path, it will + attempt to resolve it relative to the current state's parent, and then its + parent, and so on until the root is reached. For example, imagine a hierarchy + like this: + + * created + * uncommitted <-- currentState + * inFlight + * updated + * inFlight + + If we are currently in the `uncommitted` state, calling + `transitionTo('inFlight')` would transition to the `created.inFlight` state, + while calling `transitionTo('updated.inFlight')` would transition to + the `updated.inFlight` state. + + Remember that *only events* should ever cause a state transition. You should + never call `transitionTo()` from outside a state's event handler. If you are + tempted to do so, create a new event and send that to the state manager. + + #### Flags + + Flags are Boolean values that can be used to introspect a record's current + state in a more user-friendly way than examining its state path. For example, + instead of doing this: + + ```javascript + var statePath = record.get('stateManager.currentPath'); + if (statePath === 'created.inFlight') { + doSomething(); + } + ``` + + You can say: + + ```javascript + if (record.get('isNew') && record.get('isSaving')) { + doSomething(); + } + ``` + + If your state does not set a value for a given flag, the value will + be inherited from its parent (or the first place in the state hierarchy + where it is defined). + + The current set of flags are defined below. If you want to add a new flag, + in addition to the area below, you will also need to declare it in the + `DS.Model` class. + + + * [isEmpty](DS.Model.html#property_isEmpty) + * [isLoading](DS.Model.html#property_isLoading) + * [isLoaded](DS.Model.html#property_isLoaded) + * [isDirty](DS.Model.html#property_isDirty) + * [isSaving](DS.Model.html#property_isSaving) + * [isDeleted](DS.Model.html#property_isDeleted) + * [isNew](DS.Model.html#property_isNew) + * [isValid](DS.Model.html#property_isValid) + + @namespace DS + @class RootState + */ + + function hasDefinedProperties(object) { + // Ignore internal property defined by simulated `Ember.create`. + var names = Ember.keys(object); + var i, l, name; + for (i = 0, l = names.length; i < l; i++ ) { + name = names[i]; + if (object.hasOwnProperty(name) && object[name]) { return true; } + } + + return false; + } + + function didSetProperty(record, context) { + if (context.value === context.originalValue) { + delete record._attributes[context.name]; + record.send('propertyWasReset', context.name); + } else if (context.value !== context.oldValue) { + record.send('becomeDirty'); + } + + record.updateRecordArraysLater(); + } + + // Implementation notes: + // + // Each state has a boolean value for all of the following flags: + // + // * isLoaded: The record has a populated `data` property. When a + // record is loaded via `store.find`, `isLoaded` is false + // until the adapter sets it. When a record is created locally, + // its `isLoaded` property is always true. + // * isDirty: The record has local changes that have not yet been + // saved by the adapter. This includes records that have been + // created (but not yet saved) or deleted. + // * isSaving: The record has been committed, but + // the adapter has not yet acknowledged that the changes have + // been persisted to the backend. + // * isDeleted: The record was marked for deletion. When `isDeleted` + // is true and `isDirty` is true, the record is deleted locally + // but the deletion was not yet persisted. When `isSaving` is + // true, the change is in-flight. When both `isDirty` and + // `isSaving` are false, the change has persisted. + // * isError: The adapter reported that it was unable to save + // local changes to the backend. This may also result in the + // record having its `isValid` property become false if the + // adapter reported that server-side validations failed. + // * isNew: The record was created on the client and the adapter + // did not yet report that it was successfully saved. + // * isValid: The adapter did not report any server-side validation + // failures. + + // The dirty state is a abstract state whose functionality is + // shared between the `created` and `updated` states. + // + // The deleted state shares the `isDirty` flag with the + // subclasses of `DirtyState`, but with a very different + // implementation. + // + // Dirty states have three child states: + // + // `uncommitted`: the store has not yet handed off the record + // to be saved. + // `inFlight`: the store has handed off the record to be saved, + // but the adapter has not yet acknowledged success. + // `invalid`: the record has invalid information and cannot be + // send to the adapter yet. + var DirtyState = { + initialState: 'uncommitted', + + // FLAGS + isDirty: true, + + // SUBSTATES + + // When a record first becomes dirty, it is `uncommitted`. + // This means that there are local pending changes, but they + // have not yet begun to be saved, and are not invalid. + uncommitted: { + // EVENTS + didSetProperty: didSetProperty, + + propertyWasReset: function(record, name) { + var stillDirty = false; + + for (var prop in record._attributes) { + stillDirty = true; + break; + } + + if (!stillDirty) { record.send('rolledBack'); } + }, + + pushedData: Ember.K, + + becomeDirty: Ember.K, + + willCommit: function(record) { + record.transitionTo('inFlight'); + }, + + reloadRecord: function(record, resolve) { + resolve(get(record, 'store').reloadRecord(record)); + }, + + rolledBack: function(record) { + record.transitionTo('loaded.saved'); + }, + + becameInvalid: function(record) { + record.transitionTo('invalid'); + }, + + rollback: function(record) { + record.rollback(); + } + }, + + // Once a record has been handed off to the adapter to be + // saved, it is in the 'in flight' state. Changes to the + // record cannot be made during this window. + inFlight: { + // FLAGS + isSaving: true, + + // EVENTS + didSetProperty: didSetProperty, + becomeDirty: Ember.K, + pushedData: Ember.K, + + unloadRecord: function(record) { + Ember.assert("You can only unload a record which is not inFlight. `" + Ember.inspect(record) + " `", false); + }, + + // TODO: More robust semantics around save-while-in-flight + willCommit: Ember.K, + + didCommit: function(record) { + var dirtyType = get(this, 'dirtyType'); + + record.transitionTo('saved'); + record.send('invokeLifecycleCallbacks', dirtyType); + }, + + becameInvalid: function(record) { + record.transitionTo('invalid'); + record.send('invokeLifecycleCallbacks'); + }, + + becameError: function(record) { + record.transitionTo('uncommitted'); + record.triggerLater('becameError', record); + } + }, + + // A record is in the `invalid` if the adapter has indicated + // the the record failed server-side invalidations. + invalid: { + // FLAGS + isValid: false, + + // EVENTS + deleteRecord: function(record) { + record.transitionTo('deleted.uncommitted'); + record.clearRelationships(); + }, + + didSetProperty: function(record, context) { + get(record, 'errors').remove(context.name); + + didSetProperty(record, context); + }, + + becomeDirty: Ember.K, + + willCommit: function(record) { + get(record, 'errors').clear(); + record.transitionTo('inFlight'); + }, + + rolledBack: function(record) { + get(record, 'errors').clear(); + }, + + becameValid: function(record) { + record.transitionTo('uncommitted'); + }, + + invokeLifecycleCallbacks: function(record) { + record.triggerLater('becameInvalid', record); + }, + + exit: function(record) { + record._inFlightAttributes = {}; + } + } + }; + + // The created and updated states are created outside the state + // chart so we can reopen their substates and add mixins as + // necessary. + + function deepClone(object) { + var clone = {}, value; + + for (var prop in object) { + value = object[prop]; + if (value && typeof value === 'object') { + clone[prop] = deepClone(value); + } else { + clone[prop] = value; + } + } + + return clone; + } + + function mixin(original, hash) { + for (var prop in hash) { + original[prop] = hash[prop]; + } + + return original; + } + + function dirtyState(options) { + var newState = deepClone(DirtyState); + return mixin(newState, options); + } + + var createdState = dirtyState({ + dirtyType: 'created', + // FLAGS + isNew: true + }); + + createdState.uncommitted.rolledBack = function(record) { + record.transitionTo('deleted.saved'); + }; + + var updatedState = dirtyState({ + dirtyType: 'updated' + }); + + createdState.uncommitted.deleteRecord = function(record) { + record.clearRelationships(); + record.transitionTo('deleted.saved'); + }; + + createdState.uncommitted.rollback = function(record) { + DirtyState.uncommitted.rollback.apply(this, arguments); + record.transitionTo('deleted.saved'); + }; + + createdState.uncommitted.propertyWasReset = Ember.K; + + function assertAgainstUnloadRecord(record) { + Ember.assert("You can only unload a record which is not inFlight. `" + Ember.inspect(record) + "`", false); + } + + updatedState.inFlight.unloadRecord = assertAgainstUnloadRecord; + + updatedState.uncommitted.deleteRecord = function(record) { + record.transitionTo('deleted.uncommitted'); + record.clearRelationships(); + }; + + var RootState = { + // FLAGS + isEmpty: false, + isLoading: false, + isLoaded: false, + isDirty: false, + isSaving: false, + isDeleted: false, + isNew: false, + isValid: true, + + // DEFAULT EVENTS + + // Trying to roll back if you're not in the dirty state + // doesn't change your state. For example, if you're in the + // in-flight state, rolling back the record doesn't move + // you out of the in-flight state. + rolledBack: Ember.K, + unloadRecord: function(record) { + // clear relationships before moving to deleted state + // otherwise it fails + record.clearRelationships(); + record.transitionTo('deleted.saved'); + }, + + + propertyWasReset: Ember.K, + + // SUBSTATES + + // A record begins its lifecycle in the `empty` state. + // If its data will come from the adapter, it will + // transition into the `loading` state. Otherwise, if + // the record is being created on the client, it will + // transition into the `created` state. + empty: { + isEmpty: true, + + // EVENTS + loadingData: function(record, promise) { + record._loadingPromise = promise; + record.transitionTo('loading'); + }, + + loadedData: function(record) { + record.transitionTo('loaded.created.uncommitted'); + + record.suspendRelationshipObservers(function() { + record.notifyPropertyChange('data'); + }); + }, + + pushedData: function(record) { + record.transitionTo('loaded.saved'); + record.triggerLater('didLoad'); + } + }, + + // A record enters this state when the store asks + // the adapter for its data. It remains in this state + // until the adapter provides the requested data. + // + // Usually, this process is asynchronous, using an + // XHR to retrieve the data. + loading: { + // FLAGS + isLoading: true, + + exit: function(record) { + record._loadingPromise = null; + }, + + // EVENTS + pushedData: function(record) { + record.transitionTo('loaded.saved'); + record.triggerLater('didLoad'); + set(record, 'isError', false); + }, + + becameError: function(record) { + record.triggerLater('becameError', record); + }, + + notFound: function(record) { + record.transitionTo('empty'); + } + }, + + // A record enters this state when its data is populated. + // Most of a record's lifecycle is spent inside substates + // of the `loaded` state. + loaded: { + initialState: 'saved', + + // FLAGS + isLoaded: true, + + // SUBSTATES + + // If there are no local changes to a record, it remains + // in the `saved` state. + saved: { + setup: function(record) { + var attrs = record._attributes, + isDirty = false; + + for (var prop in attrs) { + if (attrs.hasOwnProperty(prop)) { + isDirty = true; + break; + } + } + + if (isDirty) { + record.adapterDidDirty(); + } + }, + + // EVENTS + didSetProperty: didSetProperty, + + pushedData: Ember.K, + + becomeDirty: function(record) { + record.transitionTo('updated.uncommitted'); + }, + + willCommit: function(record) { + record.transitionTo('updated.inFlight'); + }, + + reloadRecord: function(record, resolve) { + resolve(get(record, 'store').reloadRecord(record)); + }, + + deleteRecord: function(record) { + record.transitionTo('deleted.uncommitted'); + record.clearRelationships(); + }, + + unloadRecord: function(record) { + // clear relationships before moving to deleted state + // otherwise it fails + record.clearRelationships(); + record.transitionTo('deleted.saved'); + }, + + didCommit: function(record) { + record.send('invokeLifecycleCallbacks', get(record, 'lastDirtyType')); + }, + + // loaded.saved.notFound would be triggered by a failed + // `reload()` on an unchanged record + notFound: Ember.K + + }, + + // A record is in this state after it has been locally + // created but before the adapter has indicated that + // it has been saved. + created: createdState, + + // A record is in this state if it has already been + // saved to the server, but there are new local changes + // that have not yet been saved. + updated: updatedState + }, + + // A record is in this state if it was deleted from the store. + deleted: { + initialState: 'uncommitted', + dirtyType: 'deleted', + + // FLAGS + isDeleted: true, + isLoaded: true, + isDirty: true, + + // TRANSITIONS + setup: function(record) { + record.updateRecordArrays(); + }, + + // SUBSTATES + + // When a record is deleted, it enters the `start` + // state. It will exit this state when the record + // starts to commit. + uncommitted: { + + // EVENTS + + willCommit: function(record) { + record.transitionTo('inFlight'); + }, + + rollback: function(record) { + record.rollback(); + }, + + becomeDirty: Ember.K, + deleteRecord: Ember.K, + + rolledBack: function(record) { + record.transitionTo('loaded.saved'); + } + }, + + // After a record starts committing, but + // before the adapter indicates that the deletion + // has saved to the server, a record is in the + // `inFlight` substate of `deleted`. + inFlight: { + // FLAGS + isSaving: true, + + // EVENTS + + unloadRecord: assertAgainstUnloadRecord, + + // TODO: More robust semantics around save-while-in-flight + willCommit: Ember.K, + didCommit: function(record) { + record.transitionTo('saved'); + + record.send('invokeLifecycleCallbacks'); + }, + + becameError: function(record) { + record.transitionTo('uncommitted'); + record.triggerLater('becameError', record); + } + }, + + // Once the adapter indicates that the deletion has + // been saved, the record enters the `saved` substate + // of `deleted`. + saved: { + // FLAGS + isDirty: false, + + setup: function(record) { + var store = get(record, 'store'); + store.dematerializeRecord(record); + }, + + invokeLifecycleCallbacks: function(record) { + record.triggerLater('didDelete', record); + record.triggerLater('didCommit', record); + }, + + willCommit: Ember.K, + + didCommit: Ember.K + } + }, + + invokeLifecycleCallbacks: function(record, dirtyType) { + if (dirtyType === 'created') { + record.triggerLater('didCreate', record); + } else { + record.triggerLater('didUpdate', record); + } + + record.triggerLater('didCommit', record); + } + }; + + function wireState(object, parent, name) { + /*jshint proto:true*/ + // TODO: Use Object.create and copy instead + object = mixin(parent ? Ember.create(parent) : {}, object); + object.parentState = parent; + object.stateName = name; + + for (var prop in object) { + if (!object.hasOwnProperty(prop) || prop === 'parentState' || prop === 'stateName') { continue; } + if (typeof object[prop] === 'object') { + object[prop] = wireState(object[prop], object, name + "." + prop); + } + } + + return object; + } + + RootState = wireState(RootState, null, "root"); + + __exports__["default"] = RootState; + }); +define("ember-data/lib/system/record_array_manager", + ["./record_arrays","exports"], + function(__dependency1__, __exports__) { + "use strict"; + /** + @module ember-data + */ + + var RecordArray = __dependency1__.RecordArray; + var FilteredRecordArray = __dependency1__.FilteredRecordArray; + var AdapterPopulatedRecordArray = __dependency1__.AdapterPopulatedRecordArray; + var ManyArray = __dependency1__.ManyArray; + var get = Ember.get, set = Ember.set; + var forEach = Ember.EnumerableUtils.forEach; + + /** + @class RecordArrayManager + @namespace DS + @private + @extends Ember.Object + */ + var RecordArrayManager = Ember.Object.extend({ + init: function() { + this.filteredRecordArrays = Ember.MapWithDefault.create({ + defaultValue: function() { return []; } + }); + + this.changedRecords = []; + this._adapterPopulatedRecordArrays = []; + }, + + recordDidChange: function(record) { + if (this.changedRecords.push(record) !== 1) { return; } + + Ember.run.schedule('actions', this, this.updateRecordArrays); + }, + + recordArraysForRecord: function(record) { + record._recordArrays = record._recordArrays || Ember.OrderedSet.create(); + return record._recordArrays; + }, + + /** + This method is invoked whenever data is loaded into the store by the + adapter or updated by the adapter, or when a record has changed. + + It updates all record arrays that a record belongs to. + + To avoid thrashing, it only runs at most once per run loop. + + @method updateRecordArrays + @param {Class} type + @param {Number|String} clientId + */ + updateRecordArrays: function() { + forEach(this.changedRecords, function(record) { + if (get(record, 'isDeleted')) { + this._recordWasDeleted(record); + } else { + this._recordWasChanged(record); + } + }, this); + + this.changedRecords.length = 0; + }, + + _recordWasDeleted: function (record) { + var recordArrays = record._recordArrays; + + if (!recordArrays) { return; } + + forEach(recordArrays, function(array) { + array.removeRecord(record); + }); + }, + + _recordWasChanged: function (record) { + var type = record.constructor, + recordArrays = this.filteredRecordArrays.get(type), + filter; + + forEach(recordArrays, function(array) { + filter = get(array, 'filterFunction'); + this.updateRecordArray(array, filter, type, record); + }, this); + + // loop through all manyArrays containing an unloaded copy of this + // clientId and notify them that the record was loaded. + var manyArrays = record._loadingRecordArrays; + + if (manyArrays) { + for (var i=0, l=manyArrays.length; i [ { name: 'users', kind: 'hasMany' }, + // { name: 'owner', kind: 'belongsTo' } ] + relationships.get(App.Post); + //=> [ { name: 'posts', kind: 'hasMany' } ] + ``` + + @property relationships + @static + @type Ember.Map + @readOnly + */ + relationships: Ember.computed(function() { + var map = new Ember.MapWithDefault({ + defaultValue: function() { return []; } + }); + + // Loop through each computed property on the class + this.eachComputedProperty(function(name, meta) { + + // If the computed property is a relationship, add + // it to the map. + if (meta.isRelationship) { + meta.key = name; + var relationshipsForType = map.get(typeForRelationshipMeta(this.store, meta)); + + relationshipsForType.push({ name: name, kind: meta.kind }); + } + }); + + return map; + }).cacheable(false), + + /** + A hash containing lists of the model's relationships, grouped + by the relationship kind. For example, given a model with this + definition: + + ```javascript + App.Blog = DS.Model.extend({ + users: DS.hasMany('user'), + owner: DS.belongsTo('user'), + + posts: DS.hasMany('post') + }); + ``` + + This property would contain the following: + + ```javascript + var relationshipNames = Ember.get(App.Blog, 'relationshipNames'); + relationshipNames.hasMany; + //=> ['users', 'posts'] + relationshipNames.belongsTo; + //=> ['owner'] + ``` + + @property relationshipNames + @static + @type Object + @readOnly + */ + relationshipNames: Ember.computed(function() { + var names = { hasMany: [], belongsTo: [] }; + + this.eachComputedProperty(function(name, meta) { + if (meta.isRelationship) { + names[meta.kind].push(name); + } + }); + + return names; + }), + + /** + An array of types directly related to a model. Each type will be + included once, regardless of the number of relationships it has with + the model. + + For example, given a model with this definition: + + ```javascript + App.Blog = DS.Model.extend({ + users: DS.hasMany('user'), + owner: DS.belongsTo('user'), + + posts: DS.hasMany('post') + }); + ``` + + This property would contain the following: + + ```javascript + var relatedTypes = Ember.get(App.Blog, 'relatedTypes'); + //=> [ App.User, App.Post ] + ``` + + @property relatedTypes + @static + @type Ember.Array + @readOnly + */ + relatedTypes: Ember.computed(function() { + var type, + types = Ember.A(); + + // Loop through each computed property on the class, + // and create an array of the unique types involved + // in relationships + this.eachComputedProperty(function(name, meta) { + if (meta.isRelationship) { + meta.key = name; + type = typeForRelationshipMeta(this.store, meta); + + Ember.assert("You specified a hasMany (" + meta.type + ") on " + meta.parentType + " but " + meta.type + " was not found.", type); + + if (!types.contains(type)) { + Ember.assert("Trying to sideload " + name + " on " + this.toString() + " but the type doesn't exist.", !!type); + types.push(type); + } + } + }); + + return types; + }).cacheable(false), + + /** + A map whose keys are the relationships of a model and whose values are + relationship descriptors. + + For example, given a model with this + definition: + + ```javascript + App.Blog = DS.Model.extend({ + users: DS.hasMany('user'), + owner: DS.belongsTo('user'), + + posts: DS.hasMany('post') + }); + ``` + + This property would contain the following: + + ```javascript + var relationshipsByName = Ember.get(App.Blog, 'relationshipsByName'); + relationshipsByName.get('users'); + //=> { key: 'users', kind: 'hasMany', type: App.User } + relationshipsByName.get('owner'); + //=> { key: 'owner', kind: 'belongsTo', type: App.User } + ``` + + @property relationshipsByName + @static + @type Ember.Map + @readOnly + */ + relationshipsByName: Ember.computed(function() { + var map = Ember.Map.create(); + + this.eachComputedProperty(function(name, meta) { + if (meta.isRelationship) { + meta.key = name; + var relationship = relationshipFromMeta(this.store, meta); + relationship.type = typeForRelationshipMeta(this.store, meta); + map.set(name, relationship); + } + }); + + return map; + }).cacheable(false), + + /** + A map whose keys are the fields of the model and whose values are strings + describing the kind of the field. A model's fields are the union of all of its + attributes and relationships. + + For example: + + ```javascript + + App.Blog = DS.Model.extend({ + users: DS.hasMany('user'), + owner: DS.belongsTo('user'), + + posts: DS.hasMany('post'), + + title: DS.attr('string') + }); + + var fields = Ember.get(App.Blog, 'fields'); + fields.forEach(function(field, kind) { + console.log(field, kind); + }); + + // prints: + // users, hasMany + // owner, belongsTo + // posts, hasMany + // title, attribute + ``` + + @property fields + @static + @type Ember.Map + @readOnly + */ + fields: Ember.computed(function() { + var map = Ember.Map.create(); + + this.eachComputedProperty(function(name, meta) { + if (meta.isRelationship) { + map.set(name, meta.kind); + } else if (meta.isAttribute) { + map.set(name, 'attribute'); + } + }); + + return map; + }), + + /** + Given a callback, iterates over each of the relationships in the model, + invoking the callback with the name of each relationship and its relationship + descriptor. + + @method eachRelationship + @static + @param {Function} callback the callback to invoke + @param {any} binding the value to which the callback's `this` should be bound + */ + eachRelationship: function(callback, binding) { + get(this, 'relationshipsByName').forEach(function(name, relationship) { + callback.call(binding, name, relationship); + }); + }, + + /** + Given a callback, iterates over each of the types related to a model, + invoking the callback with the related type's class. Each type will be + returned just once, regardless of how many different relationships it has + with a model. + + @method eachRelatedType + @static + @param {Function} callback the callback to invoke + @param {any} binding the value to which the callback's `this` should be bound + */ + eachRelatedType: function(callback, binding) { + get(this, 'relatedTypes').forEach(function(type) { + callback.call(binding, type); + }); + } + }); + + Model.reopen({ + /** + Given a callback, iterates over each of the relationships in the model, + invoking the callback with the name of each relationship and its relationship + descriptor. + + @method eachRelationship + @param {Function} callback the callback to invoke + @param {any} binding the value to which the callback's `this` should be bound + */ + eachRelationship: function(callback, binding) { + this.constructor.eachRelationship(callback, binding); + } + }); + }); +define("ember-data/lib/system/relationships/has_many", + ["../store","../relationship-meta","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + /** + @module ember-data + */ + + var PromiseArray = __dependency1__.PromiseArray; + var get = Ember.get, set = Ember.set, setProperties = Ember.setProperties; + var relationshipFromMeta = __dependency2__.relationshipFromMeta; + var typeForRelationshipMeta = __dependency2__.typeForRelationshipMeta; + + function asyncHasMany(type, options, meta) { + return Ember.computed('data', function(key) { + var relationship = this._relationships[key], + promiseLabel = "DS: Async hasMany " + this + " : " + key; + + meta.key = key; + + if (!relationship) { + var resolver = Ember.RSVP.defer(promiseLabel); + relationship = buildRelationship(this, key, options, function(store, data) { + var link = data.links && data.links[key]; + var rel; + if (link) { + rel = store.findHasMany(this, link, relationshipFromMeta(store, meta), resolver); + } else { + rel = store.findMany(this, data[key], typeForRelationshipMeta(store, meta), resolver); + } + // cache the promise so we can use it + // when we come back and don't need to rebuild + // the relationship. + set(rel, 'promise', resolver.promise); + return rel; + }); + } + + var promise = relationship.get('promise').then(function() { + return relationship; + }, null, "DS: Async hasMany records received"); + + return PromiseArray.create({ + promise: promise + }); + }).meta(meta).readOnly(); + } + + function buildRelationship(record, key, options, callback) { + var rels = record._relationships; + + if (rels[key]) { return rels[key]; } + + var data = get(record, 'data'), + store = get(record, 'store'); + + var relationship = rels[key] = callback.call(record, store, data); + + return setProperties(relationship, { + owner: record, + name: key, + isPolymorphic: options.polymorphic + }); + } + + function hasRelationship(type, options) { + options = options || {}; + + var meta = { + type: type, + isRelationship: true, + options: options, + kind: 'hasMany', + key: null + }; + + if (options.async) { + return asyncHasMany(type, options, meta); + } + + return Ember.computed('data', function(key) { + return buildRelationship(this, key, options, function(store, data) { + var records = data[key]; + Ember.assert("You looked up the '" + key + "' relationship on '" + this + "' but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async (`DS.hasMany({ async: true })`)", Ember.A(records).everyProperty('isEmpty', false)); + return store.findMany(this, data[key], typeForRelationshipMeta(store, meta)); + }); + }).meta(meta).readOnly(); + } + + /** + `DS.hasMany` is used to define One-To-Many and Many-To-Many + relationships on a [DS.Model](/api/data/classes/DS.Model.html). + + `DS.hasMany` takes an optional hash as a second parameter, currently + supported options are: + + - `async`: A boolean value used to explicitly declare this to be an async relationship. + - `inverse`: A string used to identify the inverse property on a related model. + + #### One-To-Many + To declare a one-to-many relationship between two models, use + `DS.belongsTo` in combination with `DS.hasMany`, like this: + + ```javascript + App.Post = DS.Model.extend({ + comments: DS.hasMany('comment') + }); + + App.Comment = DS.Model.extend({ + post: DS.belongsTo('post') + }); + ``` + + #### Many-To-Many + To declare a many-to-many relationship between two models, use + `DS.hasMany`: + + ```javascript + App.Post = DS.Model.extend({ + tags: DS.hasMany('tag') + }); + + App.Tag = DS.Model.extend({ + posts: DS.hasMany('post') + }); + ``` + + #### Explicit Inverses + + Ember Data will do its best to discover which relationships map to + one another. In the one-to-many code above, for example, Ember Data + can figure out that changing the `comments` relationship should update + the `post` relationship on the inverse because post is the only + relationship to that model. + + However, sometimes you may have multiple `belongsTo`/`hasManys` for the + same type. You can specify which property on the related model is + the inverse using `DS.hasMany`'s `inverse` option: + + ```javascript + var belongsTo = DS.belongsTo, + hasMany = DS.hasMany; + + App.Comment = DS.Model.extend({ + onePost: belongsTo('post'), + twoPost: belongsTo('post'), + redPost: belongsTo('post'), + bluePost: belongsTo('post') + }); + + App.Post = DS.Model.extend({ + comments: hasMany('comment', { + inverse: 'redPost' + }) + }); + ``` + + You can also specify an inverse on a `belongsTo`, which works how + you'd expect. + + @namespace + @method hasMany + @for DS + @param {String or DS.Model} type the model type of the relationship + @param {Object} options a hash of options + @return {Ember.computed} relationship + */ + function hasMany(type, options) { + if (typeof type === 'object') { + options = type; + type = undefined; + } + return hasRelationship(type, options); + } + + __exports__["default"] = hasMany; + }); +define("ember-data/lib/system/store", + ["./adapter","ember-inflector/lib/system/string","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + /*globals Ember*/ + /*jshint eqnull:true*/ + + /** + @module ember-data + */ + + var InvalidError = __dependency1__.InvalidError; + var Adapter = __dependency1__.Adapter; + var singularize = __dependency2__.singularize; + var get = Ember.get, set = Ember.set; + var once = Ember.run.once; + var isNone = Ember.isNone; + var forEach = Ember.EnumerableUtils.forEach; + var indexOf = Ember.EnumerableUtils.indexOf; + var map = Ember.EnumerableUtils.map; + var Promise = Ember.RSVP.Promise; + var copy = Ember.copy; + var Store, PromiseObject, PromiseArray, RecordArrayManager, Model; + + var camelize = Ember.String.camelize; + + // Implementors Note: + // + // The variables in this file are consistently named according to the following + // scheme: + // + // * +id+ means an identifier managed by an external source, provided inside + // the data provided by that source. These are always coerced to be strings + // before being used internally. + // * +clientId+ means a transient numerical identifier generated at runtime by + // the data store. It is important primarily because newly created objects may + // not yet have an externally generated id. + // * +reference+ means a record reference object, which holds metadata about a + // record, even if it has not yet been fully materialized. + // * +type+ means a subclass of DS.Model. + + // Used by the store to normalize IDs entering the store. Despite the fact + // that developers may provide IDs as numbers (e.g., `store.find(Person, 1)`), + // it is important that internally we use strings, since IDs may be serialized + // and lose type information. For example, Ember's router may put a record's + // ID into the URL, and if we later try to deserialize that URL and find the + // corresponding record, we will not know if it is a string or a number. + function coerceId(id) { + return id == null ? null : id+''; + } + + /** + The store contains all of the data for records loaded from the server. + It is also responsible for creating instances of `DS.Model` that wrap + the individual data for a record, so that they can be bound to in your + Handlebars templates. + + Define your application's store like this: + + ```javascript + MyApp.Store = DS.Store.extend(); + ``` + + Most Ember.js applications will only have a single `DS.Store` that is + automatically created by their `Ember.Application`. + + You can retrieve models from the store in several ways. To retrieve a record + for a specific id, use `DS.Store`'s `find()` method: + + ```javascript + var person = store.find('person', 123); + ``` + + If your application has multiple `DS.Store` instances (an unusual case), you can + specify which store should be used: + + ```javascript + var person = store.find('person', 123); + ``` + + By default, the store will talk to your backend using a standard + REST mechanism. You can customize how the store talks to your + backend by specifying a custom adapter: + + ```javascript + MyApp.store = DS.Store.create({ + adapter: 'MyApp.CustomAdapter' + }); + ``` + + You can learn more about writing a custom adapter by reading the `DS.Adapter` + documentation. + + ### Store createRecord() vs. push() vs. pushPayload() vs. update() + + The store provides multiple ways to create new records object. They have + some subtle differences in their use which are detailed below: + + [createRecord](#method_createRecord) is used for creating new + records on the client side. This will return a new record in the + `created.uncommitted` state. In order to persist this record to the + backend you will need to call `record.save()`. + + [push](#method_push) is used to notify Ember Data's store of new or + updated records that exist in the backend. This will return a record + in the `loaded.saved` state. The primary use-case for `store#push` is + to notify Ember Data about record updates that happen + outside of the normal adapter methods (for example + [SSE](http://dev.w3.org/html5/eventsource/) or [Web + Sockets](http://www.w3.org/TR/2009/WD-websockets-20091222/)). + + [pushPayload](#method_pushPayload) is a convenience wrapper for + `store#push` that will deserialize payloads if the + Serializer implements a `pushPayload` method. + + [update](#method_update) works like `push`, except it can handle + partial attributes without overwriting the existing record + properties. + + Note: When creating a new record using any of the above methods + Ember Data will update `DS.RecordArray`s such as those returned by + `store#all()`, `store#findAll()` or `store#filter()`. This means any + data bindings or computed properties that depend on the RecordArray + will automatically be synced to include the new or updated record + values. + + @class Store + @namespace DS + @extends Ember.Object + */ + Store = Ember.Object.extend({ + + /** + @method init + @private + */ + init: function() { + // internal bookkeeping; not observable + if (!RecordArrayManager) { RecordArrayManager = requireModule("ember-data/lib/system/record_array_manager")["default"]; } + this.typeMaps = {}; + this.recordArrayManager = RecordArrayManager.create({ + store: this + }); + this._relationshipChanges = {}; + this._pendingSave = []; + }, + + /** + The adapter to use to communicate to a backend server or other persistence layer. + + This can be specified as an instance, class, or string. + + If you want to specify `App.CustomAdapter` as a string, do: + + ```js + adapter: 'custom' + ``` + + @property adapter + @default DS.RESTAdapter + @type {DS.Adapter|String} + */ + adapter: '-rest', + + /** + Returns a JSON representation of the record using a custom + type-specific serializer, if one exists. + + The available options are: + + * `includeId`: `true` if the record's ID should be included in + the JSON representation + + @method serialize + @private + @param {DS.Model} record the record to serialize + @param {Object} options an options hash + */ + serialize: function(record, options) { + return this.serializerFor(record.constructor.typeKey).serialize(record, options); + }, + + /** + This property returns the adapter, after resolving a possible + string key. + + If the supplied `adapter` was a class, or a String property + path resolved to a class, this property will instantiate the + class. + + This property is cacheable, so the same instance of a specified + adapter class should be used for the lifetime of the store. + + @property defaultAdapter + @private + @return DS.Adapter + */ + defaultAdapter: Ember.computed('adapter', function() { + var adapter = get(this, 'adapter'); + + Ember.assert('You tried to set `adapter` property to an instance of `DS.Adapter`, where it should be a name or a factory', !(adapter instanceof Adapter)); + + if (typeof adapter === 'string') { + adapter = this.container.lookup('adapter:' + adapter) || this.container.lookup('adapter:application') || this.container.lookup('adapter:-rest'); + } + + if (DS.Adapter.detect(adapter)) { + adapter = adapter.create({ + container: this.container + }); + } + + return adapter; + }), + + // ..................... + // . CREATE NEW RECORD . + // ..................... + + /** + Create a new record in the current store. The properties passed + to this method are set on the newly created record. + + To create a new instance of `App.Post`: + + ```js + store.createRecord('post', { + title: "Rails is omakase" + }); + ``` + + @method createRecord + @param {String} type + @param {Object} properties a hash of properties to set on the + newly created record. + @return {DS.Model} record + */ + createRecord: function(type, properties) { + type = this.modelFor(type); + + properties = copy(properties) || {}; + + // If the passed properties do not include a primary key, + // give the adapter an opportunity to generate one. Typically, + // client-side ID generators will use something like uuid.js + // to avoid conflicts. + + if (isNone(properties.id)) { + properties.id = this._generateId(type); + } + + // Coerce ID to a string + properties.id = coerceId(properties.id); + + var record = this.buildRecord(type, properties.id); + + // Move the record out of its initial `empty` state into + // the `loaded` state. + record.loadedData(); + + // Set the properties specified on the record. + record.setProperties(properties); + + return record; + }, + + /** + If possible, this method asks the adapter to generate an ID for + a newly created record. + + @method _generateId + @private + @param {String} type + @return {String} if the adapter can generate one, an ID + */ + _generateId: function(type) { + var adapter = this.adapterFor(type); + + if (adapter && adapter.generateIdForRecord) { + return adapter.generateIdForRecord(this); + } + + return null; + }, + + // ................. + // . DELETE RECORD . + // ................. + + /** + For symmetry, a record can be deleted via the store. + + Example + + ```javascript + var post = store.createRecord('post', { + title: "Rails is omakase" + }); + + store.deleteRecord(post); + ``` + + @method deleteRecord + @param {DS.Model} record + */ + deleteRecord: function(record) { + record.deleteRecord(); + }, + + /** + For symmetry, a record can be unloaded via the store. Only + non-dirty records can be unloaded. + + Example + + ```javascript + store.find('post', 1).then(function(post) { + store.unloadRecord(post); + }); + ``` + + @method unloadRecord + @param {DS.Model} record + */ + unloadRecord: function(record) { + record.unloadRecord(); + }, + + // ................ + // . FIND RECORDS . + // ................ + + /** + This is the main entry point into finding records. The first parameter to + this method is the model's name as a string. + + --- + + To find a record by ID, pass the `id` as the second parameter: + + ```javascript + store.find('person', 1); + ``` + + The `find` method will always return a **promise** that will be resolved + with the record. If the record was already in the store, the promise will + be resolved immediately. Otherwise, the store will ask the adapter's `find` + method to find the necessary data. + + The `find` method will always resolve its promise with the same object for + a given type and `id`. + + --- + + To find all records for a type, call `find` with no additional parameters: + + ```javascript + store.find('person'); + ``` + + This will ask the adapter's `findAll` method to find the records for the + given type, and return a promise that will be resolved once the server + returns the values. + + --- + + To find a record by a query, call `find` with a hash as the second + parameter: + + ```javascript + store.find('person', { page: 1 }); + ``` + + This will ask the adapter's `findQuery` method to find the records for + the query, and return a promise that will be resolved once the server + responds. + + @method find + @param {String or subclass of DS.Model} type + @param {Object|String|Integer|null} id + @return {Promise} promise + */ + find: function(type, id) { + Ember.assert("You need to pass a type to the store's find method", arguments.length >= 1); + Ember.assert("You may not pass `" + id + "` as id to the store's find method", arguments.length === 1 || !Ember.isNone(id)); + + if (arguments.length === 1) { + return this.findAll(type); + } + + // We are passed a query instead of an id. + if (Ember.typeOf(id) === 'object') { + return this.findQuery(type, id); + } + + return this.findById(type, coerceId(id)); + }, + + /** + This method returns a record for a given type and id combination. + + @method findById + @private + @param {String or subclass of DS.Model} type + @param {String|Integer} id + @return {Promise} promise + */ + findById: function(type, id) { + type = this.modelFor(type); + + var record = this.recordForId(type, id); + var fetchedRecord = this.fetchRecord(record); + + return promiseObject(fetchedRecord || record, "DS: Store#findById " + type + " with id: " + id); + }, + + /** + This method makes a series of requests to the adapter's `find` method + and returns a promise that resolves once they are all loaded. + + @private + @method findByIds + @param {String} type + @param {Array} ids + @return {Promise} promise + */ + findByIds: function(type, ids) { + var store = this; + var promiseLabel = "DS: Store#findByIds " + type; + return promiseArray(Ember.RSVP.all(map(ids, function(id) { + return store.findById(type, id); + })).then(Ember.A, null, "DS: Store#findByIds of " + type + " complete")); + }, + + /** + This method is called by `findById` if it discovers that a particular + type/id pair hasn't been loaded yet to kick off a request to the + adapter. + + @method fetchRecord + @private + @param {DS.Model} record + @return {Promise} promise + */ + fetchRecord: function(record) { + if (isNone(record)) { return null; } + if (record._loadingPromise) { return record._loadingPromise; } + if (!get(record, 'isEmpty')) { return null; } + + var type = record.constructor, + id = get(record, 'id'); + + var adapter = this.adapterFor(type); + + Ember.assert("You tried to find a record but you have no adapter (for " + type + ")", adapter); + Ember.assert("You tried to find a record but your adapter (for " + type + ") does not implement 'find'", adapter.find); + + var promise = _find(adapter, this, type, id); + record.loadingData(promise); + return promise; + }, + + /** + Get a record by a given type and ID without triggering a fetch. + + This method will synchronously return the record if it's available. + Otherwise, it will return null. + + ```js + var post = store.getById('post', 1); + ``` + + @method getById + @param {String or subclass of DS.Model} type + @param {String|Integer} id + @param {DS.Model} record + */ + getById: function(type, id) { + if (this.hasRecordForId(type, id)) { + return this.recordForId(type, id); + } else { + return null; + } + }, + + /** + This method is called by the record's `reload` method. + + This method calls the adapter's `find` method, which returns a promise. When + **that** promise resolves, `reloadRecord` will resolve the promise returned + by the record's `reload`. + + @method reloadRecord + @private + @param {DS.Model} record + @return {Promise} promise + */ + reloadRecord: function(record) { + var type = record.constructor, + adapter = this.adapterFor(type), + id = get(record, 'id'); + + Ember.assert("You cannot reload a record without an ID", id); + Ember.assert("You tried to reload a record but you have no adapter (for " + type + ")", adapter); + Ember.assert("You tried to reload a record but your adapter does not implement `find`", adapter.find); + + return _find(adapter, this, type, id); + }, + + /** + This method takes a list of records, groups the records by type, + converts the records into IDs, and then invokes the adapter's `findMany` + method. + + The records are grouped by type to invoke `findMany` on adapters + for each unique type in records. + + It is used both by a brand new relationship (via the `findMany` + method) or when the data underlying an existing relationship + changes. + + @method fetchMany + @private + @param {Array} records + @param {DS.Model} owner + @return {Promise} promise + */ + fetchMany: function(records, owner) { + if (!records.length) { + return Ember.RSVP.resolve(records); + } + + // Group By Type + var recordsByTypeMap = Ember.MapWithDefault.create({ + defaultValue: function() { return Ember.A(); } + }); + + forEach(records, function(record) { + recordsByTypeMap.get(record.constructor).push(record); + }); + + var promises = []; + + forEach(recordsByTypeMap, function(type, records) { + var ids = records.mapProperty('id'), + adapter = this.adapterFor(type); + + Ember.assert("You tried to load many records but you have no adapter (for " + type + ")", adapter); + Ember.assert("You tried to load many records but your adapter does not implement `findMany`", adapter.findMany); + + promises.push(_findMany(adapter, this, type, ids, owner)); + }, this); + + return Ember.RSVP.all(promises); + }, + + /** + Returns true if a record for a given type and ID is already loaded. + + @method hasRecordForId + @param {String or subclass of DS.Model} type + @param {String|Integer} id + @return {Boolean} + */ + hasRecordForId: function(type, id) { + id = coerceId(id); + type = this.modelFor(type); + return !!this.typeMapFor(type).idToRecord[id]; + }, + + /** + Returns id record for a given type and ID. If one isn't already loaded, + it builds a new record and leaves it in the `empty` state. + + @method recordForId + @private + @param {String or subclass of DS.Model} type + @param {String|Integer} id + @return {DS.Model} record + */ + recordForId: function(type, id) { + type = this.modelFor(type); + + id = coerceId(id); + + var record = this.typeMapFor(type).idToRecord[id]; + + if (!record) { + record = this.buildRecord(type, id); + } + + return record; + }, + + /** + @method findMany + @private + @param {DS.Model} owner + @param {Array} records + @param {String or subclass of DS.Model} type + @param {Resolver} resolver + @return {DS.ManyArray} records + */ + findMany: function(owner, records, type, resolver) { + type = this.modelFor(type); + + records = Ember.A(records); + + var unloadedRecords = records.filterProperty('isEmpty', true), + manyArray = this.recordArrayManager.createManyArray(type, records); + + forEach(unloadedRecords, function(record) { + record.loadingData(); + }); + + manyArray.loadingRecordsCount = unloadedRecords.length; + + if (unloadedRecords.length) { + forEach(unloadedRecords, function(record) { + this.recordArrayManager.registerWaitingRecordArray(record, manyArray); + }, this); + + resolver.resolve(this.fetchMany(unloadedRecords, owner)); + } else { + if (resolver) { resolver.resolve(); } + manyArray.set('isLoaded', true); + once(manyArray, 'trigger', 'didLoad'); + } + + return manyArray; + }, + + /** + If a relationship was originally populated by the adapter as a link + (as opposed to a list of IDs), this method is called when the + relationship is fetched. + + The link (which is usually a URL) is passed through unchanged, so the + adapter can make whatever request it wants. + + The usual use-case is for the server to register a URL as a link, and + then use that URL in the future to make a request for the relationship. + + @method findHasMany + @private + @param {DS.Model} owner + @param {any} link + @param {String or subclass of DS.Model} type + @return {Promise} promise + */ + findHasMany: function(owner, link, relationship, resolver) { + var adapter = this.adapterFor(owner.constructor); + + Ember.assert("You tried to load a hasMany relationship but you have no adapter (for " + owner.constructor + ")", adapter); + Ember.assert("You tried to load a hasMany relationship from a specified `link` in the original payload but your adapter does not implement `findHasMany`", adapter.findHasMany); + + var records = this.recordArrayManager.createManyArray(relationship.type, Ember.A([])); + resolver.resolve(_findHasMany(adapter, this, owner, link, relationship)); + return records; + }, + + /** + @method findBelongsTo + @private + @param {DS.Model} owner + @param {any} link + @param {Relationship} relationship + @return {Promise} promise + */ + findBelongsTo: function(owner, link, relationship) { + var adapter = this.adapterFor(owner.constructor); + + Ember.assert("You tried to load a belongsTo relationship but you have no adapter (for " + owner.constructor + ")", adapter); + Ember.assert("You tried to load a belongsTo relationship from a specified `link` in the original payload but your adapter does not implement `findBelongsTo`", adapter.findBelongsTo); + + return _findBelongsTo(adapter, this, owner, link, relationship); + }, + + /** + This method delegates a query to the adapter. This is the one place where + adapter-level semantics are exposed to the application. + + Exposing queries this way seems preferable to creating an abstract query + language for all server-side queries, and then require all adapters to + implement them. + + This method returns a promise, which is resolved with a `RecordArray` + once the server returns. + + @method findQuery + @private + @param {String or subclass of DS.Model} type + @param {any} query an opaque query to be used by the adapter + @return {Promise} promise + */ + findQuery: function(type, query) { + type = this.modelFor(type); + + var array = this.recordArrayManager + .createAdapterPopulatedRecordArray(type, query); + + var adapter = this.adapterFor(type); + + Ember.assert("You tried to load a query but you have no adapter (for " + type + ")", adapter); + Ember.assert("You tried to load a query but your adapter does not implement `findQuery`", adapter.findQuery); + + return promiseArray(_findQuery(adapter, this, type, query, array)); + }, + + /** + This method returns an array of all records adapter can find. + It triggers the adapter's `findAll` method to give it an opportunity to populate + the array with records of that type. + + @method findAll + @private + @param {String or subclass of DS.Model} type + @return {DS.AdapterPopulatedRecordArray} + */ + findAll: function(type) { + type = this.modelFor(type); + + return this.fetchAll(type, this.all(type)); + }, + + /** + @method fetchAll + @private + @param {DS.Model} type + @param {DS.RecordArray} array + @return {Promise} promise + */ + fetchAll: function(type, array) { + var adapter = this.adapterFor(type), + sinceToken = this.typeMapFor(type).metadata.since; + + set(array, 'isUpdating', true); + + Ember.assert("You tried to load all records but you have no adapter (for " + type + ")", adapter); + Ember.assert("You tried to load all records but your adapter does not implement `findAll`", adapter.findAll); + + return promiseArray(_findAll(adapter, this, type, sinceToken)); + }, + + /** + @method didUpdateAll + @param {DS.Model} type + */ + didUpdateAll: function(type) { + var findAllCache = this.typeMapFor(type).findAllCache; + set(findAllCache, 'isUpdating', false); + }, + + /** + This method returns a filtered array that contains all of the known records + for a given type. + + Note that because it's just a filter, it will have any locally + created records of the type. + + Also note that multiple calls to `all` for a given type will always + return the same RecordArray. + + Example + + ```javascript + var localPosts = store.all('post'); + ``` + + @method all + @param {String or subclass of DS.Model} type + @return {DS.RecordArray} + */ + all: function(type) { + type = this.modelFor(type); + + var typeMap = this.typeMapFor(type), + findAllCache = typeMap.findAllCache; + + if (findAllCache) { return findAllCache; } + + var array = this.recordArrayManager.createRecordArray(type); + + typeMap.findAllCache = array; + return array; + }, + + + /** + This method unloads all of the known records for a given type. + + ```javascript + store.unloadAll('post'); + ``` + + @method unloadAll + @param {String or subclass of DS.Model} type + */ + unloadAll: function(type) { + var modelType = this.modelFor(type); + var typeMap = this.typeMapFor(modelType); + var records = typeMap.records.slice(); + var record; + + for (var i = 0; i < records.length; i++) { + record = records[i]; + record.unloadRecord(); + record.destroy(); // maybe within unloadRecord + } + + typeMap.findAllCache = null; + }, + + /** + Takes a type and filter function, and returns a live RecordArray that + remains up to date as new records are loaded into the store or created + locally. + + The callback function takes a materialized record, and returns true + if the record should be included in the filter and false if it should + not. + + The filter function is called once on all records for the type when + it is created, and then once on each newly loaded or created record. + + If any of a record's properties change, or if it changes state, the + filter function will be invoked again to determine whether it should + still be in the array. + + Optionally you can pass a query which will be triggered at first. The + results returned by the server could then appear in the filter if they + match the filter function. + + Example + + ```javascript + store.filter('post', {unread: true}, function(post) { + return post.get('unread'); + }).then(function(unreadPosts) { + unreadPosts.get('length'); // 5 + var unreadPost = unreadPosts.objectAt(0); + unreadPost.set('unread', false); + unreadPosts.get('length'); // 4 + }); + ``` + + @method filter + @param {String or subclass of DS.Model} type + @param {Object} query optional query + @param {Function} filter + @return {DS.PromiseArray} + */ + filter: function(type, query, filter) { + var promise; + var length = arguments.length; + var array; + var hasQuery = length === 3; + + // allow an optional server query + if (hasQuery) { + promise = this.findQuery(type, query); + } else if (arguments.length === 2) { + filter = query; + } + + type = this.modelFor(type); + + if (hasQuery) { + array = this.recordArrayManager.createFilteredRecordArray(type, filter, query); + } else { + array = this.recordArrayManager.createFilteredRecordArray(type, filter); + } + + promise = promise || Promise.cast(array); + + + return promiseArray(promise.then(function() { + return array; + }, null, "DS: Store#filter of " + type)); + }, + + /** + This method returns if a certain record is already loaded + in the store. Use this function to know beforehand if a find() + will result in a request or that it will be a cache hit. + + Example + + ```javascript + store.recordIsLoaded('post', 1); // false + store.find('post', 1).then(function() { + store.recordIsLoaded('post', 1); // true + }); + ``` + + @method recordIsLoaded + @param {String or subclass of DS.Model} type + @param {string} id + @return {boolean} + */ + recordIsLoaded: function(type, id) { + if (!this.hasRecordForId(type, id)) { return false; } + return !get(this.recordForId(type, id), 'isEmpty'); + }, + + /** + This method returns the metadata for a specific type. + + @method metadataFor + @param {String or subclass of DS.Model} type + @return {object} + */ + metadataFor: function(type) { + type = this.modelFor(type); + return this.typeMapFor(type).metadata; + }, + + // ............ + // . UPDATING . + // ............ + + /** + If the adapter updates attributes or acknowledges creation + or deletion, the record will notify the store to update its + membership in any filters. + To avoid thrashing, this method is invoked only once per + + run loop per record. + + @method dataWasUpdated + @private + @param {Class} type + @param {DS.Model} record + */ + dataWasUpdated: function(type, record) { + this.recordArrayManager.recordDidChange(record); + }, + + // .............. + // . PERSISTING . + // .............. + + /** + This method is called by `record.save`, and gets passed a + resolver for the promise that `record.save` returns. + + It schedules saving to happen at the end of the run loop. + + @method scheduleSave + @private + @param {DS.Model} record + @param {Resolver} resolver + */ + scheduleSave: function(record, resolver) { + record.adapterWillCommit(); + this._pendingSave.push([record, resolver]); + once(this, 'flushPendingSave'); + }, + + /** + This method is called at the end of the run loop, and + flushes any records passed into `scheduleSave` + + @method flushPendingSave + @private + */ + flushPendingSave: function() { + var pending = this._pendingSave.slice(); + this._pendingSave = []; + + forEach(pending, function(tuple) { + var record = tuple[0], resolver = tuple[1], + adapter = this.adapterFor(record.constructor), + operation; + + if (get(record, 'currentState.stateName') === 'root.deleted.saved') { + return resolver.resolve(record); + } else if (get(record, 'isNew')) { + operation = 'createRecord'; + } else if (get(record, 'isDeleted')) { + operation = 'deleteRecord'; + } else { + operation = 'updateRecord'; + } + + resolver.resolve(_commit(adapter, this, operation, record)); + }, this); + }, + + /** + This method is called once the promise returned by an + adapter's `createRecord`, `updateRecord` or `deleteRecord` + is resolved. + + If the data provides a server-generated ID, it will + update the record and the store's indexes. + + @method didSaveRecord + @private + @param {DS.Model} record the in-flight record + @param {Object} data optional data (see above) + */ + didSaveRecord: function(record, data) { + if (data) { + // normalize relationship IDs into records + data = normalizeRelationships(this, record.constructor, data, record); + + this.updateId(record, data); + } + + record.adapterDidCommit(data); + }, + + /** + This method is called once the promise returned by an + adapter's `createRecord`, `updateRecord` or `deleteRecord` + is rejected with a `DS.InvalidError`. + + @method recordWasInvalid + @private + @param {DS.Model} record + @param {Object} errors + */ + recordWasInvalid: function(record, errors) { + record.adapterDidInvalidate(errors); + }, + + /** + This method is called once the promise returned by an + adapter's `createRecord`, `updateRecord` or `deleteRecord` + is rejected (with anything other than a `DS.InvalidError`). + + @method recordWasError + @private + @param {DS.Model} record + */ + recordWasError: function(record) { + record.adapterDidError(); + }, + + /** + When an adapter's `createRecord`, `updateRecord` or `deleteRecord` + resolves with data, this method extracts the ID from the supplied + data. + + @method updateId + @private + @param {DS.Model} record + @param {Object} data + */ + updateId: function(record, data) { + var oldId = get(record, 'id'), + id = coerceId(data.id); + + Ember.assert("An adapter cannot assign a new id to a record that already has an id. " + record + " had id: " + oldId + " and you tried to update it with " + id + ". This likely happened because your server returned data in response to a find or update that had a different id than the one you sent.", oldId === null || id === oldId); + + this.typeMapFor(record.constructor).idToRecord[id] = record; + + set(record, 'id', id); + }, + + /** + Returns a map of IDs to client IDs for a given type. + + @method typeMapFor + @private + @param type + @return {Object} typeMap + */ + typeMapFor: function(type) { + var typeMaps = get(this, 'typeMaps'), + guid = Ember.guidFor(type), + typeMap; + + typeMap = typeMaps[guid]; + + if (typeMap) { return typeMap; } + + typeMap = { + idToRecord: {}, + records: [], + metadata: {}, + type: type + }; + + typeMaps[guid] = typeMap; + + return typeMap; + }, + + // ................ + // . LOADING DATA . + // ................ + + /** + This internal method is used by `push`. + + @method _load + @private + @param {String or subclass of DS.Model} type + @param {Object} data + @param {Boolean} partial the data should be merged into + the existing data, not replace it. + */ + _load: function(type, data, partial) { + var id = coerceId(data.id), + record = this.recordForId(type, id); + + record.setupData(data, partial); + this.recordArrayManager.recordDidChange(record); + + return record; + }, + + /** + Returns a model class for a particular key. Used by + methods that take a type key (like `find`, `createRecord`, + etc.) + + @method modelFor + @param {String or subclass of DS.Model} key + @return {subclass of DS.Model} + */ + modelFor: function(key) { + var factory; + + + if (typeof key === 'string') { + var normalizedKey = this.container.normalize('model:' + key); + + factory = this.container.lookupFactory(normalizedKey); + if (!factory) { throw new Ember.Error("No model was found for '" + key + "'"); } + factory.typeKey = this._normalizeTypeKey(normalizedKey.split(':', 2)[1]); + } else { + // A factory already supplied. Ensure it has a normalized key. + factory = key; + if (factory.typeKey) { + factory.typeKey = this._normalizeTypeKey(factory.typeKey); + } + } + + factory.store = this; + return factory; + }, + + /** + Push some data for a given type into the store. + + This method expects normalized data: + + * The ID is a key named `id` (an ID is mandatory) + * The names of attributes are the ones you used in + your model's `DS.attr`s. + * Your relationships must be: + * represented as IDs or Arrays of IDs + * represented as model instances + * represented as URLs, under the `links` key + + For this model: + + ```js + App.Person = DS.Model.extend({ + firstName: DS.attr(), + lastName: DS.attr(), + + children: DS.hasMany('person') + }); + ``` + + To represent the children as IDs: + + ```js + { + id: 1, + firstName: "Tom", + lastName: "Dale", + children: [1, 2, 3] + } + ``` + + To represent the children relationship as a URL: + + ```js + { + id: 1, + firstName: "Tom", + lastName: "Dale", + links: { + children: "/people/1/children" + } + } + ``` + + If you're streaming data or implementing an adapter, + make sure that you have converted the incoming data + into this form. + + This method can be used both to push in brand new + records, as well as to update existing records. + + @method push + @param {String or subclass of DS.Model} type + @param {Object} data + @return {DS.Model} the record that was created or + updated. + */ + push: function(type, data, _partial) { + // _partial is an internal param used by `update`. + // If passed, it means that the data should be + // merged into the existing data, not replace it. + + Ember.assert("You must include an `id` for " + type + " in a hash passed to `push`", data.id != null); + + type = this.modelFor(type); + + // normalize relationship IDs into records + data = normalizeRelationships(this, type, data); + + this._load(type, data, _partial); + + return this.recordForId(type, data.id); + }, + + /** + Push some raw data into the store. + + This method can be used both to push in brand new + records, as well as to update existing records. You + can push in more than one type of object at once. + All objects should be in the format expected by the + serializer. + + ```js + App.ApplicationSerializer = DS.ActiveModelSerializer; + + var pushData = { + posts: [ + {id: 1, post_title: "Great post", comment_ids: [2]} + ], + comments: [ + {id: 2, comment_body: "Insightful comment"} + ] + } + + store.pushPayload(pushData); + ``` + + By default, the data will be deserialized using a default + serializer (the application serializer if it exists). + + Alternativly, `pushPayload` will accept a model type which + will determine which serializer will process the payload. + However, the serializer itself (processing this data via + `normalizePayload`) will not know which model it is + deserializing. + + ```js + App.ApplicationSerializer = DS.ActiveModelSerializer; + App.PostSerializer = DS.JSONSerializer; + store.pushPayload('comment', pushData); // Will use the ApplicationSerializer + store.pushPayload('post', pushData); // Will use the PostSerializer + ``` + + @method pushPayload + @param {String} type Optionally, a model used to determine which serializer will be used + @param {Object} payload + */ + pushPayload: function (type, payload) { + var serializer; + if (!payload) { + payload = type; + serializer = defaultSerializer(this.container); + Ember.assert("You cannot use `store#pushPayload` without a type unless your default serializer defines `pushPayload`", serializer.pushPayload); + } else { + serializer = this.serializerFor(type); + } + serializer.pushPayload(this, payload); + }, + + /** + Update existing records in the store. Unlike [push](#method_push), + update will merge the new data properties with the existing + properties. This makes it safe to use with a subset of record + attributes. This method expects normalized data. + + `update` is useful if you app broadcasts partial updates to + records. + + ```js + App.Person = DS.Model.extend({ + firstName: DS.attr('string'), + lastName: DS.attr('string') + }); + + store.get('person', 1).then(function(tom) { + tom.get('firstName'); // Tom + tom.get('lastName'); // Dale + + var updateEvent = {id: 1, firstName: "TomHuda"}; + store.update('person', updateEvent); + + tom.get('firstName'); // TomHuda + tom.get('lastName'); // Dale + }); + ``` + + @method update + @param {String} type + @param {Object} data + @return {DS.Model} the record that was updated. + */ + update: function(type, data) { + Ember.assert("You must include an `id` for " + type + " in a hash passed to `update`", data.id != null); + + return this.push(type, data, true); + }, + + /** + If you have an Array of normalized data to push, + you can call `pushMany` with the Array, and it will + call `push` repeatedly for you. + + @method pushMany + @param {String or subclass of DS.Model} type + @param {Array} datas + @return {Array} + */ + pushMany: function(type, datas) { + return map(datas, function(data) { + return this.push(type, data); + }, this); + }, + + /** + If you have some metadata to set for a type + you can call `metaForType`. + + @method metaForType + @param {String or subclass of DS.Model} type + @param {Object} metadata + */ + metaForType: function(type, metadata) { + type = this.modelFor(type); + + Ember.merge(this.typeMapFor(type).metadata, metadata); + }, + + /** + Build a brand new record for a given type, ID, and + initial data. + + @method buildRecord + @private + @param {subclass of DS.Model} type + @param {String} id + @param {Object} data + @return {DS.Model} record + */ + buildRecord: function(type, id, data) { + var typeMap = this.typeMapFor(type), + idToRecord = typeMap.idToRecord; + + Ember.assert('The id ' + id + ' has already been used with another record of type ' + type.toString() + '.', !id || !idToRecord[id]); + Ember.assert("`" + Ember.inspect(type)+ "` does not appear to be an ember-data model", (typeof type._create === 'function') ); + + // lookupFactory should really return an object that creates + // instances with the injections applied + var record = type._create({ + id: id, + store: this, + container: this.container + }); + + if (data) { + record.setupData(data); + } + + // if we're creating an item, this process will be done + // later, once the object has been persisted. + if (id) { + idToRecord[id] = record; + } + + typeMap.records.push(record); + + return record; + }, + + // ............... + // . DESTRUCTION . + // ............... + + /** + When a record is destroyed, this un-indexes it and + removes it from any record arrays so it can be GCed. + + @method dematerializeRecord + @private + @param {DS.Model} record + */ + dematerializeRecord: function(record) { + var type = record.constructor, + typeMap = this.typeMapFor(type), + id = get(record, 'id'); + + record.updateRecordArrays(); + + if (id) { + delete typeMap.idToRecord[id]; + } + + var loc = indexOf(typeMap.records, record); + typeMap.records.splice(loc, 1); + }, + + // ........................ + // . RELATIONSHIP CHANGES . + // ........................ + + addRelationshipChangeFor: function(childRecord, childKey, parentRecord, parentKey, change) { + var clientId = childRecord.clientId, + parentClientId = parentRecord ? parentRecord : parentRecord; + var key = childKey + parentKey; + var changes = this._relationshipChanges; + if (!(clientId in changes)) { + changes[clientId] = {}; + } + if (!(parentClientId in changes[clientId])) { + changes[clientId][parentClientId] = {}; + } + if (!(key in changes[clientId][parentClientId])) { + changes[clientId][parentClientId][key] = {}; + } + changes[clientId][parentClientId][key][change.changeType] = change; + }, + + removeRelationshipChangeFor: function(clientRecord, childKey, parentRecord, parentKey, type) { + var clientId = clientRecord.clientId, + parentClientId = parentRecord ? parentRecord.clientId : parentRecord; + var changes = this._relationshipChanges; + var key = childKey + parentKey; + if (!(clientId in changes) || !(parentClientId in changes[clientId]) || !(key in changes[clientId][parentClientId])){ + return; + } + delete changes[clientId][parentClientId][key][type]; + }, + + relationshipChangePairsFor: function(record){ + var toReturn = []; + + if( !record ) { return toReturn; } + + //TODO(Igor) What about the other side + var changesObject = this._relationshipChanges[record.clientId]; + for (var objKey in changesObject){ + if(changesObject.hasOwnProperty(objKey)){ + for (var changeKey in changesObject[objKey]){ + if(changesObject[objKey].hasOwnProperty(changeKey)){ + toReturn.push(changesObject[objKey][changeKey]); + } + } + } + } + return toReturn; + }, + + // ...................... + // . PER-TYPE ADAPTERS + // ...................... + + /** + Returns the adapter for a given type. + + @method adapterFor + @private + @param {subclass of DS.Model} type + @return DS.Adapter + */ + adapterFor: function(type) { + var container = this.container, adapter; + + if (container) { + adapter = container.lookup('adapter:' + type.typeKey) || container.lookup('adapter:application'); + } + + return adapter || get(this, 'defaultAdapter'); + }, + + // .............................. + // . RECORD CHANGE NOTIFICATION . + // .............................. + + /** + Returns an instance of the serializer for a given type. For + example, `serializerFor('person')` will return an instance of + `App.PersonSerializer`. + + If no `App.PersonSerializer` is found, this method will look + for an `App.ApplicationSerializer` (the default serializer for + your entire application). + + If no `App.ApplicationSerializer` is found, it will fall back + to an instance of `DS.JSONSerializer`. + + @method serializerFor + @private + @param {String} type the record to serialize + @return {DS.Serializer} + */ + serializerFor: function(type) { + type = this.modelFor(type); + var adapter = this.adapterFor(type); + + return serializerFor(this.container, type.typeKey, adapter && adapter.defaultSerializer); + }, + + willDestroy: function() { + var typeMaps = this.typeMaps; + var keys = Ember.keys(typeMaps); + var store = this; + + var types = map(keys, byType); + + this.recordArrayManager.destroy(); + + forEach(types, this.unloadAll, this); + + function byType(entry) { + return typeMaps[entry]['type']; + } + + }, + + /** + All typeKeys are camelCase internally. Changing this function may + require changes to other normalization hooks (such as typeForRoot). + + @method _normalizeTypeKey + @private + @param {String} type + @return {String} if the adapter can generate one, an ID + */ + _normalizeTypeKey: function(key) { + return camelize(singularize(key)); + } + }); + + function normalizeRelationships(store, type, data, record) { + type.eachRelationship(function(key, relationship) { + // A link (usually a URL) was already provided in + // normalized form + if (data.links && data.links[key]) { + if (record && relationship.options.async) { record._relationships[key] = null; } + return; + } + + var kind = relationship.kind, + value = data[key]; + + if (value == null) { return; } + + if (kind === 'belongsTo') { + deserializeRecordId(store, data, key, relationship, value); + } else if (kind === 'hasMany') { + deserializeRecordIds(store, data, key, relationship, value); + addUnsavedRecords(record, key, value); + } + }); + + return data; + } + + function deserializeRecordId(store, data, key, relationship, id) { + if (!Model) { Model = requireModule("ember-data/lib/system/model")["Model"]; } + if (isNone(id) || id instanceof Model) { + return; + } + + var type; + + if (typeof id === 'number' || typeof id === 'string') { + type = typeFor(relationship, key, data); + data[key] = store.recordForId(type, id); + } else if (typeof id === 'object') { + // polymorphic + data[key] = store.recordForId(id.type, id.id); + } + } + + function typeFor(relationship, key, data) { + if (relationship.options.polymorphic) { + return data[key + "Type"]; + } else { + return relationship.type; + } + } + + function deserializeRecordIds(store, data, key, relationship, ids) { + for (var i=0, l=ids.length; i 'kine' + inflector.singularize('kine'); //=> 'cow' + ``` + + Creating an inflector and adding rules later. + + ```javascript + var inflector = Ember.Inflector.inflector; + + inflector.pluralize('advice'); // => 'advices' + inflector.uncountable('advice'); + inflector.pluralize('advice'); // => 'advice' + + inflector.pluralize('formula'); // => 'formulas' + inflector.irregular('formula', 'formulae'); + inflector.pluralize('formula'); // => 'formulae' + + // you would not need to add these as they are the default rules + inflector.plural(/$/, 's'); + inflector.singular(/s$/i, ''); + ``` + + Creating an inflector with a nondefault ruleset. + + ```javascript + var rules = { + plurals: [ /$/, 's' ], + singular: [ /\s$/, '' ], + irregularPairs: [ + [ 'cow', 'kine' ] + ], + uncountable: [ 'fish' ] + }; + + var inflector = new Ember.Inflector(rules); + ``` + + @class Inflector + @namespace Ember + */ + function Inflector(ruleSet) { + ruleSet = ruleSet || {}; + ruleSet.uncountable = ruleSet.uncountable || {}; + ruleSet.irregularPairs = ruleSet.irregularPairs || {}; + + var rules = this.rules = { + plurals: ruleSet.plurals || [], + singular: ruleSet.singular || [], + irregular: {}, + irregularInverse: {}, + uncountable: {} + }; + + loadUncountable(rules, ruleSet.uncountable); + loadIrregular(rules, ruleSet.irregularPairs); + } + + Inflector.prototype = { + /** + @method plural + @param {RegExp} regex + @param {String} string + */ + plural: function(regex, string) { + this.rules.plurals.push([regex, string.toLowerCase()]); + }, + + /** + @method singular + @param {RegExp} regex + @param {String} string + */ + singular: function(regex, string) { + this.rules.singular.push([regex, string.toLowerCase()]); + }, + + /** + @method uncountable + @param {String} regex + */ + uncountable: function(string) { + loadUncountable(this.rules, [string.toLowerCase()]); + }, + + /** + @method irregular + @param {String} singular + @param {String} plural + */ + irregular: function (singular, plural) { + loadIrregular(this.rules, [[singular, plural]]); + }, + + /** + @method pluralize + @param {String} word + */ + pluralize: function(word) { + return this.inflect(word, this.rules.plurals, this.rules.irregular); + }, + + /** + @method singularize + @param {String} word + */ + singularize: function(word) { + return this.inflect(word, this.rules.singular, this.rules.irregularInverse); + }, + + /** + @protected + + @method inflect + @param {String} word + @param {Object} typeRules + @param {Object} irregular + */ + inflect: function(word, typeRules, irregular) { + var inflection, substitution, result, lowercase, isBlank, + isUncountable, isIrregular, isIrregularInverse, rule; + + isBlank = BLANK_REGEX.test(word); + + if (isBlank) { + return word; + } + + lowercase = word.toLowerCase(); + + isUncountable = this.rules.uncountable[lowercase]; + + if (isUncountable) { + return word; + } + + isIrregular = irregular && irregular[lowercase]; + + if (isIrregular) { + return isIrregular; + } + + for (var i = typeRules.length, min = 0; i > min; i--) { + inflection = typeRules[i-1]; + rule = inflection[0]; + + if (rule.test(word)) { + break; + } + } + + inflection = inflection || []; + + rule = inflection[0]; + substitution = inflection[1]; + + result = word.replace(rule, substitution); + + return result; + } + }; + + __exports__["default"] = Inflector; + }); +define("ember-inflector/lib/system/string", + ["./inflector","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Inflector = __dependency1__["default"]; + var pluralize = function(word) { + return Inflector.inflector.pluralize(word); + }; + + var singularize = function(word) { + return Inflector.inflector.singularize(word); + }; + + __exports__.pluralize = pluralize; + __exports__.singularize = singularize; + }); +global.DS = requireModule('ember-data/lib/main')['default']; +}(Ember.lookup)); \ No newline at end of file diff --git a/ember_app/js/libs/handlebars-1.0.0.js b/ember_app/js/libs/handlebars-1.0.0.js deleted file mode 100755 index c70f09d..0000000 --- a/ember_app/js/libs/handlebars-1.0.0.js +++ /dev/null @@ -1,2278 +0,0 @@ -/* - -Copyright (C) 2011 by Yehuda Katz - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -*/ - -// lib/handlebars/browser-prefix.js -var Handlebars = {}; - -(function(Handlebars, undefined) { -; -// lib/handlebars/base.js - -Handlebars.VERSION = "1.0.0"; -Handlebars.COMPILER_REVISION = 4; - -Handlebars.REVISION_CHANGES = { - 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it - 2: '== 1.0.0-rc.3', - 3: '== 1.0.0-rc.4', - 4: '>= 1.0.0' -}; - -Handlebars.helpers = {}; -Handlebars.partials = {}; - -var toString = Object.prototype.toString, - functionType = '[object Function]', - objectType = '[object Object]'; - -Handlebars.registerHelper = function(name, fn, inverse) { - if (toString.call(name) === objectType) { - if (inverse || fn) { throw new Handlebars.Exception('Arg not supported with multiple helpers'); } - Handlebars.Utils.extend(this.helpers, name); - } else { - if (inverse) { fn.not = inverse; } - this.helpers[name] = fn; - } -}; - -Handlebars.registerPartial = function(name, str) { - if (toString.call(name) === objectType) { - Handlebars.Utils.extend(this.partials, name); - } else { - this.partials[name] = str; - } -}; - -Handlebars.registerHelper('helperMissing', function(arg) { - if(arguments.length === 2) { - return undefined; - } else { - throw new Error("Missing helper: '" + arg + "'"); - } -}); - -Handlebars.registerHelper('blockHelperMissing', function(context, options) { - var inverse = options.inverse || function() {}, fn = options.fn; - - var type = toString.call(context); - - if(type === functionType) { context = context.call(this); } - - if(context === true) { - return fn(this); - } else if(context === false || context == null) { - return inverse(this); - } else if(type === "[object Array]") { - if(context.length > 0) { - return Handlebars.helpers.each(context, options); - } else { - return inverse(this); - } - } else { - return fn(context); - } -}); - -Handlebars.K = function() {}; - -Handlebars.createFrame = Object.create || function(object) { - Handlebars.K.prototype = object; - var obj = new Handlebars.K(); - Handlebars.K.prototype = null; - return obj; -}; - -Handlebars.logger = { - DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3, - - methodMap: {0: 'debug', 1: 'info', 2: 'warn', 3: 'error'}, - - // can be overridden in the host environment - log: function(level, obj) { - if (Handlebars.logger.level <= level) { - var method = Handlebars.logger.methodMap[level]; - if (typeof console !== 'undefined' && console[method]) { - console[method].call(console, obj); - } - } - } -}; - -Handlebars.log = function(level, obj) { Handlebars.logger.log(level, obj); }; - -Handlebars.registerHelper('each', function(context, options) { - var fn = options.fn, inverse = options.inverse; - var i = 0, ret = "", data; - - var type = toString.call(context); - if(type === functionType) { context = context.call(this); } - - if (options.data) { - data = Handlebars.createFrame(options.data); - } - - if(context && typeof context === 'object') { - if(context instanceof Array){ - for(var j = context.length; i 2) { - expected.push("'" + this.terminals_[p] + "'"); - } - if (this.lexer.showPosition) { - errStr = "Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", ") + ", got '" + (this.terminals_[symbol] || symbol) + "'"; - } else { - errStr = "Parse error on line " + (yylineno + 1) + ": Unexpected " + (symbol == 1?"end of input":"'" + (this.terminals_[symbol] || symbol) + "'"); - } - this.parseError(errStr, {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected}); - } - } - if (action[0] instanceof Array && action.length > 1) { - throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol); - } - switch (action[0]) { - case 1: - stack.push(symbol); - vstack.push(this.lexer.yytext); - lstack.push(this.lexer.yylloc); - stack.push(action[1]); - symbol = null; - if (!preErrorSymbol) { - yyleng = this.lexer.yyleng; - yytext = this.lexer.yytext; - yylineno = this.lexer.yylineno; - yyloc = this.lexer.yylloc; - if (recovering > 0) - recovering--; - } else { - symbol = preErrorSymbol; - preErrorSymbol = null; - } - break; - case 2: - len = this.productions_[action[1]][1]; - yyval.$ = vstack[vstack.length - len]; - yyval._$ = {first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column}; - if (ranges) { - yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]]; - } - r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack); - if (typeof r !== "undefined") { - return r; - } - if (len) { - stack = stack.slice(0, -1 * len * 2); - vstack = vstack.slice(0, -1 * len); - lstack = lstack.slice(0, -1 * len); - } - stack.push(this.productions_[action[1]][0]); - vstack.push(yyval.$); - lstack.push(yyval._$); - newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; - stack.push(newState); - break; - case 3: - return true; - } - } - return true; -} -}; -/* Jison generated lexer */ -var lexer = (function(){ -var lexer = ({EOF:1, -parseError:function parseError(str, hash) { - if (this.yy.parser) { - this.yy.parser.parseError(str, hash); - } else { - throw new Error(str); - } - }, -setInput:function (input) { - this._input = input; - this._more = this._less = this.done = false; - this.yylineno = this.yyleng = 0; - this.yytext = this.matched = this.match = ''; - this.conditionStack = ['INITIAL']; - this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0}; - if (this.options.ranges) this.yylloc.range = [0,0]; - this.offset = 0; - return this; - }, -input:function () { - var ch = this._input[0]; - this.yytext += ch; - this.yyleng++; - this.offset++; - this.match += ch; - this.matched += ch; - var lines = ch.match(/(?:\r\n?|\n).*/g); - if (lines) { - this.yylineno++; - this.yylloc.last_line++; - } else { - this.yylloc.last_column++; - } - if (this.options.ranges) this.yylloc.range[1]++; - - this._input = this._input.slice(1); - return ch; - }, -unput:function (ch) { - var len = ch.length; - var lines = ch.split(/(?:\r\n?|\n)/g); - - this._input = ch + this._input; - this.yytext = this.yytext.substr(0, this.yytext.length-len-1); - //this.yyleng -= len; - this.offset -= len; - var oldLines = this.match.split(/(?:\r\n?|\n)/g); - this.match = this.match.substr(0, this.match.length-1); - this.matched = this.matched.substr(0, this.matched.length-1); - - if (lines.length-1) this.yylineno -= lines.length-1; - var r = this.yylloc.range; - - this.yylloc = {first_line: this.yylloc.first_line, - last_line: this.yylineno+1, - first_column: this.yylloc.first_column, - last_column: lines ? - (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length: - this.yylloc.first_column - len - }; - - if (this.options.ranges) { - this.yylloc.range = [r[0], r[0] + this.yyleng - len]; - } - return this; - }, -more:function () { - this._more = true; - return this; - }, -less:function (n) { - this.unput(this.match.slice(n)); - }, -pastInput:function () { - var past = this.matched.substr(0, this.matched.length - this.match.length); - return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); - }, -upcomingInput:function () { - var next = this.match; - if (next.length < 20) { - next += this._input.substr(0, 20-next.length); - } - return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, ""); - }, -showPosition:function () { - var pre = this.pastInput(); - var c = new Array(pre.length + 1).join("-"); - return pre + this.upcomingInput() + "\n" + c+"^"; - }, -next:function () { - if (this.done) { - return this.EOF; - } - if (!this._input) this.done = true; - - var token, - match, - tempMatch, - index, - col, - lines; - if (!this._more) { - this.yytext = ''; - this.match = ''; - } - var rules = this._currentRules(); - for (var i=0;i < rules.length; i++) { - tempMatch = this._input.match(this.rules[rules[i]]); - if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { - match = tempMatch; - index = i; - if (!this.options.flex) break; - } - } - if (match) { - lines = match[0].match(/(?:\r\n?|\n).*/g); - if (lines) this.yylineno += lines.length; - this.yylloc = {first_line: this.yylloc.last_line, - last_line: this.yylineno+1, - first_column: this.yylloc.last_column, - last_column: lines ? lines[lines.length-1].length-lines[lines.length-1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length}; - this.yytext += match[0]; - this.match += match[0]; - this.matches = match; - this.yyleng = this.yytext.length; - if (this.options.ranges) { - this.yylloc.range = [this.offset, this.offset += this.yyleng]; - } - this._more = false; - this._input = this._input.slice(match[0].length); - this.matched += match[0]; - token = this.performAction.call(this, this.yy, this, rules[index],this.conditionStack[this.conditionStack.length-1]); - if (this.done && this._input) this.done = false; - if (token) return token; - else return; - } - if (this._input === "") { - return this.EOF; - } else { - return this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(), - {text: "", token: null, line: this.yylineno}); - } - }, -lex:function lex() { - var r = this.next(); - if (typeof r !== 'undefined') { - return r; - } else { - return this.lex(); - } - }, -begin:function begin(condition) { - this.conditionStack.push(condition); - }, -popState:function popState() { - return this.conditionStack.pop(); - }, -_currentRules:function _currentRules() { - return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules; - }, -topState:function () { - return this.conditionStack[this.conditionStack.length-2]; - }, -pushState:function begin(condition) { - this.begin(condition); - }}); -lexer.options = {}; -lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { - -var YYSTATE=YY_START -switch($avoiding_name_collisions) { -case 0: yy_.yytext = "\\"; return 14; -break; -case 1: - if(yy_.yytext.slice(-1) !== "\\") this.begin("mu"); - if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1), this.begin("emu"); - if(yy_.yytext) return 14; - -break; -case 2: return 14; -break; -case 3: - if(yy_.yytext.slice(-1) !== "\\") this.popState(); - if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1); - return 14; - -break; -case 4: yy_.yytext = yy_.yytext.substr(0, yy_.yyleng-4); this.popState(); return 15; -break; -case 5: return 25; -break; -case 6: return 16; -break; -case 7: return 20; -break; -case 8: return 19; -break; -case 9: return 19; -break; -case 10: return 23; -break; -case 11: return 22; -break; -case 12: this.popState(); this.begin('com'); -break; -case 13: yy_.yytext = yy_.yytext.substr(3,yy_.yyleng-5); this.popState(); return 15; -break; -case 14: return 22; -break; -case 15: return 37; -break; -case 16: return 36; -break; -case 17: return 36; -break; -case 18: return 40; -break; -case 19: /*ignore whitespace*/ -break; -case 20: this.popState(); return 24; -break; -case 21: this.popState(); return 18; -break; -case 22: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 31; -break; -case 23: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\'/g,"'"); return 31; -break; -case 24: return 38; -break; -case 25: return 33; -break; -case 26: return 33; -break; -case 27: return 32; -break; -case 28: return 36; -break; -case 29: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2); return 36; -break; -case 30: return 'INVALID'; -break; -case 31: return 5; -break; -} -}; -lexer.rules = [/^(?:\\\\(?=(\{\{)))/,/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|$)))/,/^(?:[\s\S]*?--\}\})/,/^(?:\{\{>)/,/^(?:\{\{#)/,/^(?:\{\{\/)/,/^(?:\{\{\^)/,/^(?:\{\{\s*else\b)/,/^(?:\{\{\{)/,/^(?:\{\{&)/,/^(?:\{\{!--)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{)/,/^(?:=)/,/^(?:\.(?=[}\/ ]))/,/^(?:\.\.)/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}\}\})/,/^(?:\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@)/,/^(?:true(?=[}\s]))/,/^(?:false(?=[}\s]))/,/^(?:-?[0-9]+(?=[}\s]))/,/^(?:[^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=[=}\s\/.]))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:$)/]; -lexer.conditions = {"mu":{"rules":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31],"inclusive":false},"emu":{"rules":[3],"inclusive":false},"com":{"rules":[4],"inclusive":false},"INITIAL":{"rules":[0,1,2,31],"inclusive":true}}; -return lexer;})() -parser.lexer = lexer; -function Parser () { this.yy = {}; }Parser.prototype = parser;parser.Parser = Parser; -return new Parser; -})();; -// lib/handlebars/compiler/base.js - -Handlebars.Parser = handlebars; - -Handlebars.parse = function(input) { - - // Just return if an already-compile AST was passed in. - if(input.constructor === Handlebars.AST.ProgramNode) { return input; } - - Handlebars.Parser.yy = Handlebars.AST; - return Handlebars.Parser.parse(input); -}; -; -// lib/handlebars/compiler/ast.js -Handlebars.AST = {}; - -Handlebars.AST.ProgramNode = function(statements, inverse) { - this.type = "program"; - this.statements = statements; - if(inverse) { this.inverse = new Handlebars.AST.ProgramNode(inverse); } -}; - -Handlebars.AST.MustacheNode = function(rawParams, hash, unescaped) { - this.type = "mustache"; - this.escaped = !unescaped; - this.hash = hash; - - var id = this.id = rawParams[0]; - var params = this.params = rawParams.slice(1); - - // a mustache is an eligible helper if: - // * its id is simple (a single part, not `this` or `..`) - var eligibleHelper = this.eligibleHelper = id.isSimple; - - // a mustache is definitely a helper if: - // * it is an eligible helper, and - // * it has at least one parameter or hash segment - this.isHelper = eligibleHelper && (params.length || hash); - - // if a mustache is an eligible helper but not a definite - // helper, it is ambiguous, and will be resolved in a later - // pass or at runtime. -}; - -Handlebars.AST.PartialNode = function(partialName, context) { - this.type = "partial"; - this.partialName = partialName; - this.context = context; -}; - -Handlebars.AST.BlockNode = function(mustache, program, inverse, close) { - var verifyMatch = function(open, close) { - if(open.original !== close.original) { - throw new Handlebars.Exception(open.original + " doesn't match " + close.original); - } - }; - - verifyMatch(mustache.id, close); - this.type = "block"; - this.mustache = mustache; - this.program = program; - this.inverse = inverse; - - if (this.inverse && !this.program) { - this.isInverse = true; - } -}; - -Handlebars.AST.ContentNode = function(string) { - this.type = "content"; - this.string = string; -}; - -Handlebars.AST.HashNode = function(pairs) { - this.type = "hash"; - this.pairs = pairs; -}; - -Handlebars.AST.IdNode = function(parts) { - this.type = "ID"; - - var original = "", - dig = [], - depth = 0; - - for(var i=0,l=parts.length; i 0) { throw new Handlebars.Exception("Invalid path: " + original); } - else if (part === "..") { depth++; } - else { this.isScoped = true; } - } - else { dig.push(part); } - } - - this.original = original; - this.parts = dig; - this.string = dig.join('.'); - this.depth = depth; - - // an ID is simple if it only has one part, and that part is not - // `..` or `this`. - this.isSimple = parts.length === 1 && !this.isScoped && depth === 0; - - this.stringModeValue = this.string; -}; - -Handlebars.AST.PartialNameNode = function(name) { - this.type = "PARTIAL_NAME"; - this.name = name.original; -}; - -Handlebars.AST.DataNode = function(id) { - this.type = "DATA"; - this.id = id; -}; - -Handlebars.AST.StringNode = function(string) { - this.type = "STRING"; - this.original = - this.string = - this.stringModeValue = string; -}; - -Handlebars.AST.IntegerNode = function(integer) { - this.type = "INTEGER"; - this.original = - this.integer = integer; - this.stringModeValue = Number(integer); -}; - -Handlebars.AST.BooleanNode = function(bool) { - this.type = "BOOLEAN"; - this.bool = bool; - this.stringModeValue = bool === "true"; -}; - -Handlebars.AST.CommentNode = function(comment) { - this.type = "comment"; - this.comment = comment; -}; -; -// lib/handlebars/utils.js - -var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; - -Handlebars.Exception = function(message) { - var tmp = Error.prototype.constructor.apply(this, arguments); - - // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. - for (var idx = 0; idx < errorProps.length; idx++) { - this[errorProps[idx]] = tmp[errorProps[idx]]; - } -}; -Handlebars.Exception.prototype = new Error(); - -// Build out our basic SafeString type -Handlebars.SafeString = function(string) { - this.string = string; -}; -Handlebars.SafeString.prototype.toString = function() { - return this.string.toString(); -}; - -var escape = { - "&": "&", - "<": "<", - ">": ">", - '"': """, - "'": "'", - "`": "`" -}; - -var badChars = /[&<>"'`]/g; -var possible = /[&<>"'`]/; - -var escapeChar = function(chr) { - return escape[chr] || "&"; -}; - -Handlebars.Utils = { - extend: function(obj, value) { - for(var key in value) { - if(value.hasOwnProperty(key)) { - obj[key] = value[key]; - } - } - }, - - escapeExpression: function(string) { - // don't escape SafeStrings, since they're already safe - if (string instanceof Handlebars.SafeString) { - return string.toString(); - } else if (string == null || string === false) { - return ""; - } - - // Force a string conversion as this will be done by the append regardless and - // the regex test will do this transparently behind the scenes, causing issues if - // an object's to string has escaped characters in it. - string = string.toString(); - - if(!possible.test(string)) { return string; } - return string.replace(badChars, escapeChar); - }, - - isEmpty: function(value) { - if (!value && value !== 0) { - return true; - } else if(toString.call(value) === "[object Array]" && value.length === 0) { - return true; - } else { - return false; - } - } -}; -; -// lib/handlebars/compiler/compiler.js - -/*jshint eqnull:true*/ -var Compiler = Handlebars.Compiler = function() {}; -var JavaScriptCompiler = Handlebars.JavaScriptCompiler = function() {}; - -// the foundHelper register will disambiguate helper lookup from finding a -// function in a context. This is necessary for mustache compatibility, which -// requires that context functions in blocks are evaluated by blockHelperMissing, -// and then proceed as if the resulting value was provided to blockHelperMissing. - -Compiler.prototype = { - compiler: Compiler, - - disassemble: function() { - var opcodes = this.opcodes, opcode, out = [], params, param; - - for (var i=0, l=opcodes.length; i 0) { - this.source[1] = this.source[1] + ", " + locals.join(", "); - } - - // Generate minimizer alias mappings - if (!this.isChild) { - for (var alias in this.context.aliases) { - if (this.context.aliases.hasOwnProperty(alias)) { - this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias]; - } - } - } - - if (this.source[1]) { - this.source[1] = "var " + this.source[1].substring(2) + ";"; - } - - // Merge children - if (!this.isChild) { - this.source[1] += '\n' + this.context.programs.join('\n') + '\n'; - } - - if (!this.environment.isSimple) { - this.source.push("return buffer;"); - } - - var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"]; - - for(var i=0, l=this.environment.depths.list.length; i this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } - return this.topStackName(); - }, - topStackName: function() { - return "stack" + this.stackSlot; - }, - flushInline: function() { - var inlineStack = this.inlineStack; - if (inlineStack.length) { - this.inlineStack = []; - for (var i = 0, len = inlineStack.length; i < len; i++) { - var entry = inlineStack[i]; - if (entry instanceof Literal) { - this.compileStack.push(entry); - } else { - this.pushStack(entry); - } - } - } - }, - isInline: function() { - return this.inlineStack.length; - }, - - popStack: function(wrapped) { - var inline = this.isInline(), - item = (inline ? this.inlineStack : this.compileStack).pop(); - - if (!wrapped && (item instanceof Literal)) { - return item.value; - } else { - if (!inline) { - this.stackSlot--; - } - return item; - } - }, - - topStack: function(wrapped) { - var stack = (this.isInline() ? this.inlineStack : this.compileStack), - item = stack[stack.length - 1]; - - if (!wrapped && (item instanceof Literal)) { - return item.value; - } else { - return item; - } - }, - - quotedString: function(str) { - return '"' + str - .replace(/\\/g, '\\\\') - .replace(/"/g, '\\"') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - .replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4 - .replace(/\u2029/g, '\\u2029') + '"'; - }, - - setupHelper: function(paramSize, name, missingParams) { - var params = []; - this.setupParams(paramSize, params, missingParams); - var foundHelper = this.nameLookup('helpers', name, 'helper'); - - return { - params: params, - name: foundHelper, - callParams: ["depth0"].concat(params).join(", "), - helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ") - }; - }, - - // the params and contexts arguments are passed in arrays - // to fill in - setupParams: function(paramSize, params, useRegister) { - var options = [], contexts = [], types = [], param, inverse, program; - - options.push("hash:" + this.popStack()); - - inverse = this.popStack(); - program = this.popStack(); - - // Avoid setting fn and inverse if neither are set. This allows - // helpers to do a check for `if (options.fn)` - if (program || inverse) { - if (!program) { - this.context.aliases.self = "this"; - program = "self.noop"; - } - - if (!inverse) { - this.context.aliases.self = "this"; - inverse = "self.noop"; - } - - options.push("inverse:" + inverse); - options.push("fn:" + program); - } - - for(var i=0; i": ">", + '"': """, + "'": "'", + "`": "`" + }; + + var badChars = /[&<>"'`]/g; + var possible = /[&<>"'`]/; + + function escapeChar(chr) { + return escape[chr] || "&"; + } + + function extend(obj, value) { + for(var key in value) { + if(value.hasOwnProperty(key)) { + obj[key] = value[key]; + } + } + } + + __exports__.extend = extend;var toString = Object.prototype.toString; + __exports__.toString = toString; + // Sourced from lodash + // https://github.com/bestiejs/lodash/blob/master/LICENSE.txt + var isFunction = function(value) { + return typeof value === 'function'; + }; + // fallback for older versions of Chrome and Safari + if (isFunction(/x/)) { + isFunction = function(value) { + return typeof value === 'function' && toString.call(value) === '[object Function]'; + }; + } + var isFunction; + __exports__.isFunction = isFunction; + var isArray = Array.isArray || function(value) { + return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false; + }; + __exports__.isArray = isArray; + + function escapeExpression(string) { + // don't escape SafeStrings, since they're already safe + if (string instanceof SafeString) { + return string.toString(); + } else if (!string && string !== 0) { + return ""; + } + + // Force a string conversion as this will be done by the append regardless and + // the regex test will do this transparently behind the scenes, causing issues if + // an object's to string has escaped characters in it. + string = "" + string; + + if(!possible.test(string)) { return string; } + return string.replace(badChars, escapeChar); + } + + __exports__.escapeExpression = escapeExpression;function isEmpty(value) { + if (!value && value !== 0) { + return true; + } else if (isArray(value) && value.length === 0) { + return true; + } else { + return false; + } + } + + __exports__.isEmpty = isEmpty; + return __exports__; +})(__module4__); + +// handlebars/exception.js +var __module5__ = (function() { + "use strict"; + var __exports__; + + var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; + + function Exception(/* message */) { + var tmp = Error.prototype.constructor.apply(this, arguments); + + // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. + for (var idx = 0; idx < errorProps.length; idx++) { + this[errorProps[idx]] = tmp[errorProps[idx]]; + } + } + + Exception.prototype = new Error(); + + __exports__ = Exception; + return __exports__; +})(); + +// handlebars/base.js +var __module2__ = (function(__dependency1__, __dependency2__) { + "use strict"; + var __exports__ = {}; + /*globals Exception, Utils */ + var Utils = __dependency1__; + var Exception = __dependency2__; + + var VERSION = "1.1.2"; + __exports__.VERSION = VERSION;var COMPILER_REVISION = 4; + __exports__.COMPILER_REVISION = COMPILER_REVISION; + var REVISION_CHANGES = { + 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it + 2: '== 1.0.0-rc.3', + 3: '== 1.0.0-rc.4', + 4: '>= 1.0.0' + }; + __exports__.REVISION_CHANGES = REVISION_CHANGES; + var isArray = Utils.isArray, + isFunction = Utils.isFunction, + toString = Utils.toString, + objectType = '[object Object]'; + + function HandlebarsEnvironment(helpers, partials) { + this.helpers = helpers || {}; + this.partials = partials || {}; + + registerDefaultHelpers(this); + } + + __exports__.HandlebarsEnvironment = HandlebarsEnvironment;HandlebarsEnvironment.prototype = { + constructor: HandlebarsEnvironment, + + logger: logger, + log: log, + + registerHelper: function(name, fn, inverse) { + if (toString.call(name) === objectType) { + if (inverse || fn) { throw new Exception('Arg not supported with multiple helpers'); } + Utils.extend(this.helpers, name); + } else { + if (inverse) { fn.not = inverse; } + this.helpers[name] = fn; + } + }, + + registerPartial: function(name, str) { + if (toString.call(name) === objectType) { + Utils.extend(this.partials, name); + } else { + this.partials[name] = str; + } + } + }; + + function registerDefaultHelpers(instance) { + instance.registerHelper('helperMissing', function(arg) { + if(arguments.length === 2) { + return undefined; + } else { + throw new Error("Missing helper: '" + arg + "'"); + } + }); + + instance.registerHelper('blockHelperMissing', function(context, options) { + var inverse = options.inverse || function() {}, fn = options.fn; + + if (isFunction(context)) { context = context.call(this); } + + if(context === true) { + return fn(this); + } else if(context === false || context == null) { + return inverse(this); + } else if (isArray(context)) { + if(context.length > 0) { + return instance.helpers.each(context, options); + } else { + return inverse(this); + } + } else { + return fn(context); + } + }); + + instance.registerHelper('each', function(context, options) { + var fn = options.fn, inverse = options.inverse; + var i = 0, ret = "", data; + + if (isFunction(context)) { context = context.call(this); } + + if (options.data) { + data = createFrame(options.data); + } + + if(context && typeof context === 'object') { + if (isArray(context)) { + for(var j = context.length; i 0) { throw new Exception("Invalid path: " + original); } + else if (part === "..") { depth++; } + else { this.isScoped = true; } + } + else { dig.push(part); } + } + + this.original = original; + this.parts = dig; + this.string = dig.join('.'); + this.depth = depth; + + // an ID is simple if it only has one part, and that part is not + // `..` or `this`. + this.isSimple = parts.length === 1 && !this.isScoped && depth === 0; + + this.stringModeValue = this.string; + } + + __exports__.IdNode = IdNode;function PartialNameNode(name) { + this.type = "PARTIAL_NAME"; + this.name = name.original; + } + + __exports__.PartialNameNode = PartialNameNode;function DataNode(id) { + this.type = "DATA"; + this.id = id; + } + + __exports__.DataNode = DataNode;function StringNode(string) { + this.type = "STRING"; + this.original = + this.string = + this.stringModeValue = string; + } + + __exports__.StringNode = StringNode;function IntegerNode(integer) { + this.type = "INTEGER"; + this.original = + this.integer = integer; + this.stringModeValue = Number(integer); + } + + __exports__.IntegerNode = IntegerNode;function BooleanNode(bool) { + this.type = "BOOLEAN"; + this.bool = bool; + this.stringModeValue = bool === "true"; + } + + __exports__.BooleanNode = BooleanNode;function CommentNode(comment) { + this.type = "comment"; + this.comment = comment; + } + + __exports__.CommentNode = CommentNode; + return __exports__; +})(__module5__); + +// handlebars/compiler/parser.js +var __module9__ = (function() { + "use strict"; + var __exports__; + /* Jison generated parser */ + var handlebars = (function(){ + var parser = {trace: function trace() { }, + yy: {}, + symbols_: {"error":2,"root":3,"statements":4,"EOF":5,"program":6,"simpleInverse":7,"statement":8,"openInverse":9,"closeBlock":10,"openBlock":11,"mustache":12,"partial":13,"CONTENT":14,"COMMENT":15,"OPEN_BLOCK":16,"inMustache":17,"CLOSE":18,"OPEN_INVERSE":19,"OPEN_ENDBLOCK":20,"path":21,"OPEN":22,"OPEN_UNESCAPED":23,"CLOSE_UNESCAPED":24,"OPEN_PARTIAL":25,"partialName":26,"partial_option0":27,"inMustache_repetition0":28,"inMustache_option0":29,"dataName":30,"param":31,"STRING":32,"INTEGER":33,"BOOLEAN":34,"hash":35,"hash_repetition_plus0":36,"hashSegment":37,"ID":38,"EQUALS":39,"DATA":40,"pathSegments":41,"SEP":42,"$accept":0,"$end":1}, + terminals_: {2:"error",5:"EOF",14:"CONTENT",15:"COMMENT",16:"OPEN_BLOCK",18:"CLOSE",19:"OPEN_INVERSE",20:"OPEN_ENDBLOCK",22:"OPEN",23:"OPEN_UNESCAPED",24:"CLOSE_UNESCAPED",25:"OPEN_PARTIAL",32:"STRING",33:"INTEGER",34:"BOOLEAN",38:"ID",39:"EQUALS",40:"DATA",42:"SEP"}, + productions_: [0,[3,2],[3,1],[6,2],[6,3],[6,2],[6,1],[6,1],[6,0],[4,1],[4,2],[8,3],[8,3],[8,1],[8,1],[8,1],[8,1],[11,3],[9,3],[10,3],[12,3],[12,3],[13,4],[7,2],[17,3],[17,1],[31,1],[31,1],[31,1],[31,1],[31,1],[35,1],[37,3],[26,1],[26,1],[26,1],[30,2],[21,1],[41,3],[41,1],[27,0],[27,1],[28,0],[28,2],[29,0],[29,1],[36,1],[36,2]], + performAction: function anonymous(yytext,yyleng,yylineno,yy,yystate,$$,_$) { + + var $0 = $$.length - 1; + switch (yystate) { + case 1: return new yy.ProgramNode($$[$0-1]); + break; + case 2: return new yy.ProgramNode([]); + break; + case 3:this.$ = new yy.ProgramNode([], $$[$0-1], $$[$0]); + break; + case 4:this.$ = new yy.ProgramNode($$[$0-2], $$[$0-1], $$[$0]); + break; + case 5:this.$ = new yy.ProgramNode($$[$0-1], $$[$0], []); + break; + case 6:this.$ = new yy.ProgramNode($$[$0]); + break; + case 7:this.$ = new yy.ProgramNode([]); + break; + case 8:this.$ = new yy.ProgramNode([]); + break; + case 9:this.$ = [$$[$0]]; + break; + case 10: $$[$0-1].push($$[$0]); this.$ = $$[$0-1]; + break; + case 11:this.$ = new yy.BlockNode($$[$0-2], $$[$0-1].inverse, $$[$0-1], $$[$0]); + break; + case 12:this.$ = new yy.BlockNode($$[$0-2], $$[$0-1], $$[$0-1].inverse, $$[$0]); + break; + case 13:this.$ = $$[$0]; + break; + case 14:this.$ = $$[$0]; + break; + case 15:this.$ = new yy.ContentNode($$[$0]); + break; + case 16:this.$ = new yy.CommentNode($$[$0]); + break; + case 17:this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], $$[$0-2], stripFlags($$[$0-2], $$[$0])); + break; + case 18:this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], $$[$0-2], stripFlags($$[$0-2], $$[$0])); + break; + case 19:this.$ = {path: $$[$0-1], strip: stripFlags($$[$0-2], $$[$0])}; + break; + case 20:this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], $$[$0-2], stripFlags($$[$0-2], $$[$0])); + break; + case 21:this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], $$[$0-2], stripFlags($$[$0-2], $$[$0])); + break; + case 22:this.$ = new yy.PartialNode($$[$0-2], $$[$0-1], stripFlags($$[$0-3], $$[$0])); + break; + case 23:this.$ = stripFlags($$[$0-1], $$[$0]); + break; + case 24:this.$ = [[$$[$0-2]].concat($$[$0-1]), $$[$0]]; + break; + case 25:this.$ = [[$$[$0]], null]; + break; + case 26:this.$ = $$[$0]; + break; + case 27:this.$ = new yy.StringNode($$[$0]); + break; + case 28:this.$ = new yy.IntegerNode($$[$0]); + break; + case 29:this.$ = new yy.BooleanNode($$[$0]); + break; + case 30:this.$ = $$[$0]; + break; + case 31:this.$ = new yy.HashNode($$[$0]); + break; + case 32:this.$ = [$$[$0-2], $$[$0]]; + break; + case 33:this.$ = new yy.PartialNameNode($$[$0]); + break; + case 34:this.$ = new yy.PartialNameNode(new yy.StringNode($$[$0])); + break; + case 35:this.$ = new yy.PartialNameNode(new yy.IntegerNode($$[$0])); + break; + case 36:this.$ = new yy.DataNode($$[$0]); + break; + case 37:this.$ = new yy.IdNode($$[$0]); + break; + case 38: $$[$0-2].push({part: $$[$0], separator: $$[$0-1]}); this.$ = $$[$0-2]; + break; + case 39:this.$ = [{part: $$[$0]}]; + break; + case 42:this.$ = []; + break; + case 43:$$[$0-1].push($$[$0]); + break; + case 46:this.$ = [$$[$0]]; + break; + case 47:$$[$0-1].push($$[$0]); + break; + } + }, + table: [{3:1,4:2,5:[1,3],8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],22:[1,13],23:[1,14],25:[1,15]},{1:[3]},{5:[1,16],8:17,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],22:[1,13],23:[1,14],25:[1,15]},{1:[2,2]},{5:[2,9],14:[2,9],15:[2,9],16:[2,9],19:[2,9],20:[2,9],22:[2,9],23:[2,9],25:[2,9]},{4:20,6:18,7:19,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,21],20:[2,8],22:[1,13],23:[1,14],25:[1,15]},{4:20,6:22,7:19,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,21],20:[2,8],22:[1,13],23:[1,14],25:[1,15]},{5:[2,13],14:[2,13],15:[2,13],16:[2,13],19:[2,13],20:[2,13],22:[2,13],23:[2,13],25:[2,13]},{5:[2,14],14:[2,14],15:[2,14],16:[2,14],19:[2,14],20:[2,14],22:[2,14],23:[2,14],25:[2,14]},{5:[2,15],14:[2,15],15:[2,15],16:[2,15],19:[2,15],20:[2,15],22:[2,15],23:[2,15],25:[2,15]},{5:[2,16],14:[2,16],15:[2,16],16:[2,16],19:[2,16],20:[2,16],22:[2,16],23:[2,16],25:[2,16]},{17:23,21:24,30:25,38:[1,28],40:[1,27],41:26},{17:29,21:24,30:25,38:[1,28],40:[1,27],41:26},{17:30,21:24,30:25,38:[1,28],40:[1,27],41:26},{17:31,21:24,30:25,38:[1,28],40:[1,27],41:26},{21:33,26:32,32:[1,34],33:[1,35],38:[1,28],41:26},{1:[2,1]},{5:[2,10],14:[2,10],15:[2,10],16:[2,10],19:[2,10],20:[2,10],22:[2,10],23:[2,10],25:[2,10]},{10:36,20:[1,37]},{4:38,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,7],22:[1,13],23:[1,14],25:[1,15]},{7:39,8:17,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,21],20:[2,6],22:[1,13],23:[1,14],25:[1,15]},{17:23,18:[1,40],21:24,30:25,38:[1,28],40:[1,27],41:26},{10:41,20:[1,37]},{18:[1,42]},{18:[2,42],24:[2,42],28:43,32:[2,42],33:[2,42],34:[2,42],38:[2,42],40:[2,42]},{18:[2,25],24:[2,25]},{18:[2,37],24:[2,37],32:[2,37],33:[2,37],34:[2,37],38:[2,37],40:[2,37],42:[1,44]},{21:45,38:[1,28],41:26},{18:[2,39],24:[2,39],32:[2,39],33:[2,39],34:[2,39],38:[2,39],40:[2,39],42:[2,39]},{18:[1,46]},{18:[1,47]},{24:[1,48]},{18:[2,40],21:50,27:49,38:[1,28],41:26},{18:[2,33],38:[2,33]},{18:[2,34],38:[2,34]},{18:[2,35],38:[2,35]},{5:[2,11],14:[2,11],15:[2,11],16:[2,11],19:[2,11],20:[2,11],22:[2,11],23:[2,11],25:[2,11]},{21:51,38:[1,28],41:26},{8:17,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,3],22:[1,13],23:[1,14],25:[1,15]},{4:52,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,5],22:[1,13],23:[1,14],25:[1,15]},{14:[2,23],15:[2,23],16:[2,23],19:[2,23],20:[2,23],22:[2,23],23:[2,23],25:[2,23]},{5:[2,12],14:[2,12],15:[2,12],16:[2,12],19:[2,12],20:[2,12],22:[2,12],23:[2,12],25:[2,12]},{14:[2,18],15:[2,18],16:[2,18],19:[2,18],20:[2,18],22:[2,18],23:[2,18],25:[2,18]},{18:[2,44],21:56,24:[2,44],29:53,30:60,31:54,32:[1,57],33:[1,58],34:[1,59],35:55,36:61,37:62,38:[1,63],40:[1,27],41:26},{38:[1,64]},{18:[2,36],24:[2,36],32:[2,36],33:[2,36],34:[2,36],38:[2,36],40:[2,36]},{14:[2,17],15:[2,17],16:[2,17],19:[2,17],20:[2,17],22:[2,17],23:[2,17],25:[2,17]},{5:[2,20],14:[2,20],15:[2,20],16:[2,20],19:[2,20],20:[2,20],22:[2,20],23:[2,20],25:[2,20]},{5:[2,21],14:[2,21],15:[2,21],16:[2,21],19:[2,21],20:[2,21],22:[2,21],23:[2,21],25:[2,21]},{18:[1,65]},{18:[2,41]},{18:[1,66]},{8:17,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,4],22:[1,13],23:[1,14],25:[1,15]},{18:[2,24],24:[2,24]},{18:[2,43],24:[2,43],32:[2,43],33:[2,43],34:[2,43],38:[2,43],40:[2,43]},{18:[2,45],24:[2,45]},{18:[2,26],24:[2,26],32:[2,26],33:[2,26],34:[2,26],38:[2,26],40:[2,26]},{18:[2,27],24:[2,27],32:[2,27],33:[2,27],34:[2,27],38:[2,27],40:[2,27]},{18:[2,28],24:[2,28],32:[2,28],33:[2,28],34:[2,28],38:[2,28],40:[2,28]},{18:[2,29],24:[2,29],32:[2,29],33:[2,29],34:[2,29],38:[2,29],40:[2,29]},{18:[2,30],24:[2,30],32:[2,30],33:[2,30],34:[2,30],38:[2,30],40:[2,30]},{18:[2,31],24:[2,31],37:67,38:[1,68]},{18:[2,46],24:[2,46],38:[2,46]},{18:[2,39],24:[2,39],32:[2,39],33:[2,39],34:[2,39],38:[2,39],39:[1,69],40:[2,39],42:[2,39]},{18:[2,38],24:[2,38],32:[2,38],33:[2,38],34:[2,38],38:[2,38],40:[2,38],42:[2,38]},{5:[2,22],14:[2,22],15:[2,22],16:[2,22],19:[2,22],20:[2,22],22:[2,22],23:[2,22],25:[2,22]},{5:[2,19],14:[2,19],15:[2,19],16:[2,19],19:[2,19],20:[2,19],22:[2,19],23:[2,19],25:[2,19]},{18:[2,47],24:[2,47],38:[2,47]},{39:[1,69]},{21:56,30:60,31:70,32:[1,57],33:[1,58],34:[1,59],38:[1,28],40:[1,27],41:26},{18:[2,32],24:[2,32],38:[2,32]}], + defaultActions: {3:[2,2],16:[2,1],50:[2,41]}, + parseError: function parseError(str, hash) { + throw new Error(str); + }, + parse: function parse(input) { + var self = this, stack = [0], vstack = [null], lstack = [], table = this.table, yytext = "", yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; + this.lexer.setInput(input); + this.lexer.yy = this.yy; + this.yy.lexer = this.lexer; + this.yy.parser = this; + if (typeof this.lexer.yylloc == "undefined") + this.lexer.yylloc = {}; + var yyloc = this.lexer.yylloc; + lstack.push(yyloc); + var ranges = this.lexer.options && this.lexer.options.ranges; + if (typeof this.yy.parseError === "function") + this.parseError = this.yy.parseError; + function popStack(n) { + stack.length = stack.length - 2 * n; + vstack.length = vstack.length - n; + lstack.length = lstack.length - n; + } + function lex() { + var token; + token = self.lexer.lex() || 1; + if (typeof token !== "number") { + token = self.symbols_[token] || token; + } + return token; + } + var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; + while (true) { + state = stack[stack.length - 1]; + if (this.defaultActions[state]) { + action = this.defaultActions[state]; + } else { + if (symbol === null || typeof symbol == "undefined") { + symbol = lex(); + } + action = table[state] && table[state][symbol]; + } + if (typeof action === "undefined" || !action.length || !action[0]) { + var errStr = ""; + if (!recovering) { + expected = []; + for (p in table[state]) + if (this.terminals_[p] && p > 2) { + expected.push("'" + this.terminals_[p] + "'"); + } + if (this.lexer.showPosition) { + errStr = "Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", ") + ", got '" + (this.terminals_[symbol] || symbol) + "'"; + } else { + errStr = "Parse error on line " + (yylineno + 1) + ": Unexpected " + (symbol == 1?"end of input":"'" + (this.terminals_[symbol] || symbol) + "'"); + } + this.parseError(errStr, {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected}); + } + } + if (action[0] instanceof Array && action.length > 1) { + throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol); + } + switch (action[0]) { + case 1: + stack.push(symbol); + vstack.push(this.lexer.yytext); + lstack.push(this.lexer.yylloc); + stack.push(action[1]); + symbol = null; + if (!preErrorSymbol) { + yyleng = this.lexer.yyleng; + yytext = this.lexer.yytext; + yylineno = this.lexer.yylineno; + yyloc = this.lexer.yylloc; + if (recovering > 0) + recovering--; + } else { + symbol = preErrorSymbol; + preErrorSymbol = null; + } + break; + case 2: + len = this.productions_[action[1]][1]; + yyval.$ = vstack[vstack.length - len]; + yyval._$ = {first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column}; + if (ranges) { + yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]]; + } + r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack); + if (typeof r !== "undefined") { + return r; + } + if (len) { + stack = stack.slice(0, -1 * len * 2); + vstack = vstack.slice(0, -1 * len); + lstack = lstack.slice(0, -1 * len); + } + stack.push(this.productions_[action[1]][0]); + vstack.push(yyval.$); + lstack.push(yyval._$); + newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; + stack.push(newState); + break; + case 3: + return true; + } + } + return true; + } + }; + + + function stripFlags(open, close) { + return { + left: open[2] === '~', + right: close[0] === '~' || close[1] === '~' + }; + } + + /* Jison generated lexer */ + var lexer = (function(){ + var lexer = ({EOF:1, + parseError:function parseError(str, hash) { + if (this.yy.parser) { + this.yy.parser.parseError(str, hash); + } else { + throw new Error(str); + } + }, + setInput:function (input) { + this._input = input; + this._more = this._less = this.done = false; + this.yylineno = this.yyleng = 0; + this.yytext = this.matched = this.match = ''; + this.conditionStack = ['INITIAL']; + this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0}; + if (this.options.ranges) this.yylloc.range = [0,0]; + this.offset = 0; + return this; + }, + input:function () { + var ch = this._input[0]; + this.yytext += ch; + this.yyleng++; + this.offset++; + this.match += ch; + this.matched += ch; + var lines = ch.match(/(?:\r\n?|\n).*/g); + if (lines) { + this.yylineno++; + this.yylloc.last_line++; + } else { + this.yylloc.last_column++; + } + if (this.options.ranges) this.yylloc.range[1]++; + + this._input = this._input.slice(1); + return ch; + }, + unput:function (ch) { + var len = ch.length; + var lines = ch.split(/(?:\r\n?|\n)/g); + + this._input = ch + this._input; + this.yytext = this.yytext.substr(0, this.yytext.length-len-1); + //this.yyleng -= len; + this.offset -= len; + var oldLines = this.match.split(/(?:\r\n?|\n)/g); + this.match = this.match.substr(0, this.match.length-1); + this.matched = this.matched.substr(0, this.matched.length-1); + + if (lines.length-1) this.yylineno -= lines.length-1; + var r = this.yylloc.range; + + this.yylloc = {first_line: this.yylloc.first_line, + last_line: this.yylineno+1, + first_column: this.yylloc.first_column, + last_column: lines ? + (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length: + this.yylloc.first_column - len + }; + + if (this.options.ranges) { + this.yylloc.range = [r[0], r[0] + this.yyleng - len]; + } + return this; + }, + more:function () { + this._more = true; + return this; + }, + less:function (n) { + this.unput(this.match.slice(n)); + }, + pastInput:function () { + var past = this.matched.substr(0, this.matched.length - this.match.length); + return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); + }, + upcomingInput:function () { + var next = this.match; + if (next.length < 20) { + next += this._input.substr(0, 20-next.length); + } + return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, ""); + }, + showPosition:function () { + var pre = this.pastInput(); + var c = new Array(pre.length + 1).join("-"); + return pre + this.upcomingInput() + "\n" + c+"^"; + }, + next:function () { + if (this.done) { + return this.EOF; + } + if (!this._input) this.done = true; + + var token, + match, + tempMatch, + index, + col, + lines; + if (!this._more) { + this.yytext = ''; + this.match = ''; + } + var rules = this._currentRules(); + for (var i=0;i < rules.length; i++) { + tempMatch = this._input.match(this.rules[rules[i]]); + if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { + match = tempMatch; + index = i; + if (!this.options.flex) break; + } + } + if (match) { + lines = match[0].match(/(?:\r\n?|\n).*/g); + if (lines) this.yylineno += lines.length; + this.yylloc = {first_line: this.yylloc.last_line, + last_line: this.yylineno+1, + first_column: this.yylloc.last_column, + last_column: lines ? lines[lines.length-1].length-lines[lines.length-1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length}; + this.yytext += match[0]; + this.match += match[0]; + this.matches = match; + this.yyleng = this.yytext.length; + if (this.options.ranges) { + this.yylloc.range = [this.offset, this.offset += this.yyleng]; + } + this._more = false; + this._input = this._input.slice(match[0].length); + this.matched += match[0]; + token = this.performAction.call(this, this.yy, this, rules[index],this.conditionStack[this.conditionStack.length-1]); + if (this.done && this._input) this.done = false; + if (token) return token; + else return; + } + if (this._input === "") { + return this.EOF; + } else { + return this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(), + {text: "", token: null, line: this.yylineno}); + } + }, + lex:function lex() { + var r = this.next(); + if (typeof r !== 'undefined') { + return r; + } else { + return this.lex(); + } + }, + begin:function begin(condition) { + this.conditionStack.push(condition); + }, + popState:function popState() { + return this.conditionStack.pop(); + }, + _currentRules:function _currentRules() { + return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules; + }, + topState:function () { + return this.conditionStack[this.conditionStack.length-2]; + }, + pushState:function begin(condition) { + this.begin(condition); + }}); + lexer.options = {}; + lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { + + + function strip(start, end) { + return yy_.yytext = yy_.yytext.substr(start, yy_.yyleng-end); + } + + + var YYSTATE=YY_START + switch($avoiding_name_collisions) { + case 0: + if(yy_.yytext.slice(-2) === "\\\\") { + strip(0,1); + this.begin("mu"); + } else if(yy_.yytext.slice(-1) === "\\") { + strip(0,1); + this.begin("emu"); + } else { + this.begin("mu"); + } + if(yy_.yytext) return 14; + + break; + case 1:return 14; + break; + case 2: + if(yy_.yytext.slice(-1) !== "\\") this.popState(); + if(yy_.yytext.slice(-1) === "\\") strip(0,1); + return 14; + + break; + case 3:strip(0,4); this.popState(); return 15; + break; + case 4:return 25; + break; + case 5:return 16; + break; + case 6:return 20; + break; + case 7:return 19; + break; + case 8:return 19; + break; + case 9:return 23; + break; + case 10:return 22; + break; + case 11:this.popState(); this.begin('com'); + break; + case 12:strip(3,5); this.popState(); return 15; + break; + case 13:return 22; + break; + case 14:return 39; + break; + case 15:return 38; + break; + case 16:return 38; + break; + case 17:return 42; + break; + case 18:/*ignore whitespace*/ + break; + case 19:this.popState(); return 24; + break; + case 20:this.popState(); return 18; + break; + case 21:yy_.yytext = strip(1,2).replace(/\\"/g,'"'); return 32; + break; + case 22:yy_.yytext = strip(1,2).replace(/\\'/g,"'"); return 32; + break; + case 23:return 40; + break; + case 24:return 34; + break; + case 25:return 34; + break; + case 26:return 33; + break; + case 27:return 38; + break; + case 28:yy_.yytext = strip(1,2); return 38; + break; + case 29:return 'INVALID'; + break; + case 30:return 5; + break; + } + }; + lexer.rules = [/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|$)))/,/^(?:[\s\S]*?--\}\})/,/^(?:\{\{(~)?>)/,/^(?:\{\{(~)?#)/,/^(?:\{\{(~)?\/)/,/^(?:\{\{(~)?\^)/,/^(?:\{\{(~)?\s*else\b)/,/^(?:\{\{(~)?\{)/,/^(?:\{\{(~)?&)/,/^(?:\{\{!--)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{(~)?)/,/^(?:=)/,/^(?:\.\.)/,/^(?:\.(?=([=~}\s\/.])))/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}(~)?\}\})/,/^(?:(~)?\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@)/,/^(?:true(?=([~}\s])))/,/^(?:false(?=([~}\s])))/,/^(?:-?[0-9]+(?=([~}\s])))/,/^(?:([^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=([=~}\s\/.]))))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:$)/]; + lexer.conditions = {"mu":{"rules":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30],"inclusive":false},"emu":{"rules":[2],"inclusive":false},"com":{"rules":[3],"inclusive":false},"INITIAL":{"rules":[0,1,30],"inclusive":true}}; + return lexer;})() + parser.lexer = lexer; + function Parser () { this.yy = {}; }Parser.prototype = parser;parser.Parser = Parser; + return new Parser; + })();__exports__ = handlebars; + return __exports__; +})(); + +// handlebars/compiler/base.js +var __module8__ = (function(__dependency1__, __dependency2__) { + "use strict"; + var __exports__ = {}; + var parser = __dependency1__; + var AST = __dependency2__; + + __exports__.parser = parser; + + function parse(input) { + // Just return if an already-compile AST was passed in. + if(input.constructor === AST.ProgramNode) { return input; } + + parser.yy = AST; + return parser.parse(input); + } + + __exports__.parse = parse; + return __exports__; +})(__module9__, __module7__); + +// handlebars/compiler/javascript-compiler.js +var __module11__ = (function(__dependency1__) { + "use strict"; + var __exports__; + var COMPILER_REVISION = __dependency1__.COMPILER_REVISION; + var REVISION_CHANGES = __dependency1__.REVISION_CHANGES; + var log = __dependency1__.log; + + function Literal(value) { + this.value = value; + } + + function JavaScriptCompiler() {} + + JavaScriptCompiler.prototype = { + // PUBLIC API: You can override these methods in a subclass to provide + // alternative compiled forms for name lookup and buffering semantics + nameLookup: function(parent, name /* , type*/) { + var wrap, + ret; + if (parent.indexOf('depth') === 0) { + wrap = true; + } + + if (/^[0-9]+$/.test(name)) { + ret = parent + "[" + name + "]"; + } else if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) { + ret = parent + "." + name; + } + else { + ret = parent + "['" + name + "']"; + } + + if (wrap) { + return '(' + parent + ' && ' + ret + ')'; + } else { + return ret; + } + }, + + appendToBuffer: function(string) { + if (this.environment.isSimple) { + return "return " + string + ";"; + } else { + return { + appendToBuffer: true, + content: string, + toString: function() { return "buffer += " + string + ";"; } + }; + } + }, + + initializeBuffer: function() { + return this.quotedString(""); + }, + + namespace: "Handlebars", + // END PUBLIC API + + compile: function(environment, options, context, asObject) { + this.environment = environment; + this.options = options || {}; + + log('debug', this.environment.disassemble() + "\n\n"); + + this.name = this.environment.name; + this.isChild = !!context; + this.context = context || { + programs: [], + environments: [], + aliases: { } + }; + + this.preamble(); + + this.stackSlot = 0; + this.stackVars = []; + this.registers = { list: [] }; + this.compileStack = []; + this.inlineStack = []; + + this.compileChildren(environment, options); + + var opcodes = environment.opcodes, opcode; + + this.i = 0; + + for(var l=opcodes.length; this.i 0) { + this.source[1] = this.source[1] + ", " + locals.join(", "); + } + + // Generate minimizer alias mappings + if (!this.isChild) { + for (var alias in this.context.aliases) { + if (this.context.aliases.hasOwnProperty(alias)) { + this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias]; + } + } + } + + if (this.source[1]) { + this.source[1] = "var " + this.source[1].substring(2) + ";"; + } + + // Merge children + if (!this.isChild) { + this.source[1] += '\n' + this.context.programs.join('\n') + '\n'; + } + + if (!this.environment.isSimple) { + this.pushSource("return buffer;"); + } + + var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"]; + + for(var i=0, l=this.environment.depths.list.length; i this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } + return this.topStackName(); + }, + topStackName: function() { + return "stack" + this.stackSlot; + }, + flushInline: function() { + var inlineStack = this.inlineStack; + if (inlineStack.length) { + this.inlineStack = []; + for (var i = 0, len = inlineStack.length; i < len; i++) { + var entry = inlineStack[i]; + if (entry instanceof Literal) { + this.compileStack.push(entry); + } else { + this.pushStack(entry); + } + } + } + }, + isInline: function() { + return this.inlineStack.length; + }, + + popStack: function(wrapped) { + var inline = this.isInline(), + item = (inline ? this.inlineStack : this.compileStack).pop(); + + if (!wrapped && (item instanceof Literal)) { + return item.value; + } else { + if (!inline) { + this.stackSlot--; + } + return item; + } + }, + + topStack: function(wrapped) { + var stack = (this.isInline() ? this.inlineStack : this.compileStack), + item = stack[stack.length - 1]; + + if (!wrapped && (item instanceof Literal)) { + return item.value; + } else { + return item; + } + }, + + quotedString: function(str) { + return '"' + str + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4 + .replace(/\u2029/g, '\\u2029') + '"'; + }, + + setupHelper: function(paramSize, name, missingParams) { + var params = []; + this.setupParams(paramSize, params, missingParams); + var foundHelper = this.nameLookup('helpers', name, 'helper'); + + return { + params: params, + name: foundHelper, + callParams: ["depth0"].concat(params).join(", "), + helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ") + }; + }, + + // the params and contexts arguments are passed in arrays + // to fill in + setupParams: function(paramSize, params, useRegister) { + var options = [], contexts = [], types = [], param, inverse, program; + + options.push("hash:" + this.popStack()); + + inverse = this.popStack(); + program = this.popStack(); + + // Avoid setting fn and inverse if neither are set. This allows + // helpers to do a check for `if (options.fn)` + if (program || inverse) { + if (!program) { + this.context.aliases.self = "this"; + program = "self.noop"; + } + + if (!inverse) { + this.context.aliases.self = "this"; + inverse = "self.noop"; + } + + options.push("inverse:" + inverse); + options.push("fn:" + program); + } + + for(var i=0; i to avoid XSS via location.hash (#9521) // Strict HTML recognition (#11290: must start with <) - rquickExpr = /^(?:(<[\w\W]+>)[^>]*|#([\w-]*))$/, + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, // Match a standalone tag rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, @@ -227,11 +228,6 @@ jQuery.fn = jQuery.prototype = { // The default length of a jQuery object is 0 length: 0, - // The number of elements contained in the matched element set - size: function() { - return this.length; - }, - toArray: function() { return core_slice.call( this ); }, @@ -380,6 +376,10 @@ jQuery.extend = jQuery.fn.extend = function() { }; jQuery.extend({ + // Unique for each copy of jQuery on the page + // Non-digits removed to match rinlinejQuery + expando: "jQuery" + ( core_version + Math.random() ).replace( /\D/g, "" ), + noConflict: function( deep ) { if ( window.$ === jQuery ) { window.$ = _$; @@ -450,6 +450,7 @@ jQuery.extend({ }, isWindow: function( obj ) { + /* jshint eqeqeq: false */ return obj != null && obj == obj.window; }, @@ -467,6 +468,8 @@ jQuery.extend({ }, isPlainObject: function( obj ) { + var key; + // Must be an Object. // Because of IE, we also have to check the presence of the constructor property. // Make sure that DOM nodes and window objects don't pass through, as well @@ -486,10 +489,16 @@ jQuery.extend({ return false; } + // Support: IE<9 + // Handle iteration over inherited properties before own properties. + if ( jQuery.support.ownLast ) { + for ( key in obj ) { + return core_hasOwn.call( obj, key ); + } + } + // Own properties are enumerated firstly, so to speed up, // if last one is own, then all properties are own. - - var key; for ( key in obj ) {} return key === undefined || core_hasOwn.call( obj, key ); @@ -879,6 +888,29 @@ jQuery.extend({ now: function() { return ( new Date() ).getTime(); + }, + + // A method for quickly swapping in/out CSS properties to get correct calculations. + // Note: this method belongs to the css module but it's needed here for the support module. + // If support gets modularized, this method should be moved back to the css module. + swap: function( elem, options, callback, args ) { + var ret, name, + old = {}; + + // Remember the old values, and insert the new ones + for ( name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + ret = callback.apply( elem, args || [] ); + + // Revert the old values + for ( name in options ) { + elem.style[ name ] = old[ name ]; + } + + return ret; } }); @@ -967,4585 +999,4707 @@ function isArraylike( obj ) { // All jQuery objects should point back to these rootjQuery = jQuery(document); -// String to Object options format cache -var optionsCache = {}; - -// Convert String-formatted options into Object-formatted ones and store in cache -function createOptions( options ) { - var object = optionsCache[ options ] = {}; - jQuery.each( options.match( core_rnotwhite ) || [], function( _, flag ) { - object[ flag ] = true; - }); - return object; -} - -/* - * Create a callback list using the following parameters: - * - * options: an optional list of space-separated options that will change how - * the callback list behaves or a more traditional option object - * - * By default a callback list will act like an event callback list and can be - * "fired" multiple times. - * - * Possible options: - * - * once: will ensure the callback list can only be fired once (like a Deferred) - * - * memory: will keep track of previous values and will call any callback added - * after the list has been fired right away with the latest "memorized" - * values (like a Deferred) - * - * unique: will ensure a callback can only be added once (no duplicate in the list) +/*! + * Sizzle CSS Selector Engine v1.10.2 + * http://sizzlejs.com/ * - * stopOnFalse: interrupt callings when a callback returns false + * Copyright 2013 jQuery Foundation, Inc. and other contributors + * Released under the MIT license + * http://jquery.org/license * + * Date: 2013-07-03 */ -jQuery.Callbacks = function( options ) { - - // Convert options from String-formatted to Object-formatted if needed - // (we check in cache first) - options = typeof options === "string" ? - ( optionsCache[ options ] || createOptions( options ) ) : - jQuery.extend( {}, options ); +(function( window, undefined ) { - var // Flag to know if list is currently firing - firing, - // Last fire value (for non-forgettable lists) - memory, - // Flag to know if list was already fired - fired, - // End of the loop when firing - firingLength, - // Index of currently firing callback (modified by remove if needed) - firingIndex, - // First callback to fire (used internally by add and fireWith) - firingStart, - // Actual callback list - list = [], - // Stack of fire calls for repeatable lists - stack = !options.once && [], - // Fire callbacks - fire = function( data ) { - memory = options.memory && data; - fired = true; - firingIndex = firingStart || 0; - firingStart = 0; - firingLength = list.length; - firing = true; - for ( ; list && firingIndex < firingLength; firingIndex++ ) { - if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { - memory = false; // To prevent further calls using add - break; - } - } - firing = false; - if ( list ) { - if ( stack ) { - if ( stack.length ) { - fire( stack.shift() ); - } - } else if ( memory ) { - list = []; - } else { - self.disable(); - } - } - }, - // Actual Callbacks object - self = { - // Add a callback or a collection of callbacks to the list - add: function() { - if ( list ) { - // First, we save the current length - var start = list.length; - (function add( args ) { - jQuery.each( args, function( _, arg ) { - var type = jQuery.type( arg ); - if ( type === "function" ) { - if ( !options.unique || !self.has( arg ) ) { - list.push( arg ); - } - } else if ( arg && arg.length && type !== "string" ) { - // Inspect recursively - add( arg ); - } - }); - })( arguments ); - // Do we need to add the callbacks to the - // current firing batch? - if ( firing ) { - firingLength = list.length; - // With memory, if we're not firing then - // we should call right away - } else if ( memory ) { - firingStart = start; - fire( memory ); - } - } - return this; - }, - // Remove a callback from the list - remove: function() { - if ( list ) { - jQuery.each( arguments, function( _, arg ) { - var index; - while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { - list.splice( index, 1 ); - // Handle firing indexes - if ( firing ) { - if ( index <= firingLength ) { - firingLength--; - } - if ( index <= firingIndex ) { - firingIndex--; - } - } - } - }); - } - return this; - }, - // Check if a given callback is in the list. - // If no argument is given, return whether or not list has callbacks attached. - has: function( fn ) { - return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length ); - }, - // Remove all callbacks from the list - empty: function() { - list = []; - return this; - }, - // Have the list do nothing anymore - disable: function() { - list = stack = memory = undefined; - return this; - }, - // Is it disabled? - disabled: function() { - return !list; - }, - // Lock the list in its current state - lock: function() { - stack = undefined; - if ( !memory ) { - self.disable(); - } - return this; - }, - // Is it locked? - locked: function() { - return !stack; - }, - // Call all callbacks with the given context and arguments - fireWith: function( context, args ) { - args = args || []; - args = [ context, args.slice ? args.slice() : args ]; - if ( list && ( !fired || stack ) ) { - if ( firing ) { - stack.push( args ); - } else { - fire( args ); - } - } - return this; - }, - // Call all the callbacks with the given arguments - fire: function() { - self.fireWith( this, arguments ); - return this; - }, - // To know if the callbacks have already been called at least once - fired: function() { - return !!fired; - } - }; +var i, + support, + cachedruns, + Expr, + getText, + isXML, + compile, + outermostContext, + sortInput, - return self; -}; -jQuery.extend({ + // Local document vars + setDocument, + document, + docElem, + documentIsHTML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, - Deferred: function( func ) { - var tuples = [ - // action, add listener, listener list, final state - [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], - [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], - [ "notify", "progress", jQuery.Callbacks("memory") ] - ], - state = "pending", - promise = { - state: function() { - return state; - }, - always: function() { - deferred.done( arguments ).fail( arguments ); - return this; - }, - then: function( /* fnDone, fnFail, fnProgress */ ) { - var fns = arguments; - return jQuery.Deferred(function( newDefer ) { - jQuery.each( tuples, function( i, tuple ) { - var action = tuple[ 0 ], - fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; - // deferred[ done | fail | progress ] for forwarding actions to newDefer - deferred[ tuple[1] ](function() { - var returned = fn && fn.apply( this, arguments ); - if ( returned && jQuery.isFunction( returned.promise ) ) { - returned.promise() - .done( newDefer.resolve ) - .fail( newDefer.reject ) - .progress( newDefer.notify ); - } else { - newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); - } - }); - }); - fns = null; - }).promise(); - }, - // Get a promise for this deferred - // If obj is provided, the promise aspect is added to the object - promise: function( obj ) { - return obj != null ? jQuery.extend( obj, promise ) : promise; - } - }, - deferred = {}; - - // Keep pipe for back-compat - promise.pipe = promise.then; + // Instance-specific data + expando = "sizzle" + -(new Date()), + preferredDoc = window.document, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + hasDuplicate = false, + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + return 0; + } + return 0; + }, - // Add list-specific methods - jQuery.each( tuples, function( i, tuple ) { - var list = tuple[ 2 ], - stateString = tuple[ 3 ]; + // General-purpose constants + strundefined = typeof undefined, + MAX_NEGATIVE = 1 << 31, - // promise[ done | fail | progress ] = list.add - promise[ tuple[1] ] = list.add; + // Instance methods + hasOwn = ({}).hasOwnProperty, + arr = [], + pop = arr.pop, + push_native = arr.push, + push = arr.push, + slice = arr.slice, + // Use a stripped-down indexOf if we can't use a native one + indexOf = arr.indexOf || function( elem ) { + var i = 0, + len = this.length; + for ( ; i < len; i++ ) { + if ( this[i] === elem ) { + return i; + } + } + return -1; + }, - // Handle state - if ( stateString ) { - list.add(function() { - // state = [ resolved | rejected ] - state = stateString; + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", - // [ reject_list | resolve_list ].disable; progress_list.lock - }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); - } + // Regular expressions - // deferred[ resolve | reject | notify ] - deferred[ tuple[0] ] = function() { - deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); - return this; - }; - deferred[ tuple[0] + "With" ] = list.fireWith; - }); + // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + // http://www.w3.org/TR/css3-syntax/#characters + characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", - // Make the deferred a promise - promise.promise( deferred ); + // Loosely modeled on CSS identifier characters + // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors + // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + identifier = characterEncoding.replace( "w", "w#" ), - // Call given func if any - if ( func ) { - func.call( deferred, deferred ); - } + // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace + + "*(?:([*^$|!~]?=)" + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]", - // All done! - return deferred; - }, + // Prefer arguments quoted, + // then not containing pseudos/brackets, + // then attribute selectors/non-parenthetical expressions, + // then anything else + // These preferences are here to reduce the number of selectors + // needing tokenize in the PSEUDO preFilter + pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace( 3, 8 ) + ")*)|.*)\\)|)", - // Deferred helper - when: function( subordinate /* , ..., subordinateN */ ) { - var i = 0, - resolveValues = core_slice.call( arguments ), - length = resolveValues.length, + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), - // the count of uncompleted subordinates - remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), - // the master Deferred. If resolveValues consist of only a single Deferred, just use that. - deferred = remaining === 1 ? subordinate : jQuery.Deferred(), + rsibling = new RegExp( whitespace + "*[+~]" ), + rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*)" + whitespace + "*\\]", "g" ), - // Update function for both resolve and progress values - updateFunc = function( i, contexts, values ) { - return function( value ) { - contexts[ i ] = this; - values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value; - if( values === progressValues ) { - deferred.notifyWith( contexts, values ); - } else if ( !( --remaining ) ) { - deferred.resolveWith( contexts, values ); - } - }; - }, + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), - progressValues, progressContexts, resolveContexts; + matchExpr = { + "ID": new RegExp( "^#(" + characterEncoding + ")" ), + "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), + "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, - // add listeners to Deferred subordinates; treat others as resolved - if ( length > 1 ) { - progressValues = new Array( length ); - progressContexts = new Array( length ); - resolveContexts = new Array( length ); - for ( ; i < length; i++ ) { - if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { - resolveValues[ i ].promise() - .done( updateFunc( i, resolveContexts, resolveValues ) ) - .fail( deferred.reject ) - .progress( updateFunc( i, progressContexts, progressValues ) ); - } else { - --remaining; - } - } - } + rnative = /^[^{]+\{\s*\[native \w/, - // if we're not waiting on anything, resolve the master - if ( !remaining ) { - deferred.resolveWith( resolveContexts, resolveValues ); - } + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, - return deferred.promise(); - } -}); -jQuery.support = (function() { + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, - var support, all, a, - input, select, fragment, - opt, eventName, isSupported, i, - div = document.createElement("div"); + rescape = /'|\\/g, - // Setup - div.setAttribute( "className", "t" ); - div.innerHTML = "
    a"; + // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), + funescape = function( _, escaped, escapedWhitespace ) { + var high = "0x" + escaped - 0x10000; + // NaN means non-codepoint + // Support: Firefox + // Workaround erroneous numeric interpretation of +"0x" + return high !== high || escapedWhitespace ? + escaped : + // BMP codepoint + high < 0 ? + String.fromCharCode( high + 0x10000 ) : + // Supplemental Plane codepoint (surrogate pair) + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }; - // Support tests won't run in some limited or non-browser environments - all = div.getElementsByTagName("*"); - a = div.getElementsByTagName("a")[ 0 ]; - if ( !all || !a || !all.length ) { - return {}; - } +// Optimize for push.apply( _, NodeList ) +try { + push.apply( + (arr = slice.call( preferredDoc.childNodes )), + preferredDoc.childNodes + ); + // Support: Android<4.0 + // Detect silently failing push.apply + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { apply: arr.length ? - // First batch of tests - select = document.createElement("select"); - opt = select.appendChild( document.createElement("option") ); - input = div.getElementsByTagName("input")[ 0 ]; + // Leverage slice if possible + function( target, els ) { + push_native.apply( target, slice.call(els) ); + } : - a.style.cssText = "top:1px;float:left;opacity:.5"; - support = { - // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) - getSetAttribute: div.className !== "t", - - // IE strips leading whitespace when .innerHTML is used - leadingWhitespace: div.firstChild.nodeType === 3, - - // Make sure that tbody elements aren't automatically inserted - // IE will insert them into empty tables - tbody: !div.getElementsByTagName("tbody").length, - - // Make sure that link elements get serialized correctly by innerHTML - // This requires a wrapper element in IE - htmlSerialize: !!div.getElementsByTagName("link").length, - - // Get the style information from getAttribute - // (IE uses .cssText instead) - style: /top/.test( a.getAttribute("style") ), - - // Make sure that URLs aren't manipulated - // (IE normalizes it by default) - hrefNormalized: a.getAttribute("href") === "/a", - - // Make sure that element opacity exists - // (IE uses filter instead) - // Use a regex to work around a WebKit issue. See #5145 - opacity: /^0.5/.test( a.style.opacity ), - - // Verify style float existence - // (IE uses styleFloat instead of cssFloat) - cssFloat: !!a.style.cssFloat, - - // Check the default checkbox/radio value ("" on WebKit; "on" elsewhere) - checkOn: !!input.value, - - // Make sure that a selected-by-default option has a working selected property. - // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) - optSelected: opt.selected, - - // Tests for enctype support on a form (#6743) - enctype: !!document.createElement("form").enctype, - - // Makes sure cloning an html5 element does not cause problems - // Where outerHTML is undefined, this still works - html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>", - - // jQuery.support.boxModel DEPRECATED in 1.8 since we don't support Quirks Mode - boxModel: document.compatMode === "CSS1Compat", - - // Will be defined later - deleteExpando: true, - noCloneEvent: true, - inlineBlockNeedsLayout: false, - shrinkWrapBlocks: false, - reliableMarginRight: true, - boxSizingReliable: true, - pixelPosition: false + // Support: IE<9 + // Otherwise append directly + function( target, els ) { + var j = target.length, + i = 0; + // Can't trust NodeList.length + while ( (target[j++] = els[i++]) ) {} + target.length = j - 1; + } }; +} - // Make sure checked status is properly cloned - input.checked = true; - support.noCloneChecked = input.cloneNode( true ).checked; - - // Make sure that the options inside disabled selects aren't marked as disabled - // (WebKit marks them as disabled) - select.disabled = true; - support.optDisabled = !opt.disabled; +function Sizzle( selector, context, results, seed ) { + var match, elem, m, nodeType, + // QSA vars + i, groups, old, nid, newContext, newSelector; - // Support: IE<9 - try { - delete div.test; - } catch( e ) { - support.deleteExpando = false; + if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { + setDocument( context ); } - // Check if we can trust getAttribute("value") - input = document.createElement("input"); - input.setAttribute( "value", "" ); - support.input = input.getAttribute( "value" ) === ""; + context = context || document; + results = results || []; - // Check if an input maintains its value after becoming a radio - input.value = "t"; - input.setAttribute( "type", "radio" ); - support.radioValue = input.value === "t"; + if ( !selector || typeof selector !== "string" ) { + return results; + } - // #11217 - WebKit loses check when the name is after the checked attribute - input.setAttribute( "checked", "t" ); - input.setAttribute( "name", "t" ); + if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) { + return []; + } - fragment = document.createDocumentFragment(); - fragment.appendChild( input ); + if ( documentIsHTML && !seed ) { - // Check if a disconnected checkbox will retain its checked - // value of true after appended to the DOM (IE6/7) - support.appendChecked = input.checked; - - // WebKit doesn't clone checked state correctly in fragments - support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; - - // Support: IE<9 - // Opera does not clone events (and typeof div.attachEvent === undefined). - // IE9-10 clones events bound via attachEvent, but they don't trigger with .click() - if ( div.attachEvent ) { - div.attachEvent( "onclick", function() { - support.noCloneEvent = false; - }); - - div.cloneNode( true ).click(); - } - - // Support: IE<9 (lack submit/change bubble), Firefox 17+ (lack focusin event) - // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP), test/csp.php - for ( i in { submit: true, change: true, focusin: true }) { - div.setAttribute( eventName = "on" + i, "t" ); - - support[ i + "Bubbles" ] = eventName in window || div.attributes[ eventName ].expando === false; - } - - div.style.backgroundClip = "content-box"; - div.cloneNode( true ).style.backgroundClip = ""; - support.clearCloneStyle = div.style.backgroundClip === "content-box"; + // Shortcuts + if ( (match = rquickExpr.exec( selector )) ) { + // Speed-up: Sizzle("#ID") + if ( (m = match[1]) ) { + if ( nodeType === 9 ) { + elem = context.getElementById( m ); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE, Opera, and Webkit return items + // by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + } else { + // Context is not a document + if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && + contains( context, elem ) && elem.id === m ) { + results.push( elem ); + return results; + } + } - // Run tests that need a body at doc ready - jQuery(function() { - var container, marginDiv, tds, - divReset = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;", - body = document.getElementsByTagName("body")[0]; + // Speed-up: Sizzle("TAG") + } else if ( match[2] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; - if ( !body ) { - // Return for frameset docs that don't have a body - return; + // Speed-up: Sizzle(".CLASS") + } else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) { + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } } - container = document.createElement("div"); - container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px"; - - body.appendChild( container ).appendChild( div ); - - // Support: IE8 - // Check if table cells still have offsetWidth/Height when they are set - // to display:none and there are still other visible table cells in a - // table row; if so, offsetWidth/Height are not reliable for use when - // determining if an element has been hidden directly using - // display:none (it is still safe to use offsets if a parent element is - // hidden; don safety goggles and see bug #4512 for more information). - div.innerHTML = "
    t
    "; - tds = div.getElementsByTagName("td"); - tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none"; - isSupported = ( tds[ 0 ].offsetHeight === 0 ); - - tds[ 0 ].style.display = ""; - tds[ 1 ].style.display = "none"; - - // Support: IE8 - // Check if empty table cells still have offsetWidth/Height - support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); - - // Check box-sizing and margin behavior - div.innerHTML = ""; - div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;"; - support.boxSizing = ( div.offsetWidth === 4 ); - support.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== 1 ); - - // Use window.getComputedStyle because jsdom on node.js will break without it. - if ( window.getComputedStyle ) { - support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%"; - support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px"; - - // Check if div with explicit width and no margin-right incorrectly - // gets computed margin-right based on width of container. (#3333) - // Fails in WebKit before Feb 2011 nightlies - // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right - marginDiv = div.appendChild( document.createElement("div") ); - marginDiv.style.cssText = div.style.cssText = divReset; - marginDiv.style.marginRight = marginDiv.style.width = "0"; - div.style.width = "1px"; + // QSA path + if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { + nid = old = expando; + newContext = context; + newSelector = nodeType === 9 && selector; - support.reliableMarginRight = - !parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight ); - } + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + groups = tokenize( selector ); - if ( typeof div.style.zoom !== core_strundefined ) { - // Support: IE<8 - // Check if natively block-level elements act like inline-block - // elements when setting their display to 'inline' and giving - // them layout - div.innerHTML = ""; - div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1"; - support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); + if ( (old = context.getAttribute("id")) ) { + nid = old.replace( rescape, "\\$&" ); + } else { + context.setAttribute( "id", nid ); + } + nid = "[id='" + nid + "'] "; - // Support: IE6 - // Check if elements with layout shrink-wrap their children - div.style.display = "block"; - div.innerHTML = "
    "; - div.firstChild.style.width = "5px"; - support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); + i = groups.length; + while ( i-- ) { + groups[i] = nid + toSelector( groups[i] ); + } + newContext = rsibling.test( selector ) && context.parentNode || context; + newSelector = groups.join(","); + } - if ( support.inlineBlockNeedsLayout ) { - // Prevent IE 6 from affecting layout for positioned elements #11048 - // Prevent IE from shrinking the body in IE 7 mode #12869 - // Support: IE<8 - body.style.zoom = 1; + if ( newSelector ) { + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch(qsaError) { + } finally { + if ( !old ) { + context.removeAttribute("id"); + } + } } } + } - body.removeChild( container ); + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} - // Null elements to avoid leaks in IE - container = div = tds = marginDiv = null; - }); +/** + * Create key-value caches of limited size + * @returns {Function(string, Object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; - // Null elements to avoid leaks in IE - all = select = fragment = opt = a = input = null; + function cache( key, value ) { + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key += " " ) > Expr.cacheLength ) { + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return (cache[ key ] = value); + } + return cache; +} - return support; -})(); +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} -var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/, - rmultiDash = /([A-Z])/g; +/** + * Support testing using an element + * @param {Function} fn Passed the created div and expects a boolean result + */ +function assert( fn ) { + var div = document.createElement("div"); -function internalData( elem, name, data, pvt /* Internal Use Only */ ){ - if ( !jQuery.acceptData( elem ) ) { - return; + try { + return !!fn( div ); + } catch (e) { + return false; + } finally { + // Remove from its parent by default + if ( div.parentNode ) { + div.parentNode.removeChild( div ); + } + // release memory in IE + div = null; } +} - var thisCache, ret, - internalKey = jQuery.expando, - getByName = typeof name === "string", - - // We have to handle DOM nodes and JS objects differently because IE6-7 - // can't GC object references properly across the DOM-JS boundary - isNode = elem.nodeType, +/** + * Adds the same handler for all of the specified attrs + * @param {String} attrs Pipe-separated list of attributes + * @param {Function} handler The method that will be applied + */ +function addHandle( attrs, handler ) { + var arr = attrs.split("|"), + i = attrs.length; - // Only DOM nodes need the global jQuery cache; JS object data is - // attached directly to the object so GC can occur automatically - cache = isNode ? jQuery.cache : elem, + while ( i-- ) { + Expr.attrHandle[ arr[i] ] = handler; + } +} - // Only defining an ID for JS objects if its cache already exists allows - // the code to shortcut on the same path as a DOM node with no cache - id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; +/** + * Checks document order of two siblings + * @param {Element} a + * @param {Element} b + * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b + */ +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && a.nodeType === 1 && b.nodeType === 1 && + ( ~b.sourceIndex || MAX_NEGATIVE ) - + ( ~a.sourceIndex || MAX_NEGATIVE ); - // Avoid doing any more work than we need to when trying to get data on an - // object that has no data at all - if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && getByName && data === undefined ) { - return; + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; } - if ( !id ) { - // Only DOM nodes need a new unique ID for each element since their data - // ends up in the global cache - if ( isNode ) { - elem[ internalKey ] = id = core_deletedIds.pop() || jQuery.guid++; - } else { - id = internalKey; + // Check if b follows a + if ( cur ) { + while ( (cur = cur.nextSibling) ) { + if ( cur === b ) { + return -1; + } } } - if ( !cache[ id ] ) { - cache[ id ] = {}; + return a ? 1 : -1; +} - // Avoids exposing jQuery metadata on plain JS objects when the object - // is serialized using JSON.stringify - if ( !isNode ) { - cache[ id ].toJSON = jQuery.noop; - } - } - - // An object can be passed to jQuery.data instead of a key/value pair; this gets - // shallow copied over onto the existing cache - if ( typeof name === "object" || typeof name === "function" ) { - if ( pvt ) { - cache[ id ] = jQuery.extend( cache[ id ], name ); - } else { - cache[ id ].data = jQuery.extend( cache[ id ].data, name ); - } - } - - thisCache = cache[ id ]; +/** + * Returns a function to use in pseudos for input types + * @param {String} type + */ +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} - // jQuery data() is stored in a separate object inside the object's internal data - // cache in order to avoid key collisions between internal data and user-defined - // data. - if ( !pvt ) { - if ( !thisCache.data ) { - thisCache.data = {}; - } +/** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && elem.type === type; + }; +} - thisCache = thisCache.data; - } +/** + * Returns a function to use in pseudos for positionals + * @param {Function} fn + */ +function createPositionalPseudo( fn ) { + return markFunction(function( argument ) { + argument = +argument; + return markFunction(function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; - if ( data !== undefined ) { - thisCache[ jQuery.camelCase( name ) ] = data; - } + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ (j = matchIndexes[i]) ] ) { + seed[j] = !(matches[j] = seed[j]); + } + } + }); + }); +} - // Check for both converted-to-camel and non-converted data property names - // If a data property was specified - if ( getByName ) { +/** + * Detect xml + * @param {Element|Object} elem An element or a document + */ +isXML = Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = elem && (elem.ownerDocument || elem).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; - // First Try to find as-is property data - ret = thisCache[ name ]; +// Expose support vars for convenience +support = Sizzle.support = {}; - // Test for null|undefined property data - if ( ret == null ) { +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var doc = node ? node.ownerDocument || node : preferredDoc, + parent = doc.defaultView; - // Try to find the camelCased property - ret = thisCache[ jQuery.camelCase( name ) ]; - } - } else { - ret = thisCache; + // If no document and documentElement is available, return + if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; } - return ret; -} + // Set our document + document = doc; + docElem = doc.documentElement; -function internalRemoveData( elem, name, pvt ) { - if ( !jQuery.acceptData( elem ) ) { - return; + // Support tests + documentIsHTML = !isXML( doc ); + + // Support: IE>8 + // If iframe document is assigned to "document" variable and if iframe has been reloaded, + // IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936 + // IE6-8 do not support the defaultView property so parent will be undefined + if ( parent && parent.attachEvent && parent !== parent.top ) { + parent.attachEvent( "onbeforeunload", function() { + setDocument(); + }); } - var i, l, thisCache, - isNode = elem.nodeType, + /* Attributes + ---------------------------------------------------------------------- */ - // See jQuery.data for more information - cache = isNode ? jQuery.cache : elem, - id = isNode ? elem[ jQuery.expando ] : jQuery.expando; + // Support: IE<8 + // Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans) + support.attributes = assert(function( div ) { + div.className = "i"; + return !div.getAttribute("className"); + }); - // If there is already no cache entry for this object, there is no - // purpose in continuing - if ( !cache[ id ] ) { - return; - } + /* getElement(s)By* + ---------------------------------------------------------------------- */ - if ( name ) { + // Check if getElementsByTagName("*") returns only elements + support.getElementsByTagName = assert(function( div ) { + div.appendChild( doc.createComment("") ); + return !div.getElementsByTagName("*").length; + }); - thisCache = pvt ? cache[ id ] : cache[ id ].data; + // Check if getElementsByClassName can be trusted + support.getElementsByClassName = assert(function( div ) { + div.innerHTML = "
    "; + + // Support: Safari<4 + // Catch class over-caching + div.firstChild.className = "i"; + // Support: Opera<10 + // Catch gEBCN failure to find non-leading classes + return div.getElementsByClassName("i").length === 2; + }); - if ( thisCache ) { + // Support: IE<10 + // Check if getElementById returns elements by name + // The broken getElementById methods don't pick up programatically-set names, + // so use a roundabout getElementsByName test + support.getById = assert(function( div ) { + docElem.appendChild( div ).id = expando; + return !doc.getElementsByName || !doc.getElementsByName( expando ).length; + }); - // Support array or space separated string names for data keys - if ( !jQuery.isArray( name ) ) { + // ID find and filter + if ( support.getById ) { + Expr.find["ID"] = function( id, context ) { + if ( typeof context.getElementById !== strundefined && documentIsHTML ) { + var m = context.getElementById( id ); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; + } + }; + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute("id") === attrId; + }; + }; + } else { + // Support: IE6/7 + // getElementById is not reliable as a find shortcut + delete Expr.find["ID"]; - // try the string as a key before any manipulation - if ( name in thisCache ) { - name = [ name ]; - } else { + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); + return node && node.value === attrId; + }; + }; + } - // split the camel cased version by spaces unless a key with the spaces exists - name = jQuery.camelCase( name ); - if ( name in thisCache ) { - name = [ name ]; - } else { - name = name.split(" "); + // Tag + Expr.find["TAG"] = support.getElementsByTagName ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== strundefined ) { + return context.getElementsByTagName( tag ); + } + } : + function( tag, context ) { + var elem, + tmp = [], + i = 0, + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + while ( (elem = results[i++]) ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); } } - } else { - // If "name" is an array of keys... - // When data is initially created, via ("key", "val") signature, - // keys will be converted to camelCase. - // Since there is no way to tell _how_ a key was added, remove - // both plain key and camelCase key. #12786 - // This will only penalize the array argument path. - name = name.concat( jQuery.map( name, jQuery.camelCase ) ); - } - for ( i = 0, l = name.length; i < l; i++ ) { - delete thisCache[ name[i] ]; + return tmp; } + return results; + }; - // If there is no data left in the cache, we want to continue - // and let the cache object itself get destroyed - if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { - return; - } + // Class + Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) { + return context.getElementsByClassName( className ); } - } + }; - // See jQuery.data for more information - if ( !pvt ) { - delete cache[ id ].data; + /* QSA/matchesSelector + ---------------------------------------------------------------------- */ - // Don't destroy the parent cache unless the internal data object - // had been the only thing left in it - if ( !isEmptyDataObject( cache[ id ] ) ) { - return; - } - } + // QSA and matchesSelector support - // Destroy the cache - if ( isNode ) { - jQuery.cleanData( [ elem ], true ); + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; - // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) - } else if ( jQuery.support.deleteExpando || cache != cache.window ) { - delete cache[ id ]; + // qSa(:focus) reports false when true (Chrome 21) + // We allow this because of a bug in IE8/9 that throws an error + // whenever `document.activeElement` is accessed on an iframe + // So, we allow :focus to pass through QSA all the time to avoid the IE error + // See http://bugs.jquery.com/ticket/13378 + rbuggyQSA = []; - // When all else fails, null - } else { - cache[ id ] = null; - } -} + if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) { + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert(function( div ) { + // Select is set to empty string on purpose + // This is to test IE's treatment of not explicitly + // setting a boolean content attribute, + // since its presence should be enough + // http://bugs.jquery.com/ticket/12359 + div.innerHTML = ""; -jQuery.extend({ - cache: {}, + // Support: IE8 + // Boolean attributes and "value" are not treated correctly + if ( !div.querySelectorAll("[selected]").length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } - // Unique for each copy of jQuery on the page - // Non-digits removed to match rinlinejQuery - expando: "jQuery" + ( core_version + Math.random() ).replace( /\D/g, "" ), + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":checked").length ) { + rbuggyQSA.push(":checked"); + } + }); - // The following elements throw uncatchable exceptions if you - // attempt to add expando properties to them. - noData: { - "embed": true, - // Ban all objects except for Flash (which handle expandos) - "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", - "applet": true - }, + assert(function( div ) { - hasData: function( elem ) { - elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; - return !!elem && !isEmptyDataObject( elem ); - }, + // Support: Opera 10-12/IE8 + // ^= $= *= and empty values + // Should not select anything + // Support: Windows 8 Native Apps + // The type attribute is restricted during .innerHTML assignment + var input = doc.createElement("input"); + input.setAttribute( "type", "hidden" ); + div.appendChild( input ).setAttribute( "t", "" ); - data: function( elem, name, data ) { - return internalData( elem, name, data ); - }, - - removeData: function( elem, name ) { - return internalRemoveData( elem, name ); - }, + if ( div.querySelectorAll("[t^='']").length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + } - // For internal use only. - _data: function( elem, name, data ) { - return internalData( elem, name, data, true ); - }, + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":enabled").length ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } - _removeData: function( elem, name ) { - return internalRemoveData( elem, name, true ); - }, + // Opera 10-11 does not throw on post-comma invalid pseudos + div.querySelectorAll("*,:x"); + rbuggyQSA.push(",.*:"); + }); + } - // A method for determining if a DOM node can handle the data expando - acceptData: function( elem ) { - // Do not set data on non-element because it will not be cleared (#8335). - if ( elem.nodeType && elem.nodeType !== 1 && elem.nodeType !== 9 ) { - return false; - } + if ( (support.matchesSelector = rnative.test( (matches = docElem.webkitMatchesSelector || + docElem.mozMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector) )) ) { - var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ]; + assert(function( div ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( div, "div" ); - // nodes accept data unless otherwise specified; rejection can be conditional - return !noData || noData !== true && elem.getAttribute("classid") === noData; + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( div, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + }); } -}); - -jQuery.fn.extend({ - data: function( key, value ) { - var attrs, name, - elem = this[0], - i = 0, - data = null; - - // Gets all values - if ( key === undefined ) { - if ( this.length ) { - data = jQuery.data( elem ); - if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { - attrs = elem.attributes; - for ( ; i < attrs.length; i++ ) { - name = attrs[i].name; + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") ); + rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); - if ( !name.indexOf( "data-" ) ) { - name = jQuery.camelCase( name.slice(5) ); + /* Contains + ---------------------------------------------------------------------- */ - dataAttr( elem, name, data[ name ] ); - } + // Element contains another + // Purposefully does not implement inclusive descendent + // As in, an element does not contain itself + contains = rnative.test( docElem.contains ) || docElem.compareDocumentPosition ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + )); + } : + function( a, b ) { + if ( b ) { + while ( (b = b.parentNode) ) { + if ( b === a ) { + return true; } - jQuery._data( elem, "parsedAttrs", true ); } } + return false; + }; - return data; - } + /* Sorting + ---------------------------------------------------------------------- */ - // Sets multiple values - if ( typeof key === "object" ) { - return this.each(function() { - jQuery.data( this, key ); - }); - } + // Document order sorting + sortOrder = docElem.compareDocumentPosition ? + function( a, b ) { - return jQuery.access( this, function( value ) { + // Flag for duplicate removal + if ( a === b ) { + hasDuplicate = true; + return 0; + } - if ( value === undefined ) { - // Try to fetch any internally stored data first - return elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : null; - } + var compare = b.compareDocumentPosition && a.compareDocumentPosition && a.compareDocumentPosition( b ); - this.each(function() { - jQuery.data( this, key, value ); - }); - }, null, value, arguments.length > 1, null, true ); - }, + if ( compare ) { + // Disconnected nodes + if ( compare & 1 || + (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { - removeData: function( key ) { - return this.each(function() { - jQuery.removeData( this, key ); - }); - } -}); + // Choose the first element that is related to our preferred document + if ( a === doc || contains(preferredDoc, a) ) { + return -1; + } + if ( b === doc || contains(preferredDoc, b) ) { + return 1; + } -function dataAttr( elem, key, data ) { - // If nothing was found internally, try to fetch any - // data from the HTML5 data-* attribute - if ( data === undefined && elem.nodeType === 1 ) { + // Maintain original order + return sortInput ? + ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : + 0; + } - var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + return compare & 4 ? -1 : 1; + } - data = elem.getAttribute( name ); + // Not directly comparable, sort on existence of method + return a.compareDocumentPosition ? -1 : 1; + } : + function( a, b ) { + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; - if ( typeof data === "string" ) { - try { - data = data === "true" ? true : - data === "false" ? false : - data === "null" ? null : - // Only convert to a number if it doesn't change the string - +data + "" === data ? +data : - rbrace.test( data ) ? jQuery.parseJSON( data ) : - data; - } catch( e ) {} + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; - // Make sure we set the data so it isn't changed later - jQuery.data( elem, key, data ); + // Parentless nodes are either documents or disconnected + } else if ( !aup || !bup ) { + return a === doc ? -1 : + b === doc ? 1 : + aup ? -1 : + bup ? 1 : + sortInput ? + ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : + 0; - } else { - data = undefined; + // If the nodes are siblings, we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); } - } - return data; -} - -// checks a cache object for emptiness -function isEmptyDataObject( obj ) { - var name; - for ( name in obj ) { - - // if the public data object is empty, the private is still empty - if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { - continue; + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ( (cur = cur.parentNode) ) { + ap.unshift( cur ); } - if ( name !== "toJSON" ) { - return false; + cur = b; + while ( (cur = cur.parentNode) ) { + bp.unshift( cur ); } - } - - return true; -} -jQuery.extend({ - queue: function( elem, type, data ) { - var queue; - - if ( elem ) { - type = ( type || "fx" ) + "queue"; - queue = jQuery._data( elem, type ); - // Speed up dequeue by getting out quickly if this is just a lookup - if ( data ) { - if ( !queue || jQuery.isArray(data) ) { - queue = jQuery._data( elem, type, jQuery.makeArray(data) ); - } else { - queue.push( data ); - } - } - return queue || []; + // Walk down the tree looking for a discrepancy + while ( ap[i] === bp[i] ) { + i++; } - }, - dequeue: function( elem, type ) { - type = type || "fx"; + return i ? + // Do a sibling check if the nodes have a common ancestor + siblingCheck( ap[i], bp[i] ) : - var queue = jQuery.queue( elem, type ), - startLength = queue.length, - fn = queue.shift(), - hooks = jQuery._queueHooks( elem, type ), - next = function() { - jQuery.dequeue( elem, type ); - }; + // Otherwise nodes in our document sort first + ap[i] === preferredDoc ? -1 : + bp[i] === preferredDoc ? 1 : + 0; + }; - // If the fx queue is dequeued, always remove the progress sentinel - if ( fn === "inprogress" ) { - fn = queue.shift(); - startLength--; - } + return doc; +}; - hooks.cur = fn; - if ( fn ) { +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; - // Add a progress sentinel to prevent the fx queue from being - // automatically dequeued - if ( type === "fx" ) { - queue.unshift( "inprogress" ); - } +Sizzle.matchesSelector = function( elem, expr ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } - // clear up the last queue stop function - delete hooks.stop; - fn.call( elem, next, hooks ); - } + // Make sure that attribute selectors are quoted + expr = expr.replace( rattributeQuotes, "='$1']" ); - if ( !startLength && hooks ) { - hooks.empty.fire(); - } - }, + if ( support.matchesSelector && documentIsHTML && + ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && + ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { - // not intended for public consumption - generates a queueHooks object, or returns the current one - _queueHooks: function( elem, type ) { - var key = type + "queueHooks"; - return jQuery._data( elem, key ) || jQuery._data( elem, key, { - empty: jQuery.Callbacks("once memory").add(function() { - jQuery._removeData( elem, type + "queue" ); - jQuery._removeData( elem, key ); - }) - }); + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch(e) {} } -}); -jQuery.fn.extend({ - queue: function( type, data ) { - var setter = 2; + return Sizzle( expr, document, null, [elem] ).length > 0; +}; - if ( typeof type !== "string" ) { - data = type; - type = "fx"; - setter--; - } +Sizzle.contains = function( context, elem ) { + // Set document vars if needed + if ( ( context.ownerDocument || context ) !== document ) { + setDocument( context ); + } + return contains( context, elem ); +}; - if ( arguments.length < setter ) { - return jQuery.queue( this[0], type ); - } +Sizzle.attr = function( elem, name ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } - return data === undefined ? - this : - this.each(function() { - var queue = jQuery.queue( this, type, data ); + var fn = Expr.attrHandle[ name.toLowerCase() ], + // Don't get fooled by Object.prototype properties (jQuery #13807) + val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? + fn( elem, name, !documentIsHTML ) : + undefined; - // ensure a hooks for this queue - jQuery._queueHooks( this, type ); + return val === undefined ? + support.attributes || !documentIsHTML ? + elem.getAttribute( name ) : + (val = elem.getAttributeNode(name)) && val.specified ? + val.value : + null : + val; +}; - if ( type === "fx" && queue[0] !== "inprogress" ) { - jQuery.dequeue( this, type ); - } - }); - }, - dequeue: function( type ) { - return this.each(function() { - jQuery.dequeue( this, type ); - }); - }, - // Based off of the plugin by Clint Helfers, with permission. - // http://blindsignals.com/index.php/2009/07/jquery-delay/ - delay: function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; - type = type || "fx"; +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; - return this.queue( type, function( next, hooks ) { - var timeout = setTimeout( next, time ); - hooks.stop = function() { - clearTimeout( timeout ); - }; - }); - }, - clearQueue: function( type ) { - return this.queue( type || "fx", [] ); - }, - // Get a promise resolved when queues of a certain type - // are emptied (fx is the type by default) - promise: function( type, obj ) { - var tmp, - count = 1, - defer = jQuery.Deferred(), - elements = this, - i = this.length, - resolve = function() { - if ( !( --count ) ) { - defer.resolveWith( elements, [ elements ] ); - } - }; +/** + * Document sorting and removing duplicates + * @param {ArrayLike} results + */ +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; - if ( typeof type !== "string" ) { - obj = type; - type = undefined; - } - type = type || "fx"; + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + sortInput = !support.sortStable && results.slice( 0 ); + results.sort( sortOrder ); - while( i-- ) { - tmp = jQuery._data( elements[ i ], type + "queueHooks" ); - if ( tmp && tmp.empty ) { - count++; - tmp.empty.add( resolve ); + if ( hasDuplicate ) { + while ( (elem = results[i++]) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); } } - resolve(); - return defer.promise( obj ); + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } } -}); -var nodeHook, boolHook, - rclass = /[\t\r\n]/g, - rreturn = /\r/g, - rfocusable = /^(?:input|select|textarea|button|object)$/i, - rclickable = /^(?:a|area)$/i, - rboolean = /^(?:checked|selected|autofocus|autoplay|async|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped)$/i, - ruseDefault = /^(?:checked|selected)$/i, - getSetAttribute = jQuery.support.getSetAttribute, - getSetInput = jQuery.support.input; -jQuery.fn.extend({ - attr: function( name, value ) { - return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); - }, + return results; +}; - removeAttr: function( name ) { - return this.each(function() { - jQuery.removeAttr( this, name ); - }); - }, +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; - prop: function( name, value ) { - return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); - }, + if ( !nodeType ) { + // If no nodeType, this is expected to be an array + for ( ; (node = elem[i]); i++ ) { + // Do not traverse comment nodes + ret += getText( node ); + } + } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent for elements + // innerText usage removed for consistency of new lines (see #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + // Do not include comment or processing instruction nodes - removeProp: function( name ) { - name = jQuery.propFix[ name ] || name; - return this.each(function() { - // try/catch handles cases where IE balks (such as removing a property on window) - try { - this[ name ] = undefined; - delete this[ name ]; - } catch( e ) {} - }); - }, + return ret; +}; - addClass: function( value ) { - var classes, elem, cur, clazz, j, - i = 0, - len = this.length, - proceed = typeof value === "string" && value; +Expr = Sizzle.selectors = { - if ( jQuery.isFunction( value ) ) { - return this.each(function( j ) { - jQuery( this ).addClass( value.call( this, j, this.className ) ); - }); - } + // Can be adjusted by the user + cacheLength: 50, - if ( proceed ) { - // The disjunction here is for better compressibility (see removeClass) - classes = ( value || "" ).match( core_rnotwhite ) || []; + createPseudo: markFunction, - for ( ; i < len; i++ ) { - elem = this[ i ]; - cur = elem.nodeType === 1 && ( elem.className ? - ( " " + elem.className + " " ).replace( rclass, " " ) : - " " - ); + match: matchExpr, - if ( cur ) { - j = 0; - while ( (clazz = classes[j++]) ) { - if ( cur.indexOf( " " + clazz + " " ) < 0 ) { - cur += clazz + " "; - } - } - elem.className = jQuery.trim( cur ); + attrHandle: {}, - } - } - } + find: {}, - return this; + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } }, - removeClass: function( value ) { - var classes, elem, cur, clazz, j, - i = 0, - len = this.length, - proceed = arguments.length === 0 || typeof value === "string" && value; + preFilter: { + "ATTR": function( match ) { + match[1] = match[1].replace( runescape, funescape ); - if ( jQuery.isFunction( value ) ) { - return this.each(function( j ) { - jQuery( this ).removeClass( value.call( this, j, this.className ) ); - }); - } - if ( proceed ) { - classes = ( value || "" ).match( core_rnotwhite ) || []; + // Move the given value to match[3] whether quoted or unquoted + match[3] = ( match[4] || match[5] || "" ).replace( runescape, funescape ); - for ( ; i < len; i++ ) { - elem = this[ i ]; - // This expression is here for better compressibility (see addClass) - cur = elem.nodeType === 1 && ( elem.className ? - ( " " + elem.className + " " ).replace( rclass, " " ) : - "" - ); + if ( match[2] === "~=" ) { + match[3] = " " + match[3] + " "; + } - if ( cur ) { - j = 0; - while ( (clazz = classes[j++]) ) { - // Remove *all* instances - while ( cur.indexOf( " " + clazz + " " ) >= 0 ) { - cur = cur.replace( " " + clazz + " ", " " ); - } - } - elem.className = value ? jQuery.trim( cur ) : ""; - } - } - } - - return this; - }, - - toggleClass: function( value, stateVal ) { - var type = typeof value, - isBool = typeof stateVal === "boolean"; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( i ) { - jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); - }); - } - - return this.each(function() { - if ( type === "string" ) { - // toggle individual class names - var className, - i = 0, - self = jQuery( this ), - state = stateVal, - classNames = value.match( core_rnotwhite ) || []; + return match.slice( 0, 4 ); + }, - while ( (className = classNames[ i++ ]) ) { - // check each className given, space separated list - state = isBool ? state : !self.hasClass( className ); - self[ state ? "addClass" : "removeClass" ]( className ); - } + "CHILD": function( match ) { + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[1] = match[1].toLowerCase(); - // Toggle whole class name - } else if ( type === core_strundefined || type === "boolean" ) { - if ( this.className ) { - // store className if set - jQuery._data( this, "__className__", this.className ); + if ( match[1].slice( 0, 3 ) === "nth" ) { + // nth-* requires argument + if ( !match[3] ) { + Sizzle.error( match[0] ); } - // If the element has a class name or if we're passed "false", - // then remove the whole classname (if there was one, the above saved it). - // Otherwise bring back whatever was previously saved (if anything), - // falling back to the empty string if nothing was stored. - this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; - } - }); - }, + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); + match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); - hasClass: function( selector ) { - var className = " " + selector + " ", - i = 0, - l = this.length; - for ( ; i < l; i++ ) { - if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) { - return true; + // other types prohibit arguments + } else if ( match[3] ) { + Sizzle.error( match[0] ); } - } - return false; - }, + return match; + }, - val: function( value ) { - var ret, hooks, isFunction, - elem = this[0]; + "PSEUDO": function( match ) { + var excess, + unquoted = !match[5] && match[2]; - if ( !arguments.length ) { - if ( elem ) { - hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + if ( matchExpr["CHILD"].test( match[0] ) ) { + return null; + } - if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { - return ret; - } + // Accept quoted arguments as-is + if ( match[3] && match[4] !== undefined ) { + match[2] = match[4]; - ret = elem.value; + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + // Get excess from tokenize (recursively) + (excess = tokenize( unquoted, true )) && + // advance to the next closing parenthesis + (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { - return typeof ret === "string" ? - // handle most common string cases - ret.replace(rreturn, "") : - // handle cases where value is null/undef or number - ret == null ? "" : ret; + // excess is a negative index + match[0] = match[0].slice( 0, excess ); + match[2] = unquoted.slice( 0, excess ); } - return; + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); } + }, - isFunction = jQuery.isFunction( value ); - - return this.each(function( i ) { - var val, - self = jQuery(this); + filter: { - if ( this.nodeType !== 1 ) { - return; - } + "TAG": function( nodeNameSelector ) { + var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + return nodeNameSelector === "*" ? + function() { return true; } : + function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, - if ( isFunction ) { - val = value.call( this, i, self.val() ); - } else { - val = value; - } + "CLASS": function( className ) { + var pattern = classCache[ className + " " ]; - // Treat null/undefined as ""; convert numbers to string - if ( val == null ) { - val = ""; - } else if ( typeof val === "number" ) { - val += ""; - } else if ( jQuery.isArray( val ) ) { - val = jQuery.map(val, function ( value ) { - return value == null ? "" : value + ""; + return pattern || + (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && + classCache( className, function( elem ) { + return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || "" ); }); - } + }, - hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + "ATTR": function( name, operator, check ) { + return function( elem ) { + var result = Sizzle.attr( elem, name ); - // If set returns undefined, fall back to normal setting - if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { - this.value = val; - } - }); - } -}); + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } -jQuery.extend({ - valHooks: { - option: { - get: function( elem ) { - // attributes.value is undefined in Blackberry 4.7 but - // uses .value. See #6932 - var val = elem.attributes.value; - return !val || val.specified ? elem.value : elem.text; - } + result += ""; + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.slice( -check.length ) === check : + operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : + false; + }; }, - select: { - get: function( elem ) { - var value, option, - options = elem.options, - index = elem.selectedIndex, - one = elem.type === "select-one" || index < 0, - values = one ? null : [], - max = one ? index + 1 : options.length, - i = index < 0 ? - max : - one ? index : 0; - // Loop through all the selected options - for ( ; i < max; i++ ) { - option = options[ i ]; + "CHILD": function( type, what, argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; - // oldIE doesn't update selected after form reset (#2551) - if ( ( option.selected || i === index ) && - // Don't return options that are disabled or in a disabled optgroup - ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) && - ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { + return first === 1 && last === 0 ? - // Get the specific value for the option - value = jQuery( option ).val(); + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : - // We don't need an array for one selects - if ( one ) { - return value; - } + function( elem, context, xml ) { + var cache, outerCache, node, diff, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType; - // Multi-Selects return an array - values.push( value ); - } - } + if ( parent ) { - return values; - }, + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( (node = node[ dir ]) ) { + if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { + return false; + } + } + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } - set: function( elem, value ) { - var values = jQuery.makeArray( value ); + start = [ forward ? parent.firstChild : parent.lastChild ]; - jQuery(elem).find("option").each(function() { - this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; - }); + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + // Seek `elem` from a previously-cached index + outerCache = parent[ expando ] || (parent[ expando ] = {}); + cache = outerCache[ type ] || []; + nodeIndex = cache[0] === dirruns && cache[1]; + diff = cache[0] === dirruns && cache[2]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; - if ( !values.length ) { - elem.selectedIndex = -1; - } - return values; - } - } - }, + while ( (node = ++nodeIndex && node && node[ dir ] || - attr: function( elem, name, value ) { - var hooks, notxml, ret, - nType = elem.nodeType; + // Fallback to seeking `elem` from the start + (diff = nodeIndex = 0) || start.pop()) ) { - // don't get/set attributes on text, comment and attribute nodes - if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return; - } + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + outerCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } - // Fallback to prop when attributes are not supported - if ( typeof elem.getAttribute === core_strundefined ) { - return jQuery.prop( elem, name, value ); - } + // Use previously-cached element index if available + } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) { + diff = cache[1]; - notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) + } else { + // Use the same loop as above to seek `elem` from the start + while ( (node = ++nodeIndex && node && node[ dir ] || + (diff = nodeIndex = 0) || start.pop()) ) { - // All attributes are lowercase - // Grab necessary hook if one is defined - if ( notxml ) { - name = name.toLowerCase(); - hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); - } + if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { + // Cache the index of each encountered element + if ( useCache ) { + (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; + } - if ( value !== undefined ) { + if ( node === elem ) { + break; + } + } + } + } - if ( value === null ) { - jQuery.removeAttr( elem, name ); + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, - } else if ( hooks && notxml && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { - return ret; + "PSEUDO": function( pseudo, argument ) { + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); - } else { - elem.setAttribute( name, value + "" ); - return value; + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); } - } else if ( hooks && notxml && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { - return ret; - - } else { - - // In IE9+, Flash objects don't have .getAttribute (#12945) - // Support: IE9+ - if ( typeof elem.getAttribute !== core_strundefined ) { - ret = elem.getAttribute( name ); + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction(function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf.call( seed, matched[i] ); + seed[ idx ] = !( matches[ idx ] = matched[i] ); + } + }) : + function( elem ) { + return fn( elem, 0, args ); + }; } - // Non-existent attributes return null, we normalize to undefined - return ret == null ? - undefined : - ret; + return fn; } }, - removeAttr: function( elem, value ) { - var name, propName, - i = 0, - attrNames = value && value.match( core_rnotwhite ); + pseudos: { + // Potentially complex pseudos + "not": markFunction(function( selector ) { + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); - if ( attrNames && elem.nodeType === 1 ) { - while ( (name = attrNames[i++]) ) { - propName = jQuery.propFix[ name ] || name; + return matcher[ expando ] ? + markFunction(function( seed, matches, context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; - // Boolean attributes get special treatment (#10870) - if ( rboolean.test( name ) ) { - // Set corresponding property to false for boolean attributes - // Also clear defaultChecked/defaultSelected (if appropriate) for IE<8 - if ( !getSetAttribute && ruseDefault.test( name ) ) { - elem[ jQuery.camelCase( "default-" + name ) ] = - elem[ propName ] = false; - } else { - elem[ propName ] = false; + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( (elem = unmatched[i]) ) { + seed[i] = !(matches[i] = elem); + } } + }) : + function( elem, context, xml ) { + input[0] = elem; + matcher( input, null, xml, results ); + return !results.pop(); + }; + }), - // See #9699 for explanation of this approach (setting first, then removal) - } else { - jQuery.attr( elem, name, "" ); - } + "has": markFunction(function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + }), - elem.removeAttribute( getSetAttribute ? name : propName ); + "contains": markFunction(function( text ) { + return function( elem ) { + return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; + }; + }), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction( function( lang ) { + // lang value must be a valid identifier + if ( !ridentifier.test(lang || "") ) { + Sizzle.error( "unsupported lang: " + lang ); } - } - }, + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( (elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { - attrHooks: { - type: { - set: function( elem, value ) { - if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { - // Setting the type on a radio button after the value resets the value in IE6-9 - // Reset value to default in case type is set after value during creation - var val = elem.value; - elem.setAttribute( "type", value ); - if ( val ) { - elem.value = val; + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; } - return value; - } - } - } - }, + } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); + return false; + }; + }), - propFix: { - tabindex: "tabIndex", - readonly: "readOnly", - "for": "htmlFor", - "class": "className", - maxlength: "maxLength", - cellspacing: "cellSpacing", - cellpadding: "cellPadding", - rowspan: "rowSpan", - colspan: "colSpan", - usemap: "useMap", - frameborder: "frameBorder", - contenteditable: "contentEditable" - }, + // Miscellaneous + "target": function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, - prop: function( elem, name, value ) { - var ret, hooks, notxml, - nType = elem.nodeType; + "root": function( elem ) { + return elem === docElem; + }, - // don't get/set properties on text, comment and attribute nodes - if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return; - } + "focus": function( elem ) { + return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); + }, - notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + // Boolean properties + "enabled": function( elem ) { + return elem.disabled === false; + }, - if ( notxml ) { - // Fix name and attach hooks - name = jQuery.propFix[ name ] || name; - hooks = jQuery.propHooks[ name ]; - } + "disabled": function( elem ) { + return elem.disabled === true; + }, - if ( value !== undefined ) { - if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { - return ret; + "checked": function( elem ) { + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); + }, - } else { - return ( elem[ name ] = value ); + "selected": function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; } - } else { - if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { - return ret; + return elem.selected === true; + }, - } else { - return elem[ name ]; + // Contents + "empty": function( elem ) { + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is only affected by element nodes and content nodes(including text(3), cdata(4)), + // not comment, processing instructions, or others + // Thanks to Diego Perini for the nodeName shortcut + // Greater than "@" means alpha characters (specifically not starting with "#" or "?") + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4 ) { + return false; + } } - } - }, + return true; + }, - propHooks: { - tabIndex: { - get: function( elem ) { - // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set - // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - var attributeNode = elem.getAttributeNode("tabindex"); - - return attributeNode && attributeNode.specified ? - parseInt( attributeNode.value, 10 ) : - rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? - 0 : - undefined; - } - } - } -}); - -// Hook for boolean attributes -boolHook = { - get: function( elem, name ) { - var - // Use .prop to determine if this attribute is understood as boolean - prop = jQuery.prop( elem, name ), - - // Fetch it accordingly - attr = typeof prop === "boolean" && elem.getAttribute( name ), - detail = typeof prop === "boolean" ? - - getSetInput && getSetAttribute ? - attr != null : - // oldIE fabricates an empty string for missing boolean attributes - // and conflates checked/selected into attroperties - ruseDefault.test( name ) ? - elem[ jQuery.camelCase( "default-" + name ) ] : - !!attr : - - // fetch an attribute node for properties not recognized as boolean - elem.getAttributeNode( name ); - - return detail && detail.value !== false ? - name.toLowerCase() : - undefined; - }, - set: function( elem, value, name ) { - if ( value === false ) { - // Remove boolean attributes when set to false - jQuery.removeAttr( elem, name ); - } else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { - // IE<8 needs the *property* name - elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name ); - - // Use defaultChecked and defaultSelected for oldIE - } else { - elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true; - } - - return name; - } -}; - -// fix oldIE value attroperty -if ( !getSetInput || !getSetAttribute ) { - jQuery.attrHooks.value = { - get: function( elem, name ) { - var ret = elem.getAttributeNode( name ); - return jQuery.nodeName( elem, "input" ) ? + "parent": function( elem ) { + return !Expr.pseudos["empty"]( elem ); + }, - // Ignore the value *property* by using defaultValue - elem.defaultValue : + // Element/input types + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, - ret && ret.specified ? ret.value : undefined; + "input": function( elem ) { + return rinputs.test( elem.nodeName ); }, - set: function( elem, value, name ) { - if ( jQuery.nodeName( elem, "input" ) ) { - // Does not return so that setAttribute is also used - elem.defaultValue = value; - } else { - // Use nodeHook if defined (#1954); otherwise setAttribute is fine - return nodeHook && nodeHook.set( elem, value, name ); - } - } - }; -} -// IE6/7 do not support getting/setting some attributes with get/setAttribute -if ( !getSetAttribute ) { + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, - // Use this for any attribute in IE6/7 - // This fixes almost every IE6/7 issue - nodeHook = jQuery.valHooks.button = { - get: function( elem, name ) { - var ret = elem.getAttributeNode( name ); - return ret && ( name === "id" || name === "name" || name === "coords" ? ret.value !== "" : ret.specified ) ? - ret.value : - undefined; + "text": function( elem ) { + var attr; + // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) + // use getAttribute instead to test this case + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === elem.type ); }, - set: function( elem, value, name ) { - // Set the existing or create a new attribute node - var ret = elem.getAttributeNode( name ); - if ( !ret ) { - elem.setAttributeNode( - (ret = elem.ownerDocument.createAttribute( name )) - ); - } - ret.value = value += ""; + // Position-in-collection + "first": createPositionalPseudo(function() { + return [ 0 ]; + }), - // Break association with cloned elements by also using setAttribute (#9646) - return name === "value" || value === elem.getAttribute( name ) ? - value : - undefined; - } - }; + "last": createPositionalPseudo(function( matchIndexes, length ) { + return [ length - 1 ]; + }), - // Set contenteditable to false on removals(#10429) - // Setting to empty string throws an error as an invalid value - jQuery.attrHooks.contenteditable = { - get: nodeHook.get, - set: function( elem, value, name ) { - nodeHook.set( elem, value === "" ? false : value, name ); - } - }; + "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + }), - // Set width and height to auto instead of 0 on empty string( Bug #8150 ) - // This is for removals - jQuery.each([ "width", "height" ], function( i, name ) { - jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { - set: function( elem, value ) { - if ( value === "" ) { - elem.setAttribute( name, "auto" ); - return value; - } + "even": createPositionalPseudo(function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); } - }); - }); -} + return matchIndexes; + }), + "odd": createPositionalPseudo(function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), -// Some attributes require a special call on IE -// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx -if ( !jQuery.support.hrefNormalized ) { - jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { - jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { - get: function( elem ) { - var ret = elem.getAttribute( name, 2 ); - return ret == null ? undefined : ret; + "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; --i >= 0; ) { + matchIndexes.push( i ); } - }); - }); + return matchIndexes; + }), - // href/src property should get the full normalized URL (#10299/#12915) - jQuery.each([ "href", "src" ], function( i, name ) { - jQuery.propHooks[ name ] = { - get: function( elem ) { - return elem.getAttribute( name, 4 ); + "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); } - }; - }); -} + return matchIndexes; + }) + } +}; -if ( !jQuery.support.style ) { - jQuery.attrHooks.style = { - get: function( elem ) { - // Return undefined in the case of empty string - // Note: IE uppercases css property names, but if we were to .toLowerCase() - // .cssText, that would destroy case senstitivity in URL's, like in "background" - return elem.style.cssText || undefined; - }, - set: function( elem, value ) { - return ( elem.style.cssText = value + "" ); - } - }; +Expr.pseudos["nth"] = Expr.pseudos["eq"]; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); } -// Safari mis-reports the default selected property of an option -// Accessing the parent's selectedIndex property fixes it -if ( !jQuery.support.optSelected ) { - jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { - get: function( elem ) { - var parent = elem.parentNode; +// Easy API for creating new setFilters +function setFilters() {} +setFilters.prototype = Expr.filters = Expr.pseudos; +Expr.setFilters = new setFilters(); - if ( parent ) { - parent.selectedIndex; +function tokenize( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; - // Make sure that it also works with optgroups, see #5701 - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - return null; - } - }); -} + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } -// IE6/7 call enctype encoding -if ( !jQuery.support.enctype ) { - jQuery.propFix.enctype = "encoding"; -} + soFar = selector; + groups = []; + preFilters = Expr.preFilter; -// Radios and checkboxes getter/setter -if ( !jQuery.support.checkOn ) { - jQuery.each([ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = { - get: function( elem ) { - // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified - return elem.getAttribute("value") === null ? "on" : elem.value; - } - }; - }); -} -jQuery.each([ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { - set: function( elem, value ) { - if ( jQuery.isArray( value ) ) { - return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); + while ( soFar ) { + + // Comma and first run + if ( !matched || (match = rcomma.exec( soFar )) ) { + if ( match ) { + // Don't consume trailing commas as valid + soFar = soFar.slice( match[0].length ) || soFar; } + groups.push( tokens = [] ); } - }); -}); -var rformElems = /^(?:input|select|textarea)$/i, - rkeyEvent = /^key/, - rmouseEvent = /^(?:mouse|contextmenu)|click/, - rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, - rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; -function returnTrue() { - return true; -} + matched = false; -function returnFalse() { - return false; -} + // Combinators + if ( (match = rcombinators.exec( soFar )) ) { + matched = match.shift(); + tokens.push({ + value: matched, + // Cast descendant combinators to space + type: match[0].replace( rtrim, " " ) + }); + soFar = soFar.slice( matched.length ); + } -/* - * Helper functions for managing events -- not part of the public interface. - * Props to Dean Edwards' addEvent library for many of the ideas. - */ -jQuery.event = { - - global: {}, - - add: function( elem, types, handler, data, selector ) { - var tmp, events, t, handleObjIn, - special, eventHandle, handleObj, - handlers, type, namespaces, origType, - elemData = jQuery._data( elem ); - - // Don't attach events to noData or text/comment nodes (but allow plain objects) - if ( !elemData ) { - return; - } - - // Caller can pass in an object of custom data in lieu of the handler - if ( handler.handler ) { - handleObjIn = handler; - handler = handleObjIn.handler; - selector = handleObjIn.selector; - } - - // Make sure that the handler has a unique ID, used to find/remove it later - if ( !handler.guid ) { - handler.guid = jQuery.guid++; + // Filters + for ( type in Expr.filter ) { + if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || + (match = preFilters[ type ]( match ))) ) { + matched = match.shift(); + tokens.push({ + value: matched, + type: type, + matches: match + }); + soFar = soFar.slice( matched.length ); + } } - // Init the element's event structure and main handler, if this is the first - if ( !(events = elemData.events) ) { - events = elemData.events = {}; - } - if ( !(eventHandle = elemData.handle) ) { - eventHandle = elemData.handle = function( e ) { - // Discard the second event of a jQuery.event.trigger() and - // when an event is called after a page has unloaded - return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ? - jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : - undefined; - }; - // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events - eventHandle.elem = elem; + if ( !matched ) { + break; } + } - // Handle multiple events separated by a space - // jQuery(...).bind("mouseover mouseout", fn); - types = ( types || "" ).match( core_rnotwhite ) || [""]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[t] ) || []; - type = origType = tmp[1]; - namespaces = ( tmp[2] || "" ).split( "." ).sort(); - - // If event changes its type, use the special event handlers for the changed type - special = jQuery.event.special[ type ] || {}; - - // If selector defined, determine special event api type, otherwise given type - type = ( selector ? special.delegateType : special.bindType ) || type; + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +} - // Update special based on newly reset type - special = jQuery.event.special[ type ] || {}; +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[i].value; + } + return selector; +} - // handleObj is passed to all event handlers - handleObj = jQuery.extend({ - type: type, - origType: origType, - data: data, - handler: handler, - guid: handler.guid, - selector: selector, - needsContext: selector && jQuery.expr.match.needsContext.test( selector ), - namespace: namespaces.join(".") - }, handleObjIn ); +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + checkNonElements = base && dir === "parentNode", + doneName = done++; - // Init the event handler queue if we're the first - if ( !(handlers = events[ type ]) ) { - handlers = events[ type ] = []; - handlers.delegateCount = 0; + return combinator.first ? + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + } : - // Only use addEventListener/attachEvent if the special events handler returns false - if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { - // Bind the global event handler to the element - if ( elem.addEventListener ) { - elem.addEventListener( type, eventHandle, false ); + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var data, cache, outerCache, + dirkey = dirruns + " " + doneName; - } else if ( elem.attachEvent ) { - elem.attachEvent( "on" + type, eventHandle ); + // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching + if ( xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || (elem[ expando ] = {}); + if ( (cache = outerCache[ dir ]) && cache[0] === dirkey ) { + if ( (data = cache[1]) === true || data === cachedruns ) { + return data === true; + } + } else { + cache = outerCache[ dir ] = [ dirkey ]; + cache[1] = matcher( elem, context, xml ) || cachedruns; + if ( cache[1] === true ) { + return true; + } + } } } } + }; +} - if ( special.add ) { - special.add.call( elem, handleObj ); - - if ( !handleObj.handler.guid ) { - handleObj.handler.guid = handler.guid; +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[i]( elem, context, xml ) ) { + return false; } } + return true; + } : + matchers[0]; +} - // Add to the element's handler list, delegates in front - if ( selector ) { - handlers.splice( handlers.delegateCount++, 0, handleObj ); - } else { - handlers.push( handleObj ); - } +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; - // Keep track of which events have ever been used, for event optimization - jQuery.event.global[ type ] = true; + for ( ; i < len; i++ ) { + if ( (elem = unmatched[i]) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } } + } - // Nullify elem to prevent memory leaks in IE - elem = null; - }, + return newUnmatched; +} - // Detach an event or set of events from an element - remove: function( elem, types, handler, selector, mappedTypes ) { - var j, handleObj, tmp, - origCount, t, events, - special, handlers, type, - namespaces, origType, - elemData = jQuery.hasData( elem ) && jQuery._data( elem ); +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction(function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, - if ( !elemData || !(events = elemData.events) ) { - return; - } + // Get initial elements from seed or context + elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), - // Once for each type.namespace in types; type may be omitted - types = ( types || "" ).match( core_rnotwhite ) || [""]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[t] ) || []; - type = origType = tmp[1]; - namespaces = ( tmp[2] || "" ).split( "." ).sort(); + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, - // Unbind all events (on this namespace, if provided) for the element - if ( !type ) { - for ( type in events ) { - jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); - } - continue; - } + matcherOut = matcher ? + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? - special = jQuery.event.special[ type ] || {}; - type = ( selector ? special.delegateType : special.bindType ) || type; - handlers = events[ type ] || []; - tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); + // ...intermediate processing is necessary + [] : - // Remove matching events - origCount = j = handlers.length; - while ( j-- ) { - handleObj = handlers[ j ]; + // ...otherwise use results directly + results : + matcherIn; - if ( ( mappedTypes || origType === handleObj.origType ) && - ( !handler || handler.guid === handleObj.guid ) && - ( !tmp || tmp.test( handleObj.namespace ) ) && - ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { - handlers.splice( j, 1 ); + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } - if ( handleObj.selector ) { - handlers.delegateCount--; - } - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( (elem = temp[i]) ) { + matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); } } + } - // Remove generic event handler if we removed something and no more handlers exist - // (avoids potential for endless recursion during removal of special event handlers) - if ( origCount && !handlers.length ) { - if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { - jQuery.removeEvent( elem, type, elemData.handle ); + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) ) { + // Restore matcherIn since elem is not yet a final match + temp.push( (matcherIn[i] = elem) ); + } + } + postFinder( null, (matcherOut = []), temp, xml ); } - delete events[ type ]; + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) && + (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { + + seed[temp] = !(results[temp] = elem); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); } } + }); +} - // Remove the expando if it's no longer used - if ( jQuery.isEmptyObject( events ) ) { - delete elemData.handle; +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[0].type ], + implicitRelative = leadingRelative || Expr.relative[" "], + i = leadingRelative ? 1 : 0, - // removeData also checks for emptiness and clears the expando if empty - // so use it instead of delete - jQuery._removeData( elem, "events" ); + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf.call( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + (checkContext = context).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + } ]; + + for ( ; i < len; i++ ) { + if ( (matcher = Expr.relative[ tokens[i].type ]) ) { + matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; + } else { + matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[j].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( + // If the preceding token was a descendant combinator, insert an implicit any-element `*` + tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) + ).replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); } - }, + } - trigger: function( event, data, elem, onlyHandlers ) { - var handle, ontype, cur, - bubbleType, special, tmp, i, - eventPath = [ elem || document ], - type = core_hasOwn.call( event, "type" ) ? event.type : event, - namespaces = core_hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; + return elementMatcher( matchers ); +} - cur = tmp = elem = elem || document; +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + // A counter to specify which element is currently being matched + var matcherCachedRuns = 0, + bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, expandContext ) { + var elem, j, matcher, + setMatched = [], + matchedCount = 0, + i = "0", + unmatched = seed && [], + outermost = expandContext != null, + contextBackup = outermostContext, + // We must always have either seed elements or context + elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ), + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1); - // Don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } + if ( outermost ) { + outermostContext = context !== document && context; + cachedruns = matcherCachedRuns; + } - // focus/blur morphs to focusin/out; ensure we're not firing them right now - if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { - return; - } + // Add elements passing elementMatchers directly to results + // Keep `i` a string if there are no elements so `matchedCount` will be "00" below + for ( ; (elem = elems[i]) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + while ( (matcher = elementMatchers[j++]) ) { + if ( matcher( elem, context, xml ) ) { + results.push( elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + cachedruns = ++matcherCachedRuns; + } + } - if ( type.indexOf(".") >= 0 ) { - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split("."); - type = namespaces.shift(); - namespaces.sort(); - } - ontype = type.indexOf(":") < 0 && "on" + type; + // Track unmatched elements for set filters + if ( bySet ) { + // They will have gone through all possible matchers + if ( (elem = !matcher && elem) ) { + matchedCount--; + } - // Caller can pass in a jQuery.Event object, Object, or just an event type string - event = event[ jQuery.expando ] ? - event : - new jQuery.Event( type, typeof event === "object" && event ); + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } - event.isTrigger = true; - event.namespace = namespaces.join("."); - event.namespace_re = event.namespace ? - new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : - null; + // Apply set filters to unmatched elements + matchedCount += i; + if ( bySet && i !== matchedCount ) { + j = 0; + while ( (matcher = setMatchers[j++]) ) { + matcher( unmatched, setMatched, context, xml ); + } - // Clean up the event in case it is being reused - event.result = undefined; - if ( !event.target ) { - event.target = elem; - } + if ( seed ) { + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !(unmatched[i] || setMatched[i]) ) { + setMatched[i] = pop.call( results ); + } + } + } - // Clone any incoming data and prepend the event, creating the handler arg list - data = data == null ? - [ event ] : - jQuery.makeArray( data, [ event ] ); + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + // Generate a function of recursive functions that can be used to check each element + if ( !group ) { + group = tokenize( selector ); + } + i = group.length; + while ( i-- ) { + cached = matcherFromTokens( group[i] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + } + return cached; +}; + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[i], results ); + } + return results; +} + +function select( selector, context, results, seed ) { + var i, tokens, token, type, find, + match = tokenize( selector ); + + if ( !seed ) { + // Try to minimize operations if there is only one group + if ( match.length === 1 ) { + + // Take a shortcut and set the context if the root selector is an ID + tokens = match[0] = match[0].slice( 0 ); + if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && + support.getById && context.nodeType === 9 && documentIsHTML && + Expr.relative[ tokens[1].type ] ) { + + context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; + if ( !context ) { + return results; + } + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[i]; + + // Abort if we hit a combinator + if ( Expr.relative[ (type = token.type) ] ) { + break; + } + if ( (find = Expr.find[ type ]) ) { + // Search, expanding context for leading sibling combinators + if ( (seed = find( + token.matches[0].replace( runescape, funescape ), + rsibling.test( tokens[0].type ) && context.parentNode || context + )) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; + } + + break; + } + } + } + } + } + + // Compile and execute a filtering function + // Provide `match` to avoid retokenization if we modified the selector above + compile( selector, match )( + seed, + context, + !documentIsHTML, + results, + rsibling.test( selector ) + ); + return results; +} + +// One-time assignments + +// Sort stability +support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; + +// Support: Chrome<14 +// Always assume duplicates if they aren't passed to the comparison function +support.detectDuplicates = hasDuplicate; + +// Initialize against the default document +setDocument(); + +// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) +// Detached nodes confoundingly follow *each other* +support.sortDetached = assert(function( div1 ) { + // Should return 1, but returns 4 (following) + return div1.compareDocumentPosition( document.createElement("div") ) & 1; +}); + +// Support: IE<8 +// Prevent attribute/property "interpolation" +// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !assert(function( div ) { + div.innerHTML = ""; + return div.firstChild.getAttribute("href") === "#" ; +}) ) { + addHandle( "type|href|height|width", function( elem, name, isXML ) { + if ( !isXML ) { + return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); + } + }); +} + +// Support: IE<9 +// Use defaultValue in place of getAttribute("value") +if ( !support.attributes || !assert(function( div ) { + div.innerHTML = ""; + div.firstChild.setAttribute( "value", "" ); + return div.firstChild.getAttribute( "value" ) === ""; +}) ) { + addHandle( "value", function( elem, name, isXML ) { + if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { + return elem.defaultValue; + } + }); +} + +// Support: IE<9 +// Use getAttributeNode to fetch booleans when getAttribute lies +if ( !assert(function( div ) { + return div.getAttribute("disabled") == null; +}) ) { + addHandle( booleans, function( elem, name, isXML ) { + var val; + if ( !isXML ) { + return (val = elem.getAttributeNode( name )) && val.specified ? + val.value : + elem[ name ] === true ? name.toLowerCase() : null; + } + }); +} + +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.pseudos; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + +})( window ); +// String to Object options format cache +var optionsCache = {}; + +// Convert String-formatted options into Object-formatted ones and store in cache +function createOptions( options ) { + var object = optionsCache[ options ] = {}; + jQuery.each( options.match( core_rnotwhite ) || [], function( _, flag ) { + object[ flag ] = true; + }); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + ( optionsCache[ options ] || createOptions( options ) ) : + jQuery.extend( {}, options ); + + var // Flag to know if list is currently firing + firing, + // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list was already fired + fired, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // First callback to fire (used internally by add and fireWith) + firingStart, + // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = !options.once && [], + // Fire callbacks + fire = function( data ) { + memory = options.memory && data; + fired = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + firing = true; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { + memory = false; // To prevent further calls using add + break; + } + } + firing = false; + if ( list ) { + if ( stack ) { + if ( stack.length ) { + fire( stack.shift() ); + } + } else if ( memory ) { + list = []; + } else { + self.disable(); + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + // First, we save the current length + var start = list.length; + (function add( args ) { + jQuery.each( args, function( _, arg ) { + var type = jQuery.type( arg ); + if ( type === "function" ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && type !== "string" ) { + // Inspect recursively + add( arg ); + } + }); + })( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away + } else if ( memory ) { + firingStart = start; + fire( memory ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + jQuery.each( arguments, function( _, arg ) { + var index; + while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + // Handle firing indexes + if ( firing ) { + if ( index <= firingLength ) { + firingLength--; + } + if ( index <= firingIndex ) { + firingIndex--; + } + } + } + }); + } + return this; + }, + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length ); + }, + // Remove all callbacks from the list + empty: function() { + list = []; + firingLength = 0; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( list && ( !fired || stack ) ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + if ( firing ) { + stack.push( args ); + } else { + fire( args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; +jQuery.extend({ + + Deferred: function( func ) { + var tuples = [ + // action, add listener, listener list, final state + [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], + [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], + [ "notify", "progress", jQuery.Callbacks("memory") ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return jQuery.Deferred(function( newDefer ) { + jQuery.each( tuples, function( i, tuple ) { + var action = tuple[ 0 ], + fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; + // deferred[ done | fail | progress ] for forwarding actions to newDefer + deferred[ tuple[1] ](function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise() + .done( newDefer.resolve ) + .fail( newDefer.reject ) + .progress( newDefer.notify ); + } else { + newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); + } + }); + }); + fns = null; + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[1] ] = list.add; + + // Handle state + if ( stateString ) { + list.add(function() { + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] + deferred[ tuple[0] ] = function() { + deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); + return this; + }; + deferred[ tuple[0] + "With" ] = list.fireWith; + }); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + resolveValues = core_slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, - // Allow special events to draw outside the lines - special = jQuery.event.special[ type ] || {}; - if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { - return; - } + // the master Deferred. If resolveValues consist of only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : jQuery.Deferred(), - // Determine event propagation path in advance, per W3C events spec (#9951) - // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) - if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value; + if( values === progressValues ) { + deferred.notifyWith( contexts, values ); + } else if ( !( --remaining ) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, - bubbleType = special.delegateType || type; - if ( !rfocusMorph.test( bubbleType + type ) ) { - cur = cur.parentNode; - } - for ( ; cur; cur = cur.parentNode ) { - eventPath.push( cur ); - tmp = cur; - } + progressValues, progressContexts, resolveContexts; - // Only add window if we got to document (e.g., not plain obj or detached DOM) - if ( tmp === (elem.ownerDocument || document) ) { - eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + // add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { + resolveValues[ i ].promise() + .done( updateFunc( i, resolveContexts, resolveValues ) ) + .fail( deferred.reject ) + .progress( updateFunc( i, progressContexts, progressValues ) ); + } else { + --remaining; + } } } - // Fire handlers on the event path - i = 0; - while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { + // if we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } - event.type = i > 1 ? - bubbleType : - special.bindType || type; + return deferred.promise(); + } +}); +jQuery.support = (function( support ) { - // jQuery handler - handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); - if ( handle ) { - handle.apply( cur, data ); - } + var all, a, input, select, fragment, opt, eventName, isSupported, i, + div = document.createElement("div"); - // Native handler - handle = ontype && cur[ ontype ]; - if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) { - event.preventDefault(); - } - } - event.type = type; + // Setup + div.setAttribute( "className", "t" ); + div.innerHTML = "
    a"; - // If nobody prevented the default action, do it now - if ( !onlyHandlers && !event.isDefaultPrevented() ) { + // Finish early in limited (non-browser) environments + all = div.getElementsByTagName("*") || []; + a = div.getElementsByTagName("a")[ 0 ]; + if ( !a || !a.style || !all.length ) { + return support; + } - if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && - !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { + // First batch of tests + select = document.createElement("select"); + opt = select.appendChild( document.createElement("option") ); + input = div.getElementsByTagName("input")[ 0 ]; - // Call a native DOM method on the target with the same name name as the event. - // Can't use an .isFunction() check here because IE6/7 fails that test. - // Don't do default actions on window, that's where global variables be (#6170) - if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) { + a.style.cssText = "top:1px;float:left;opacity:.5"; - // Don't re-trigger an onFOO event when we call its FOO() method - tmp = elem[ ontype ]; + // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) + support.getSetAttribute = div.className !== "t"; - if ( tmp ) { - elem[ ontype ] = null; - } + // IE strips leading whitespace when .innerHTML is used + support.leadingWhitespace = div.firstChild.nodeType === 3; - // Prevent re-triggering of the same event, since we already bubbled it above - jQuery.event.triggered = type; - try { - elem[ type ](); - } catch ( e ) { - // IE<9 dies on focus/blur to hidden element (#1486,#12518) - // only reproducible on winXP IE8 native, not IE9 in IE8 mode - } - jQuery.event.triggered = undefined; + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + support.tbody = !div.getElementsByTagName("tbody").length; - if ( tmp ) { - elem[ ontype ] = tmp; - } - } - } - } + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + support.htmlSerialize = !!div.getElementsByTagName("link").length; - return event.result; - }, + // Get the style information from getAttribute + // (IE uses .cssText instead) + support.style = /top/.test( a.getAttribute("style") ); - dispatch: function( event ) { + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + support.hrefNormalized = a.getAttribute("href") === "/a"; - // Make a writable jQuery.Event from the native event object - event = jQuery.event.fix( event ); + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + support.opacity = /^0.5/.test( a.style.opacity ); - var i, ret, handleObj, matched, j, - handlerQueue = [], - args = core_slice.call( arguments ), - handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [], - special = jQuery.event.special[ event.type ] || {}; + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + support.cssFloat = !!a.style.cssFloat; - // Use the fix-ed jQuery.Event rather than the (read-only) native event - args[0] = event; - event.delegateTarget = this; + // Check the default checkbox/radio value ("" on WebKit; "on" elsewhere) + support.checkOn = !!input.value; - // Call the preDispatch hook for the mapped type, and let it bail if desired - if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { - return; - } + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + support.optSelected = opt.selected; - // Determine handlers - handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + // Tests for enctype support on a form (#6743) + support.enctype = !!document.createElement("form").enctype; - // Run delegates first; they may want to stop propagation beneath us - i = 0; - while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { - event.currentTarget = matched.elem; + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + support.html5Clone = document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>"; - j = 0; - while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { + // Will be defined later + support.inlineBlockNeedsLayout = false; + support.shrinkWrapBlocks = false; + support.pixelPosition = false; + support.deleteExpando = true; + support.noCloneEvent = true; + support.reliableMarginRight = true; + support.boxSizingReliable = true; - // Triggered event must either 1) have no namespace, or - // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). - if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { + // Make sure checked status is properly cloned + input.checked = true; + support.noCloneChecked = input.cloneNode( true ).checked; - event.handleObj = handleObj; - event.data = handleObj.data; + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as disabled) + select.disabled = true; + support.optDisabled = !opt.disabled; - ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) - .apply( matched.elem, args ); + // Support: IE<9 + try { + delete div.test; + } catch( e ) { + support.deleteExpando = false; + } - if ( ret !== undefined ) { - if ( (event.result = ret) === false ) { - event.preventDefault(); - event.stopPropagation(); - } - } - } - } - } + // Check if we can trust getAttribute("value") + input = document.createElement("input"); + input.setAttribute( "value", "" ); + support.input = input.getAttribute( "value" ) === ""; - // Call the postDispatch hook for the mapped type - if ( special.postDispatch ) { - special.postDispatch.call( this, event ); - } + // Check if an input maintains its value after becoming a radio + input.value = "t"; + input.setAttribute( "type", "radio" ); + support.radioValue = input.value === "t"; - return event.result; - }, + // #11217 - WebKit loses check when the name is after the checked attribute + input.setAttribute( "checked", "t" ); + input.setAttribute( "name", "t" ); - handlers: function( event, handlers ) { - var sel, handleObj, matches, i, - handlerQueue = [], - delegateCount = handlers.delegateCount, - cur = event.target; + fragment = document.createDocumentFragment(); + fragment.appendChild( input ); - // Find delegate handlers - // Black-hole SVG instance trees (#13180) - // Avoid non-left-click bubbling in Firefox (#3861) - if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { + // Check if a disconnected checkbox will retain its checked + // value of true after appended to the DOM (IE6/7) + support.appendChecked = input.checked; - for ( ; cur != this; cur = cur.parentNode || this ) { + // WebKit doesn't clone checked state correctly in fragments + support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; - // Don't check non-elements (#13208) - // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) - if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) { - matches = []; - for ( i = 0; i < delegateCount; i++ ) { - handleObj = handlers[ i ]; + // Support: IE<9 + // Opera does not clone events (and typeof div.attachEvent === undefined). + // IE9-10 clones events bound via attachEvent, but they don't trigger with .click() + if ( div.attachEvent ) { + div.attachEvent( "onclick", function() { + support.noCloneEvent = false; + }); - // Don't conflict with Object.prototype properties (#13203) - sel = handleObj.selector + " "; + div.cloneNode( true ).click(); + } - if ( matches[ sel ] === undefined ) { - matches[ sel ] = handleObj.needsContext ? - jQuery( sel, this ).index( cur ) >= 0 : - jQuery.find( sel, this, null, [ cur ] ).length; - } - if ( matches[ sel ] ) { - matches.push( handleObj ); - } - } - if ( matches.length ) { - handlerQueue.push({ elem: cur, handlers: matches }); - } - } - } - } + // Support: IE<9 (lack submit/change bubble), Firefox 17+ (lack focusin event) + // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP) + for ( i in { submit: true, change: true, focusin: true }) { + div.setAttribute( eventName = "on" + i, "t" ); - // Add the remaining (directly-bound) handlers - if ( delegateCount < handlers.length ) { - handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); - } + support[ i + "Bubbles" ] = eventName in window || div.attributes[ eventName ].expando === false; + } - return handlerQueue; - }, + div.style.backgroundClip = "content-box"; + div.cloneNode( true ).style.backgroundClip = ""; + support.clearCloneStyle = div.style.backgroundClip === "content-box"; - fix: function( event ) { - if ( event[ jQuery.expando ] ) { - return event; + // Support: IE<9 + // Iteration over object's inherited properties before its own. + for ( i in jQuery( support ) ) { + break; + } + support.ownLast = i !== "0"; + + // Run tests that need a body at doc ready + jQuery(function() { + var container, marginDiv, tds, + divReset = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;", + body = document.getElementsByTagName("body")[0]; + + if ( !body ) { + // Return for frameset docs that don't have a body + return; } - // Create a writable copy of the event object and normalize some properties - var i, prop, copy, - type = event.type, - originalEvent = event, - fixHook = this.fixHooks[ type ]; + container = document.createElement("div"); + container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px"; - if ( !fixHook ) { - this.fixHooks[ type ] = fixHook = - rmouseEvent.test( type ) ? this.mouseHooks : - rkeyEvent.test( type ) ? this.keyHooks : - {}; - } - copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + body.appendChild( container ).appendChild( div ); - event = new jQuery.Event( originalEvent ); + // Support: IE8 + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + div.innerHTML = "
    t
    "; + tds = div.getElementsByTagName("td"); + tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none"; + isSupported = ( tds[ 0 ].offsetHeight === 0 ); - i = copy.length; - while ( i-- ) { - prop = copy[ i ]; - event[ prop ] = originalEvent[ prop ]; - } + tds[ 0 ].style.display = ""; + tds[ 1 ].style.display = "none"; - // Support: IE<9 - // Fix target property (#1925) - if ( !event.target ) { - event.target = originalEvent.srcElement || document; - } + // Support: IE8 + // Check if empty table cells still have offsetWidth/Height + support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); - // Support: Chrome 23+, Safari? - // Target should not be a text node (#504, #13143) - if ( event.target.nodeType === 3 ) { - event.target = event.target.parentNode; - } + // Check box-sizing and margin behavior. + div.innerHTML = ""; + div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;"; - // Support: IE<9 - // For mouse/key events, metaKey==false if it's undefined (#3368, #11328) - event.metaKey = !!event.metaKey; + // Workaround failing boxSizing test due to offsetWidth returning wrong value + // with some non-1 values of body zoom, ticket #13543 + jQuery.swap( body, body.style.zoom != null ? { zoom: 1 } : {}, function() { + support.boxSizing = div.offsetWidth === 4; + }); - return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; - }, + // Use window.getComputedStyle because jsdom on node.js will break without it. + if ( window.getComputedStyle ) { + support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%"; + support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px"; - // Includes some event props shared by KeyEvent and MouseEvent - props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + // Check if div with explicit width and no margin-right incorrectly + // gets computed margin-right based on width of container. (#3333) + // Fails in WebKit before Feb 2011 nightlies + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + marginDiv = div.appendChild( document.createElement("div") ); + marginDiv.style.cssText = div.style.cssText = divReset; + marginDiv.style.marginRight = marginDiv.style.width = "0"; + div.style.width = "1px"; - fixHooks: {}, + support.reliableMarginRight = + !parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight ); + } - keyHooks: { - props: "char charCode key keyCode".split(" "), - filter: function( event, original ) { + if ( typeof div.style.zoom !== core_strundefined ) { + // Support: IE<8 + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + div.innerHTML = ""; + div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1"; + support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); - // Add which for key events - if ( event.which == null ) { - event.which = original.charCode != null ? original.charCode : original.keyCode; - } + // Support: IE6 + // Check if elements with layout shrink-wrap their children + div.style.display = "block"; + div.innerHTML = "
    "; + div.firstChild.style.width = "5px"; + support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); - return event; + if ( support.inlineBlockNeedsLayout ) { + // Prevent IE 6 from affecting layout for positioned elements #11048 + // Prevent IE from shrinking the body in IE 7 mode #12869 + // Support: IE<8 + body.style.zoom = 1; + } } - }, - mouseHooks: { - props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), - filter: function( event, original ) { - var body, eventDoc, doc, - button = original.button, - fromElement = original.fromElement; + body.removeChild( container ); - // Calculate pageX/Y if missing and clientX/Y available - if ( event.pageX == null && original.clientX != null ) { - eventDoc = event.target.ownerDocument || document; - doc = eventDoc.documentElement; - body = eventDoc.body; + // Null elements to avoid leaks in IE + container = div = tds = marginDiv = null; + }); - event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); - event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); - } + // Null elements to avoid leaks in IE + all = select = fragment = opt = a = input = null; - // Add relatedTarget, if necessary - if ( !event.relatedTarget && fromElement ) { - event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; - } + return support; +})({}); - // Add which for click: 1 === left; 2 === middle; 3 === right - // Note: button is not normalized, so don't use it - if ( !event.which && button !== undefined ) { - event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); - } +var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/, + rmultiDash = /([A-Z])/g; - return event; - } - }, +function internalData( elem, name, data, pvt /* Internal Use Only */ ){ + if ( !jQuery.acceptData( elem ) ) { + return; + } - special: { - load: { - // Prevent triggered image.load events from bubbling to window.load - noBubble: true - }, - click: { - // For checkbox, fire native event so checked state will be right - trigger: function() { - if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) { - this.click(); - return false; - } - } - }, - focus: { - // Fire native event if possible so blur/focus sequence is correct - trigger: function() { - if ( this !== document.activeElement && this.focus ) { - try { - this.focus(); - return false; - } catch ( e ) { - // Support: IE<9 - // If we error on focus to hidden element (#1486, #12518), - // let .trigger() run the handlers - } - } - }, - delegateType: "focusin" - }, - blur: { - trigger: function() { - if ( this === document.activeElement && this.blur ) { - this.blur(); - return false; - } - }, - delegateType: "focusout" - }, + var ret, thisCache, + internalKey = jQuery.expando, - beforeunload: { - postDispatch: function( event ) { + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, - // Even when returnValue equals to undefined Firefox will still show alert - if ( event.result !== undefined ) { - event.originalEvent.returnValue = event.result; - } - } - } - }, + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, - simulate: function( type, elem, event, bubble ) { - // Piggyback on a donor event to simulate a different one. - // Fake originalEvent to avoid donor's stopPropagation, but if the - // simulated event prevents default then we do the same on the donor. - var e = jQuery.extend( - new jQuery.Event(), - event, - { type: type, - isSimulated: true, - originalEvent: {} - } - ); - if ( bubble ) { - jQuery.event.trigger( e, null, elem ); - } else { - jQuery.event.dispatch.call( elem, e ); - } - if ( e.isDefaultPrevented() ) { - event.preventDefault(); - } + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string" ) { + return; } -}; -jQuery.removeEvent = document.removeEventListener ? - function( elem, type, handle ) { - if ( elem.removeEventListener ) { - elem.removeEventListener( type, handle, false ); + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + id = elem[ internalKey ] = core_deletedIds.pop() || jQuery.guid++; + } else { + id = internalKey; } - } : - function( elem, type, handle ) { - var name = "on" + type; - - if ( elem.detachEvent ) { + } - // #8545, #7054, preventing memory leaks for custom events in IE6-8 - // detachEvent needed property on element, by name of that event, to properly expose it to GC - if ( typeof elem[ name ] === core_strundefined ) { - elem[ name ] = null; - } + if ( !cache[ id ] ) { + // Avoid exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify + cache[ id ] = isNode ? {} : { toJSON: jQuery.noop }; + } - elem.detachEvent( name, handle ); + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" || typeof name === "function" ) { + if ( pvt ) { + cache[ id ] = jQuery.extend( cache[ id ], name ); + } else { + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); } - }; - -jQuery.Event = function( src, props ) { - // Allow instantiation without the 'new' keyword - if ( !(this instanceof jQuery.Event) ) { - return new jQuery.Event( src, props ); } - // Event object - if ( src && src.type ) { - this.originalEvent = src; - this.type = src.type; + thisCache = cache[ id ]; - // Events bubbling up the document may have been marked as prevented - // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || - src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; + // jQuery data() is stored in a separate object inside the object's internal data + // cache in order to avoid key collisions between internal data and user-defined + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; + } - // Event type - } else { - this.type = src; + thisCache = thisCache.data; } - // Put explicitly provided properties onto the event object - if ( props ) { - jQuery.extend( this, props ); + if ( data !== undefined ) { + thisCache[ jQuery.camelCase( name ) ] = data; } - // Create a timestamp if incoming event doesn't have one - this.timeStamp = src && src.timeStamp || jQuery.now(); - - // Mark it as fixed - this[ jQuery.expando ] = true; -}; + // Check for both converted-to-camel and non-converted data property names + // If a data property was specified + if ( typeof name === "string" ) { -// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding -// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html -jQuery.Event.prototype = { - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse, + // First Try to find as-is property data + ret = thisCache[ name ]; - preventDefault: function() { - var e = this.originalEvent; + // Test for null|undefined property data + if ( ret == null ) { - this.isDefaultPrevented = returnTrue; - if ( !e ) { - return; + // Try to find the camelCased property + ret = thisCache[ jQuery.camelCase( name ) ]; } + } else { + ret = thisCache; + } - // If preventDefault exists, run it on the original event - if ( e.preventDefault ) { - e.preventDefault(); + return ret; +} - // Support: IE - // Otherwise set the returnValue property of the original event to false - } else { - e.returnValue = false; - } - }, - stopPropagation: function() { - var e = this.originalEvent; +function internalRemoveData( elem, name, pvt ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } - this.isPropagationStopped = returnTrue; - if ( !e ) { - return; - } - // If stopPropagation exists, run it on the original event - if ( e.stopPropagation ) { - e.stopPropagation(); - } + var thisCache, i, + isNode = elem.nodeType, - // Support: IE - // Set the cancelBubble property of the original event to true - e.cancelBubble = true; - }, - stopImmediatePropagation: function() { - this.isImmediatePropagationStopped = returnTrue; - this.stopPropagation(); + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + id = isNode ? elem[ jQuery.expando ] : jQuery.expando; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; } -}; -// Create mouseenter/leave events using mouseover/out and event-time checks -jQuery.each({ - mouseenter: "mouseover", - mouseleave: "mouseout" -}, function( orig, fix ) { - jQuery.event.special[ orig ] = { - delegateType: fix, - bindType: fix, + if ( name ) { - handle: function( event ) { - var ret, - target = this, - related = event.relatedTarget, - handleObj = event.handleObj; + thisCache = pvt ? cache[ id ] : cache[ id ].data; - // For mousenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || (related !== target && !jQuery.contains( target, related )) ) { - event.type = handleObj.origType; - ret = handleObj.handler.apply( this, arguments ); - event.type = fix; - } - return ret; - } - }; -}); + if ( thisCache ) { -// IE submit delegation -if ( !jQuery.support.submitBubbles ) { + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { - jQuery.event.special.submit = { - setup: function() { - // Only need this for delegated form submit events - if ( jQuery.nodeName( this, "form" ) ) { - return false; - } + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { - // Lazy-add a submit handler when a descendant form may potentially be submitted - jQuery.event.add( this, "click._submit keypress._submit", function( e ) { - // Node name check avoids a VML-related crash in IE (#9807) - var elem = e.target, - form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; - if ( form && !jQuery._data( form, "submitBubbles" ) ) { - jQuery.event.add( form, "submit._submit", function( event ) { - event._submit_bubble = true; - }); - jQuery._data( form, "submitBubbles", true ); + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split(" "); + } } - }); - // return undefined since we don't need an event listener - }, + } else { + // If "name" is an array of keys... + // When data is initially created, via ("key", "val") signature, + // keys will be converted to camelCase. + // Since there is no way to tell _how_ a key was added, remove + // both plain key and camelCase key. #12786 + // This will only penalize the array argument path. + name = name.concat( jQuery.map( name, jQuery.camelCase ) ); + } - postDispatch: function( event ) { - // If form was submitted by the user, bubble the event up the tree - if ( event._submit_bubble ) { - delete event._submit_bubble; - if ( this.parentNode && !event.isTrigger ) { - jQuery.event.simulate( "submit", this.parentNode, event, true ); - } + i = name.length; + while ( i-- ) { + delete thisCache[ name[i] ]; } - }, - teardown: function() { - // Only need this for delegated form submit events - if ( jQuery.nodeName( this, "form" ) ) { - return false; + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache) ) { + return; } + } + } - // Remove delegated handlers; cleanData eventually reaps submit handlers attached above - jQuery.event.remove( this, "._submit" ); + // See jQuery.data for more information + if ( !pvt ) { + delete cache[ id ].data; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !isEmptyDataObject( cache[ id ] ) ) { + return; } - }; -} + } -// IE change delegation and checkbox/radio fix -if ( !jQuery.support.changeBubbles ) { + // Destroy the cache + if ( isNode ) { + jQuery.cleanData( [ elem ], true ); - jQuery.event.special.change = { + // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) + /* jshint eqeqeq: false */ + } else if ( jQuery.support.deleteExpando || cache != cache.window ) { + /* jshint eqeqeq: true */ + delete cache[ id ]; - setup: function() { + // When all else fails, null + } else { + cache[ id ] = null; + } +} - if ( rformElems.test( this.nodeName ) ) { - // IE doesn't fire change on a check/radio until blur; trigger it on click - // after a propertychange. Eat the blur-change in special.change.handle. - // This still fires onchange a second time for check/radio after blur. - if ( this.type === "checkbox" || this.type === "radio" ) { - jQuery.event.add( this, "propertychange._change", function( event ) { - if ( event.originalEvent.propertyName === "checked" ) { - this._just_changed = true; - } - }); - jQuery.event.add( this, "click._change", function( event ) { - if ( this._just_changed && !event.isTrigger ) { - this._just_changed = false; - } - // Allow triggered, simulated change events (#11500) - jQuery.event.simulate( "change", this, event, true ); - }); - } - return false; - } - // Delegated event; lazy-add a change handler on descendant inputs - jQuery.event.add( this, "beforeactivate._change", function( e ) { - var elem = e.target; +jQuery.extend({ + cache: {}, + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "applet": true, + "embed": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" + }, + + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + return !!elem && !isEmptyDataObject( elem ); + }, - if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) { - jQuery.event.add( elem, "change._change", function( event ) { - if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { - jQuery.event.simulate( "change", this.parentNode, event, true ); - } - }); - jQuery._data( elem, "changeBubbles", true ); - } - }); - }, + data: function( elem, name, data ) { + return internalData( elem, name, data ); + }, - handle: function( event ) { - var elem = event.target; + removeData: function( elem, name ) { + return internalRemoveData( elem, name ); + }, - // Swallow native change events from checkbox/radio, we already triggered them above - if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { - return event.handleObj.handler.apply( this, arguments ); - } - }, + // For internal use only. + _data: function( elem, name, data ) { + return internalData( elem, name, data, true ); + }, - teardown: function() { - jQuery.event.remove( this, "._change" ); + _removeData: function( elem, name ) { + return internalRemoveData( elem, name, true ); + }, - return !rformElems.test( this.nodeName ); + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + // Do not set data on non-element because it will not be cleared (#8335). + if ( elem.nodeType && elem.nodeType !== 1 && elem.nodeType !== 9 ) { + return false; } - }; -} -// Create "bubbling" focus and blur events -if ( !jQuery.support.focusinBubbles ) { - jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { - - // Attach a single capturing handler while someone wants focusin/focusout - var attaches = 0, - handler = function( event ) { - jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); - }; + var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ]; - jQuery.event.special[ fix ] = { - setup: function() { - if ( attaches++ === 0 ) { - document.addEventListener( orig, handler, true ); - } - }, - teardown: function() { - if ( --attaches === 0 ) { - document.removeEventListener( orig, handler, true ); - } - } - }; - }); -} + // nodes accept data unless otherwise specified; rejection can be conditional + return !noData || noData !== true && elem.getAttribute("classid") === noData; + } +}); jQuery.fn.extend({ + data: function( key, value ) { + var attrs, name, + data = null, + i = 0, + elem = this[0]; - on: function( types, selector, data, fn, /*INTERNAL*/ one ) { - var type, origFn; + // Special expections of .data basically thwart jQuery.access, + // so implement the relevant behavior ourselves - // Types can be a map of types/handlers - if ( typeof types === "object" ) { - // ( types-Object, selector, data ) - if ( typeof selector !== "string" ) { - // ( types-Object, data ) - data = data || selector; - selector = undefined; - } - for ( type in types ) { - this.on( type, selector, data, types[ type ], one ); - } - return this; - } + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = jQuery.data( elem ); - if ( data == null && fn == null ) { - // ( types, fn ) - fn = selector; - data = selector = undefined; - } else if ( fn == null ) { - if ( typeof selector === "string" ) { - // ( types, selector, fn ) - fn = data; - data = undefined; - } else { - // ( types, data, fn ) - fn = data; - data = selector; - selector = undefined; - } - } - if ( fn === false ) { - fn = returnFalse; - } else if ( !fn ) { - return this; - } + if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { + attrs = elem.attributes; + for ( ; i < attrs.length; i++ ) { + name = attrs[i].name; - if ( one === 1 ) { - origFn = fn; - fn = function( event ) { - // Can use an empty set, since event contains the info - jQuery().off( event ); - return origFn.apply( this, arguments ); - }; - // Use same guid so caller can remove using origFn - fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); - } - return this.each( function() { - jQuery.event.add( this, types, fn, data, selector ); - }); - }, - one: function( types, selector, data, fn ) { - return this.on( types, selector, data, fn, 1 ); - }, - off: function( types, selector, fn ) { - var handleObj, type; - if ( types && types.preventDefault && types.handleObj ) { - // ( event ) dispatched jQuery.Event - handleObj = types.handleObj; - jQuery( types.delegateTarget ).off( - handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, - handleObj.selector, - handleObj.handler - ); - return this; - } - if ( typeof types === "object" ) { - // ( types-object [, selector] ) - for ( type in types ) { - this.off( type, selector, types[ type ] ); + if ( name.indexOf("data-") === 0 ) { + name = jQuery.camelCase( name.slice(5) ); + + dataAttr( elem, name, data[ name ] ); + } + } + jQuery._data( elem, "parsedAttrs", true ); + } } - return this; - } - if ( selector === false || typeof selector === "function" ) { - // ( types [, fn] ) - fn = selector; - selector = undefined; + + return data; } - if ( fn === false ) { - fn = returnFalse; + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); } - return this.each(function() { - jQuery.event.remove( this, types, fn, selector ); - }); - }, - bind: function( types, data, fn ) { - return this.on( types, null, data, fn ); - }, - unbind: function( types, fn ) { - return this.off( types, null, fn ); - }, + return arguments.length > 1 ? - delegate: function( selector, types, data, fn ) { - return this.on( types, selector, data, fn ); - }, - undelegate: function( selector, types, fn ) { - // ( namespace ) or ( selector, types [, fn] ) - return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn ); + // Sets one value + this.each(function() { + jQuery.data( this, key, value ); + }) : + + // Gets one value + // Try to fetch any internally stored data first + elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : null; }, - trigger: function( type, data ) { + removeData: function( key ) { return this.each(function() { - jQuery.event.trigger( type, data, this ); + jQuery.removeData( this, key ); }); - }, - triggerHandler: function( type, data ) { - var elem = this[0]; - if ( elem ) { - return jQuery.event.trigger( type, data, elem, true ); - } } }); -/*! - * Sizzle CSS Selector Engine - * Copyright 2012 jQuery Foundation and other contributors - * Released under the MIT license - * http://sizzlejs.com/ - */ -(function( window, undefined ) { -var i, - cachedruns, - Expr, - getText, - isXML, - compile, - hasDuplicate, - outermostContext, +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { - // Local document vars - setDocument, - document, - docElem, - documentIsXML, - rbuggyQSA, - rbuggyMatches, - matches, - contains, - sortOrder, + var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + // Only convert to a number if it doesn't change the string + +data + "" === data ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + +// checks a cache object for emptiness +function isEmptyDataObject( obj ) { + var name; + for ( name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } + if ( name !== "toJSON" ) { + return false; + } + } - // Instance-specific data - expando = "sizzle" + -(new Date()), - preferredDoc = window.document, - support = {}, - dirruns = 0, - done = 0, - classCache = createCache(), - tokenCache = createCache(), - compilerCache = createCache(), + return true; +} +jQuery.extend({ + queue: function( elem, type, data ) { + var queue; - // General-purpose constants - strundefined = typeof undefined, - MAX_NEGATIVE = 1 << 31, + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = jQuery._data( elem, type ); - // Array methods - arr = [], - pop = arr.pop, - push = arr.push, - slice = arr.slice, - // Use a stripped-down indexOf if we can't use a native one - indexOf = arr.indexOf || function( elem ) { - var i = 0, - len = this.length; - for ( ; i < len; i++ ) { - if ( this[i] === elem ) { - return i; + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || jQuery.isArray(data) ) { + queue = jQuery._data( elem, type, jQuery.makeArray(data) ); + } else { + queue.push( data ); + } } + return queue || []; } - return -1; }, + dequeue: function( elem, type ) { + type = type || "fx"; - // Regular expressions - - // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace - whitespace = "[\\x20\\t\\r\\n\\f]", - // http://www.w3.org/TR/css3-syntax/#characters - characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", - - // Loosely modeled on CSS identifier characters - // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors - // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier - identifier = characterEncoding.replace( "w", "w#" ), + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; - // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors - operators = "([*^$|!~]?=)", - attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace + - "*(?:" + operators + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]", + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } - // Prefer arguments quoted, - // then not containing pseudos/brackets, - // then attribute selectors/non-parenthetical expressions, - // then anything else - // These preferences are here to reduce the number of selectors - // needing tokenize in the PSEUDO preFilter - pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace( 3, 8 ) + ")*)|.*)\\)|)", + if ( fn ) { - // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter - rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } - rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), - rcombinators = new RegExp( "^" + whitespace + "*([\\x20\\t\\r\\n\\f>+~])" + whitespace + "*" ), - rpseudo = new RegExp( pseudos ), - ridentifier = new RegExp( "^" + identifier + "$" ), + // clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } - matchExpr = { - "ID": new RegExp( "^#(" + characterEncoding + ")" ), - "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), - "NAME": new RegExp( "^\\[name=['\"]?(" + characterEncoding + ")['\"]?\\]" ), - "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), - "ATTR": new RegExp( "^" + attributes ), - "PSEUDO": new RegExp( "^" + pseudos ), - "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + - "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + - "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), - // For use in libraries implementing .is() - // We use this for POS matching in `select` - "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + - whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + if ( !startLength && hooks ) { + hooks.empty.fire(); + } }, - rsibling = /[\x20\t\r\n\f]*[+~]/, - - rnative = /^[^{]+\{\s*\[native code/, + // not intended for public consumption - generates a queueHooks object, or returns the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return jQuery._data( elem, key ) || jQuery._data( elem, key, { + empty: jQuery.Callbacks("once memory").add(function() { + jQuery._removeData( elem, type + "queue" ); + jQuery._removeData( elem, key ); + }) + }); + } +}); - // Easily-parseable/retrievable ID or TAG or CLASS selectors - rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, +jQuery.fn.extend({ + queue: function( type, data ) { + var setter = 2; - rinputs = /^(?:input|select|textarea|button)$/i, - rheader = /^h\d$/i, + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } - rescape = /'|\\/g, - rattributeQuotes = /\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g, + if ( arguments.length < setter ) { + return jQuery.queue( this[0], type ); + } - // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters - runescape = /\\([\da-fA-F]{1,6}[\x20\t\r\n\f]?|.)/g, - funescape = function( _, escaped ) { - var high = "0x" + escaped - 0x10000; - // NaN means non-codepoint - return high !== high ? - escaped : - // BMP codepoint - high < 0 ? - String.fromCharCode( high + 0x10000 ) : - // Supplemental Plane codepoint (surrogate pair) - String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); - }; + return data === undefined ? + this : + this.each(function() { + var queue = jQuery.queue( this, type, data ); -// Use a stripped-down slice if we can't use a native one -try { - slice.call( preferredDoc.documentElement.childNodes, 0 )[0].nodeType; -} catch ( e ) { - slice = function( i ) { - var elem, - results = []; - while ( (elem = this[i++]) ) { - results.push( elem ); - } - return results; - }; -} + // ensure a hooks for this queue + jQuery._queueHooks( this, type ); -/** - * For feature detection - * @param {Function} fn The function to test for native support - */ -function isNative( fn ) { - return rnative.test( fn + "" ); -} + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; -/** - * Create key-value caches of limited size - * @returns {Function(string, Object)} Returns the Object data after storing it on itself with - * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) - * deleting the oldest entry - */ -function createCache() { - var cache, - keys = []; + return this.queue( type, function( next, hooks ) { + var timeout = setTimeout( next, time ); + hooks.stop = function() { + clearTimeout( timeout ); + }; + }); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; - return (cache = function( key, value ) { - // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) - if ( keys.push( key += " " ) > Expr.cacheLength ) { - // Only keep the most recent entries - delete cache[ keys.shift() ]; + if ( typeof type !== "string" ) { + obj = type; + type = undefined; } - return (cache[ key ] = value); - }); -} + type = type || "fx"; -/** - * Mark a function for special use by Sizzle - * @param {Function} fn The function to mark - */ -function markFunction( fn ) { - fn[ expando ] = true; - return fn; -} + while( i-- ) { + tmp = jQuery._data( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +}); +var nodeHook, boolHook, + rclass = /[\t\r\n\f]/g, + rreturn = /\r/g, + rfocusable = /^(?:input|select|textarea|button|object)$/i, + rclickable = /^(?:a|area)$/i, + ruseDefault = /^(?:checked|selected)$/i, + getSetAttribute = jQuery.support.getSetAttribute, + getSetInput = jQuery.support.input; -/** - * Support testing using an element - * @param {Function} fn Passed the created div and expects a boolean result - */ -function assert( fn ) { - var div = document.createElement("div"); +jQuery.fn.extend({ + attr: function( name, value ) { + return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, - try { - return fn( div ); - } catch (e) { - return false; - } finally { - // release memory in IE - div = null; - } -} + removeAttr: function( name ) { + return this.each(function() { + jQuery.removeAttr( this, name ); + }); + }, -function Sizzle( selector, context, results, seed ) { - var match, elem, m, nodeType, - // QSA vars - i, groups, old, nid, newContext, newSelector; + prop: function( name, value ) { + return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, - if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { - setDocument( context ); - } + removeProp: function( name ) { + name = jQuery.propFix[ name ] || name; + return this.each(function() { + // try/catch handles cases where IE balks (such as removing a property on window) + try { + this[ name ] = undefined; + delete this[ name ]; + } catch( e ) {} + }); + }, - context = context || document; - results = results || []; + addClass: function( value ) { + var classes, elem, cur, clazz, j, + i = 0, + len = this.length, + proceed = typeof value === "string" && value; - if ( !selector || typeof selector !== "string" ) { - return results; - } + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).addClass( value.call( this, j, this.className ) ); + }); + } - if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) { - return []; - } + if ( proceed ) { + // The disjunction here is for better compressibility (see removeClass) + classes = ( value || "" ).match( core_rnotwhite ) || []; - if ( !documentIsXML && !seed ) { + for ( ; i < len; i++ ) { + elem = this[ i ]; + cur = elem.nodeType === 1 && ( elem.className ? + ( " " + elem.className + " " ).replace( rclass, " " ) : + " " + ); - // Shortcuts - if ( (match = rquickExpr.exec( selector )) ) { - // Speed-up: Sizzle("#ID") - if ( (m = match[1]) ) { - if ( nodeType === 9 ) { - elem = context.getElementById( m ); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if ( elem && elem.parentNode ) { - // Handle the case where IE, Opera, and Webkit return items - // by name instead of ID - if ( elem.id === m ) { - results.push( elem ); - return results; + if ( cur ) { + j = 0; + while ( (clazz = classes[j++]) ) { + if ( cur.indexOf( " " + clazz + " " ) < 0 ) { + cur += clazz + " "; } - } else { - return results; - } - } else { - // Context is not a document - if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && - contains( context, elem ) && elem.id === m ) { - results.push( elem ); - return results; } - } - - // Speed-up: Sizzle("TAG") - } else if ( match[2] ) { - push.apply( results, slice.call(context.getElementsByTagName( selector ), 0) ); - return results; + elem.className = jQuery.trim( cur ); - // Speed-up: Sizzle(".CLASS") - } else if ( (m = match[3]) && support.getByClassName && context.getElementsByClassName ) { - push.apply( results, slice.call(context.getElementsByClassName( m ), 0) ); - return results; + } } } - // QSA path - if ( support.qsa && !rbuggyQSA.test(selector) ) { - old = true; - nid = expando; - newContext = context; - newSelector = nodeType === 9 && selector; + return this; + }, - // qSA works strangely on Element-rooted queries - // We can work around this by specifying an extra ID on the root - // and working up from there (Thanks to Andrew Dupont for the technique) - // IE 8 doesn't work on object elements - if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { - groups = tokenize( selector ); + removeClass: function( value ) { + var classes, elem, cur, clazz, j, + i = 0, + len = this.length, + proceed = arguments.length === 0 || typeof value === "string" && value; - if ( (old = context.getAttribute("id")) ) { - nid = old.replace( rescape, "\\$&" ); - } else { - context.setAttribute( "id", nid ); - } - nid = "[id='" + nid + "'] "; + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).removeClass( value.call( this, j, this.className ) ); + }); + } + if ( proceed ) { + classes = ( value || "" ).match( core_rnotwhite ) || []; - i = groups.length; - while ( i-- ) { - groups[i] = nid + toSelector( groups[i] ); - } - newContext = rsibling.test( selector ) && context.parentNode || context; - newSelector = groups.join(","); - } + for ( ; i < len; i++ ) { + elem = this[ i ]; + // This expression is here for better compressibility (see addClass) + cur = elem.nodeType === 1 && ( elem.className ? + ( " " + elem.className + " " ).replace( rclass, " " ) : + "" + ); - if ( newSelector ) { - try { - push.apply( results, slice.call( newContext.querySelectorAll( - newSelector - ), 0 ) ); - return results; - } catch(qsaError) { - } finally { - if ( !old ) { - context.removeAttribute("id"); + if ( cur ) { + j = 0; + while ( (clazz = classes[j++]) ) { + // Remove *all* instances + while ( cur.indexOf( " " + clazz + " " ) >= 0 ) { + cur = cur.replace( " " + clazz + " ", " " ); + } } + elem.className = value ? jQuery.trim( cur ) : ""; } } } - } - - // All others - return select( selector.replace( rtrim, "$1" ), context, results, seed ); -} - -/** - * Detect xml - * @param {Element|Object} elem An element or a document - */ -isXML = Sizzle.isXML = function( elem ) { - // documentElement is verified for cases where it doesn't yet exist - // (such as loading iframes in IE - #4833) - var documentElement = elem && (elem.ownerDocument || elem).documentElement; - return documentElement ? documentElement.nodeName !== "HTML" : false; -}; - -/** - * Sets document-related variables once based on the current document - * @param {Element|Object} [doc] An element or document object to use to set the document - * @returns {Object} Returns the current document - */ -setDocument = Sizzle.setDocument = function( node ) { - var doc = node ? node.ownerDocument || node : preferredDoc; - - // If no document and documentElement is available, return - if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { - return document; - } - - // Set our document - document = doc; - docElem = doc.documentElement; - - // Support tests - documentIsXML = isXML( doc ); - // Check if getElementsByTagName("*") returns only elements - support.tagNameNoComments = assert(function( div ) { - div.appendChild( doc.createComment("") ); - return !div.getElementsByTagName("*").length; - }); + return this; + }, - // Check if attributes should be retrieved by attribute nodes - support.attributes = assert(function( div ) { - div.innerHTML = ""; - var type = typeof div.lastChild.getAttribute("multiple"); - // IE8 returns a string for some attributes even when not present - return type !== "boolean" && type !== "string"; - }); + toggleClass: function( value, stateVal ) { + var type = typeof value; - // Check if getElementsByClassName can be trusted - support.getByClassName = assert(function( div ) { - // Opera can't find a second classname (in 9.6) - div.innerHTML = ""; - if ( !div.getElementsByClassName || !div.getElementsByClassName("e").length ) { - return false; + if ( typeof stateVal === "boolean" && type === "string" ) { + return stateVal ? this.addClass( value ) : this.removeClass( value ); } - // Safari 3.2 caches class attributes and doesn't catch changes - div.lastChild.className = "e"; - return div.getElementsByClassName("e").length === 2; - }); - - // Check if getElementById returns elements by name - // Check if getElementsByName privileges form controls or returns elements by ID - support.getByName = assert(function( div ) { - // Inject content - div.id = expando + 0; - div.innerHTML = "
    "; - docElem.insertBefore( div, docElem.firstChild ); - - // Test - var pass = doc.getElementsByName && - // buggy browsers will return fewer than the correct 2 - doc.getElementsByName( expando ).length === 2 + - // buggy browsers will return more than the correct 0 - doc.getElementsByName( expando + 0 ).length; - support.getIdNotName = !doc.getElementById( expando ); - - // Cleanup - docElem.removeChild( div ); - - return pass; - }); - - // IE6/7 return modified attributes - Expr.attrHandle = assert(function( div ) { - div.innerHTML = ""; - return div.firstChild && typeof div.firstChild.getAttribute !== strundefined && - div.firstChild.getAttribute("href") === "#"; - }) ? - {} : - { - "href": function( elem ) { - return elem.getAttribute( "href", 2 ); - }, - "type": function( elem ) { - return elem.getAttribute("type"); - } - }; - - // ID find and filter - if ( support.getIdNotName ) { - Expr.find["ID"] = function( id, context ) { - if ( typeof context.getElementById !== strundefined && !documentIsXML ) { - var m = context.getElementById( id ); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - return m && m.parentNode ? [m] : []; - } - }; - Expr.filter["ID"] = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - return elem.getAttribute("id") === attrId; - }; - }; - } else { - Expr.find["ID"] = function( id, context ) { - if ( typeof context.getElementById !== strundefined && !documentIsXML ) { - var m = context.getElementById( id ); - - return m ? - m.id === id || typeof m.getAttributeNode !== strundefined && m.getAttributeNode("id").value === id ? - [m] : - undefined : - []; - } - }; - Expr.filter["ID"] = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); - return node && node.value === attrId; - }; - }; - } + if ( jQuery.isFunction( value ) ) { + return this.each(function( i ) { + jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); + }); + } - // Tag - Expr.find["TAG"] = support.tagNameNoComments ? - function( tag, context ) { - if ( typeof context.getElementsByTagName !== strundefined ) { - return context.getElementsByTagName( tag ); - } - } : - function( tag, context ) { - var elem, - tmp = [], - i = 0, - results = context.getElementsByTagName( tag ); + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, + i = 0, + self = jQuery( this ), + classNames = value.match( core_rnotwhite ) || []; - // Filter out possible comments - if ( tag === "*" ) { - while ( (elem = results[i++]) ) { - if ( elem.nodeType === 1 ) { - tmp.push( elem ); + while ( (className = classNames[ i++ ]) ) { + // check each className given, space separated list + if ( self.hasClass( className ) ) { + self.removeClass( className ); + } else { + self.addClass( className ); } } - return tmp; + // Toggle whole class name + } else if ( type === core_strundefined || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery._data( this, "__className__", this.className ); + } + + // If the element has a class name or if we're passed "false", + // then remove the whole classname (if there was one, the above saved it). + // Otherwise bring back whatever was previously saved (if anything), + // falling back to the empty string if nothing was stored. + this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; } - return results; - }; + }); + }, - // Name - Expr.find["NAME"] = support.getByName && function( tag, context ) { - if ( typeof context.getElementsByName !== strundefined ) { - return context.getElementsByName( name ); + hasClass: function( selector ) { + var className = " " + selector + " ", + i = 0, + l = this.length; + for ( ; i < l; i++ ) { + if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) { + return true; + } } - }; - // Class - Expr.find["CLASS"] = support.getByClassName && function( className, context ) { - if ( typeof context.getElementsByClassName !== strundefined && !documentIsXML ) { - return context.getElementsByClassName( className ); - } - }; + return false; + }, - // QSA and matchesSelector support + val: function( value ) { + var ret, hooks, isFunction, + elem = this[0]; - // matchesSelector(:active) reports false when true (IE9/Opera 11.5) - rbuggyMatches = []; + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; - // qSa(:focus) reports false when true (Chrome 21), - // no need to also add to buggyMatches since matches checks buggyQSA - // A support test would require too much code (would include document ready) - rbuggyQSA = [ ":focus" ]; + if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { + return ret; + } - if ( (support.qsa = isNative(doc.querySelectorAll)) ) { - // Build QSA regex - // Regex strategy adopted from Diego Perini - assert(function( div ) { - // Select is set to empty string on purpose - // This is to test IE's treatment of not explictly - // setting a boolean content attribute, - // since its presence should be enough - // http://bugs.jquery.com/ticket/12359 - div.innerHTML = ""; + ret = elem.value; - // IE8 - Some boolean attributes are not treated correctly - if ( !div.querySelectorAll("[selected]").length ) { - rbuggyQSA.push( "\\[" + whitespace + "*(?:checked|disabled|ismap|multiple|readonly|selected|value)" ); + return typeof ret === "string" ? + // handle most common string cases + ret.replace(rreturn, "") : + // handle cases where value is null/undef or number + ret == null ? "" : ret; } - // Webkit/Opera - :checked should return selected option elements - // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - // IE8 throws error here and will not see later tests - if ( !div.querySelectorAll(":checked").length ) { - rbuggyQSA.push(":checked"); - } - }); + return; + } - assert(function( div ) { + isFunction = jQuery.isFunction( value ); - // Opera 10-12/IE8 - ^= $= *= and empty values - // Should not select anything - div.innerHTML = ""; - if ( div.querySelectorAll("[i^='']").length ) { - rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:\"\"|'')" ); + return this.each(function( i ) { + var val; + + if ( this.nodeType !== 1 ) { + return; } - // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) - // IE8 throws error here and will not see later tests - if ( !div.querySelectorAll(":enabled").length ) { - rbuggyQSA.push( ":enabled", ":disabled" ); + if ( isFunction ) { + val = value.call( this, i, jQuery( this ).val() ); + } else { + val = value; } - // Opera 10-11 does not throw on post-comma invalid pseudos - div.querySelectorAll("*,:x"); - rbuggyQSA.push(",.*:"); + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray( val ) ) { + val = jQuery.map(val, function ( value ) { + return value == null ? "" : value + ""; + }); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } }); } +}); - if ( (support.matchesSelector = isNative( (matches = docElem.matchesSelector || - docElem.mozMatchesSelector || - docElem.webkitMatchesSelector || - docElem.oMatchesSelector || - docElem.msMatchesSelector) )) ) { +jQuery.extend({ + valHooks: { + option: { + get: function( elem ) { + // Use proper attribute retrieval(#6932, #12072) + var val = jQuery.find.attr( elem, "value" ); + return val != null ? + val : + elem.text; + } + }, + select: { + get: function( elem ) { + var value, option, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one" || index < 0, + values = one ? null : [], + max = one ? index + 1 : options.length, + i = index < 0 ? + max : + one ? index : 0; - assert(function( div ) { - // Check to see if it's possible to do matchesSelector - // on a disconnected node (IE 9) - support.disconnectedMatch = matches.call( div, "div" ); + // Loop through all the selected options + for ( ; i < max; i++ ) { + option = options[ i ]; - // This should fail with an exception - // Gecko does not error, returns false instead - matches.call( div, "[s!='']:x" ); - rbuggyMatches.push( "!=", pseudos ); - }); - } + // oldIE doesn't update selected after form reset (#2551) + if ( ( option.selected || i === index ) && + // Don't return options that are disabled or in a disabled optgroup + ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) && + ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { - rbuggyQSA = new RegExp( rbuggyQSA.join("|") ); - rbuggyMatches = new RegExp( rbuggyMatches.join("|") ); + // Get the specific value for the option + value = jQuery( option ).val(); - // Element contains another - // Purposefully does not implement inclusive descendent - // As in, an element does not contain itself - contains = isNative(docElem.contains) || docElem.compareDocumentPosition ? - function( a, b ) { - var adown = a.nodeType === 9 ? a.documentElement : a, - bup = b && b.parentNode; - return a === bup || !!( bup && bup.nodeType === 1 && ( - adown.contains ? - adown.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - )); - } : - function( a, b ) { - if ( b ) { - while ( (b = b.parentNode) ) { - if ( b === a ) { - return true; + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); } } - } - return false; - }; - // Document order sorting - sortOrder = docElem.compareDocumentPosition ? - function( a, b ) { - var compare; + return values; + }, - if ( a === b ) { - hasDuplicate = true; - return 0; - } + set: function( elem, value ) { + var optionSet, option, + options = elem.options, + values = jQuery.makeArray( value ), + i = options.length; - if ( (compare = b.compareDocumentPosition && a.compareDocumentPosition && a.compareDocumentPosition( b )) ) { - if ( compare & 1 || a.parentNode && a.parentNode.nodeType === 11 ) { - if ( a === doc || contains( preferredDoc, a ) ) { - return -1; + while ( i-- ) { + option = options[ i ]; + if ( (option.selected = jQuery.inArray( jQuery(option).val(), values ) >= 0) ) { + optionSet = true; + } } - if ( b === doc || contains( preferredDoc, b ) ) { - return 1; + + // force browsers to behave consistently when non-matching value is set + if ( !optionSet ) { + elem.selectedIndex = -1; } - return 0; + return values; } - return compare & 4 ? -1 : 1; } + }, - return a.compareDocumentPosition ? -1 : 1; - } : - function( a, b ) { - var cur, + attr: function( elem, name, value ) { + var hooks, ret, + nType = elem.nodeType; + + // don't get/set attributes on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === core_strundefined ) { + return jQuery.prop( elem, name, value ); + } + + // All attributes are lowercase + // Grab necessary hook if one is defined + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + name = name.toLowerCase(); + hooks = jQuery.attrHooks[ name ] || + ( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook ); + } + + if ( value !== undefined ) { + + if ( value === null ) { + jQuery.removeAttr( elem, name ); + + } else if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + elem.setAttribute( name, value + "" ); + return value; + } + + } else if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + ret = jQuery.find.attr( elem, name ); + + // Non-existent attributes return null, we normalize to undefined + return ret == null ? + undefined : + ret; + } + }, + + removeAttr: function( elem, value ) { + var name, propName, i = 0, - aup = a.parentNode, - bup = b.parentNode, - ap = [ a ], - bp = [ b ]; + attrNames = value && value.match( core_rnotwhite ); - // Exit early if the nodes are identical - if ( a === b ) { - hasDuplicate = true; - return 0; + if ( attrNames && elem.nodeType === 1 ) { + while ( (name = attrNames[i++]) ) { + propName = jQuery.propFix[ name ] || name; - // Parentless nodes are either documents or disconnected - } else if ( !aup || !bup ) { - return a === doc ? -1 : - b === doc ? 1 : - aup ? -1 : - bup ? 1 : - 0; + // Boolean attributes get special treatment (#10870) + if ( jQuery.expr.match.bool.test( name ) ) { + // Set corresponding property to false + if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { + elem[ propName ] = false; + // Support: IE<9 + // Also clear defaultChecked/defaultSelected (if appropriate) + } else { + elem[ jQuery.camelCase( "default-" + name ) ] = + elem[ propName ] = false; + } - // If the nodes are siblings, we can do a quick check - } else if ( aup === bup ) { - return siblingCheck( a, b ); - } + // See #9699 for explanation of this approach (setting first, then removal) + } else { + jQuery.attr( elem, name, "" ); + } - // Otherwise we need full lists of their ancestors for comparison - cur = a; - while ( (cur = cur.parentNode) ) { - ap.unshift( cur ); - } - cur = b; - while ( (cur = cur.parentNode) ) { - bp.unshift( cur ); + elem.removeAttribute( getSetAttribute ? name : propName ); + } } + }, - // Walk down the tree looking for a discrepancy - while ( ap[i] === bp[i] ) { - i++; + attrHooks: { + type: { + set: function( elem, value ) { + if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { + // Setting the type on a radio button after the value resets the value in IE6-9 + // Reset value to default in case type is set after value during creation + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } } + }, - return i ? - // Do a sibling check if the nodes have a common ancestor - siblingCheck( ap[i], bp[i] ) : + propFix: { + "for": "htmlFor", + "class": "className" + }, - // Otherwise nodes in our document sort first - ap[i] === preferredDoc ? -1 : - bp[i] === preferredDoc ? 1 : - 0; - }; + prop: function( elem, name, value ) { + var ret, hooks, notxml, + nType = elem.nodeType; - // Always assume the presence of duplicates if sort doesn't - // pass them to our comparison function (as in Google Chrome). - hasDuplicate = false; - [0, 0].sort( sortOrder ); - support.detectDuplicates = hasDuplicate; + // don't get/set properties on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } - return document; -}; + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); -Sizzle.matches = function( expr, elements ) { - return Sizzle( expr, null, null, elements ); -}; + if ( notxml ) { + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } -Sizzle.matchesSelector = function( elem, expr ) { - // Set document vars if needed - if ( ( elem.ownerDocument || elem ) !== document ) { - setDocument( elem ); - } + if ( value !== undefined ) { + return hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ? + ret : + ( elem[ name ] = value ); - // Make sure that attribute selectors are quoted - expr = expr.replace( rattributeQuotes, "='$1']" ); + } else { + return hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ? + ret : + elem[ name ]; + } + }, - // rbuggyQSA always contains :focus, so no need for an existence check - if ( support.matchesSelector && !documentIsXML && (!rbuggyMatches || !rbuggyMatches.test(expr)) && !rbuggyQSA.test(expr) ) { - try { - var ret = matches.call( elem, expr ); + propHooks: { + tabIndex: { + get: function( elem ) { + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + // Use proper attribute retrieval(#12072) + var tabindex = jQuery.find.attr( elem, "tabindex" ); - // IE 9's matchesSelector returns false on disconnected nodes - if ( ret || support.disconnectedMatch || - // As well, disconnected nodes are said to be in a document - // fragment in IE 9 - elem.document && elem.document.nodeType !== 11 ) { - return ret; + return tabindex ? + parseInt( tabindex, 10 ) : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + -1; } - } catch(e) {} - } - - return Sizzle( expr, document, null, [elem] ).length > 0; -}; - -Sizzle.contains = function( context, elem ) { - // Set document vars if needed - if ( ( context.ownerDocument || context ) !== document ) { - setDocument( context ); + } } - return contains( context, elem ); -}; +}); -Sizzle.attr = function( elem, name ) { - var val; +// Hooks for boolean attributes +boolHook = { + set: function( elem, value, name ) { + if ( value === false ) { + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { + // IE<8 needs the *property* name + elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name ); - // Set document vars if needed - if ( ( elem.ownerDocument || elem ) !== document ) { - setDocument( elem ); - } + // Use defaultChecked and defaultSelected for oldIE + } else { + elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true; + } - if ( !documentIsXML ) { - name = name.toLowerCase(); - } - if ( (val = Expr.attrHandle[ name ]) ) { - return val( elem ); - } - if ( documentIsXML || support.attributes ) { - return elem.getAttribute( name ); + return name; } - return ( (val = elem.getAttributeNode( name )) || elem.getAttribute( name ) ) && elem[ name ] === true ? - name : - val && val.specified ? val.value : null; -}; - -Sizzle.error = function( msg ) { - throw new Error( "Syntax error, unrecognized expression: " + msg ); }; +jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) { + var getter = jQuery.expr.attrHandle[ name ] || jQuery.find.attr; + + jQuery.expr.attrHandle[ name ] = getSetInput && getSetAttribute || !ruseDefault.test( name ) ? + function( elem, name, isXML ) { + var fn = jQuery.expr.attrHandle[ name ], + ret = isXML ? + undefined : + /* jshint eqeqeq: false */ + (jQuery.expr.attrHandle[ name ] = undefined) != + getter( elem, name, isXML ) ? + + name.toLowerCase() : + null; + jQuery.expr.attrHandle[ name ] = fn; + return ret; + } : + function( elem, name, isXML ) { + return isXML ? + undefined : + elem[ jQuery.camelCase( "default-" + name ) ] ? + name.toLowerCase() : + null; + }; +}); -// Document sorting and removing duplicates -Sizzle.uniqueSort = function( results ) { - var elem, - duplicates = [], - i = 1, - j = 0; - - // Unless we *know* we can detect duplicates, assume their presence - hasDuplicate = !support.detectDuplicates; - results.sort( sortOrder ); - - if ( hasDuplicate ) { - for ( ; (elem = results[i]); i++ ) { - if ( elem === results[ i - 1 ] ) { - j = duplicates.push( i ); +// fix oldIE attroperties +if ( !getSetInput || !getSetAttribute ) { + jQuery.attrHooks.value = { + set: function( elem, value, name ) { + if ( jQuery.nodeName( elem, "input" ) ) { + // Does not return so that setAttribute is also used + elem.defaultValue = value; + } else { + // Use nodeHook if defined (#1954); otherwise setAttribute is fine + return nodeHook && nodeHook.set( elem, value, name ); } } - while ( j-- ) { - results.splice( duplicates[ j ], 1 ); - } - } + }; +} - return results; -}; +// IE6/7 do not support getting/setting some attributes with get/setAttribute +if ( !getSetAttribute ) { -function siblingCheck( a, b ) { - var cur = b && a, - diff = cur && ( ~b.sourceIndex || MAX_NEGATIVE ) - ( ~a.sourceIndex || MAX_NEGATIVE ); + // Use this for any attribute in IE6/7 + // This fixes almost every IE6/7 issue + nodeHook = { + set: function( elem, value, name ) { + // Set the existing or create a new attribute node + var ret = elem.getAttributeNode( name ); + if ( !ret ) { + elem.setAttributeNode( + (ret = elem.ownerDocument.createAttribute( name )) + ); + } - // Use IE sourceIndex if available on both nodes - if ( diff ) { - return diff; - } + ret.value = value += ""; - // Check if b follows a - if ( cur ) { - while ( (cur = cur.nextSibling) ) { - if ( cur === b ) { - return -1; - } + // Break association with cloned elements by also using setAttribute (#9646) + return name === "value" || value === elem.getAttribute( name ) ? + value : + undefined; } - } + }; + jQuery.expr.attrHandle.id = jQuery.expr.attrHandle.name = jQuery.expr.attrHandle.coords = + // Some attributes are constructed with empty-string values when not defined + function( elem, name, isXML ) { + var ret; + return isXML ? + undefined : + (ret = elem.getAttributeNode( name )) && ret.value !== "" ? + ret.value : + null; + }; + jQuery.valHooks.button = { + get: function( elem, name ) { + var ret = elem.getAttributeNode( name ); + return ret && ret.specified ? + ret.value : + undefined; + }, + set: nodeHook.set + }; - return a ? 1 : -1; + // Set contenteditable to false on removals(#10429) + // Setting to empty string throws an error as an invalid value + jQuery.attrHooks.contenteditable = { + set: function( elem, value, name ) { + nodeHook.set( elem, value === "" ? false : value, name ); + } + }; + + // Set width and height to auto instead of 0 on empty string( Bug #8150 ) + // This is for removals + jQuery.each([ "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = { + set: function( elem, value ) { + if ( value === "" ) { + elem.setAttribute( name, "auto" ); + return value; + } + } + }; + }); } -// Returns a function to use in pseudos for input types -function createInputPseudo( type ) { - return function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === type; - }; + +// Some attributes require a special call on IE +// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !jQuery.support.hrefNormalized ) { + // href/src property should get the full normalized URL (#10299/#12915) + jQuery.each([ "href", "src" ], function( i, name ) { + jQuery.propHooks[ name ] = { + get: function( elem ) { + return elem.getAttribute( name, 4 ); + } + }; + }); } -// Returns a function to use in pseudos for buttons -function createButtonPseudo( type ) { - return function( elem ) { - var name = elem.nodeName.toLowerCase(); - return (name === "input" || name === "button") && elem.type === type; +if ( !jQuery.support.style ) { + jQuery.attrHooks.style = { + get: function( elem ) { + // Return undefined in the case of empty string + // Note: IE uppercases css property names, but if we were to .toLowerCase() + // .cssText, that would destroy case senstitivity in URL's, like in "background" + return elem.style.cssText || undefined; + }, + set: function( elem, value ) { + return ( elem.style.cssText = value + "" ); + } }; } -// Returns a function to use in pseudos for positionals -function createPositionalPseudo( fn ) { - return markFunction(function( argument ) { - argument = +argument; - return markFunction(function( seed, matches ) { - var j, - matchIndexes = fn( [], seed.length, argument ), - i = matchIndexes.length; +// Safari mis-reports the default selected property of an option +// Accessing the parent's selectedIndex property fixes it +if ( !jQuery.support.optSelected ) { + jQuery.propHooks.selected = { + get: function( elem ) { + var parent = elem.parentNode; - // Match elements found at the specified indexes - while ( i-- ) { - if ( seed[ (j = matchIndexes[i]) ] ) { - seed[j] = !(matches[j] = seed[j]); + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; } } - }); - }); + return null; + } + }; } -/** - * Utility function for retrieving the text value of an array of DOM nodes - * @param {Array|Element} elem - */ -getText = Sizzle.getText = function( elem ) { - var node, - ret = "", - i = 0, - nodeType = elem.nodeType; +jQuery.each([ + "tabIndex", + "readOnly", + "maxLength", + "cellSpacing", + "cellPadding", + "rowSpan", + "colSpan", + "useMap", + "frameBorder", + "contentEditable" +], function() { + jQuery.propFix[ this.toLowerCase() ] = this; +}); - if ( !nodeType ) { - // If no nodeType, this is expected to be an array - for ( ; (node = elem[i]); i++ ) { - // Do not traverse comment nodes - ret += getText( node ); - } - } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { - // Use textContent for elements - // innerText usage removed for consistency of new lines (see #11153) - if ( typeof elem.textContent === "string" ) { - return elem.textContent; - } else { - // Traverse its children - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - ret += getText( elem ); +// IE6/7 call enctype encoding +if ( !jQuery.support.enctype ) { + jQuery.propFix.enctype = "encoding"; +} + +// Radios and checkboxes getter/setter +jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + set: function( elem, value ) { + if ( jQuery.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); } } - } else if ( nodeType === 3 || nodeType === 4 ) { - return elem.nodeValue; + }; + if ( !jQuery.support.checkOn ) { + jQuery.valHooks[ this ].get = function( elem ) { + // Support: Webkit + // "" is returned instead of "on" if a value isn't specified + return elem.getAttribute("value") === null ? "on" : elem.value; + }; } - // Do not include comment or processing instruction nodes +}); +var rformElems = /^(?:input|select|textarea)$/i, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; - return ret; -}; +function returnTrue() { + return true; +} -Expr = Sizzle.selectors = { +function returnFalse() { + return false; +} - // Can be adjusted by the user - cacheLength: 50, +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} - createPseudo: markFunction, +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { - match: matchExpr, + global: {}, - find: {}, + add: function( elem, types, handler, data, selector ) { + var tmp, events, t, handleObjIn, + special, eventHandle, handleObj, + handlers, type, namespaces, origType, + elemData = jQuery._data( elem ); - relative: { - ">": { dir: "parentNode", first: true }, - " ": { dir: "parentNode" }, - "+": { dir: "previousSibling", first: true }, - "~": { dir: "previousSibling" } - }, + // Don't attach events to noData or text/comment nodes (but allow plain objects) + if ( !elemData ) { + return; + } - preFilter: { - "ATTR": function( match ) { - match[1] = match[1].replace( runescape, funescape ); + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } - // Move the given value to match[3] whether quoted or unquoted - match[3] = ( match[4] || match[5] || "" ).replace( runescape, funescape ); + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } - if ( match[2] === "~=" ) { - match[3] = " " + match[3] + " "; - } + // Init the element's event structure and main handler, if this is the first + if ( !(events = elemData.events) ) { + events = elemData.events = {}; + } + if ( !(eventHandle = elemData.handle) ) { + eventHandle = elemData.handle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ? + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : + undefined; + }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; + } - return match.slice( 0, 4 ); - }, + // Handle multiple events separated by a space + types = ( types || "" ).match( core_rnotwhite ) || [""]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); - "CHILD": function( match ) { - /* matches from matchExpr["CHILD"] - 1 type (only|nth|...) - 2 what (child|of-type) - 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) - 4 xn-component of xn+y argument ([+-]?\d*n|) - 5 sign of xn-component - 6 x of xn-component - 7 sign of y-component - 8 y of y-component - */ - match[1] = match[1].toLowerCase(); + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } - if ( match[1].slice( 0, 3 ) === "nth" ) { - // nth-* requires argument - if ( !match[3] ) { - Sizzle.error( match[0] ); - } + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; - // numeric x and y parameters for Expr.filter.CHILD - // remember that false/true cast respectively to 0/1 - match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); - match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; - // other types prohibit arguments - } else if ( match[3] ) { - Sizzle.error( match[0] ); - } + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; - return match; - }, + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); - "PSEUDO": function( match ) { - var excess, - unquoted = !match[5] && match[2]; + // Init the event handler queue if we're the first + if ( !(handlers = events[ type ]) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; - if ( matchExpr["CHILD"].test( match[0] ) ) { - return null; + // Only use addEventListener/attachEvent if the special events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } } - // Accept quoted arguments as-is - if ( match[4] ) { - match[2] = match[4]; + if ( special.add ) { + special.add.call( elem, handleObj ); - // Strip excess characters from unquoted arguments - } else if ( unquoted && rpseudo.test( unquoted ) && - // Get excess from tokenize (recursively) - (excess = tokenize( unquoted, true )) && - // advance to the next closing parenthesis - (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } - // excess is a negative index - match[0] = match[0].slice( 0, excess ); - match[2] = unquoted.slice( 0, excess ); + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); } - // Return only captures needed by the pseudo filter method (type and argument) - return match.slice( 0, 3 ); + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; } + + // Nullify elem to prevent memory leaks in IE + elem = null; }, - filter: { + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + var j, handleObj, tmp, + origCount, t, events, + special, handlers, type, + namespaces, origType, + elemData = jQuery.hasData( elem ) && jQuery._data( elem ); - "TAG": function( nodeName ) { - if ( nodeName === "*" ) { - return function() { return true; }; - } + if ( !elemData || !(events = elemData.events) ) { + return; + } - nodeName = nodeName.replace( runescape, funescape ).toLowerCase(); - return function( elem ) { - return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; - }; - }, + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( core_rnotwhite ) || [""]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); - "CLASS": function( className ) { - var pattern = classCache[ className + " " ]; + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } - return pattern || - (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && - classCache( className, function( elem ) { - return pattern.test( elem.className || (typeof elem.getAttribute !== strundefined && elem.getAttribute("class")) || "" ); - }); - }, + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); - "ATTR": function( name, operator, check ) { - return function( elem ) { - var result = Sizzle.attr( elem, name ); + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; - if ( result == null ) { - return operator === "!="; - } - if ( !operator ) { - return true; - } + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); - result += ""; + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } - return operator === "=" ? result === check : - operator === "!=" ? result !== check : - operator === "^=" ? check && result.indexOf( check ) === 0 : - operator === "*=" ? check && result.indexOf( check ) > -1 : - operator === "$=" ? check && result.slice( -check.length ) === check : - operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : - operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : - false; - }; - }, + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } - "CHILD": function( type, what, argument, first, last ) { - var simple = type.slice( 0, 3 ) !== "nth", - forward = type.slice( -4 ) !== "last", - ofType = what === "of-type"; + delete events[ type ]; + } + } - return first === 1 && last === 0 ? + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + delete elemData.handle; - // Shortcut for :nth-*(n) - function( elem ) { - return !!elem.parentNode; - } : + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery._removeData( elem, "events" ); + } + }, - function( elem, context, xml ) { - var cache, outerCache, node, diff, nodeIndex, start, - dir = simple !== forward ? "nextSibling" : "previousSibling", - parent = elem.parentNode, - name = ofType && elem.nodeName.toLowerCase(), - useCache = !xml && !ofType; + trigger: function( event, data, elem, onlyHandlers ) { + var handle, ontype, cur, + bubbleType, special, tmp, i, + eventPath = [ elem || document ], + type = core_hasOwn.call( event, "type" ) ? event.type : event, + namespaces = core_hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; - if ( parent ) { + cur = tmp = elem = elem || document; - // :(first|last|only)-(child|of-type) - if ( simple ) { - while ( dir ) { - node = elem; - while ( (node = node[ dir ]) ) { - if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { - return false; - } - } - // Reverse direction for :only-* (if we haven't yet done so) - start = dir = type === "only" && !start && "nextSibling"; - } - return true; - } + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } - start = [ forward ? parent.firstChild : parent.lastChild ]; + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } - // non-xml :nth-child(...) stores cache data on `parent` - if ( forward && useCache ) { - // Seek `elem` from a previously-cached index - outerCache = parent[ expando ] || (parent[ expando ] = {}); - cache = outerCache[ type ] || []; - nodeIndex = cache[0] === dirruns && cache[1]; - diff = cache[0] === dirruns && cache[2]; - node = nodeIndex && parent.childNodes[ nodeIndex ]; + if ( type.indexOf(".") >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf(":") < 0 && "on" + type; - while ( (node = ++nodeIndex && node && node[ dir ] || + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); - // Fallback to seeking `elem` from the start - (diff = nodeIndex = 0) || start.pop()) ) { + // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) + event.isTrigger = onlyHandlers ? 2 : 3; + event.namespace = namespaces.join("."); + event.namespace_re = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : + null; - // When found, cache indexes on `parent` and break - if ( node.nodeType === 1 && ++diff && node === elem ) { - outerCache[ type ] = [ dirruns, nodeIndex, diff ]; - break; - } - } + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } - // Use previously-cached element index if available - } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) { - diff = cache[1]; + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); - // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) - } else { - // Use the same loop as above to seek `elem` from the start - while ( (node = ++nodeIndex && node && node[ dir ] || - (diff = nodeIndex = 0) || start.pop()) ) { + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } - if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { - // Cache the index of each encountered element - if ( useCache ) { - (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; - } + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { - if ( node === elem ) { - break; - } - } - } - } + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } - // Incorporate the offset, then check against cycle size - diff -= last; - return diff === first || ( diff % first === 0 && diff / first >= 0 ); - } - }; - }, + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === (elem.ownerDocument || document) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } - "PSEUDO": function( pseudo, argument ) { - // pseudo-class names are case-insensitive - // http://www.w3.org/TR/selectors/#pseudo-classes - // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters - // Remember that setFilters inherits from pseudos - var args, - fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || - Sizzle.error( "unsupported pseudo: " + pseudo ); + // Fire handlers on the event path + i = 0; + while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { - // The user may use createPseudo to indicate that - // arguments are needed to create the filter function - // just as Sizzle does - if ( fn[ expando ] ) { - return fn( argument ); - } + event.type = i > 1 ? + bubbleType : + special.bindType || type; - // But maintain support for old signatures - if ( fn.length > 1 ) { - args = [ pseudo, pseudo, "", argument ]; - return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? - markFunction(function( seed, matches ) { - var idx, - matched = fn( seed, argument ), - i = matched.length; - while ( i-- ) { - idx = indexOf.call( seed, matched[i] ); - seed[ idx ] = !( matches[ idx ] = matched[i] ); - } - }) : - function( elem ) { - return fn( elem, 0, args ); - }; + // jQuery handler + handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); } - return fn; + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) { + event.preventDefault(); + } } - }, + event.type = type; - pseudos: { - // Potentially complex pseudos - "not": markFunction(function( selector ) { - // Trim the selector passed to compile - // to avoid treating leading and trailing - // spaces as combinators - var input = [], - results = [], - matcher = compile( selector.replace( rtrim, "$1" ) ); + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { - return matcher[ expando ] ? - markFunction(function( seed, matches, context, xml ) { - var elem, - unmatched = matcher( seed, null, xml, [] ), - i = seed.length; + if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) && + jQuery.acceptData( elem ) ) { - // Match elements unmatched by `matcher` - while ( i-- ) { - if ( (elem = unmatched[i]) ) { - seed[i] = !(matches[i] = elem); - } - } - }) : - function( elem, context, xml ) { - input[0] = elem; - matcher( input, null, xml, results ); - return !results.pop(); - }; - }), + // Call a native DOM method on the target with the same name name as the event. + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) { - "has": markFunction(function( selector ) { - return function( elem ) { - return Sizzle( selector, elem ).length > 0; - }; - }), + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; - "contains": markFunction(function( text ) { - return function( elem ) { - return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; - }; - }), + if ( tmp ) { + elem[ ontype ] = null; + } - // "Whether an element is represented by a :lang() selector - // is based solely on the element's language value - // being equal to the identifier C, - // or beginning with the identifier C immediately followed by "-". - // The matching of C against the element's language value is performed case-insensitively. - // The identifier C does not have to be a valid language name." - // http://www.w3.org/TR/selectors/#lang-pseudo - "lang": markFunction( function( lang ) { - // lang value must be a valid identifider - if ( !ridentifier.test(lang || "") ) { - Sizzle.error( "unsupported lang: " + lang ); - } - lang = lang.replace( runescape, funescape ).toLowerCase(); - return function( elem ) { - var elemLang; - do { - if ( (elemLang = documentIsXML ? - elem.getAttribute("xml:lang") || elem.getAttribute("lang") : - elem.lang) ) { + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + try { + elem[ type ](); + } catch ( e ) { + // IE<9 dies on focus/blur to hidden element (#1486,#12518) + // only reproducible on winXP IE8 native, not IE9 in IE8 mode + } + jQuery.event.triggered = undefined; - elemLang = elemLang.toLowerCase(); - return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + if ( tmp ) { + elem[ ontype ] = tmp; } - } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); - return false; - }; - }), + } + } + } - // Miscellaneous - "target": function( elem ) { - var hash = window.location && window.location.hash; - return hash && hash.slice( 1 ) === elem.id; - }, + return event.result; + }, - "root": function( elem ) { - return elem === docElem; - }, + dispatch: function( event ) { - "focus": function( elem ) { - return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); - }, + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event ); - // Boolean properties - "enabled": function( elem ) { - return elem.disabled === false; - }, + var i, ret, handleObj, matched, j, + handlerQueue = [], + args = core_slice.call( arguments ), + handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; - "disabled": function( elem ) { - return elem.disabled === true; - }, + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; - "checked": function( elem ) { - // In CSS3, :checked should return both checked and selected elements - // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - var nodeName = elem.nodeName.toLowerCase(); - return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); - }, + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } - "selected": function( elem ) { - // Accessing this property makes selected-by-default - // options in Safari work properly - if ( elem.parentNode ) { - elem.parentNode.selectedIndex; - } + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); - return elem.selected === true; - }, + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; - // Contents - "empty": function( elem ) { - // http://www.w3.org/TR/selectors/#empty-pseudo - // :empty is only affected by element nodes and content nodes(including text(3), cdata(4)), - // not comment, processing instructions, or others - // Thanks to Diego Perini for the nodeName shortcut - // Greater than "@" means alpha characters (specifically not starting with "#" or "?") - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - if ( elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4 ) { - return false; - } - } - return true; - }, + j = 0; + while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { - "parent": function( elem ) { - return !Expr.pseudos["empty"]( elem ); - }, + // Triggered event must either 1) have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { - // Element/input types - "header": function( elem ) { - return rheader.test( elem.nodeName ); - }, + event.handleObj = handleObj; + event.data = handleObj.data; - "input": function( elem ) { - return rinputs.test( elem.nodeName ); - }, + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); - "button": function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === "button" || name === "button"; - }, + if ( ret !== undefined ) { + if ( (event.result = ret) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } - "text": function( elem ) { - var attr; - // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) - // use getAttribute instead to test this case - return elem.nodeName.toLowerCase() === "input" && - elem.type === "text" && - ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === elem.type ); - }, + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } - // Position-in-collection - "first": createPositionalPseudo(function() { - return [ 0 ]; - }), + return event.result; + }, - "last": createPositionalPseudo(function( matchIndexes, length ) { - return [ length - 1 ]; - }), + handlers: function( event, handlers ) { + var sel, handleObj, matches, i, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; - "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { - return [ argument < 0 ? argument + length : argument ]; - }), + // Find delegate handlers + // Black-hole SVG instance trees (#13180) + // Avoid non-left-click bubbling in Firefox (#3861) + if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { - "even": createPositionalPseudo(function( matchIndexes, length ) { - var i = 0; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), + /* jshint eqeqeq: false */ + for ( ; cur != this; cur = cur.parentNode || this ) { + /* jshint eqeqeq: true */ + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) { + matches = []; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; - "odd": createPositionalPseudo(function( matchIndexes, length ) { - var i = 1; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; - "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; - for ( ; --i >= 0; ) { - matchIndexes.push( i ); + if ( matches[ sel ] === undefined ) { + matches[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) >= 0 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matches[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, handlers: matches }); + } + } } - return matchIndexes; - }), + } - "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; - for ( ; ++i < length; ) { - matchIndexes.push( i ); - } - return matchIndexes; - }) - } -}; + // Add the remaining (directly-bound) handlers + if ( delegateCount < handlers.length ) { + handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); + } -// Add button/input type pseudos -for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { - Expr.pseudos[ i ] = createInputPseudo( i ); -} -for ( i in { submit: true, reset: true } ) { - Expr.pseudos[ i ] = createButtonPseudo( i ); -} + return handlerQueue; + }, -function tokenize( selector, parseOnly ) { - var matched, match, tokens, type, - soFar, groups, preFilters, - cached = tokenCache[ selector + " " ]; + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } - if ( cached ) { - return parseOnly ? 0 : cached.slice( 0 ); - } + // Create a writable copy of the event object and normalize some properties + var i, prop, copy, + type = event.type, + originalEvent = event, + fixHook = this.fixHooks[ type ]; - soFar = selector; - groups = []; - preFilters = Expr.preFilter; + if ( !fixHook ) { + this.fixHooks[ type ] = fixHook = + rmouseEvent.test( type ) ? this.mouseHooks : + rkeyEvent.test( type ) ? this.keyHooks : + {}; + } + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; - while ( soFar ) { + event = new jQuery.Event( originalEvent ); - // Comma and first run - if ( !matched || (match = rcomma.exec( soFar )) ) { - if ( match ) { - // Don't consume trailing commas as valid - soFar = soFar.slice( match[0].length ) || soFar; - } - groups.push( tokens = [] ); + i = copy.length; + while ( i-- ) { + prop = copy[ i ]; + event[ prop ] = originalEvent[ prop ]; } - matched = false; + // Support: IE<9 + // Fix target property (#1925) + if ( !event.target ) { + event.target = originalEvent.srcElement || document; + } - // Combinators - if ( (match = rcombinators.exec( soFar )) ) { - matched = match.shift(); - tokens.push( { - value: matched, - // Cast descendant combinators to space - type: match[0].replace( rtrim, " " ) - } ); - soFar = soFar.slice( matched.length ); + // Support: Chrome 23+, Safari? + // Target should not be a text node (#504, #13143) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; } - // Filters - for ( type in Expr.filter ) { - if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || - (match = preFilters[ type ]( match ))) ) { - matched = match.shift(); - tokens.push( { - value: matched, - type: type, - matches: match - } ); - soFar = soFar.slice( matched.length ); + // Support: IE<9 + // For mouse/key events, metaKey==false if it's undefined (#3368, #11328) + event.metaKey = !!event.metaKey; + + return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; } - } - if ( !matched ) { - break; + return event; } - } + }, - // Return the length of the invalid excess - // if we're just parsing - // Otherwise, throw an error or return tokens - return parseOnly ? - soFar.length : - soFar ? - Sizzle.error( selector ) : - // Cache the tokens - tokenCache( selector, groups ).slice( 0 ); -} + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var body, eventDoc, doc, + button = original.button, + fromElement = original.fromElement; -function toSelector( tokens ) { - var i = 0, - len = tokens.length, - selector = ""; - for ( ; i < len; i++ ) { - selector += tokens[i].value; - } - return selector; -} + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; -function addCombinator( matcher, combinator, base ) { - var dir = combinator.dir, - checkNonElements = base && dir === "parentNode", - doneName = done++; + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } - return combinator.first ? - // Check against closest ancestor/preceding element - function( elem, context, xml ) { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - return matcher( elem, context, xml ); - } + // Add relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; } - } : - // Check against all ancestor/preceding elements - function( elem, context, xml ) { - var data, cache, outerCache, - dirkey = dirruns + " " + doneName; + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } - // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching - if ( xml ) { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - if ( matcher( elem, context, xml ) ) { - return true; - } - } - } - } else { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - outerCache = elem[ expando ] || (elem[ expando ] = {}); - if ( (cache = outerCache[ dir ]) && cache[0] === dirkey ) { - if ( (data = cache[1]) === true || data === cachedruns ) { - return data === true; - } - } else { - cache = outerCache[ dir ] = [ dirkey ]; - cache[1] = matcher( elem, context, xml ) || cachedruns; - if ( cache[1] === true ) { - return true; - } - } + return event; + } + }, + + special: { + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + focus: { + // Fire native event if possible so blur/focus sequence is correct + trigger: function() { + if ( this !== safeActiveElement() && this.focus ) { + try { + this.focus(); + return false; + } catch ( e ) { + // Support: IE<9 + // If we error on focus to hidden element (#1486, #12518), + // let .trigger() run the handlers } } - } - }; -} - -function elementMatcher( matchers ) { - return matchers.length > 1 ? - function( elem, context, xml ) { - var i = matchers.length; - while ( i-- ) { - if ( !matchers[i]( elem, context, xml ) ) { + }, + delegateType: "focusin" + }, + blur: { + trigger: function() { + if ( this === safeActiveElement() && this.blur ) { + this.blur(); + return false; + } + }, + delegateType: "focusout" + }, + click: { + // For checkbox, fire native event so checked state will be right + trigger: function() { + if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) { + this.click(); return false; } + }, + + // For cross-browser consistency, don't fire native .click() on links + _default: function( event ) { + return jQuery.nodeName( event.target, "a" ); } - return true; - } : - matchers[0]; -} + }, -function condense( unmatched, map, filter, context, xml ) { - var elem, - newUnmatched = [], - i = 0, - len = unmatched.length, - mapped = map != null; + beforeunload: { + postDispatch: function( event ) { - for ( ; i < len; i++ ) { - if ( (elem = unmatched[i]) ) { - if ( !filter || filter( elem, context, xml ) ) { - newUnmatched.push( elem ); - if ( mapped ) { - map.push( i ); + // Even when returnValue equals to undefined Firefox will still show alert + if ( event.result !== undefined ) { + event.originalEvent.returnValue = event.result; } } } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { + type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } } +}; - return newUnmatched; -} +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + var name = "on" + type; -function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { - if ( postFilter && !postFilter[ expando ] ) { - postFilter = setMatcher( postFilter ); - } - if ( postFinder && !postFinder[ expando ] ) { - postFinder = setMatcher( postFinder, postSelector ); - } - return markFunction(function( seed, results, context, xml ) { - var temp, i, elem, - preMap = [], - postMap = [], - preexisting = results.length, + if ( elem.detachEvent ) { - // Get initial elements from seed or context - elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), + // #8545, #7054, preventing memory leaks for custom events in IE6-8 + // detachEvent needed property on element, by name of that event, to properly expose it to GC + if ( typeof elem[ name ] === core_strundefined ) { + elem[ name ] = null; + } - // Prefilter to get matcher input, preserving a map for seed-results synchronization - matcherIn = preFilter && ( seed || !selector ) ? - condense( elems, preMap, preFilter, context, xml ) : - elems, + elem.detachEvent( name, handle ); + } + }; - matcherOut = matcher ? - // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, - postFinder || ( seed ? preFilter : preexisting || postFilter ) ? +jQuery.Event = function( src, props ) { + // Allow instantiation without the 'new' keyword + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); + } - // ...intermediate processing is necessary - [] : + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; - // ...otherwise use results directly - results : - matcherIn; + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || + src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; - // Find primary matches - if ( matcher ) { - matcher( matcherIn, matcherOut, context, xml ); - } + // Event type + } else { + this.type = src; + } - // Apply postFilter - if ( postFilter ) { - temp = condense( matcherOut, postMap ); - postFilter( temp, [], context, xml ); + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } - // Un-match failing elements by moving them back to matcherIn - i = temp.length; - while ( i-- ) { - if ( (elem = temp[i]) ) { - matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); - } - } - } + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); - if ( seed ) { - if ( postFinder || preFilter ) { - if ( postFinder ) { - // Get the final matcherOut by condensing this intermediate into postFinder contexts - temp = []; - i = matcherOut.length; - while ( i-- ) { - if ( (elem = matcherOut[i]) ) { - // Restore matcherIn since elem is not yet a final match - temp.push( (matcherIn[i] = elem) ); - } - } - postFinder( null, (matcherOut = []), temp, xml ); - } + // Mark it as fixed + this[ jQuery.expando ] = true; +}; - // Move matched elements from seed to results to keep them synchronized - i = matcherOut.length; - while ( i-- ) { - if ( (elem = matcherOut[i]) && - (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, - seed[temp] = !(results[temp] = elem); - } - } - } + preventDefault: function() { + var e = this.originalEvent; - // Add elements to results, through postFinder if defined - } else { - matcherOut = condense( - matcherOut === results ? - matcherOut.splice( preexisting, matcherOut.length ) : - matcherOut - ); - if ( postFinder ) { - postFinder( null, results, matcherOut, xml ); - } else { - push.apply( results, matcherOut ); - } + this.isDefaultPrevented = returnTrue; + if ( !e ) { + return; } - }); -} - -function matcherFromTokens( tokens ) { - var checkContext, matcher, j, - len = tokens.length, - leadingRelative = Expr.relative[ tokens[0].type ], - implicitRelative = leadingRelative || Expr.relative[" "], - i = leadingRelative ? 1 : 0, - // The foundational matcher ensures that elements are reachable from top-level context(s) - matchContext = addCombinator( function( elem ) { - return elem === checkContext; - }, implicitRelative, true ), - matchAnyContext = addCombinator( function( elem ) { - return indexOf.call( checkContext, elem ) > -1; - }, implicitRelative, true ), - matchers = [ function( elem, context, xml ) { - return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( - (checkContext = context).nodeType ? - matchContext( elem, context, xml ) : - matchAnyContext( elem, context, xml ) ); - } ]; + // If preventDefault exists, run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); - for ( ; i < len; i++ ) { - if ( (matcher = Expr.relative[ tokens[i].type ]) ) { - matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; + // Support: IE + // Otherwise set the returnValue property of the original event to false } else { - matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); + e.returnValue = false; + } + }, + stopPropagation: function() { + var e = this.originalEvent; - // Return special upon seeing a positional matcher - if ( matcher[ expando ] ) { - // Find the next relative operator (if any) for proper handling - j = ++i; - for ( ; j < len; j++ ) { - if ( Expr.relative[ tokens[j].type ] ) { - break; - } - } - return setMatcher( - i > 1 && elementMatcher( matchers ), - i > 1 && toSelector( tokens.slice( 0, i - 1 ) ).replace( rtrim, "$1" ), - matcher, - i < j && matcherFromTokens( tokens.slice( i, j ) ), - j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), - j < len && toSelector( tokens ) - ); - } - matchers.push( matcher ); + this.isPropagationStopped = returnTrue; + if ( !e ) { + return; + } + // If stopPropagation exists, run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); } + + // Support: IE + // Set the cancelBubble property of the original event to true + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); } +}; - return elementMatcher( matchers ); -} +// Create mouseenter/leave events using mouseover/out and event-time checks +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, -function matcherFromGroupMatchers( elementMatchers, setMatchers ) { - // A counter to specify which element is currently being matched - var matcherCachedRuns = 0, - bySet = setMatchers.length > 0, - byElement = elementMatchers.length > 0, - superMatcher = function( seed, context, xml, results, expandContext ) { - var elem, j, matcher, - setMatched = [], - matchedCount = 0, - i = "0", - unmatched = seed && [], - outermost = expandContext != null, - contextBackup = outermostContext, - // We must always have either seed elements or context - elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ), - // Use integer dirruns iff this is the outermost matcher - dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1); + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; - if ( outermost ) { - outermostContext = context !== document && context; - cachedruns = matcherCachedRuns; + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; } + return ret; + } + }; +}); - // Add elements passing elementMatchers directly to results - // Keep `i` a string if there are no elements so `matchedCount` will be "00" below - for ( ; (elem = elems[i]) != null; i++ ) { - if ( byElement && elem ) { - j = 0; - while ( (matcher = elementMatchers[j++]) ) { - if ( matcher( elem, context, xml ) ) { - results.push( elem ); - break; - } - } - if ( outermost ) { - dirruns = dirrunsUnique; - cachedruns = ++matcherCachedRuns; - } - } - - // Track unmatched elements for set filters - if ( bySet ) { - // They will have gone through all possible matchers - if ( (elem = !matcher && elem) ) { - matchedCount--; - } +// IE submit delegation +if ( !jQuery.support.submitBubbles ) { - // Lengthen the array for every element, matched or not - if ( seed ) { - unmatched.push( elem ); - } - } + jQuery.event.special.submit = { + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; } - // Apply set filters to unmatched elements - matchedCount += i; - if ( bySet && i !== matchedCount ) { - j = 0; - while ( (matcher = setMatchers[j++]) ) { - matcher( unmatched, setMatched, context, xml ); - } - - if ( seed ) { - // Reintegrate element matches to eliminate the need for sorting - if ( matchedCount > 0 ) { - while ( i-- ) { - if ( !(unmatched[i] || setMatched[i]) ) { - setMatched[i] = pop.call( results ); - } - } - } - - // Discard index placeholder values to get only actual matches - setMatched = condense( setMatched ); + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !jQuery._data( form, "submitBubbles" ) ) { + jQuery.event.add( form, "submit._submit", function( event ) { + event._submit_bubble = true; + }); + jQuery._data( form, "submitBubbles", true ); } + }); + // return undefined since we don't need an event listener + }, - // Add matches to results - push.apply( results, setMatched ); - - // Seedless set matches succeeding multiple successful matchers stipulate sorting - if ( outermost && !seed && setMatched.length > 0 && - ( matchedCount + setMatchers.length ) > 1 ) { - - Sizzle.uniqueSort( results ); + postDispatch: function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( event._submit_bubble ) { + delete event._submit_bubble; + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); } } + }, - // Override manipulation of globals by nested matchers - if ( outermost ) { - dirruns = dirrunsUnique; - outermostContext = contextBackup; + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; } - return unmatched; - }; - - return bySet ? - markFunction( superMatcher ) : - superMatcher; -} - -compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { - var i, - setMatchers = [], - elementMatchers = [], - cached = compilerCache[ selector + " " ]; - - if ( !cached ) { - // Generate a function of recursive functions that can be used to check each element - if ( !group ) { - group = tokenize( selector ); - } - i = group.length; - while ( i-- ) { - cached = matcherFromTokens( group[i] ); - if ( cached[ expando ] ) { - setMatchers.push( cached ); - } else { - elementMatchers.push( cached ); - } + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); } - - // Cache the compiled function - cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); - } - return cached; -}; - -function multipleContexts( selector, contexts, results ) { - var i = 0, - len = contexts.length; - for ( ; i < len; i++ ) { - Sizzle( selector, contexts[i], results ); - } - return results; + }; } -function select( selector, context, results, seed ) { - var i, tokens, token, type, find, - match = tokenize( selector ); +// IE change delegation and checkbox/radio fix +if ( !jQuery.support.changeBubbles ) { - if ( !seed ) { - // Try to minimize operations if there is only one group - if ( match.length === 1 ) { + jQuery.event.special.change = { - // Take a shortcut and set the context if the root selector is an ID - tokens = match[0] = match[0].slice( 0 ); - if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && - context.nodeType === 9 && !documentIsXML && - Expr.relative[ tokens[1].type ] ) { + setup: function() { - context = Expr.find["ID"]( token.matches[0].replace( runescape, funescape ), context )[0]; - if ( !context ) { - return results; + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + } + // Allow triggered, simulated change events (#11500) + jQuery.event.simulate( "change", this, event, true ); + }); + } + return false; + } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; + + if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + jQuery._data( elem, "changeBubbles", true ); } + }); + }, - selector = selector.slice( tokens.shift().value.length ); + handle: function( event ) { + var elem = event.target; + + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); } + }, - // Fetch a seed set for right-to-left matching - i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; - while ( i-- ) { - token = tokens[i]; + teardown: function() { + jQuery.event.remove( this, "._change" ); + + return !rformElems.test( this.nodeName ); + } + }; +} - // Abort if we hit a combinator - if ( Expr.relative[ (type = token.type) ] ) { - break; - } - if ( (find = Expr.find[ type ]) ) { - // Search, expanding context for leading sibling combinators - if ( (seed = find( - token.matches[0].replace( runescape, funescape ), - rsibling.test( tokens[0].type ) && context.parentNode || context - )) ) { +// Create "bubbling" focus and blur events +if ( !jQuery.support.focusinBubbles ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { - // If seed is empty or no tokens remain, we can return early - tokens.splice( i, 1 ); - selector = seed.length && toSelector( tokens ); - if ( !selector ) { - push.apply( results, slice.call( seed, 0 ) ); - return results; - } + // Attach a single capturing handler while someone wants focusin/focusout + var attaches = 0, + handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; - break; - } + jQuery.event.special[ fix ] = { + setup: function() { + if ( attaches++ === 0 ) { + document.addEventListener( orig, handler, true ); + } + }, + teardown: function() { + if ( --attaches === 0 ) { + document.removeEventListener( orig, handler, true ); } } - } - } - - // Compile and execute a filtering function - // Provide `match` to avoid retokenization if we modified the selector above - compile( selector, match )( - seed, - context, - documentIsXML, - results, - rsibling.test( selector ) - ); - return results; + }; + }); } -// Deprecated -Expr.pseudos["nth"] = Expr.pseudos["eq"]; +jQuery.fn.extend({ -// Easy API for creating new setFilters -function setFilters() {} -Expr.filters = setFilters.prototype = Expr.pseudos; -Expr.setFilters = new setFilters(); + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var type, origFn; -// Initialize with the default document -setDocument(); + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); + } + return this; + } -// Override sizzle attribute retrieval -Sizzle.attr = jQuery.attr; -jQuery.find = Sizzle; -jQuery.expr = Sizzle.selectors; -jQuery.expr[":"] = jQuery.expr.pseudos; -jQuery.unique = Sizzle.uniqueSort; -jQuery.text = Sizzle.getText; -jQuery.isXMLDoc = Sizzle.isXML; -jQuery.contains = Sizzle.contains; + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on( types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, -})( window ); -var runtil = /Until$/, + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + triggerHandler: function( type, data ) { + var elem = this[0]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +}); +var isSimple = /^.[^:#\[\.,]*$/, rparentsprev = /^(?:parents|prev(?:Until|All))/, - isSimple = /^.[^:#\[\.,]*$/, rneedsContext = jQuery.expr.match.needsContext, // methods guaranteed to produce a unique set when starting from a unique set guaranteedUnique = { @@ -5557,11 +5711,12 @@ var runtil = /Until$/, jQuery.fn.extend({ find: function( selector ) { - var i, ret, self, - len = this.length; + var i, + ret = [], + self = this, + len = self.length; if ( typeof selector !== "string" ) { - self = this; return this.pushStack( jQuery( selector ).filter(function() { for ( i = 0; i < len; i++ ) { if ( jQuery.contains( self[ i ], this ) ) { @@ -5571,14 +5726,13 @@ jQuery.fn.extend({ }) ); } - ret = []; for ( i = 0; i < len; i++ ) { - jQuery.find( selector, this[ i ], ret ); + jQuery.find( selector, self[ i ], ret ); } // Needed because $( selector, context ) becomes $( context ).find( selector ) ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); - ret.selector = ( this.selector ? this.selector + " " : "" ) + selector; + ret.selector = this.selector ? this.selector + " " + selector : selector; return ret; }, @@ -5597,22 +5751,24 @@ jQuery.fn.extend({ }, not: function( selector ) { - return this.pushStack( winnow(this, selector, false) ); + return this.pushStack( winnow(this, selector || [], true) ); }, filter: function( selector ) { - return this.pushStack( winnow(this, selector, true) ); + return this.pushStack( winnow(this, selector || [], false) ); }, is: function( selector ) { - return !!selector && ( - typeof selector === "string" ? - // If this is a positional/relative selector, check membership in the returned set - // so $("p:first").is("p:last") won't return true for a doc with two "p". - rneedsContext.test( selector ) ? - jQuery( selector, this.context ).index( this[0] ) >= 0 : - jQuery.filter( selector, this ).length > 0 : - this.filter( selector ).length > 0 ); + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; }, closest: function( selectors, context ) { @@ -5625,14 +5781,18 @@ jQuery.fn.extend({ 0; for ( ; i < l; i++ ) { - cur = this[i]; + for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) { + // Always skip document fragments + if ( cur.nodeType < 11 && (pos ? + pos.index(cur) > -1 : - while ( cur && cur.ownerDocument && cur !== context && cur.nodeType !== 11 ) { - if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { - ret.push( cur ); + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector(cur, selectors)) ) { + + cur = ret.push( cur ); break; } - cur = cur.parentNode; } } @@ -5675,8 +5835,6 @@ jQuery.fn.extend({ } }); -jQuery.fn.andSelf = jQuery.fn.addBack; - function sibling( cur, dir ) { do { cur = cur[ dir ]; @@ -5729,7 +5887,7 @@ jQuery.each({ jQuery.fn[ name ] = function( until, selector ) { var ret = jQuery.map( this, fn, until ); - if ( !runtil.test( name ) ) { + if ( name.slice( -5 ) !== "Until" ) { selector = until; } @@ -5737,10 +5895,16 @@ jQuery.each({ ret = jQuery.filter( selector, ret ); } - ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; + if ( this.length > 1 ) { + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + ret = jQuery.unique( ret ); + } - if ( this.length > 1 && rparentsprev.test( name ) ) { - ret = ret.reverse(); + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + ret = ret.reverse(); + } } return this.pushStack( ret ); @@ -5749,13 +5913,17 @@ jQuery.each({ jQuery.extend({ filter: function( expr, elems, not ) { + var elem = elems[ 0 ]; + if ( not ) { expr = ":not(" + expr + ")"; } - return elems.length === 1 ? - jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : - jQuery.find.matches(expr, elems); + return elems.length === 1 && elem.nodeType === 1 ? + jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : + jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + })); }, dir: function( elem, dir, until ) { @@ -5785,37 +5953,32 @@ jQuery.extend({ }); // Implement the identical functionality for filter and not -function winnow( elements, qualifier, keep ) { - - // Can't pass null or undefined to indexOf in Firefox 4 - // Set to 0 to skip string check - qualifier = qualifier || 0; - +function winnow( elements, qualifier, not ) { if ( jQuery.isFunction( qualifier ) ) { - return jQuery.grep(elements, function( elem, i ) { - var retVal = !!qualifier.call( elem, i, elem ); - return retVal === keep; + return jQuery.grep( elements, function( elem, i ) { + /* jshint -W018 */ + return !!qualifier.call( elem, i, elem ) !== not; }); - } else if ( qualifier.nodeType ) { - return jQuery.grep(elements, function( elem ) { - return ( elem === qualifier ) === keep; - }); + } - } else if ( typeof qualifier === "string" ) { - var filtered = jQuery.grep(elements, function( elem ) { - return elem.nodeType === 1; + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; }); + } + + if ( typeof qualifier === "string" ) { if ( isSimple.test( qualifier ) ) { - return jQuery.filter(qualifier, filtered, !keep); - } else { - qualifier = jQuery.filter( qualifier, filtered ); + return jQuery.filter( qualifier, elements, not ); } + + qualifier = jQuery.filter( qualifier, elements ); } - return jQuery.grep(elements, function( elem ) { - return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; + return jQuery.grep( elements, function( elem ) { + return ( jQuery.inArray( elem, qualifier ) >= 0 ) !== not; }); } function createSafeFragment( document ) { @@ -5854,115 +6017,52 @@ var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figca option: [ 1, "" ], legend: [ 1, "
    ", "
    " ], area: [ 1, "", "" ], - param: [ 1, "", "" ], - thead: [ 1, "", "
    " ], - tr: [ 2, "", "
    " ], - col: [ 2, "", "
    " ], - td: [ 3, "", "
    " ], - - // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, - // unless wrapped in a div with non-breaking characters in front of it. - _default: jQuery.support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X
    ", "
    " ] - }, - safeFragment = createSafeFragment( document ), - fragmentDiv = safeFragment.appendChild( document.createElement("div") ); - -wrapMap.optgroup = wrapMap.option; -wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; -wrapMap.th = wrapMap.td; - -jQuery.fn.extend({ - text: function( value ) { - return jQuery.access( this, function( value ) { - return value === undefined ? - jQuery.text( this ) : - this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) ); - }, null, value, arguments.length ); - }, - - wrapAll: function( html ) { - if ( jQuery.isFunction( html ) ) { - return this.each(function(i) { - jQuery(this).wrapAll( html.call(this, i) ); - }); - } - - if ( this[0] ) { - // The elements to wrap the target around - var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true); - - if ( this[0].parentNode ) { - wrap.insertBefore( this[0] ); - } - - wrap.map(function() { - var elem = this; - - while ( elem.firstChild && elem.firstChild.nodeType === 1 ) { - elem = elem.firstChild; - } - - return elem; - }).append( this ); - } - - return this; - }, - - wrapInner: function( html ) { - if ( jQuery.isFunction( html ) ) { - return this.each(function(i) { - jQuery(this).wrapInner( html.call(this, i) ); - }); - } - - return this.each(function() { - var self = jQuery( this ), - contents = self.contents(); - - if ( contents.length ) { - contents.wrapAll( html ); - - } else { - self.append( html ); - } - }); - }, - - wrap: function( html ) { - var isFunction = jQuery.isFunction( html ); + param: [ 1, "", "" ], + thead: [ 1, "", "
    " ], + tr: [ 2, "", "
    " ], + col: [ 2, "", "
    " ], + td: [ 3, "", "
    " ], - return this.each(function(i) { - jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); - }); + // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, + // unless wrapped in a div with non-breaking characters in front of it. + _default: jQuery.support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X
    ", "
    " ] }, + safeFragment = createSafeFragment( document ), + fragmentDiv = safeFragment.appendChild( document.createElement("div") ); - unwrap: function() { - return this.parent().each(function() { - if ( !jQuery.nodeName( this, "body" ) ) { - jQuery( this ).replaceWith( this.childNodes ); - } - }).end(); +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +jQuery.fn.extend({ + text: function( value ) { + return jQuery.access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) ); + }, null, value, arguments.length ); }, append: function() { - return this.domManip(arguments, true, function( elem ) { + return this.domManip( arguments, function( elem ) { if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - this.appendChild( elem ); + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); } }); }, prepend: function() { - return this.domManip(arguments, true, function( elem ) { + return this.domManip( arguments, function( elem ) { if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - this.insertBefore( elem, this.firstChild ); + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); } }); }, before: function() { - return this.domManip( arguments, false, function( elem ) { + return this.domManip( arguments, function( elem ) { if ( this.parentNode ) { this.parentNode.insertBefore( elem, this ); } @@ -5970,7 +6070,7 @@ jQuery.fn.extend({ }, after: function() { - return this.domManip( arguments, false, function( elem ) { + return this.domManip( arguments, function( elem ) { if ( this.parentNode ) { this.parentNode.insertBefore( elem, this.nextSibling ); } @@ -5980,20 +6080,20 @@ jQuery.fn.extend({ // keepData is for internal use only--do not document remove: function( selector, keepData ) { var elem, + elems = selector ? jQuery.filter( selector, this ) : this, i = 0; - for ( ; (elem = this[i]) != null; i++ ) { - if ( !selector || jQuery.filter( selector, [ elem ] ).length > 0 ) { - if ( !keepData && elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem ) ); - } + for ( ; (elem = elems[i]) != null; i++ ) { - if ( elem.parentNode ) { - if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) { - setGlobalEval( getAll( elem, "script" ) ); - } - elem.parentNode.removeChild( elem ); + if ( !keepData && elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem ) ); + } + + if ( elem.parentNode ) { + if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) { + setGlobalEval( getAll( elem, "script" ) ); } + elem.parentNode.removeChild( elem ); } } @@ -6076,31 +6176,39 @@ jQuery.fn.extend({ }, null, value, arguments.length ); }, - replaceWith: function( value ) { - var isFunc = jQuery.isFunction( value ); - - // Make sure that the elements are removed from the DOM before they are inserted - // this can help fix replacing a parent with child elements - if ( !isFunc && typeof value !== "string" ) { - value = jQuery( value ).not( this ).detach(); - } + replaceWith: function() { + var + // Snapshot the DOM in case .domManip sweeps something relevant into its fragment + args = jQuery.map( this, function( elem ) { + return [ elem.nextSibling, elem.parentNode ]; + }), + i = 0; - return this.domManip( [ value ], true, function( elem ) { - var next = this.nextSibling, - parent = this.parentNode; + // Make the changes, replacing each context element with the new content + this.domManip( arguments, function( elem ) { + var next = args[ i++ ], + parent = args[ i++ ]; if ( parent ) { + // Don't use the snapshot next if it has moved (#13810) + if ( next && next.parentNode !== parent ) { + next = this.nextSibling; + } jQuery( this ).remove(); parent.insertBefore( elem, next ); } - }); + // Allow new content to include elements from the context set + }, true ); + + // Force removal if there was no new content (e.g., from empty arguments) + return i ? this : this.remove(); }, detach: function( selector ) { return this.remove( selector, true ); }, - domManip: function( args, table, callback ) { + domManip: function( args, callback, allowIntersection ) { // Flatten any nested arrays args = core_concat.apply( [], args ); @@ -6119,14 +6227,14 @@ jQuery.fn.extend({ return this.each(function( index ) { var self = set.eq( index ); if ( isFunction ) { - args[0] = value.call( this, index, table ? self.html() : undefined ); + args[0] = value.call( this, index, self.html() ); } - self.domManip( args, table, callback ); + self.domManip( args, callback, allowIntersection ); }); } if ( l ) { - fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this ); + fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, !allowIntersection && this ); first = fragment.firstChild; if ( fragment.childNodes.length === 1 ) { @@ -6134,7 +6242,6 @@ jQuery.fn.extend({ } if ( first ) { - table = table && jQuery.nodeName( first, "tr" ); scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); hasScripts = scripts.length; @@ -6152,13 +6259,7 @@ jQuery.fn.extend({ } } - callback.call( - table && jQuery.nodeName( this[i], "table" ) ? - findOrAppend( this[i], "tbody" ) : - this[i], - node, - i - ); + callback.call( this[i], node, i ); } if ( hasScripts ) { @@ -6175,14 +6276,7 @@ jQuery.fn.extend({ if ( node.src ) { // Hope ajax is available... - jQuery.ajax({ - url: node.src, - type: "GET", - dataType: "script", - async: false, - global: false, - "throws": true - }); + jQuery._evalUrl( node.src ); } else { jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) ); } @@ -6199,14 +6293,20 @@ jQuery.fn.extend({ } }); -function findOrAppend( elem, tag ) { - return elem.getElementsByTagName( tag )[0] || elem.appendChild( elem.ownerDocument.createElement( tag ) ); +// Support: IE<8 +// Manipulating tables requires a tbody +function manipulationTarget( elem, content ) { + return jQuery.nodeName( elem, "table" ) && + jQuery.nodeName( content.nodeType === 1 ? content : content.firstChild, "tr" ) ? + + elem.getElementsByTagName("tbody")[0] || + elem.appendChild( elem.ownerDocument.createElement("tbody") ) : + elem; } // Replace/restore the type attribute of script elements for safe DOM manipulation function disableScript( elem ) { - var attr = elem.getAttributeNode("type"); - elem.type = ( attr && attr.specified ) + "/" + elem.type; + elem.type = (jQuery.find.attr( elem, "type" ) !== null) + "/" + elem.type; return elem; } function restoreScript( elem ) { @@ -6612,6 +6712,83 @@ jQuery.extend({ } } } + }, + + _evalUrl: function( url ) { + return jQuery.ajax({ + url: url, + type: "GET", + dataType: "script", + async: false, + global: false, + "throws": true + }); + } +}); +jQuery.fn.extend({ + wrapAll: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapAll( html.call(this, i) ); + }); + } + + if ( this[0] ) { + // The elements to wrap the target around + var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true); + + if ( this[0].parentNode ) { + wrap.insertBefore( this[0] ); + } + + wrap.map(function() { + var elem = this; + + while ( elem.firstChild && elem.firstChild.nodeType === 1 ) { + elem = elem.firstChild; + } + + return elem; + }).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapInner( html.call(this, i) ); + }); + } + + return this.each(function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + }); + }, + + wrap: function( html ) { + var isFunction = jQuery.isFunction( html ); + + return this.each(function(i) { + jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); + }); + }, + + unwrap: function() { + return this.parent().each(function() { + if ( !jQuery.nodeName( this, "body" ) ) { + jQuery( this ).replaceWith( this.childNodes ); + } + }).end(); } }); var iframe, getStyles, curCSS, @@ -6750,10 +6927,12 @@ jQuery.fn.extend({ return showHide( this ); }, toggle: function( state ) { - var bool = typeof state === "boolean"; + if ( typeof state === "boolean" ) { + return state ? this.show() : this.hide(); + } return this.each(function() { - if ( bool ? state : isHidden( this ) ) { + if ( isHidden( this ) ) { jQuery( this ).show(); } else { jQuery( this ).hide(); @@ -6777,13 +6956,14 @@ jQuery.extend({ } }, - // Exclude the following css properties to add px + // Don't automatically add "px" to these possibly-unitless properties cssNumber: { "columnCount": true, "fillOpacity": true, "fontWeight": true, "lineHeight": true, "opacity": true, + "order": true, "orphans": true, "widows": true, "zIndex": true, @@ -6895,27 +7075,6 @@ jQuery.extend({ return extra === true || jQuery.isNumeric( num ) ? num || 0 : val; } return val; - }, - - // A method for quickly swapping in/out CSS properties to get correct calculations - swap: function( elem, options, callback, args ) { - var ret, name, - old = {}; - - // Remember the old values, and insert the new ones - for ( name in options ) { - old[ name ] = elem.style[ name ]; - elem.style[ name ] = options[ name ]; - } - - ret = callback.apply( elem, args || [] ); - - // Revert the old values - for ( name in options ) { - elem.style[ name ] = old[ name ]; - } - - return ret; } }); @@ -7404,9 +7563,26 @@ jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblcl }; }); -jQuery.fn.hover = function( fnOver, fnOut ) { - return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); -}; +jQuery.fn.extend({ + hover: function( fnOver, fnOut ) { + return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); + }, + + bind: function( types, data, fn ) { + return this.on( types, null, data, fn ); + }, + unbind: function( types, fn ) { + return this.off( types, null, fn ); + }, + + delegate: function( selector, types, data, fn ) { + return this.on( types, selector, data, fn ); + }, + undelegate: function( selector, types, fn ) { + // ( namespace ) or ( selector, types [, fn] ) + return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn ); + } +}); var // Document location ajaxLocParts, @@ -7602,25 +7778,6 @@ jQuery.each( [ "ajaxStart", "ajaxStop", "ajaxComplete", "ajaxError", "ajaxSucces }; }); -jQuery.each( [ "get", "post" ], function( i, method ) { - jQuery[ method ] = function( url, data, callback, type ) { - // shift arguments if data argument was omitted - if ( jQuery.isFunction( data ) ) { - type = type || callback; - callback = data; - data = undefined; - } - - return jQuery.ajax({ - url: url, - type: method, - dataType: type, - data: data, - success: callback - }); - }; -}); - jQuery.extend({ // Counter for holding the number of active queries @@ -7666,7 +7823,8 @@ jQuery.extend({ responseFields: { xml: "responseXML", - text: "responseText" + text: "responseText", + json: "responseJSON" }, // Data converters @@ -7674,7 +7832,7 @@ jQuery.extend({ converters: { // Convert anything to text - "* text": window.String, + "* text": String, // Text to html (true = no transformation) "text html": true, @@ -7853,8 +8011,8 @@ jQuery.extend({ parts = rurl.exec( s.url.toLowerCase() ); s.crossDomain = !!( parts && ( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] || - ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? 80 : 443 ) ) != - ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) ) ) + ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? "80" : "443" ) ) !== + ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? "80" : "443" ) ) ) ); } @@ -8015,13 +8173,19 @@ jQuery.extend({ // Set readyState jqXHR.readyState = status > 0 ? 4 : 0; + // Determine if successful + isSuccess = status >= 200 && status < 300 || status === 304; + // Get response data if ( responses ) { response = ajaxHandleResponses( s, jqXHR, responses ); } + // Convert no matter what (that way responseXXX fields are always set) + response = ajaxConvert( s, response, jqXHR, isSuccess ); + // If successful, handle type chaining - if ( status >= 200 && status < 300 || status === 304 ) { + if ( isSuccess ) { // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. if ( s.ifModified ) { @@ -8036,21 +8200,18 @@ jQuery.extend({ } // if no content - if ( status === 204 ) { - isSuccess = true; + if ( status === 204 || s.type === "HEAD" ) { statusText = "nocontent"; // if not modified } else if ( status === 304 ) { - isSuccess = true; statusText = "notmodified"; // If we have data, let's convert it } else { - isSuccess = ajaxConvert( s, response ); - statusText = isSuccess.state; - success = isSuccess.data; - error = isSuccess.error; + statusText = response.state; + success = response.data; + error = response.error; isSuccess = !error; } } else { @@ -8100,32 +8261,42 @@ jQuery.extend({ return jqXHR; }, - getScript: function( url, callback ) { - return jQuery.get( url, undefined, callback, "script" ); - }, - getJSON: function( url, data, callback ) { return jQuery.get( url, data, callback, "json" ); + }, + + getScript: function( url, callback ) { + return jQuery.get( url, undefined, callback, "script" ); } }); +jQuery.each( [ "get", "post" ], function( i, method ) { + jQuery[ method ] = function( url, data, callback, type ) { + // shift arguments if data argument was omitted + if ( jQuery.isFunction( data ) ) { + type = type || callback; + callback = data; + data = undefined; + } + + return jQuery.ajax({ + url: url, + type: method, + dataType: type, + data: data, + success: callback + }); + }; +}); + /* Handles responses to an ajax request: - * - sets all responseXXX fields accordingly * - finds the right dataType (mediates between content-type and expected dataType) * - returns the corresponding response */ function ajaxHandleResponses( s, jqXHR, responses ) { var firstDataType, ct, finalDataType, type, contents = s.contents, - dataTypes = s.dataTypes, - responseFields = s.responseFields; - - // Fill responseXXX fields - for ( type in responseFields ) { - if ( type in responses ) { - jqXHR[ responseFields[type] ] = responses[ type ]; - } - } + dataTypes = s.dataTypes; // Remove auto dataType and get content-type in the process while( dataTypes[ 0 ] === "*" ) { @@ -8174,19 +8345,14 @@ function ajaxHandleResponses( s, jqXHR, responses ) { } } -// Chain conversions given the request and the original response -function ajaxConvert( s, response ) { - var conv2, current, conv, tmp, +/* Chain conversions given the request and the original response + * Also sets the responseXXX fields on the jqXHR instance + */ +function ajaxConvert( s, response, jqXHR, isSuccess ) { + var conv2, current, conv, tmp, prev, converters = {}, - i = 0, // Work with a copy of dataTypes in case we need to modify it for conversion - dataTypes = s.dataTypes.slice(), - prev = dataTypes[ 0 ]; - - // Apply the dataFilter if provided - if ( s.dataFilter ) { - response = s.dataFilter( response, s.dataType ); - } + dataTypes = s.dataTypes.slice(); // Create converters map with lowercased keys if ( dataTypes[ 1 ] ) { @@ -8195,14 +8361,32 @@ function ajaxConvert( s, response ) { } } - // Convert to each sequential dataType, tolerating list modification - for ( ; (current = dataTypes[++i]); ) { + current = dataTypes.shift(); + + // Convert to each sequential dataType + while ( current ) { + + if ( s.responseFields[ current ] ) { + jqXHR[ s.responseFields[ current ] ] = response; + } + + // Apply the dataFilter if provided + if ( !prev && isSuccess && s.dataFilter ) { + response = s.dataFilter( response, s.dataType ); + } + + prev = current; + current = dataTypes.shift(); + + if ( current ) { - // There's only work to do if current dataType is non-auto - if ( current !== "*" ) { + // There's only work to do if current dataType is non-auto + if ( current === "*" ) { + + current = prev; // Convert response if prev dataType is non-auto and differs from current - if ( prev !== "*" && prev !== current ) { + } else if ( prev !== "*" && prev !== current ) { // Seek a direct converter conv = converters[ prev + " " + current ] || converters[ "* " + current ]; @@ -8212,7 +8396,7 @@ function ajaxConvert( s, response ) { for ( conv2 in converters ) { // If conv2 outputs current - tmp = conv2.split(" "); + tmp = conv2.split( " " ); if ( tmp[ 1 ] === current ) { // If prev can be converted to accepted input @@ -8226,9 +8410,8 @@ function ajaxConvert( s, response ) { // Otherwise, insert the intermediate dataType } else if ( converters[ conv2 ] !== true ) { current = tmp[ 0 ]; - dataTypes.splice( i--, 0, current ); + dataTypes.unshift( tmp[ 1 ] ); } - break; } } @@ -8239,7 +8422,7 @@ function ajaxConvert( s, response ) { if ( conv !== true ) { // Unless errors are allowed to bubble, catch and return them - if ( conv && s["throws"] ) { + if ( conv && s[ "throws" ] ) { response = conv( response ); } else { try { @@ -8250,9 +8433,6 @@ function ajaxConvert( s, response ) { } } } - - // Update prev for next iteration - prev = current; } } @@ -8638,44 +8818,51 @@ var fxNow, timerId, animationPrefilters = [ defaultPrefilter ], tweeners = { "*": [function( prop, value ) { - var end, unit, - tween = this.createTween( prop, value ), - parts = rfxnum.exec( value ), + var tween = this.createTween( prop, value ), target = tween.cur(), - start = +target || 0, + parts = rfxnum.exec( value ), + unit = parts && parts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), + + // Starting value computation is required for potential unit mismatches + start = ( jQuery.cssNumber[ prop ] || unit !== "px" && +target ) && + rfxnum.exec( jQuery.css( tween.elem, prop ) ), scale = 1, maxIterations = 20; - if ( parts ) { - end = +parts[2]; - unit = parts[3] || ( jQuery.cssNumber[ prop ] ? "" : "px" ); + if ( start && start[ 3 ] !== unit ) { + // Trust units reported by jQuery.css + unit = unit || start[ 3 ]; + + // Make sure we update the tween properties later on + parts = parts || []; - // We need to compute starting value - if ( unit !== "px" && start ) { - // Iteratively approximate from a nonzero starting point - // Prefer the current property, because this process will be trivial if it uses the same units - // Fallback to end or a simple constant - start = jQuery.css( tween.elem, prop, true ) || end || 1; + // Iteratively approximate from a nonzero starting point + start = +target || 1; - do { - // If previous iteration zeroed out, double until we get *something* - // Use a string for doubling factor so we don't accidentally see scale as unchanged below - scale = scale || ".5"; + do { + // If previous iteration zeroed out, double until we get *something* + // Use a string for doubling factor so we don't accidentally see scale as unchanged below + scale = scale || ".5"; - // Adjust and apply - start = start / scale; - jQuery.style( tween.elem, prop, start + unit ); + // Adjust and apply + start = start / scale; + jQuery.style( tween.elem, prop, start + unit ); - // Update scale, tolerating zero or NaN from tween.cur() - // And breaking the loop if scale is unchanged or perfect, or if we've just had enough - } while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations ); - } + // Update scale, tolerating zero or NaN from tween.cur() + // And breaking the loop if scale is unchanged or perfect, or if we've just had enough + } while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations ); + } + // Update tween properties + if ( parts ) { + start = tween.start = +start || +target || 0; tween.unit = unit; - tween.start = start; // If a +=/-= token was provided, we're doing a relative animation - tween.end = parts[1] ? start + ( parts[1] + 1 ) * end : end; + tween.end = parts[ 1 ] ? + start + ( parts[ 1 ] + 1 ) * parts[ 2 ] : + +parts[ 2 ]; } + return tween; }] }; @@ -8688,19 +8875,18 @@ function createFxNow() { return ( fxNow = jQuery.now() ); } -function createTweens( animation, props ) { - jQuery.each( props, function( prop, value ) { - var collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ), - index = 0, - length = collection.length; - for ( ; index < length; index++ ) { - if ( collection[ index ].call( animation, prop, value ) ) { +function createTween( value, prop, animation ) { + var tween, + collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ), + index = 0, + length = collection.length; + for ( ; index < length; index++ ) { + if ( (tween = collection[ index ].call( animation, prop, value )) ) { - // we're done with this property - return; - } + // we're done with this property + return tween; } - }); + } } function Animation( elem, properties, options ) { @@ -8786,7 +8972,7 @@ function Animation( elem, properties, options ) { } } - createTweens( animation, props ); + jQuery.map( props, createTween, animation ); if ( jQuery.isFunction( animation.opts.start ) ) { animation.opts.start.call( elem, animation ); @@ -8808,7 +8994,7 @@ function Animation( elem, properties, options ) { } function propFilter( props, specialEasing ) { - var value, name, index, easing, hooks; + var index, name, easing, value, hooks; // camelCase, specialEasing and expand cssHook pass for ( index in props ) { @@ -8875,15 +9061,13 @@ jQuery.Animation = jQuery.extend( Animation, { }); function defaultPrefilter( elem, props, opts ) { - /*jshint validthis:true */ - var prop, index, length, - value, dataShow, toggle, - tween, hooks, oldfire, + /* jshint validthis: true */ + var prop, value, toggle, tween, hooks, oldfire, anim = this, - style = elem.style, orig = {}, - handled = [], - hidden = elem.nodeType && isHidden( elem ); + style = elem.style, + hidden = elem.nodeType && isHidden( elem ), + dataShow = jQuery._data( elem, "fxshow" ); // handle queue: false promises if ( !opts.queue ) { @@ -8948,23 +9132,25 @@ function defaultPrefilter( elem, props, opts ) { // show/hide pass - for ( index in props ) { - value = props[ index ]; + for ( prop in props ) { + value = props[ prop ]; if ( rfxtypes.exec( value ) ) { - delete props[ index ]; + delete props[ prop ]; toggle = toggle || value === "toggle"; if ( value === ( hidden ? "hide" : "show" ) ) { continue; } - handled.push( index ); + orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); } } - length = handled.length; - if ( length ) { - dataShow = jQuery._data( elem, "fxshow" ) || jQuery._data( elem, "fxshow", {} ); - if ( "hidden" in dataShow ) { - hidden = dataShow.hidden; + if ( !jQuery.isEmptyObject( orig ) ) { + if ( dataShow ) { + if ( "hidden" in dataShow ) { + hidden = dataShow.hidden; + } + } else { + dataShow = jQuery._data( elem, "fxshow", {} ); } // store state if its toggle - enables .stop().toggle() to "reverse" @@ -8985,10 +9171,8 @@ function defaultPrefilter( elem, props, opts ) { jQuery.style( elem, prop, orig[ prop ] ); } }); - for ( index = 0 ; index < length ; index++ ) { - prop = handled[ index ]; - tween = anim.createTween( prop, hidden ? dataShow[ prop ] : 0 ); - orig[ prop ] = dataShow[ prop ] || jQuery.style( elem, prop ); + for ( prop in orig ) { + tween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); if ( !( prop in dataShow ) ) { dataShow[ prop ] = tween.start; @@ -9084,8 +9268,8 @@ Tween.propHooks = { } }; -// Remove in 2.0 - this supports IE8's panic based approach -// to setting things on disconnected nodes +// Support: IE <=9 +// Panic based approach to setting things on disconnected nodes Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { set: function( tween ) { @@ -9119,9 +9303,7 @@ jQuery.fn.extend({ doAnimation = function() { // Operate on a copy of prop so per-property easing won't be lost var anim = Animation( this, jQuery.extend( {}, prop ), optall ); - doAnimation.finish = function() { - anim.stop( true ); - }; + // Empty animations, or finishing resolves immediately if ( empty || jQuery._data( this, "finish" ) ) { anim.stop( true ); @@ -9201,8 +9383,8 @@ jQuery.fn.extend({ // empty the queue first jQuery.queue( this, type, [] ); - if ( hooks && hooks.cur && hooks.cur.finish ) { - hooks.cur.finish.call( this ); + if ( hooks && hooks.stop ) { + hooks.stop.call( this, true ); } // look for any active animations, and finish them @@ -9486,11 +9668,11 @@ jQuery.fn.extend({ offsetParent: function() { return this.map(function() { - var offsetParent = this.offsetParent || document.documentElement; + var offsetParent = this.offsetParent || docElem; while ( offsetParent && ( !jQuery.nodeName( offsetParent, "html" ) && jQuery.css( offsetParent, "position") === "static" ) ) { offsetParent = offsetParent.offsetParent; } - return offsetParent || document.documentElement; + return offsetParent || docElem; }); } }); @@ -9574,24 +9756,34 @@ jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { // Limit scope pollution from any deprecated API // (function() { +// The number of elements contained in the matched element set +jQuery.fn.size = function() { + return this.length; +}; + +jQuery.fn.andSelf = jQuery.fn.addBack; + // })(); -// Expose jQuery to the global object -window.jQuery = window.$ = jQuery; - -// Expose jQuery as an AMD module, but only for AMD loaders that -// understand the issues with loading multiple versions of jQuery -// in a page that all might call define(). The loader will indicate -// they have special allowances for multiple jQuery versions by -// specifying define.amd.jQuery = true. Register as a named module, -// since jQuery can be concatenated with other files that may use define, -// but not use a proper concatenation script that understands anonymous -// AMD modules. A named AMD is safest and most robust way to register. -// Lowercase jquery is used because AMD module names are derived from -// file names, and jQuery is normally delivered in a lowercase file name. -// Do this after creating the global so that if an AMD module wants to call -// noConflict to hide this version of jQuery, it will work. -if ( typeof define === "function" && define.amd && define.amd.jQuery ) { - define( "jquery", [], function () { return jQuery; } ); +if ( typeof module === "object" && module && typeof module.exports === "object" ) { + // Expose jQuery as module.exports in loaders that implement the Node + // module pattern (including browserify). Do not create the global, since + // the user will be storing it themselves locally, and globals are frowned + // upon in the Node module world. + module.exports = jQuery; +} else { + // Otherwise expose jQuery to the global object as usual + window.jQuery = window.$ = jQuery; + + // Register as a named AMD module, since jQuery can be concatenated with other + // files that may use define, but not via a proper concatenation script that + // understands anonymous AMD modules. A named AMD is safest and most robust + // way to register. Lowercase jquery is used because AMD module names are + // derived from file names, and jQuery is normally delivered in a lowercase + // file name. Do this after creating the global so that if an AMD module wants + // to call noConflict to hide this version of jQuery, it will work. + if ( typeof define === "function" && define.amd ) { + define( "jquery", [], function () { return jQuery; } ); + } } })( window ); diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..6a502e9 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,16 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle + +# Ignore the default SQLite database. +/db/*.sqlite3 +/db/*.sqlite3-journal + +# Ignore all logfiles and tempfiles. +/log/*.log +/tmp diff --git a/server/Gemfile b/server/Gemfile new file mode 100644 index 0000000..1976995 --- /dev/null +++ b/server/Gemfile @@ -0,0 +1,26 @@ +source 'https://rubygems.org' + +gem 'rails', '4.1.2.rc1' + +gem 'rails-api' + +gem 'active_model_serializers' + +gem 'pg' +gem 'rack-cors', :require => 'rack/cors' + + +# To use ActiveModel has_secure_password +# gem 'bcrypt-ruby', '~> 3.1.2' + +# To use Jbuilder templates for JSON +# gem 'jbuilder' + +# Use unicorn as the app server +# gem 'unicorn' + +# Deploy with Capistrano +# gem 'capistrano', :group => :development + +# To use debugger +# gem 'ruby-debug19', :require => 'ruby-debug' diff --git a/server/Gemfile.lock b/server/Gemfile.lock new file mode 100644 index 0000000..e26371c --- /dev/null +++ b/server/Gemfile.lock @@ -0,0 +1,95 @@ +GEM + remote: https://rubygems.org/ + specs: + actionmailer (4.1.2.rc1) + actionpack (= 4.1.2.rc1) + actionview (= 4.1.2.rc1) + mail (~> 2.5.4) + actionpack (4.1.2.rc1) + actionview (= 4.1.2.rc1) + activesupport (= 4.1.2.rc1) + rack (~> 1.5.2) + rack-test (~> 0.6.2) + actionview (4.1.2.rc1) + activesupport (= 4.1.2.rc1) + builder (~> 3.1) + erubis (~> 2.7.0) + active_model_serializers (0.8.1) + activemodel (>= 3.0) + activemodel (4.1.2.rc1) + activesupport (= 4.1.2.rc1) + builder (~> 3.1) + activerecord (4.1.2.rc1) + activemodel (= 4.1.2.rc1) + activesupport (= 4.1.2.rc1) + arel (~> 5.0.0) + activesupport (4.1.2.rc1) + i18n (~> 0.6, >= 0.6.9) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.1) + tzinfo (~> 1.1) + arel (5.0.1.20140414130214) + builder (3.2.2) + erubis (2.7.0) + hike (1.2.3) + i18n (0.6.9) + json (1.8.1) + mail (2.5.4) + mime-types (~> 1.16) + treetop (~> 1.4.8) + mime-types (1.25.1) + minitest (5.3.4) + multi_json (1.10.1) + pg (0.17.1) + polyglot (0.3.5) + rack (1.5.2) + rack-cors (0.2.9) + rack-test (0.6.2) + rack (>= 1.0) + rails (4.1.2.rc1) + actionmailer (= 4.1.2.rc1) + actionpack (= 4.1.2.rc1) + actionview (= 4.1.2.rc1) + activemodel (= 4.1.2.rc1) + activerecord (= 4.1.2.rc1) + activesupport (= 4.1.2.rc1) + bundler (>= 1.3.0, < 2.0) + railties (= 4.1.2.rc1) + sprockets-rails (~> 2.0) + rails-api (0.2.1) + actionpack (>= 3.2.11) + railties (>= 3.2.11) + railties (4.1.2.rc1) + actionpack (= 4.1.2.rc1) + activesupport (= 4.1.2.rc1) + rake (>= 0.8.7) + thor (>= 0.18.1, < 2.0) + rake (10.3.2) + sprockets (2.12.1) + hike (~> 1.2) + multi_json (~> 1.0) + rack (~> 1.0) + tilt (~> 1.1, != 1.3.0) + sprockets-rails (2.1.3) + actionpack (>= 3.0) + activesupport (>= 3.0) + sprockets (~> 2.8) + thor (0.19.1) + thread_safe (0.3.4) + tilt (1.4.1) + treetop (1.4.15) + polyglot + polyglot (>= 0.3.1) + tzinfo (1.2.1) + thread_safe (~> 0.1) + +PLATFORMS + ruby + +DEPENDENCIES + active_model_serializers + pg + rack-cors + rails (= 4.1.2.rc1) + rails-api diff --git a/server/README.rdoc b/server/README.rdoc new file mode 100644 index 0000000..dd4e97e --- /dev/null +++ b/server/README.rdoc @@ -0,0 +1,28 @@ +== README + +This README would normally document whatever steps are necessary to get the +application up and running. + +Things you may want to cover: + +* Ruby version + +* System dependencies + +* Configuration + +* Database creation + +* Database initialization + +* How to run the test suite + +* Services (job queues, cache servers, search engines, etc.) + +* Deployment instructions + +* ... + + +Please feel free to use a different markup language if you do not plan to run +rake doc:app. diff --git a/server/Rakefile b/server/Rakefile new file mode 100644 index 0000000..ba6b733 --- /dev/null +++ b/server/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require File.expand_path('../config/application', __FILE__) + +Rails.application.load_tasks diff --git a/server/app/controllers/application_controller.rb b/server/app/controllers/application_controller.rb new file mode 100644 index 0000000..ee6dab7 --- /dev/null +++ b/server/app/controllers/application_controller.rb @@ -0,0 +1,4 @@ +class ApplicationController < ActionController::API + include ActionController::StrongParameters + +end diff --git a/server/app/controllers/concerns/.keep b/server/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/server/app/controllers/todos_controller.rb b/server/app/controllers/todos_controller.rb new file mode 100644 index 0000000..64009a3 --- /dev/null +++ b/server/app/controllers/todos_controller.rb @@ -0,0 +1,39 @@ +class TodosController < ApplicationController + + def index + @todos = Todo.all + render json: @todos + end + + def show + @todo = Todo.find(params[:id]) + render json: @todo + end + + def create + @todo = Todo.new(todo_params) + + if @todo.save + render json: @todo, status: :created + else + render json: @todo.errors, status: :unprocessable_entity + end + end + + def update + @todo = Todo.find(params[:id]) + + if @todo.update(todo_params) + render nothing: true, status: 204 + else + render json: @todo.errors, status: :unprocessable_entity + end + end + + protected + + def todo_params + params.require(:todo).permit(:name, :priority) + end + +end diff --git a/server/app/mailers/.keep b/server/app/mailers/.keep new file mode 100644 index 0000000..e69de29 diff --git a/server/app/models/.keep b/server/app/models/.keep new file mode 100644 index 0000000..e69de29 diff --git a/server/app/models/concerns/.keep b/server/app/models/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/server/app/models/todo.rb b/server/app/models/todo.rb new file mode 100644 index 0000000..8886e9b --- /dev/null +++ b/server/app/models/todo.rb @@ -0,0 +1,2 @@ +class Todo < ActiveRecord::Base +end diff --git a/server/app/serializers/todo_serializer.rb b/server/app/serializers/todo_serializer.rb new file mode 100644 index 0000000..d7022cd --- /dev/null +++ b/server/app/serializers/todo_serializer.rb @@ -0,0 +1,3 @@ +class TodoSerializer < ActiveModel::Serializer + attributes :id, :name, :priority +end diff --git a/server/bin/bundle b/server/bin/bundle new file mode 100755 index 0000000..66e9889 --- /dev/null +++ b/server/bin/bundle @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +load Gem.bin_path('bundler', 'bundle') diff --git a/server/bin/rails b/server/bin/rails new file mode 100755 index 0000000..7feb6a3 --- /dev/null +++ b/server/bin/rails @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +begin + load File.expand_path("../spring", __FILE__) +rescue LoadError +end +APP_PATH = File.expand_path('../../config/application', __FILE__) +require_relative '../config/boot' +require 'rails/commands' diff --git a/server/bin/rake b/server/bin/rake new file mode 100755 index 0000000..8017a02 --- /dev/null +++ b/server/bin/rake @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +begin + load File.expand_path("../spring", __FILE__) +rescue LoadError +end +require_relative '../config/boot' +require 'rake' +Rake.application.run diff --git a/server/bin/spring b/server/bin/spring new file mode 100755 index 0000000..253ec37 --- /dev/null +++ b/server/bin/spring @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +# This file loads spring without using Bundler, in order to be fast +# It gets overwritten when you run the `spring binstub` command + +unless defined?(Spring) + require "rubygems" + require "bundler" + + if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ spring \((.*?)\)$.*?^$/m) + ENV["GEM_PATH"] = ([Bundler.bundle_path.to_s] + Gem.path).join(File::PATH_SEPARATOR) + ENV["GEM_HOME"] = "" + Gem.paths = ENV + + gem "spring", match[1] + require "spring/binstub" + end +end diff --git a/server/config.ru b/server/config.ru new file mode 100644 index 0000000..8ff1ea2 --- /dev/null +++ b/server/config.ru @@ -0,0 +1,16 @@ +# This file is used by Rack-based servers to start the application. + +require ::File.expand_path('../config/environment', __FILE__) +run Rails.application + +require 'rack/cors' +use Rack::Cors do + + # allow all origins in development + allow do + origins '*' + resource '*', + :headers => :any, + :methods => [:get, :post, :delete, :put, :options] + end +end \ No newline at end of file diff --git a/server/config/application.rb b/server/config/application.rb new file mode 100644 index 0000000..8ed0859 --- /dev/null +++ b/server/config/application.rb @@ -0,0 +1,23 @@ +require File.expand_path('../boot', __FILE__) + +require 'rails/all' + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module EmberAngularServer + class Application < Rails::Application + # Settings in config/environments/* take precedence over those specified here. + # Application configuration should go into files in config/initializers + # -- all .rb files in that directory are automatically loaded. + + # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. + # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. + # config.time_zone = 'Central Time (US & Canada)' + + # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. + # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] + # config.i18n.default_locale = :de + end +end diff --git a/server/config/boot.rb b/server/config/boot.rb new file mode 100644 index 0000000..5e5f0c1 --- /dev/null +++ b/server/config/boot.rb @@ -0,0 +1,4 @@ +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) + +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) diff --git a/server/config/database.yml b/server/config/database.yml new file mode 100644 index 0000000..0fb5990 --- /dev/null +++ b/server/config/database.yml @@ -0,0 +1,85 @@ +# PostgreSQL. Versions 8.2 and up are supported. +# +# Install the pg driver: +# gem install pg +# On OS X with Homebrew: +# gem install pg -- --with-pg-config=/usr/local/bin/pg_config +# On OS X with MacPorts: +# gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config +# On Windows: +# gem install pg +# Choose the win32 build. +# Install PostgreSQL and put its /bin directory on your path. +# +# Configure Using Gemfile +# gem 'pg' +# +default: &default + adapter: postgresql + encoding: unicode + # For details on connection pooling, see rails configuration guide + # http://guides.rubyonrails.org/configuring.html#database-pooling + pool: 5 + +development: + <<: *default + database: ember-angular-server_development + + # The specified database role being used to connect to postgres. + # To create additional roles in postgres see `$ createuser --help`. + # When left blank, postgres will use the default role. This is + # the same name as the operating system user that initialized the database. + #username: ember-angular-server + + # The password associated with the postgres role (username). + #password: + + # Connect on a TCP socket. Omitted by default since the client uses a + # domain socket that doesn't need configuration. Windows does not have + # domain sockets, so uncomment these lines. + #host: localhost + + # The TCP port the server listens on. Defaults to 5432. + # If your server runs on a different port number, change accordingly. + #port: 5432 + + # Schema search path. The server defaults to $user,public + #schema_search_path: myapp,sharedapp,public + + # Minimum log levels, in increasing order: + # debug5, debug4, debug3, debug2, debug1, + # log, notice, warning, error, fatal, and panic + # Defaults to warning. + #min_messages: notice + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: ember-angular-server_test + +# As with config/secrets.yml, you never want to store sensitive information, +# like your database password, in your source code. If your source code is +# ever seen by anyone, they now have access to your database. +# +# Instead, provide the password as a unix environment variable when you boot +# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database +# for a full rundown on how to provide these environment variables in a +# production deployment. +# +# On Heroku and other platform providers, you may have a full connection URL +# available as an environment variable. For example: +# +# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" +# +# You can use this database configuration with: +# +# production: +# url: <%= ENV['DATABASE_URL'] %> +# +production: + <<: *default + database: ember-angular-server_production + username: ember-angular-server + password: <%= ENV['EMBER-ANGULAR-SERVER_DATABASE_PASSWORD'] %> diff --git a/server/config/environment.rb b/server/config/environment.rb new file mode 100644 index 0000000..ee8d90d --- /dev/null +++ b/server/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require File.expand_path('../application', __FILE__) + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/server/config/environments/development.rb b/server/config/environments/development.rb new file mode 100644 index 0000000..ddf0e90 --- /dev/null +++ b/server/config/environments/development.rb @@ -0,0 +1,37 @@ +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded on + # every request. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Debug mode disables concatenation and preprocessing of assets. + # This option may cause significant delays in view rendering with a large + # number of complex assets. + config.assets.debug = true + + # Adds additional error checking when serving assets at runtime. + # Checks for improperly declared sprockets dependencies. + # Raises helpful error messages. + config.assets.raise_runtime_errors = true + + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true +end diff --git a/server/config/environments/production.rb b/server/config/environments/production.rb new file mode 100644 index 0000000..a8836c3 --- /dev/null +++ b/server/config/environments/production.rb @@ -0,0 +1,82 @@ +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.cache_classes = true + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Enable Rack::Cache to put a simple HTTP cache in front of your application + # Add `rack-cache` to your Gemfile before enabling this. + # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. + # config.action_dispatch.rack_cache = true + + # Disable Rails's static asset server (Apache or nginx will already do this). + config.serve_static_assets = false + + # Compress JavaScripts and CSS. + config.assets.js_compressor = :uglifier + # config.assets.css_compressor = :sass + + # Do not fallback to assets pipeline if a precompiled asset is missed. + config.assets.compile = false + + # Generate digests for assets URLs. + config.assets.digest = true + + # `config.assets.precompile` has moved to config/initializers/assets.rb + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Set to :debug to see everything in the log. + config.log_level = :info + + # Prepend all log lines with the following tags. + # config.log_tags = [ :subdomain, :uuid ] + + # Use a different logger for distributed setups. + # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.action_controller.asset_host = "http://assets.example.com" + + # Precompile additional assets. + # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. + # config.assets.precompile += %w( search.js ) + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners. + config.active_support.deprecation = :notify + + # Disable automatic flushing of the log to improve performance. + # config.autoflush_log = false + + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = ::Logger::Formatter.new + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false +end diff --git a/server/config/environments/test.rb b/server/config/environments/test.rb new file mode 100644 index 0000000..053f5b6 --- /dev/null +++ b/server/config/environments/test.rb @@ -0,0 +1,39 @@ +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # The test environment is used exclusively to run your application's + # test suite. You never need to work with it otherwise. Remember that + # your test database is "scratch space" for the test suite and is wiped + # and recreated between test runs. Don't rely on the data there! + config.cache_classes = true + + # Do not eager load code on boot. This avoids loading your whole application + # just for the purpose of running a single test. If you are using a tool that + # preloads Rails for running tests, you may have to set it to true. + config.eager_load = false + + # Configure static asset server for tests with Cache-Control for performance. + config.serve_static_assets = true + config.static_cache_control = 'public, max-age=3600' + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Raise exceptions instead of rendering exception templates. + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true +end diff --git a/server/config/initializers/secret_token.rb b/server/config/initializers/secret_token.rb new file mode 100644 index 0000000..f94b5d4 --- /dev/null +++ b/server/config/initializers/secret_token.rb @@ -0,0 +1,19 @@ +# Be sure to restart your server when you modify this file. + +# Your secret key for verifying the integrity of signed cookies. +# If you change this key, all old signed cookies will become invalid! + +# Make sure the secret is at least 30 characters and all random, +# no regular words or you'll be exposed to dictionary attacks. +# You can use `rake secret` to generate a secure secret key. + +# Make sure your secret_key_base is kept private +# if you're sharing your code publicly. + +# Although this is not needed for an api-only application, rails4 +# requires secret_key_base or secret_token to be defined, otherwise an +# error is raised. +# Using secret_token for rails3 compatibility. Change to secret_key_base +# to avoid deprecation warning. +# Can be safely removed in a rails3 api-only application. +EmberAngularServer::Application.config.secret_token = '7e60e097a86ab60a526d8c51dd1435e04dc07eebc57faf5db8fd5a5bcdd69e2dbb60f143c012d4a948f4f3ab78176da701de9c73e8a111ff35de9baee7584fb8' diff --git a/server/config/initializers/wrap_parameters.rb b/server/config/initializers/wrap_parameters.rb new file mode 100644 index 0000000..54961ef --- /dev/null +++ b/server/config/initializers/wrap_parameters.rb @@ -0,0 +1,13 @@ +# Be sure to restart your server when you modify this file. +# +# This file contains settings for ActionController::ParamsWrapper + +# Enable parameter wrapping for JSON. +# ActiveSupport.on_load(:action_controller) do +# wrap_parameters format: [:json] if respond_to?(:wrap_parameters) +# end + +# To enable root element in JSON for ActiveRecord objects. +# ActiveSupport.on_load(:active_record) do +# self.include_root_in_json = true +# end diff --git a/server/config/locales/en.yml b/server/config/locales/en.yml new file mode 100644 index 0000000..0653957 --- /dev/null +++ b/server/config/locales/en.yml @@ -0,0 +1,23 @@ +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t 'hello' +# +# In views, this is aliased to just `t`: +# +# <%= t('hello') %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more, please read the Rails Internationalization guide +# available at http://guides.rubyonrails.org/i18n.html. + +en: + hello: "Hello world" diff --git a/server/config/routes.rb b/server/config/routes.rb new file mode 100644 index 0000000..4be672c --- /dev/null +++ b/server/config/routes.rb @@ -0,0 +1,57 @@ +Rails.application.routes.draw do + resources :todos, except: [:new, :edit], format: :json + # The priority is based upon order of creation: first created -> highest priority. + # See how all your routes lay out with "rake routes". + + # You can have the root of your site routed with "root" + # root 'welcome#index' + + # Example of regular route: + # get 'products/:id' => 'catalog#view' + + # Example of named route that can be invoked with purchase_url(id: product.id) + # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase + + # Example resource route (maps HTTP verbs to controller actions automatically): + # resources :products + + # Example resource route with options: + # resources :products do + # member do + # get 'short' + # post 'toggle' + # end + # + # collection do + # get 'sold' + # end + # end + + # Example resource route with sub-resources: + # resources :products do + # resources :comments, :sales + # resource :seller + # end + + # Example resource route with more complex sub-resources: + # resources :products do + # resources :comments + # resources :sales do + # get 'recent', on: :collection + # end + # end + + # Example resource route with concerns: + # concern :toggleable do + # post 'toggle' + # end + # resources :posts, concerns: :toggleable + # resources :photos, concerns: :toggleable + + # Example resource route within a namespace: + # namespace :admin do + # # Directs /admin/products/* to Admin::ProductsController + # # (app/controllers/admin/products_controller.rb) + # resources :products + # end +end diff --git a/server/config/secrets.yml b/server/config/secrets.yml new file mode 100644 index 0000000..7583307 --- /dev/null +++ b/server/config/secrets.yml @@ -0,0 +1,22 @@ +# Be sure to restart your server when you modify this file. + +# Your secret key is used for verifying the integrity of signed cookies. +# If you change this key, all old signed cookies will become invalid! + +# Make sure the secret is at least 30 characters and all random, +# no regular words or you'll be exposed to dictionary attacks. +# You can use `rake secret` to generate a secure secret key. + +# Make sure the secrets in this file are kept private +# if you're sharing your code publicly. + +development: + secret_key_base: f1c38dc52854f610499efd9361984e7406feaec9610065159e5cb0d9eb75f0275ad0e4bf1b360f0ef1f452e5d4abca5ed6acc72f1f9815f5fd7e0f6f7175b627 + +test: + secret_key_base: c1311cb7d45c3de1fe37481b69fabeb01c0487dfded6cc9f8d9052d3a6cef6c083a7fccf8ab60e2b32669fbdb861a6e8371aeefbdbc4e4e4926473bd208f46f2 + +# Do not keep production secrets in the repository, +# instead read values from the environment. +production: + secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> diff --git a/server/db/migrate/20140611202854_create_todos.rb b/server/db/migrate/20140611202854_create_todos.rb new file mode 100644 index 0000000..844c829 --- /dev/null +++ b/server/db/migrate/20140611202854_create_todos.rb @@ -0,0 +1,10 @@ +class CreateTodos < ActiveRecord::Migration + def change + create_table :todos do |t| + t.string :name + t.integer :priority + + t.timestamps + end + end +end diff --git a/server/db/schema.rb b/server/db/schema.rb new file mode 100644 index 0000000..aa877f5 --- /dev/null +++ b/server/db/schema.rb @@ -0,0 +1,26 @@ +# encoding: UTF-8 +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# Note that this schema.rb definition is the authoritative source for your +# database schema. If you need to create the application database on another +# system, you should be using db:schema:load, not running all the migrations +# from scratch. The latter is a flawed and unsustainable approach (the more migrations +# you'll amass, the slower it'll run and the greater likelihood for issues). +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 20140611202854) do + + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + + create_table "todos", force: true do |t| + t.string "name" + t.integer "priority" + t.datetime "created_at" + t.datetime "updated_at" + end + +end diff --git a/server/db/seeds.rb b/server/db/seeds.rb new file mode 100644 index 0000000..4edb1e8 --- /dev/null +++ b/server/db/seeds.rb @@ -0,0 +1,7 @@ +# This file should contain all the record creation needed to seed the database with its default values. +# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). +# +# Examples: +# +# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) +# Mayor.create(name: 'Emanuel', city: cities.first) diff --git a/server/lib/assets/.keep b/server/lib/assets/.keep new file mode 100644 index 0000000..e69de29 diff --git a/server/lib/tasks/.keep b/server/lib/tasks/.keep new file mode 100644 index 0000000..e69de29 diff --git a/server/log/.keep b/server/log/.keep new file mode 100644 index 0000000..e69de29 diff --git a/server/public/404.html b/server/public/404.html new file mode 100644 index 0000000..b612547 --- /dev/null +++ b/server/public/404.html @@ -0,0 +1,67 @@ + + + + The page you were looking for doesn't exist (404) + + + + + + +
    +
    +

    The page you were looking for doesn't exist.

    +

    You may have mistyped the address or the page may have moved.

    +
    +

    If you are the application owner check the logs for more information.

    +
    + + diff --git a/server/public/422.html b/server/public/422.html new file mode 100644 index 0000000..a21f82b --- /dev/null +++ b/server/public/422.html @@ -0,0 +1,67 @@ + + + + The change you wanted was rejected (422) + + + + + + +
    +
    +

    The change you wanted was rejected.

    +

    Maybe you tried to change something you didn't have access to.

    +
    +

    If you are the application owner check the logs for more information.

    +
    + + diff --git a/server/public/500.html b/server/public/500.html new file mode 100644 index 0000000..061abc5 --- /dev/null +++ b/server/public/500.html @@ -0,0 +1,66 @@ + + + + We're sorry, but something went wrong (500) + + + + + + +
    +
    +

    We're sorry, but something went wrong.

    +
    +

    If you are the application owner check the logs for more information.

    +
    + + diff --git a/server/public/favicon.ico b/server/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/server/public/robots.txt b/server/public/robots.txt new file mode 100644 index 0000000..3c9c7c0 --- /dev/null +++ b/server/public/robots.txt @@ -0,0 +1,5 @@ +# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file +# +# To ban all spiders from the entire site uncomment the next two lines: +# User-agent: * +# Disallow: /