diff --git a/architecture-examples/emberjs/bower.json b/architecture-examples/emberjs/bower.json index 8b3ad667cd..6ccfac9539 100644 --- a/architecture-examples/emberjs/bower.json +++ b/architecture-examples/emberjs/bower.json @@ -5,7 +5,8 @@ "todomvc-common": "~0.1.4", "jquery": "~1.9.1", "handlebars": "~1.0.0-rc.3", - "ember": "~1.0.0-rc.1", + "ember": "~1.0.0", + "ember-data": "1.0.0-beta.1", "ember-localstorage-adapter": "latest" } } diff --git a/architecture-examples/emberjs/bower_components/ember-data/ember-data.js b/architecture-examples/emberjs/bower_components/ember-data/ember-data.js new file mode 100644 index 0000000000..67e1c1c7c3 --- /dev/null +++ b/architecture-examples/emberjs/bower_components/ember-data/ember-data.js @@ -0,0 +1,6947 @@ +// Version: v1.0.0-beta.1 +// Last commit: cafab9d (2013-09-01 00:42:39 -0700) + + +(function() { +var define, requireModule; + +(function() { + var registry = {}, seen = {}; + + define = function(name, deps, callback) { + registry[name] = { deps: deps, callback: callback }; + }; + + requireModule = function(name) { + 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."); + } + + deps = mod.deps; + callback = mod.callback; + reified = []; + exports; + + for (var i=0, l=deps.length; 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; + } +}; + +Ember.Inflector = Inflector; + +})(); + + + +(function() { +Ember.Inflector.defaultRules = { + plurals: [ + [/$/, 's'], + [/s$/i, 's'], + [/^(ax|test)is$/i, '$1es'], + [/(octop|vir)us$/i, '$1i'], + [/(octop|vir)i$/i, '$1i'], + [/(alias|status)$/i, '$1es'], + [/(bu)s$/i, '$1ses'], + [/(buffal|tomat)o$/i, '$1oes'], + [/([ti])um$/i, '$1a'], + [/([ti])a$/i, '$1a'], + [/sis$/i, 'ses'], + [/(?:([^f])fe|([lr])f)$/i, '$1$2ves'], + [/(hive)$/i, '$1s'], + [/([^aeiouy]|qu)y$/i, '$1ies'], + [/(x|ch|ss|sh)$/i, '$1es'], + [/(matr|vert|ind)(?:ix|ex)$/i, '$1ices'], + [/^(m|l)ouse$/i, '$1ice'], + [/^(m|l)ice$/i, '$1ice'], + [/^(ox)$/i, '$1en'], + [/^(oxen)$/i, '$1'], + [/(quiz)$/i, '$1zes'] + ], + + singular: [ + [/s$/i, ''], + [/(ss)$/i, '$1'], + [/(n)ews$/i, '$1ews'], + [/([ti])a$/i, '$1um'], + [/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i, '$1sis'], + [/(^analy)(sis|ses)$/i, '$1sis'], + [/([^f])ves$/i, '$1fe'], + [/(hive)s$/i, '$1'], + [/(tive)s$/i, '$1'], + [/([lr])ves$/i, '$1f'], + [/([^aeiouy]|qu)ies$/i, '$1y'], + [/(s)eries$/i, '$1eries'], + [/(m)ovies$/i, '$1ovie'], + [/(x|ch|ss|sh)es$/i, '$1'], + [/^(m|l)ice$/i, '$1ouse'], + [/(bus)(es)?$/i, '$1'], + [/(o)es$/i, '$1'], + [/(shoe)s$/i, '$1'], + [/(cris|test)(is|es)$/i, '$1is'], + [/^(a)x[ie]s$/i, '$1xis'], + [/(octop|vir)(us|i)$/i, '$1us'], + [/(alias|status)(es)?$/i, '$1'], + [/^(ox)en/i, '$1'], + [/(vert|ind)ices$/i, '$1ex'], + [/(matr)ices$/i, '$1ix'], + [/(quiz)zes$/i, '$1'], + [/(database)s$/i, '$1'] + ], + + irregularPairs: [ + ['person', 'people'], + ['man', 'men'], + ['child', 'children'], + ['sex', 'sexes'], + ['move', 'moves'], + ['cow', 'kine'], + ['zombie', 'zombies'] + ], + + uncountable: [ + 'equipment', + 'information', + 'rice', + 'money', + 'species', + 'series', + 'fish', + 'sheep', + 'jeans', + 'police' + ] +}; + +})(); + + + +(function() { +if (Ember.EXTEND_PROTOTYPES) { + /** + See {{#crossLink "Ember.String/pluralize"}}{{/crossLink}} + + @method pluralize + @for String + */ + String.prototype.pluralize = function() { + return Ember.String.pluralize(this); + }; + + /** + See {{#crossLink "Ember.String/singularize"}}{{/crossLink}} + + @method singularize + @for String + */ + String.prototype.singularize = function() { + return Ember.String.singularize(this); + }; +} + +})(); + + + +(function() { +Ember.Inflector.inflector = new Ember.Inflector(Ember.Inflector.defaultRules); + +})(); + + + +(function() { + +})(); + + +})(); +// Version: v1.0.0-beta.1 +// Last commit: cafab9d (2013-09-01 00:42:39 -0700) + + +(function() { +/** + @module ember-data +*/ + +/** + All Ember Data methods and functions are defined inside of this namespace. + + @class DS + @static +*/ + +if ('undefined' === typeof DS) { + DS = Ember.Namespace.create({ + VERSION: '1.0.0-beta.1' + }); + + if ('undefined' !== typeof window) { + window.DS = DS; + } +} +})(); + + + +(function() { +/** + @module ember-data +*/ + +var isNone = Ember.isNone, isEmpty = Ember.isEmpty; + +/** + DS.JSONTransforms is a hash of transforms used by DS.Serializer. + + @class JSONTransforms + @static + @namespace DS +*/ +DS.JSONTransforms = { + string: { + deserialize: function(serialized) { + return isNone(serialized) ? null : String(serialized); + }, + + serialize: function(deserialized) { + return isNone(deserialized) ? null : String(deserialized); + } + }, + + number: { + deserialize: function(serialized) { + return isEmpty(serialized) ? null : Number(serialized); + }, + + serialize: function(deserialized) { + return isEmpty(deserialized) ? null : Number(deserialized); + } + }, + + // Handles the following boolean inputs: + // "TrUe", "t", "f", "FALSE", 0, (non-zero), or boolean true/false + 'boolean': { + deserialize: function(serialized) { + var type = typeof serialized; + + if (type === "boolean") { + return serialized; + } else if (type === "string") { + return serialized.match(/^true$|^t$|^1$/i) !== null; + } else if (type === "number") { + return serialized === 1; + } else { + return false; + } + }, + + serialize: function(deserialized) { + return Boolean(deserialized); + } + }, + + date: { + deserialize: function(serialized) { + var type = typeof serialized; + + if (type === "string") { + return new Date(Ember.Date.parse(serialized)); + } else if (type === "number") { + return new Date(serialized); + } else if (serialized === null || serialized === undefined) { + // if the value is not present in the data, + // return undefined, not null. + return serialized; + } else { + return null; + } + }, + + serialize: function(date) { + if (date instanceof Date) { + var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + + var pad = function(num) { + return num < 10 ? "0"+num : ""+num; + }; + + var utcYear = date.getUTCFullYear(), + utcMonth = date.getUTCMonth(), + utcDayOfMonth = date.getUTCDate(), + utcDay = date.getUTCDay(), + utcHours = date.getUTCHours(), + utcMinutes = date.getUTCMinutes(), + utcSeconds = date.getUTCSeconds(); + + + var dayOfWeek = days[utcDay]; + var dayOfMonth = pad(utcDayOfMonth); + var month = months[utcMonth]; + + return dayOfWeek + ", " + dayOfMonth + " " + month + " " + utcYear + " " + + pad(utcHours) + ":" + pad(utcMinutes) + ":" + pad(utcSeconds) + " GMT"; + } else { + return null; + } + } + } +}; + +})(); + + + +(function() { +var get = Ember.get, set = Ember.set, isNone = Ember.isNone; + +var transforms = DS.JSONTransforms; + +// Simple dispatcher to support overriding the aliased +// method in subclasses. +function aliasMethod(methodName) { + return function() { + return this[methodName].apply(this, arguments); + }; +} + +DS.JSONSerializer = Ember.Object.extend({ + primaryKey: 'id', + + deserialize: function(type, data) { + var store = get(this, 'store'); + + type.eachTransformedAttribute(function(key, type) { + data[key] = transforms[type].deserialize(data[key]); + }); + + type.eachRelationship(function(key, relationship) { + // A link (usually a URL) was already provided in + // normalized form + if (data.links && data.links[key]) { + return; + } + + var type = relationship.type, + value = data[key]; + + if (value == null) { return; } + + if (relationship.kind === 'belongsTo') { + this.deserializeRecordId(data, key, relationship, value); + } else if (relationship.kind === 'hasMany') { + this.deserializeRecordIds(data, key, relationship, value); + } + }, this); + + return data; + }, + + deserializeRecordId: function(data, key, relationship, id) { + if (isNone(id) || id instanceof DS.Model) { + return; + } + + var type; + + if (typeof id === 'number' || typeof id === 'string') { + type = this.typeFor(relationship, key, data); + data[key] = get(this, 'store').recordForId(type, id); + } else if (typeof id === 'object') { + // polymorphic + data[key] = get(this, 'store').recordForId(id.type, id.id); + } + }, + + deserializeRecordIds: function(data, key, relationship, ids) { + for (var i=0, l=ids.length; i 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; + } + +}); + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var set = Ember.set; + +/* + This code registers an injection for Ember.Application. + + If an Ember.js developer defines a subclass of DS.Store on their 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.Store = DS.Store.extend({ + adapter: 'App.MyCustomAdapter' + }); + + App.PostsController = Ember.ArrayController.extend({ + // ... + }); + + When the application is initialized, `App.Store` 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: "store", + + initialize: function(container, application) { + application.register('store:main', application.Store || DS.Store); + application.register('serializer:_default', DS.JSONSerializer); + application.register('serializer:_rest', DS.RESTSerializer); + application.register('adapter:_rest', DS.RESTAdapter); + + // Eagerly generate the store so defaultStore is populated. + // TODO: Do this in a finisher hook + container.lookup('store:main'); + } + }); + + // Keep ED compatible with previous versions of ember + // TODO: Remove the if statement for Ember 1.0 + if (DS.DebugAdapter) { + Application.initializer({ + name: "dataAdapter", + + initialize: function(container, application) { + application.register('dataAdapter:main', DS.DebugAdapter); + } + }); + } + + Application.initializer({ + name: "dataAdapter", + + initialize: function(container, application) { + application.register('dataAdapter:main', DS.DebugAdapter); + } + }); + + Application.initializer({ + name: "injectStore", + + initialize: function(container, application) { + application.inject('controller', 'store', 'store:main'); + application.inject('route', 'store', 'store:main'); + application.inject('serializer', 'store', 'store:main'); + application.inject('dataAdapter', 'store', 'store:main'); + } + }); + +}); + +})(); + + + +(function() { +/** + @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; +} + +})(); + + + +(function() { + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var get = Ember.get, set = Ember.set; + +/** + A record array is an array that contains records of a certain type. The record + array materializes records as needed when they are retrieved for the first + time. You should not create record arrays yourself. Instead, an instance of + DS.RecordArray or its subclasses will be returned by your application's store + in response to queries. + + @class RecordArray + @namespace DS + @extends Ember.ArrayProxy + @uses Ember.Evented +*/ + +DS.RecordArray = Ember.ArrayProxy.extend(Ember.Evented, { + /** + The model type contained by this record array. + + @property type + @type DS.Model + */ + type: null, + + // The array of client ids backing the record array. When a + // record is requested from the record array, the record + // for the client id at the same index is materialized, if + // necessary, by the store. + content: null, + + isLoaded: false, + isUpdating: false, + + // The store that created this record array. + store: null, + + objectAtContent: function(index) { + var content = get(this, 'content'); + + return content.objectAt(index); + }, + + update: function() { + if (get(this, 'isUpdating')) { return; } + + var store = get(this, 'store'), + type = get(this, 'type'); + + store.fetchAll(type, this); + }, + + addRecord: function(record) { + get(this, 'content').addObject(record); + }, + + removeRecord: function(record) { + get(this, 'content').removeObject(record); + } +}); + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var get = Ember.get; + +/** + @class FilteredRecordArray + @namespace DS + @extends DS.RecordArray +*/ +DS.FilteredRecordArray = DS.RecordArray.extend({ + filterFunction: null, + isLoaded: true, + + replace: function() { + var type = get(this, 'type').toString(); + throw new Error("The result of a client-side filter (on " + type + ") is immutable."); + }, + + updateFilter: Ember.observer(function() { + var manager = get(this, 'manager'); + manager.updateFilter(this, get(this, 'type'), get(this, 'filterFunction')); + }, 'filterFunction') +}); + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var get = Ember.get, set = Ember.set; + +/** + @class AdapterPopulatedRecordArray + @namespace DS + @extends DS.RecordArray +*/ +DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({ + query: null, + + replace: function() { + var type = get(this, 'type').toString(); + throw new Error("The result of a server query (on " + type + ") is immutable."); + }, + + load: function(data) { + var store = get(this, 'store'), + type = get(this, 'type'), + records = store.pushMany(type, data); + + this.setProperties({ + content: Ember.A(records), + isLoaded: true + }); + + // TODO: does triggering didLoad event should be the last action of the runLoop? + Ember.run.once(this, 'trigger', 'didLoad'); + } +}); + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var get = Ember.get, set = Ember.set; +var map = Ember.EnumerableUtils.map; + +/** + A ManyArray is a RecordArray that represents the contents of a has-many + relationship. + + The ManyArray is instantiated lazily the first time the relationship is + requested. + + ### Inverses + + Often, the relationships in Ember Data applications will have + an inverse. For example, imagine the following models are + defined: + + App.Post = DS.Model.extend({ + comments: DS.hasMany('App.Comment') + }); + + App.Comment = DS.Model.extend({ + post: DS.belongsTo('App.Post') + }); + + If you created a new instance of `App.Post` and added + a `App.Comment` record to its `comments` has-many + relationship, you would expect the comment's `post` + property to be set to the post that contained + the has-many. + + We call the record to which a relationship belongs the + relationship's _owner_. + + @class ManyArray + @namespace DS + @extends DS.RecordArray +*/ +DS.ManyArray = DS.RecordArray.extend({ + init: function() { + this._super.apply(this, arguments); + this._changesToSync = Ember.OrderedSet.create(); + }, + + /** + The record to which this relationship belongs. + + @property {DS.Model} + @private + */ + owner: null, + + /** + `true` if the relationship is polymorphic, `false` otherwise. + + @property {Boolean} + @private + */ + isPolymorphic: false, + + // LOADING STATE + + isLoaded: false, + + loadingRecordsCount: function(count) { + this.loadingRecordsCount = count; + }, + + loadedRecord: function() { + this.loadingRecordsCount--; + if (this.loadingRecordsCount === 0) { + set(this, 'isLoaded', true); + this.trigger('didLoad'); + } + }, + + fetch: function() { + var records = get(this, 'content'), + store = get(this, 'store'), + owner = get(this, 'owner'); + + var unloadedRecords = records.filterProperty('isEmpty', true); + store.fetchMany(unloadedRecords, owner); + }, + + // Overrides Ember.Array's replace method to implement + replaceContent: function(index, removed, added) { + // Map the array of record objects into an array of client ids. + added = map(added, function(record) { + Ember.assert("You cannot add '" + record.constructor.typeKey + "' records to this relationship (only '" + this.type.typeKey + "' allowed)", !this.type || record instanceof this.type); + return record; + }, this); + + this._super(index, removed, added); + }, + + arrangedContentDidChange: function() { + this.fetch(); + }, + + arrayContentWillChange: function(index, removed, added) { + var owner = get(this, 'owner'), + name = get(this, 'name'); + + if (!owner._suspendedRelationships) { + // This code is the first half of code that continues inside + // of arrayContentDidChange. It gets or creates a change from + // the child object, adds the current owner as the old + // parent if this is the first time the object was removed + // from a ManyArray, and sets `newParent` to null. + // + // Later, if the object is added to another ManyArray, + // the `arrayContentDidChange` will set `newParent` on + // the change. + for (var i=index; i} records + @param {String} type + @param {Resolver} resolver + @return DS.ManyArray + */ + 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); + + unloadedRecords.forEach(function(record) { + record.loadingData(); + }); + + manyArray.loadingRecordsCount = unloadedRecords.length; + + if (unloadedRecords.length) { + unloadedRecords.forEach(function(record) { + this.recordArrayManager.registerWaitingRecordArray(record, manyArray); + }, this); + + this.fetchMany(unloadedRecords, owner, resolver); + } else { + manyArray.set('isLoaded', true); + Ember.run.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. + + @private + @param {DS.Model} owner + @param {any} link + @param {String} type + @param {Resolver} resolver + @return DS.ManyArray + */ + findHasMany: function(owner, link, relationship, resolver) { + var adapter = this.adapterForType(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([])); + _findHasMany(adapter, this, owner, link, relationship, resolver); + return records; + }, + + /** + 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} type + @param {any} query an opaque query to be used by the adapter + @return Promise + */ + findQuery: function(type, query) { + type = this.modelFor(type); + + var array = DS.AdapterPopulatedRecordArray.create({ + type: type, + query: query, + content: Ember.A(), + store: this + }); + + var adapter = this.adapterForType(type), + resolver = Ember.RSVP.defer(); + + 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); + + _findQuery(adapter, this, type, query, array, resolver); + + return promiseArray(resolver.promise); + }, + + /** + 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 {Class} type + @return {DS.AdapterPopulatedRecordArray} + */ + findAll: function(type) { + type = this.modelFor(type); + + return this.fetchAll(type, this.all(type)); + }, + + /** + @method fetchAll + @private + @param type + @param array + @returns Promise + */ + fetchAll: function(type, array) { + var adapter = this.adapterForType(type), + sinceToken = this.typeMapFor(type).metadata.since, + resolver = Ember.RSVP.defer(); + + 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); + + _findAll(adapter, this, type, sinceToken, resolver); + + return promiseArray(resolver.promise); + }, + + /** + @method didUpdateAll + @param 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. + + @method all + @param {Class} type + @return {DS.RecordArray} + */ + all: function(type) { + var typeMap = this.typeMapFor(type), + findAllCache = typeMap.findAllCache; + + if (findAllCache) { return findAllCache; } + + var array = DS.RecordArray.create({ + type: type, + content: Ember.A(), + store: this, + isLoaded: true + }); + + this.recordArrayManager.registerFilteredRecordArray(array, type); + + typeMap.findAllCache = array; + return array; + }, + + /** + 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. + + Note that the existence of a filter on a type will trigger immediate + materialization of all loaded data for a given type, so you might + not want to use filters for a type if you are loading many records + into the store, many of which are not active at any given time. + + In this scenario, you might want to consider filtering the raw + data before loading it into the store. + + @method filter + @param {Class} type + @param {Function} filter + @return {DS.FilteredRecordArray} + */ + filter: function(type, query, filter) { + var promise; + + // allow an optional server query + if (arguments.length === 3) { + promise = this.findQuery(type, query); + } else if (arguments.length === 2) { + filter = query; + } + + type = this.modelFor(type); + + var array = DS.FilteredRecordArray.create({ + type: type, + content: Ember.A(), + store: this, + manager: this.recordArrayManager, + filterFunction: filter + }); + + this.recordArrayManager.registerFilteredRecordArray(array, type, filter); + + if (promise) { + return promise.then(function() { return array; }); + } else { + return array; + } + }, + + /** + 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. + + @method recordIsLoaded + @param {Class} type + @param {string} id + @return {boolean} + */ + recordIsLoaded: function(type, id) { + if (!this.hasRecordForId(type, id)) { return false; } + return !get(this.recordForId(type, id), 'isEmpty'); + }, + + // ............ + // . 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 {Number|String} clientId + @param {DS.Model} record + */ + dataWasUpdated: function(type, record) { + // Because data updates are invoked at the end of the run loop, + // it is possible that a record might be deleted after its data + // has been modified and this method was scheduled to be called. + // + // If that's the case, the record would have already been removed + // from all record arrays; calling updateRecordArrays would just + // add it back. If the record is deleted, just bail. It shouldn't + // give us any more trouble after this. + + if (get(record, 'isDeleted')) { return; } + + if (get(record, 'isLoaded')) { + 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.adapterForType(record.constructor), + operation; + + if (get(record, 'isNew')) { + operation = 'createRecord'; + } else if (get(record, 'isDeleted')) { + operation = 'deleteRecord'; + } else { + operation = 'updateRecord'; + } + + _commit(adapter, this, operation, record, resolver); + }, 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) { + 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 + */ + typeMapFor: function(type) { + var typeMaps = get(this, 'typeMaps'), + guid = Ember.guidFor(type), + typeMap; + + typeMap = typeMaps[guid]; + + if (typeMap) { return typeMap; } + + typeMap = { + idToRecord: {}, + records: [], + metadata: {} + }; + + typeMaps[guid] = typeMap; + + return typeMap; + }, + + // ................ + // . LOADING DATA . + // ................ + + /** + This internal method is used by `push`. + + @method _load + @private + @param {DS.Model} type + @param data + @param prematerialized + */ + _load: function(type, data) { + var id = coerceId(data.id), + record = this.recordForId(type, id); + + record.setupData(data); + 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.) + + @param {String} key + @returns {subclass of DS.Model} + */ + modelFor: function(key) { + if (typeof key !== 'string') { + return key; + } + + var factory = this.container.lookupFactory('model:'+key); + + Ember.assert("No model was found for '" + key + "'", factory); + + factory.store = this; + factory.typeKey = key; + + 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} type + @param {Object} data + @returns DS.Model the record that was created or + updated. + */ + push: function(type, data) { + var serializer = this.serializerFor(type); + type = this.modelFor(type); + + data = serializer.deserialize(type, data); + + this._load(type, data); + + return this.recordForId(type, data.id); + }, + + /** + 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} type + @param {Array} datas + @return {Array} + */ + pushMany: function(type, datas) { + return map(datas, function(data) { + return this.push(type, data); + }, this); + }, + + /** + 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 + @returns DS.Model + */ + 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]); + + var record = type._create({ + id: id, + store: this, + }); + + 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 adapterForType + @private + @param {subclass of DS.Model} type + @returns DS.Adapter + */ + adapterForType: function(type) { + var container = this.container, adapter; + + if (container) { + adapter = container.lookup('adapter:' + type.typeKey) || container.lookup('adapter:application'); + } + + return adapter || get(this, '_adapter'); + }, + + // .............................. + // . 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 + */ + serializerFor: function(type) { + var container = this.container; + + // TODO: Make tests pass without this + + if (!container) { + return DS.JSONSerializer.create({ store: this }); + } + + return container.lookup('serializer:'+type) || + container.lookup('serializer:application') || + container.lookup('serializer:_default'); + } +}); + +// Delegation to the adapter and promise management + +DS.PromiseArray = Ember.ArrayProxy.extend(Ember.PromiseProxyMixin); +DS.PromiseObject = Ember.ObjectProxy.extend(Ember.PromiseProxyMixin); + +function promiseObject(promise) { + return DS.PromiseObject.create({ promise: promise }); +} + +function promiseArray(promise) { + return DS.PromiseArray.create({ promise: promise }); +} + +function isThenable(object) { + return object && typeof object.then === 'function'; +} + +function serializerFor(adapter, type) { + var serializer = adapter.serializer, + defaultSerializer = adapter.defaultSerializer, + container = adapter.container; + + if (container && serializer === undefined) { + serializer = container.lookup('serializer:'+type.typeKey) || + container.lookup('serializer:application') || + container.lookup('serializer:' + defaultSerializer || 'serializer:_default'); + } + + if (serializer === null || serializer === undefined) { + serializer = { + extract: function(store, type, payload) { return payload; } + }; + } + + return serializer; +} + +function _find(adapter, store, type, id, resolver) { + var promise = adapter.find(store, type, id), + serializer = serializerFor(adapter, type); + + return resolve(promise).then(function(payload) { + Ember.assert("You made a request for a " + type.typeKey + " with id " + id + ", but the adapter's response did not have any data", payload); + payload = serializer.extract(store, type, payload, id, 'find'); + + return store.push(type, payload); + }).then(resolver.resolve, resolver.reject); +} + +function _findMany(adapter, store, type, ids, owner, resolver) { + var promise = adapter.findMany(store, type, ids, owner), + serializer = serializerFor(adapter, type); + + return resolve(promise).then(function(payload) { + payload = serializer.extract(store, type, payload, null, 'findMany'); + + store.pushMany(type, payload); + }).then(resolver.resolve, resolver.reject); +} + +function _findHasMany(adapter, store, record, link, relationship, resolver) { + var promise = adapter.findHasMany(store, record, link, relationship), + serializer = serializerFor(adapter, relationship.type); + + return resolve(promise).then(function(payload) { + payload = serializer.extract(store, relationship.type, payload, null, 'findHasMany'); + + var records = store.pushMany(relationship.type, payload); + record.updateHasMany(relationship.key, records); + }).then(resolver.resolve, resolver.reject); +} + +function _findAll(adapter, store, type, sinceToken, resolver) { + var promise = adapter.findAll(store, type, sinceToken), + serializer = serializerFor(adapter, type); + + return resolve(promise).then(function(payload) { + payload = serializer.extract(store, type, payload, null, 'findAll'); + + store.pushMany(type, payload); + store.didUpdateAll(type); + return store.all(type); + }).then(resolver.resolve, resolver.reject); +} + +function _findQuery(adapter, store, type, query, recordArray, resolver) { + var promise = adapter.findQuery(store, type, query, recordArray), + serializer = serializerFor(adapter, type); + + return resolve(promise).then(function(payload) { + payload = serializer.extract(store, type, payload, null, 'findAll'); + + recordArray.load(payload); + return recordArray; + }).then(resolver.resolve, resolver.reject); +} + +function _commit(adapter, store, operation, record, resolver) { + var type = record.constructor, + promise = adapter[operation](store, type, record), + serializer = serializerFor(adapter, type); + + Ember.assert("Your adapter's '" + operation + "' method must return a promise, but it returned " + promise, isThenable(promise)); + + return promise.then(function(payload) { + payload = serializer.extract(store, type, payload, get(record, 'id'), operation); + store.didSaveRecord(record, payload); + return record; + }, function(reason) { + if (reason instanceof DS.InvalidError) { + store.recordWasInvalid(record, reason.errors); + } else { + store.recordWasError(record, reason); + } + + throw reason; + }).then(resolver.resolve, resolver.reject); +} + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var get = Ember.get, set = Ember.set, + once = Ember.run.once, arrayMap = Ember.ArrayPolyfills.map; + +/* + WARNING: Much of these docs are inaccurate as of bf8497. + + This file encapsulates the various states that a record can transition + through during its lifecycle. + + ### State Manager + + A record's state manager explicitly tracks what state a record is in + at any given time. For instance, if a record is newly created and has + not yet been sent to the adapter to be saved, it would be in the + `created.uncommitted` state. If a record has had local modifications + made to it that are in the process of being saved, the record would be + in the `updated.inFlight` state. (These state paths will be explained + in more detail below.) + + Events are sent by the record or its store to the record's state manager. + How the state manager reacts to these events is dependent on which state + it is in. In some states, certain events will be invalid and will cause + an exception to be raised. + + States are hierarchical. For example, a record can be in the + `deleted.start` state, then transition into the `deleted.inFlight` state. + If a child state does not implement an event handler, the state manager + will attempt to invoke the event on all parent states until the root state is + reached. The state hierarchy of a record is described in terms of a path + string. You can determine a record's current state by getting its manager's + current state path: + + record.get('stateManager.currentPath'); + //=> "created.uncommitted" + + The `DS.Model` states are themselves stateless. What we mean is that, + though each instance of a record also has a unique instance of a + `DS.StateManager`, 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 state + manager points to this global, immutable shared instance. How does a + state know which record it should be acting on? We pass a reference to + the current state manager as the first parameter to every method invoked + on a state. + + The state manager 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. If you need access to the record being acted on, you can + retrieve the state manager's `record` property. For example, if you had + an event handler `myEvent`: + + myEvent: function(manager) { + var record = manager.get('record'); + record.doSomething(); + } + + For more information about state managers in general, see the Ember.js + documentation on `Ember.StateManager`. + + ### Events, Flags, and Transitions + + A state may implement zero or more events, flags, or transitions. + + #### Events + + Events are named functions that are invoked when sent to a record. The + state manager 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: + + aState: DS.State.create({ + myEvent: function(manager, param) { + console.log("Received myEvent with "+param); + } + }) + + To trigger this event: + + 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 state manager'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 + * start <-- currentState + * inFlight + * updated + * inFlight + + If we are currently in the `start` 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: + + var statePath = record.get('stateManager.currentPath'); + if (statePath === 'created.inFlight') { + doSomething(); + } + + You can say: + + 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. + + #### Transitions + + Transitions are like event handlers but are called automatically upon + entering or exiting a state. To implement a transition, just call a method + either `enter` or `exit`: + + myState: DS.State.create({ + // Gets called automatically when entering + // this state. + enter: function(manager) { + console.log("Entered myState"); + } + }) + + Note that enter and exit events are called once per transition. If the + current state changes, but changes to another child state of the parent, + the transition event on the parent will not be triggered. +*/ + +var hasDefinedProperties = function(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; +}; + +var didSetProperty = function(record, context) { + 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: No client-side validations have failed and 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, + + pushedData: Ember.K, + + becomeDirty: Ember.K, + + willCommit: function(record) { + record.transitionTo('inFlight'); + }, + + reloadRecord: function(record, resolver) { + get(record, 'store').reloadRecord(record, resolver); + }, + + becameClean: 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, + + // 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, errors) { + set(record, 'errors', errors); + + record.transitionTo('invalid'); + record.send('invokeLifecycleCallbacks'); + }, + + becameError: function(record) { + record.transitionTo('uncommitted'); + record.triggerLater('becameError', record); + } + }, + + // A record is in the `invalid` state when its client-side + // invalidations have failed, or 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) { + var errors = get(record, 'errors'), + key = context.name; + + set(errors, key, null); + + if (!hasDefinedProperties(errors)) { + record.send('becameValid'); + } + + didSetProperty(record, context); + }, + + becomeDirty: Ember.K, + + rollback: function(record) { + record.send('becameValid'); + record.send('rollback'); + }, + + becameValid: function(record) { + record.transitionTo('uncommitted'); + }, + + invokeLifecycleCallbacks: function(record) { + record.triggerLater('becameInvalid', record); + } + } +}; + +// 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 +}); + +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'); +}; + +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, + + // 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) { + record.transitionTo('loading'); + }, + + loadedData: function(record) { + record.transitionTo('loaded.created.uncommitted'); + + record.suspendRelationshipObservers(function() { + record.notifyPropertyChange('data'); + }); + }, + + pushedData: function(record) { + record.transitionTo('loaded.saved'); + } + }, + + // A record enters this state when the store askes + // 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, + + // EVENTS + pushedData: function(record) { + record.transitionTo('loaded.saved'); + record.triggerLater('didLoad'); + set(record, 'isError', false); + }, + + becameError: function(record) { + record.triggerLater('becameError', record); + } + }, + + // 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, resolver) { + get(record, 'store').reloadRecord(record, resolver); + }, + + 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')); + }, + + }, + + // 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) { + var store = get(record, 'store'); + + store.recordArrayManager.remove(record); + }, + + // 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, + + becameClean: 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 + + // TODO: More robust semantics around save-while-in-flight + willCommit: Ember.K, + didCommit: function(record) { + record.transitionTo('saved'); + + record.send('invokeLifecycleCallbacks'); + } + }, + + // 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); + } + } + }, + + invokeLifecycleCallbacks: function(record, dirtyType) { + if (dirtyType === 'created') { + record.triggerLater('didCreate', record); + } else { + record.triggerLater('didUpdate', record); + } + + record.triggerLater('didCommit', record); + } +}; + +var hasOwnProp = {}.hasOwnProperty; + +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"); + +DS.RootState = RootState; + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var get = Ember.get, set = Ember.set, map = Ember.EnumerableUtils.map, + merge = Ember.merge, once = Ember.run.once; + +var arrayMap = Ember.ArrayPolyfills.map; + +var retrieveFromCurrentState = Ember.computed(function(key, value) { + return get(get(this, 'currentState'), key); +}).property('currentState').readOnly(); + +/** + + The model class that all Ember Data records descend from. + + @class Model + @namespace DS + @extends Ember.Object + @uses Ember.Evented +*/ +DS.Model = Ember.Object.extend(Ember.Evented, { + isEmpty: retrieveFromCurrentState, + isLoading: retrieveFromCurrentState, + isLoaded: retrieveFromCurrentState, + isDirty: retrieveFromCurrentState, + isSaving: retrieveFromCurrentState, + isDeleted: retrieveFromCurrentState, + isNew: retrieveFromCurrentState, + isValid: retrieveFromCurrentState, + dirtyType: retrieveFromCurrentState, + + isError: false, + isReloading: false, + + clientId: null, + id: null, + transaction: null, + currentState: null, + errors: null, + + /** + Create a JSON representation of the record, using the serialization + strategy of the store's adapter. + + @method serialize + @param {Object} options Available options: + + * `includeId`: `true` if the record's ID should be included in the + JSON representation. + + @returns {Object} an object whose values are primitive JSON values only + */ + serialize: function(options) { + var store = get(this, 'store'); + return store.serialize(this, options); + }, + + /** + Use {{#crossLink "DS.JSONSerializer"}}DS.JSONSerializer{{/crossLink}} to + get the JSON representation of a record. + + @method toJSON + @param {Object} options Available options: + + * `includeId`: `true` if the record's ID should be included in the + JSON representation. + + @returns {Object} A JSON representation of the object. + */ + toJSON: function(options) { + var serializer = DS.JSONSerializer.create(); + return serializer.serialize(this, options); + }, + + /** + Fired when the record is loaded from the server. + + @event didLoad + */ + didLoad: Ember.K, + + /** + Fired when the record is reloaded from the server. + + @event didReload + */ + didReload: 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, + + data: Ember.computed(function() { + this._data = this._data || {}; + return this._data; + }).property(), + + _data: null, + + init: function() { + set(this, 'currentState', DS.RootState.empty); + this._super(); + this._setup(); + }, + + _setup: function() { + this._changesToSync = {}; + this._deferredTriggers = []; + this._data = {}; + this._attributes = {}; + this._inFlightAttributes = {}; + this._relationships = {}; + }, + + send: function(name, context) { + var currentState = get(this, 'currentState'); + + if (!currentState[name]) { + this._unhandledEvent(currentState, name, context); + } + + return currentState[name](this, context); + }, + + 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')` from " + this.toString(), name !== 'id'); + + meta.name = name; + map.set(name, meta); + } + }); + + return map; + }), + + transformedAttributes: Ember.computed(function() { + var map = Ember.Map.create(); + + this.eachAttribute(function(key, meta) { + if (meta.type) { + map.set(key, meta.type); + } + }); + + return map; + }), + + eachAttribute: function(callback, binding) { + get(this, 'attributes').forEach(function(name, meta) { + callback.call(binding, name, meta); + }, binding); + }, + + eachTransformedAttribute: function(callback, binding) { + get(this, 'transformedAttributes').forEach(function(name, type) { + callback.call(binding, name, type); + }); + } +}); + + +DS.Model.reopen({ + eachAttribute: function(callback, binding) { + this.constructor.eachAttribute(callback, binding); + } +}); + +function getAttr(record, options, key) { + var attributes = get(record, 'data'); + var value = attributes[key]; + + if (value === undefined) { + if (typeof options.defaultValue === "function") { + value = options.defaultValue(); + } else { + value = options.defaultValue; + } + } + + return value; +} + +DS.attr = function(type, options) { + options = options || {}; + + var meta = { + type: type, + isAttribute: true, + options: options + }; + + return Ember.computed(function(key, value, oldValue) { + 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'); + this.send('didSetProperty', { name: key, oldValue: this._attributes[key] || this._inFlightAttributes[key] || this._data[key], value: value }); + this._attributes[key] = value; + } else if (this._attributes[key]) { + return this._attributes[key]; + } else { + value = getAttr(this, options, key); + } + + return value; + // `data` is never set directly. However, it may be + // invalidated from the state manager's setData + // event. + }).property('data').meta(meta); +}; + + +})(); + + + +(function() { +/** + @module ember-data +*/ + +})(); + + + +(function() { +/** + @module ember-data +*/ + +/** + An AttributeChange object is created whenever a record's + attribute changes value. It is used to track changes to a + record between transaction commits. + + @class AttributeChange + @namespace DS + @private + @constructor +*/ +var AttributeChange = DS.AttributeChange = function(options) { + this.record = options.record; + this.store = options.store; + this.name = options.name; + this.value = options.value; + this.oldValue = options.oldValue; +}; + +AttributeChange.createChange = function(options) { + return new AttributeChange(options); +}; + +AttributeChange.prototype = { + sync: function() { + if (this.value !== this.oldValue) { + this.record.send('becomeDirty'); + this.record.updateRecordArraysLater(); + } + + // TODO: Use this object in the commit process + this.destroy(); + }, + + /** + If the AttributeChange is destroyed (either by being rolled back + or being committed), remove it from the list of pending changes + on the record. + + @method destroy + */ + destroy: function() { + delete this.record._changesToSync[this.name]; + } +}; + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var get = Ember.get, set = Ember.set; +var forEach = Ember.EnumerableUtils.forEach; + +/** + @class RelationshipChange + @namespace DS + @private + @construtor +*/ +DS.RelationshipChange = function(options) { + this.parentRecord = options.parentRecord; + this.childRecord = options.childRecord; + this.firstRecord = options.firstRecord; + this.firstRecordKind = options.firstRecordKind; + this.firstRecordName = options.firstRecordName; + this.secondRecord = options.secondRecord; + this.secondRecordKind = options.secondRecordKind; + this.secondRecordName = options.secondRecordName; + this.changeType = options.changeType; + this.store = options.store; + + this.committed = {}; +}; + +/** + @class RelationshipChangeAdd + @namespace DS + @private + @construtor +*/ +DS.RelationshipChangeAdd = function(options){ + DS.RelationshipChange.call(this, options); +}; + +/** + @class RelationshipChangeRemove + @namespace DS + @private + @construtor +*/ +DS.RelationshipChangeRemove = function(options){ + DS.RelationshipChange.call(this, options); +}; + +DS.RelationshipChange.create = function(options) { + return new DS.RelationshipChange(options); +}; + +DS.RelationshipChangeAdd.create = function(options) { + return new DS.RelationshipChangeAdd(options); +}; + +DS.RelationshipChangeRemove.create = function(options) { + return new DS.RelationshipChangeRemove(options); +}; + +DS.OneToManyChange = {}; +DS.OneToNoneChange = {}; +DS.ManyToNoneChange = {}; +DS.OneToOneChange = {}; +DS.ManyToManyChange = {}; + +DS.RelationshipChange._createChange = function(options){ + if(options.changeType === "add"){ + return DS.RelationshipChangeAdd.create(options); + } + if(options.changeType === "remove"){ + return DS.RelationshipChangeRemove.create(options); + } +}; + + +DS.RelationshipChange.determineRelationshipType = function(recordType, knownSide){ + var knownKey = knownSide.key, key, otherKind; + var knownKind = knownSide.kind; + + var inverse = recordType.inverseFor(knownKey); + + if (inverse){ + key = inverse.name; + otherKind = inverse.kind; + } + + if (!inverse){ + return knownKind === "belongsTo" ? "oneToNone" : "manyToNone"; + } + else{ + if(otherKind === "belongsTo"){ + return knownKind === "belongsTo" ? "oneToOne" : "manyToOne"; + } + else{ + return knownKind === "belongsTo" ? "oneToMany" : "manyToMany"; + } + } + +}; + +DS.RelationshipChange.createChange = function(firstRecord, secondRecord, store, options){ + // Get the type of the child based on the child's client ID + var firstRecordType = firstRecord.constructor, changeType; + changeType = DS.RelationshipChange.determineRelationshipType(firstRecordType, options); + if (changeType === "oneToMany"){ + return DS.OneToManyChange.createChange(firstRecord, secondRecord, store, options); + } + else if (changeType === "manyToOne"){ + return DS.OneToManyChange.createChange(secondRecord, firstRecord, store, options); + } + else if (changeType === "oneToNone"){ + return DS.OneToNoneChange.createChange(firstRecord, secondRecord, store, options); + } + else if (changeType === "manyToNone"){ + return DS.ManyToNoneChange.createChange(firstRecord, secondRecord, store, options); + } + else if (changeType === "oneToOne"){ + return DS.OneToOneChange.createChange(firstRecord, secondRecord, store, options); + } + else if (changeType === "manyToMany"){ + return DS.ManyToManyChange.createChange(firstRecord, secondRecord, store, options); + } +}; + +DS.OneToNoneChange.createChange = function(childRecord, parentRecord, store, options) { + var key = options.key; + var change = DS.RelationshipChange._createChange({ + parentRecord: parentRecord, + childRecord: childRecord, + firstRecord: childRecord, + store: store, + changeType: options.changeType, + firstRecordName: key, + firstRecordKind: "belongsTo" + }); + + store.addRelationshipChangeFor(childRecord, key, parentRecord, null, change); + + return change; +}; + +DS.ManyToNoneChange.createChange = function(childRecord, parentRecord, store, options) { + var key = options.key; + var change = DS.RelationshipChange._createChange({ + parentRecord: childRecord, + childRecord: parentRecord, + secondRecord: childRecord, + store: store, + changeType: options.changeType, + secondRecordName: options.key, + secondRecordKind: "hasMany" + }); + + store.addRelationshipChangeFor(childRecord, key, parentRecord, null, change); + return change; +}; + + +DS.ManyToManyChange.createChange = function(childRecord, parentRecord, store, options) { + // If the name of the belongsTo side of the relationship is specified, + // use that + // If the type of the parent is specified, look it up on the child's type + // definition. + var key = options.key; + + var change = DS.RelationshipChange._createChange({ + parentRecord: parentRecord, + childRecord: childRecord, + firstRecord: childRecord, + secondRecord: parentRecord, + firstRecordKind: "hasMany", + secondRecordKind: "hasMany", + store: store, + changeType: options.changeType, + firstRecordName: key + }); + + store.addRelationshipChangeFor(childRecord, key, parentRecord, null, change); + + + return change; +}; + +DS.OneToOneChange.createChange = function(childRecord, parentRecord, store, options) { + var key; + + // If the name of the belongsTo side of the relationship is specified, + // use that + // If the type of the parent is specified, look it up on the child's type + // definition. + if (options.parentType) { + key = options.parentType.inverseFor(options.key).name; + } else if (options.key) { + key = options.key; + } else { + Ember.assert("You must pass either a parentType or belongsToName option to OneToManyChange.forChildAndParent", false); + } + + var change = DS.RelationshipChange._createChange({ + parentRecord: parentRecord, + childRecord: childRecord, + firstRecord: childRecord, + secondRecord: parentRecord, + firstRecordKind: "belongsTo", + secondRecordKind: "belongsTo", + store: store, + changeType: options.changeType, + firstRecordName: key + }); + + store.addRelationshipChangeFor(childRecord, key, parentRecord, null, change); + + + return change; +}; + +DS.OneToOneChange.maintainInvariant = function(options, store, childRecord, key){ + if (options.changeType === "add" && store.recordIsMaterialized(childRecord)) { + var oldParent = get(childRecord, key); + if (oldParent){ + var correspondingChange = DS.OneToOneChange.createChange(childRecord, oldParent, store, { + parentType: options.parentType, + hasManyName: options.hasManyName, + changeType: "remove", + key: options.key + }); + store.addRelationshipChangeFor(childRecord, key, options.parentRecord , null, correspondingChange); + correspondingChange.sync(); + } + } +}; + +DS.OneToManyChange.createChange = function(childRecord, parentRecord, store, options) { + var key; + + // If the name of the belongsTo side of the relationship is specified, + // use that + // If the type of the parent is specified, look it up on the child's type + // definition. + if (options.parentType) { + key = options.parentType.inverseFor(options.key).name; + DS.OneToManyChange.maintainInvariant( options, store, childRecord, key ); + } else if (options.key) { + key = options.key; + } else { + Ember.assert("You must pass either a parentType or belongsToName option to OneToManyChange.forChildAndParent", false); + } + + var change = DS.RelationshipChange._createChange({ + parentRecord: parentRecord, + childRecord: childRecord, + firstRecord: childRecord, + secondRecord: parentRecord, + firstRecordKind: "belongsTo", + secondRecordKind: "hasMany", + store: store, + changeType: options.changeType, + firstRecordName: key + }); + + store.addRelationshipChangeFor(childRecord, key, parentRecord, change.getSecondRecordName(), change); + + + return change; +}; + + +DS.OneToManyChange.maintainInvariant = function(options, store, childRecord, key){ + if (options.changeType === "add" && childRecord) { + var oldParent = get(childRecord, key); + if (oldParent){ + var correspondingChange = DS.OneToManyChange.createChange(childRecord, oldParent, store, { + parentType: options.parentType, + hasManyName: options.hasManyName, + changeType: "remove", + key: options.key + }); + store.addRelationshipChangeFor(childRecord, key, options.parentRecord, correspondingChange.getSecondRecordName(), correspondingChange); + correspondingChange.sync(); + } + } +}; + +/** + @class RelationshipChange + @namespace DS +*/ +DS.RelationshipChange.prototype = { + + getSecondRecordName: function() { + var name = this.secondRecordName, parent; + + if (!name) { + parent = this.secondRecord; + if (!parent) { return; } + + var childType = this.firstRecord.constructor; + var inverse = childType.inverseFor(this.firstRecordName); + this.secondRecordName = inverse.name; + } + + return this.secondRecordName; + }, + + /** + Get the name of the relationship on the belongsTo side. + + @method getFirstRecordName + @return {String} + */ + getFirstRecordName: function() { + var name = this.firstRecordName; + return name; + }, + + /** + @method destroy + @private + */ + destroy: function() { + var childRecord = this.childRecord, + belongsToName = this.getFirstRecordName(), + hasManyName = this.getSecondRecordName(), + store = this.store; + + store.removeRelationshipChangeFor(childRecord, belongsToName, this.parentRecord, hasManyName, this.changeType); + }, + + getSecondRecord: function(){ + return this.secondRecord; + }, + + /** + @method getFirstRecord + @private + */ + getFirstRecord: function() { + return this.firstRecord; + }, + + coalesce: function(){ + var relationshipPairs = this.store.relationshipChangePairsFor(this.firstRecord); + forEach(relationshipPairs, function(pair){ + var addedChange = pair["add"]; + var removedChange = pair["remove"]; + if(addedChange && removedChange) { + addedChange.destroy(); + removedChange.destroy(); + } + }); + } +}; + +DS.RelationshipChangeAdd.prototype = Ember.create(DS.RelationshipChange.create({})); +DS.RelationshipChangeRemove.prototype = Ember.create(DS.RelationshipChange.create({})); + +// the object is a value, and not a promise +function isValue(object) { + return typeof object === 'object' && (!object.then || typeof object.then !== 'function'); +} + +DS.RelationshipChangeAdd.prototype.changeType = "add"; +DS.RelationshipChangeAdd.prototype.sync = function() { + var secondRecordName = this.getSecondRecordName(), + firstRecordName = this.getFirstRecordName(), + firstRecord = this.getFirstRecord(), + secondRecord = this.getSecondRecord(); + + //Ember.assert("You specified a hasMany (" + hasManyName + ") on " + (!belongsToName && (newParent || oldParent || this.lastParent).constructor) + " but did not specify an inverse belongsTo on " + child.constructor, belongsToName); + //Ember.assert("You specified a belongsTo (" + belongsToName + ") on " + child.constructor + " but did not specify an inverse hasMany on " + (!hasManyName && (newParent || oldParent || this.lastParentRecord).constructor), hasManyName); + + if (secondRecord instanceof DS.Model && firstRecord instanceof DS.Model) { + if(this.secondRecordKind === "belongsTo"){ + secondRecord.suspendRelationshipObservers(function(){ + set(secondRecord, secondRecordName, firstRecord); + }); + + } + else if(this.secondRecordKind === "hasMany"){ + secondRecord.suspendRelationshipObservers(function(){ + var relationship = get(secondRecord, secondRecordName); + if (isValue(relationship)) { relationship.addObject(firstRecord); } + }); + } + } + + if (firstRecord instanceof DS.Model && secondRecord instanceof DS.Model && get(firstRecord, firstRecordName) !== secondRecord) { + if(this.firstRecordKind === "belongsTo"){ + firstRecord.suspendRelationshipObservers(function(){ + set(firstRecord, firstRecordName, secondRecord); + }); + } + else if(this.firstRecordKind === "hasMany"){ + firstRecord.suspendRelationshipObservers(function(){ + var relationship = get(firstRecord, firstRecordName); + if (isValue(relationship)) { relationship.addObject(secondRecord); } + }); + } + } + + this.coalesce(); +}; + +DS.RelationshipChangeRemove.prototype.changeType = "remove"; +DS.RelationshipChangeRemove.prototype.sync = function() { + var secondRecordName = this.getSecondRecordName(), + firstRecordName = this.getFirstRecordName(), + firstRecord = this.getFirstRecord(), + secondRecord = this.getSecondRecord(); + + //Ember.assert("You specified a hasMany (" + hasManyName + ") on " + (!belongsToName && (newParent || oldParent || this.lastParent).constructor) + " but did not specify an inverse belongsTo on " + child.constructor, belongsToName); + //Ember.assert("You specified a belongsTo (" + belongsToName + ") on " + child.constructor + " but did not specify an inverse hasMany on " + (!hasManyName && (newParent || oldParent || this.lastParentRecord).constructor), hasManyName); + + if (secondRecord instanceof DS.Model && firstRecord instanceof DS.Model) { + if(this.secondRecordKind === "belongsTo"){ + secondRecord.suspendRelationshipObservers(function(){ + set(secondRecord, secondRecordName, null); + }); + } + else if(this.secondRecordKind === "hasMany"){ + secondRecord.suspendRelationshipObservers(function(){ + var relationship = get(secondRecord, secondRecordName); + if (isValue(relationship)) { relationship.removeObject(firstRecord); } + }); + } + } + + if (firstRecord instanceof DS.Model && get(firstRecord, firstRecordName)) { + if(this.firstRecordKind === "belongsTo"){ + firstRecord.suspendRelationshipObservers(function(){ + set(firstRecord, firstRecordName, null); + }); + } + else if(this.firstRecordKind === "hasMany"){ + firstRecord.suspendRelationshipObservers(function(){ + var relationship = get(firstRecord, firstRecordName); + if (isValue(relationship)) { relationship.removeObject(secondRecord); } + }); + } + } + + this.coalesce(); +}; + +})(); + + + +(function() { +/** + @module ember-data +*/ + +})(); + + + +(function() { +var get = Ember.get, set = Ember.set, + isNone = Ember.isNone; + +/** + @module ember-data +*/ + +function asyncBelongsTo(type, options, meta) { + return Ember.computed(function(key, value) { + var data = get(this, 'data'), + store = get(this, 'store'); + + if (arguments.length === 2) { + Ember.assert("You can only add a '" + type + "' record to this relationship", !value || store.modelFor(type).detectInstance(value)); + return value === undefined ? null : value; + } + + return store.fetchRecord(data[key]); + }).property('data').meta(meta); +} + +DS.belongsTo = function(type, options) { + Ember.assert("The first argument DS.belongsTo must be a model type or string, like DS.belongsTo(App.Person)", !!type && (typeof type === 'string' || DS.Model.detect(type))); + + options = options || {}; + + var meta = { type: type, isRelationship: true, options: options, kind: 'belongsTo' }; + + if (options.async) { + return asyncBelongsTo(type, options, meta); + } + + return Ember.computed(function(key, value) { + var data = get(this, 'data'), + store = get(this, 'store'), belongsTo, typeClass; + + if (typeof type === 'string') { + if (type.indexOf(".") === -1) { + typeClass = store.modelFor(type); + } else { + typeClass = get(Ember.lookup, type); + } + } else { + typeClass = type; + } + + if (arguments.length === 2) { + Ember.assert("You can only add a '" + type + "' record to this relationship", !value || typeClass.detectInstance(value)); + return value === undefined ? null : value; + } + + belongsTo = data[key]; + + if (isNone(belongsTo)) { return null; } + + if (get(belongsTo, 'isEmpty')) { + store.fetchRecord(belongsTo); + } + + return belongsTo; + }).property('data').meta(meta); +}; + +/* + These observers observe all `belongsTo` relationships on the record. See + `relationships/ext` to see how these observers get their dependencies. + + @class Model + @namespace DS +*/ +DS.Model.reopen({ + + /** + @method belongsToWillChange + @private + @static + @param record + @param key + */ + belongsToWillChange: Ember.beforeObserver(function(record, key) { + if (get(record, 'isLoaded')) { + var oldParent = get(record, key), + store = get(record, 'store'); + + if (oldParent){ + var change = DS.RelationshipChange.createChange(record, oldParent, store, { key: key, kind: "belongsTo", changeType: "remove" }); + change.sync(); + this._changesToSync[key] = change; + } + } + }), + + /** + @method belongsToDidChange + @private + @static + @param record + @param key + */ + belongsToDidChange: Ember.immediateObserver(function(record, key) { + if (get(record, 'isLoaded')) { + var newParent = get(record, key); + if(newParent){ + var store = get(record, 'store'), + change = DS.RelationshipChange.createChange(record, newParent, store, { key: key, kind: "belongsTo", changeType: "add" }); + + change.sync(); + } + } + + delete this._changesToSync[key]; + }) +}); + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var get = Ember.get, set = Ember.set, setProperties = Ember.setProperties; +var forEach = Ember.EnumerableUtils.forEach; + +function asyncHasMany(type, options, meta) { + return Ember.computed(function(key, value) { + var resolver = Ember.RSVP.defer(); + + var relationship = buildRelationship(this, key, options, function(store, data) { + var link = data.links && data.links[key]; + + if (link) { + return store.findHasMany(this, link, meta, resolver); + } else { + return store.findMany(this, data[key], meta.type, resolver); + } + }); + + var promise = resolver.promise.then(function() { + return relationship; + }); + + return DS.PromiseArray.create({ promise: promise }); + }).property('data').meta(meta); +} + +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' }; + + if (options.async) { + return asyncHasMany(type, options, meta); + } + + return Ember.computed(function(key, value) { + 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.attr({ async: true })`)", Ember.A(records).everyProperty('isEmpty', false)); + return store.findMany(this, data[key], meta.type); + }); + }).property('data').meta(meta); +} + +DS.hasMany = function(type, options) { + Ember.assert("The type passed to DS.hasMany must be defined", !!type); + return hasRelationship(type, options); +}; + +})(); + + + +(function() { +var get = Ember.get, set = Ember.set; + +/** + @module ember-data +*/ + +/* + This file defines several extensions to the base `DS.Model` class that + add support for one-to-many relationships. +*/ + +/** + @class Model + @namespace DS +*/ +DS.Model.reopen({ + + /** + This Ember.js hook allows an object to be notified when a property + is defined. + + In this case, we use it to be notified when an Ember Data user defines a + belongs-to relationship. In that case, we need to set up observers for + each one, allowing us to track relationship changes and automatically + reflect changes in the inverse has-many array. + + This hook passes the class being set up, as well as the key and value + being defined. So, for example, when the user does this: + + DS.Model.extend({ + parent: DS.belongsTo(App.User) + }); + + This hook would be called with "parent" as the key and the computed + property returned by `DS.belongsTo` as the value. + + @method didDefineProperty + @param proto + @param key + @param value + */ + didDefineProperty: function(proto, key, value) { + // Check if the value being set is a computed property. + if (value instanceof Ember.Descriptor) { + + // If it is, get the metadata for the relationship. This is + // populated by the `DS.belongsTo` helper when it is creating + // the computed property. + var meta = value.meta(); + + if (meta.isRelationship && meta.kind === 'belongsTo') { + Ember.addObserver(proto, key, null, 'belongsToDidChange'); + Ember.addBeforeObserver(proto, key, null, 'belongsToWillChange'); + } + + meta.parentType = proto.constructor; + } + } +}); + +/* + These DS.Model extensions add class methods that provide relationship + introspection abilities about relationships. + + A note about the computed properties contained here: + + **These properties are effectively sealed once called for the first time.** + To avoid repeatedly doing expensive iteration over a model's fields, these + values are computed once and then cached for the remainder of the runtime of + your application. + + If your application needs to modify a class after its initial definition + (for example, using `reopen()` to add additional attributes), make sure you + do it before using your model with the store, which uses these properties + extensively. +*/ + +DS.Model.reopenClass({ + /** + For a given relationship name, returns the model type of the relationship. + + For example, if you define a model like this: + + App.Post = DS.Model.extend({ + comments: DS.hasMany(App.Comment) + }); + + Calling `App.Post.typeForRelationship('comments')` will return `App.Comment`. + + @method typeForRelationship + @static + @param {String} name the name of the relationship + @return {subclass of DS.Model} the type of the relationship, or undefined + */ + typeForRelationship: function(name) { + var relationship = get(this, 'relationshipsByName').get(name); + return relationship && relationship.type; + }, + + inverseFor: function(name) { + var inverseType = this.typeForRelationship(name); + + if (!inverseType) { return null; } + + var options = this.metaForProperty(name).options; + + if (options.inverse === null) { return null; } + + var inverseName, inverseKind; + + if (options.inverse) { + inverseName = options.inverse; + inverseKind = Ember.get(inverseType, 'relationshipsByName').get(inverseName).kind; + } else { + var possibleRelationships = findPossibleInverses(this, inverseType); + + if (possibleRelationships.length === 0) { return null; } + + Ember.assert("You defined the '" + name + "' relationship on " + this + ", but multiple possible inverse relationships of type " + this + " were found on " + inverseType + ".", possibleRelationships.length === 1); + + inverseName = possibleRelationships[0].name; + inverseKind = possibleRelationships[0].kind; + } + + function findPossibleInverses(type, inverseType, possibleRelationships) { + possibleRelationships = possibleRelationships || []; + + var relationshipMap = get(inverseType, 'relationships'); + if (!relationshipMap) { return; } + + var relationships = relationshipMap.get(type); + if (relationships) { + possibleRelationships.push.apply(possibleRelationships, relationshipMap.get(type)); + } + + if (type.superclass) { + findPossibleInverses(type.superclass, inverseType, possibleRelationships); + } + + return possibleRelationships; + } + + return { + type: inverseType, + name: inverseName, + kind: inverseKind + }; + }, + + /** + The model's relationships as a map, keyed on the type of the + relationship. The value of each entry is an array containing a descriptor + for each relationship with that type, describing the name of the relationship + as well as the type. + + For example, given the following model definition: + + App.Blog = DS.Model.extend({ + users: DS.hasMany(App.User), + owner: DS.belongsTo(App.User), + posts: DS.hasMany(App.Post) + }); + + This computed property would return a map describing these + relationships, like this: + + var relationships = Ember.get(App.Blog, 'relationships'); + relationships.get(App.User); + //=> [ { 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) { + if (typeof meta.type === 'string') { + meta.type = Ember.get(Ember.lookup, meta.type); + } + + var relationshipsForType = map.get(meta.type); + + relationshipsForType.push({ name: name, kind: meta.kind }); + } + }); + + return map; + }), + + /** + A hash containing lists of the model's relationships, grouped + by the relationship kind. For example, given a model with this + definition: + + App.Blog = DS.Model.extend({ + users: DS.hasMany(App.User), + owner: DS.belongsTo(App.User), + + posts: DS.hasMany(App.Post) + }); + + This property would contain the following: + + 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: + + App.Blog = DS.Model.extend({ + users: DS.hasMany(App.User), + owner: DS.belongsTo(App.User), + posts: DS.hasMany(App.Post) + }); + + This property would contain the following: + + 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) { + type = meta.type; + + if (typeof type === 'string') { + type = get(this, type, false) || get(Ember.lookup, type); + } + + 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; + }), + + /** + A map whose keys are the relationships of a model and whose values are + relationship descriptors. + + For example, given a model with this + definition: + + App.Blog = DS.Model.extend({ + users: DS.hasMany(App.User), + owner: DS.belongsTo(App.User), + + posts: DS.hasMany(App.Post) + }); + + This property would contain the following: + + 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(), type; + + this.eachComputedProperty(function(name, meta) { + if (meta.isRelationship) { + meta.key = name; + type = meta.type; + + if (typeof type === 'string') { + meta.type = this.store.modelFor(type); + } + + map.set(name, meta); + } + }); + + return map; + }), + + /** + 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: + + App.Blog = DS.Model.extend({ + users: DS.hasMany(App.User), + owner: DS.belongsTo(App.User), + + posts: DS.hasMany(App.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); + }); + } +}); + +DS.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); + } +}); + +})(); + + + +(function() { +/** + @module ember-data +*/ + +})(); + + + +(function() { +/** + @module ember-data +*/ + +var get = Ember.get, set = Ember.set; +var once = Ember.run.once; +var forEach = Ember.EnumerableUtils.forEach; + +/** + @class RecordArrayManager + @namespace DS + @private + @extends Ember.Object +*/ +DS.RecordArrayManager = Ember.Object.extend({ + init: function() { + this.filteredRecordArrays = Ember.MapWithDefault.create({ + defaultValue: function() { return []; } + }); + + this.changedRecords = []; + }, + + recordDidChange: function(record) { + this.changedRecords.push(record); + once(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 an attribute + changes on a record. + + It updates all filters that a record belongs to. + + To avoid thrashing, it only runs once per run loop per record. + + @method updateRecordArrays + @param {Class} type + @param {Number|String} clientId + */ + updateRecordArrays: function() { + forEach(this.changedRecords, 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} The primary array that was returned in response + to the original query. + */ + extractArray: function(store, primaryType, payload) { + var primaryTypeName = primaryType.typeKey, + primaryArray; + + for (var prop in payload) { + var typeName = this.singularize(prop), + type = store.modelFor(typeName), + isPrimary = typeName === primaryTypeName; + + /*jshint loopfunc:true*/ + var normalizedArray = payload[prop].map(function(hash) { + return this.normalize(type, prop, hash); + }, this); + + if (isPrimary) { + primaryArray = normalizedArray; + } else { + store.pushMany(typeName, normalizedArray); + } + } + + return primaryArray; + }, + + /** + @private + @method pluralize + @param {String} key + */ + pluralize: function(key) { + return Ember.String.pluralize(key); + }, + + /** + @private + @method singularize + @param {String} key + */ + singularize: function(key) { + return Ember.String.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; + } + }); + ``` + */ + serialize: function(record, options) { + return this._super.apply(this, arguments); + } +}); + +/** + 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 underscored 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": { + "first_name": "Barack", + "last_name": "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 `url` property. + + ```js + DS.RESTAdapter.reopen({ + url: 'https://api.example.com' + }); + ``` + + ### Headers customization + + Some APIs require HTTP headers, eg to provide an API key. An array of + headers can be added to the adapter which are passed with every request: + + ```js + DS.RESTAdapter.reopen({ + headers: { + "API_KEY": "secret key", + "ANOTHER_HEADER": "asdsada" + } + }); + ``` + + @class RESTAdapter + @constructor + @namespace DS + @extends DS.Adapter +*/ +DS.RESTAdapter = DS.Adapter.extend({ + defaultSerializer: '_rest', + + /** + Called by the store in order to fetch the JSON for a given + type and ID. + + It makes an Ajax request to a URL computed by `buildURL`, and returns a + promise for the resulting payload. + + @method find + @see RESTAdapter/buildURL + @see RESTAdapter/ajax + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {String} id + @returns Promise + */ + find: function(store, type, id) { + return this.ajax(this.buildURL(type, id), 'GET'); + }, + + /** + Called by the store in order to fetch a JSON array for all + of the records for a given type. + + It makes an Ajax request to a URL computed by `buildURL`, and returns a + promise for the resulting payload. + + @method findAll + @see RESTAdapter/buildURL + @see RESTAdapter/ajax + @param {DS.Store} store + @param {subclass of DS.Model} type + @returns Promise + */ + findAll: function(store, type) { + return this.ajax(this.buildURL(type), 'GET'); + }, + + /** + Called by the store in order to fetch a JSON array for + the records that match a particular query. + + The query is a simple JavaScript object that will be passed directly + to the server as parameters. + + It makes an Ajax request to a URL computed by `buildURL`, and returns a + promise for the resulting payload. + + @method findQuery + @see RESTAdapter/buildURL + @see RESTAdapter/ajax + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Object} query + @returns Promise + */ + findQuery: function(store, type, query) { + return this.ajax(this.buildURL(type), 'GET', 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 + into an Array for you on the server-side. If you want to encode the + IDs, differently, just override this (one-line) method. + + It makes an Ajax request to a URL computed by `buildURL`, and returns a + promise for the resulting payload. + + @method findMany + @see RESTAdapter/buildURL + @see RESTAdapter/ajax + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {Array} ids + @returns Promise + */ + findMany: function(store, type, ids) { + return this.ajax(this.buildURL(type), 'GET', { 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`. + + It will make an Ajax request to the originally specified URL. + + @method findHasMany + @see RESTAdapter/buildURL + @see RESTAdapter/ajax + @param {DS.Store} store + @param {DS.Model} record + @param {String} url + @returns Promise + */ + findHasMany: function(store, record, url) { + return this.ajax(url, 'GET'); + }, + + /** + Called by the store when a newly created record is + `save`d. + + It serializes the record, and `POST`s it to a URL generated by `buildURL`. + + See `serialize` for information on how to customize the serialized form + of a record. + + @method createRecord + @see RESTAdapter/buildURL + @see RESTAdapter/ajax + @see RESTAdapter/serialize + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {DS.Model} record + @returns Promise + */ + createRecord: function(store, type, record) { + var data = {}; + data[type.typeKey] = this.serializerFor(type.typeKey).serialize(record, { includeId: true }); + + return this.ajax(this.buildURL(type), "POST", { data: data }); + }, + + /** + Called by the store when an existing record is `save`d. + + It serializes the record, and `POST`s it to a URL generated by `buildURL`. + + See `serialize` for information on how to customize the serialized form + of a record. + + @method updateRecord + @see RESTAdapter/buildURL + @see RESTAdapter/ajax + @see RESTAdapter/serialize + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {DS.Model} record + @returns Promise + */ + updateRecord: function(store, type, record) { + var data = {}; + data[type.typeKey] = this.serializerFor(type.typeKey).serialize(record); + + var id = get(record, 'id'); + + return this.ajax(this.buildURL(type, id), "PUT", { data: data }); + }, + + /** + Called by the store when an deleted record is `save`d. + + It serializes the record, and `POST`s it to a URL generated by `buildURL`. + + @method deleteRecord + @see RESTAdapter/buildURL + @see RESTAdapter/ajax + @see RESTAdapter/serialize + @param {DS.Store} store + @param {subclass of DS.Model} type + @param {DS.Model} record + @returns Promise + */ + deleteRecord: function(store, type, record) { + var id = get(record, 'id'); + + return this.ajax(this.buildURL(type, 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'). + + If an ID is specified, it adds the ID to the plural form + of the type, separated by a `/`. + + @method buildURL + @param {subclass of DS.Model} type + @param {String} id + @returns String + */ + buildURL: function(type, id) { + var url = "/" + Ember.String.pluralize(type.typeKey); + if (id) { url += "/" + id; } + + return url; + }, + + serializerFor: function(type) { + // This logic has to be kept in sync with DS.Store#serializerFor + return this.container.lookup('serializer:' + type) || + this.container.lookup('serializer:application') || + this.container.lookup('serializer:_rest'); + }, + + + /** + 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, it 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 url + @param type + @param hash + */ + ajax: function(url, type, hash) { + var adapter = this; + + return new Ember.RSVP.Promise(function(resolve, reject) { + hash = hash || {}; + hash.url = url; + hash.type = type; + hash.dataType = 'json'; + hash.context = adapter; + + if (hash.data && type !== 'GET') { + hash.contentType = 'application/json; charset=utf-8'; + hash.data = JSON.stringify(hash.data); + } + + if (adapter.headers !== undefined) { + var headers = adapter.headers; + hash.beforeSend = function (xhr) { + Ember.keys(headers).forEach(function(key) { + xhr.setRequestHeader(key, headers[key]); + }); + }; + } + + hash.success = function(json) { + Ember.run(null, resolve, json); + }; + + hash.error = function(jqXHR, textStatus, errorThrown) { + if (jqXHR) { + jqXHR.then = null; + } + + Ember.run(null, reject, jqXHR); + }; + + Ember.$.ajax(hash); + }); + } + +}); + +})(); + + + +(function() { +/** + @module ember-data +*/ + +})(); + + + +(function() { +DS.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. + */ + _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 + } + }; + } + +}); + +})(); + + + +(function() { +/** + @module ember-data +*/ + +})(); + + + +(function() { +//Copyright (C) 2011 by Living Social, Inc. + +//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. + +/** + Ember Data + + @module ember-data + @main ember-data +*/ + +})(); + diff --git a/architecture-examples/emberjs/bower_components/ember-localstorage-adapter/localstorage_adapter.js b/architecture-examples/emberjs/bower_components/ember-localstorage-adapter/localstorage_adapter.js index 60bc04c7bd..1f4bdf2a3d 100644 --- a/architecture-examples/emberjs/bower_components/ember-localstorage-adapter/localstorage_adapter.js +++ b/architecture-examples/emberjs/bower_components/ember-localstorage-adapter/localstorage_adapter.js @@ -1,65 +1,30 @@ -DS.LSSerializer = DS.JSONSerializer.extend({ - - addBelongsTo: function(data, record, key, association) { - data[key] = record.get(key + '.id'); - }, - - addHasMany: function(data, record, key, association) { - data[key] = record.get(key).map(function(record) { - return record.get('id'); - }); - }, - - // extract expects a root key, we don't want to save all these keys to - // localStorage so we generate the root keys here - extract: function(loader, json, type, record) { - this._super(loader, this.rootJSON(json, type), type, record); - }, - - extractMany: function(loader, json, type, records) { - this._super(loader, this.rootJSON(json, type, 'pluralize'), type, records); - }, - - rootJSON: function(json, type, pluralize) { - var root = this.rootForType(type); - if (pluralize == 'pluralize') { root = this.pluralize(root); } - var rootedJSON = {}; - rootedJSON[root] = json; - return rootedJSON; - } - -}); +/*global Ember*/ +/*global DS*/ +'use strict'; DS.LSAdapter = DS.Adapter.extend(Ember.Evented, { - init: function() { - this._loadData(); - }, - - generateIdForRecord: function() { - return Math.random().toString(32).slice(2).substr(0,5); - }, - - serializer: DS.LSSerializer.create(), - - find: function(store, type, id) { - var namespace = this._namespaceForType(type); - this._async(function(){ - var copy = Ember.copy(namespace.records[id]); - this.didFindRecord(store, type, copy, id); - }); - }, - - findMany: function(store, type, ids) { - var namespace = this._namespaceForType(type); - this._async(function(){ - var results = []; - for (var i = 0; i < ids.length; i++) { - results.push(Ember.copy(namespace.records[ids[i]])); - } - this.didFindMany(store, type, results); - }); - }, + init: function () { + this._loadData(); + }, + + generateIdForRecord: function () { + return Math.random().toString(32).slice(2).substr(0, 5); + }, + + find: function (store, type, id) { + var namespace = this._namespaceForType(type); + return Ember.RSVP.resolve(Ember.copy(namespace.records[id])); + }, + + findMany: function (store, type, ids) { + var namespace = this._namespaceForType(type); + var results = []; + for (var i = 0; i < ids.length; i++) { + results.push(Ember.copy(namespace.records[ids[i]])); + } + return Ember.RSVP.resolve(results); + }, // Supports queries that look like this: // @@ -75,141 +40,89 @@ DS.LSAdapter = DS.Adapter.extend(Ember.Evented, { // match records with "complete: true" and the name "foo" or "bar" // // { complete: true, name: /foo|bar/ } - findQuery: function(store, type, query, recordArray) { - var namespace = this._namespaceForType(type); - this._async(function() { - var results = this.query(namespace.records, query); - this.didFindQuery(store, type, results, recordArray); - }); - }, - - query: function(records, query) { - var results = []; - var id, record, property, test, push; - for (id in records) { - record = records[id]; - for (property in query) { - test = query[property]; - push = false; - if (Object.prototype.toString.call(test) == '[object RegExp]') { - push = test.test(record[property]); - } else { - push = record[property] === test; - } - } - if (push) { - results.push(record); - } - } - return results; - }, - - findAll: function(store, type) { - var namespace = this._namespaceForType(type); - this._async(function() { - var results = []; - for (var id in namespace.records) { - results.push(Ember.copy(namespace.records[id])); - } - this.didFindAll(store, type, results); - }); - }, - - createRecords: function(store, type, records) { - var namespace = this._namespaceForType(type); - records.forEach(function(record) { - this._addRecordToNamespace(namespace, record); - }, this); - this._async(function() { - this._didSaveRecords(store, type, records); - }); - }, - - updateRecords: function(store, type, records) { - var namespace = this._namespaceForType(type); - this._async(function() { - records.forEach(function(record) { - var id = record.get('id'); - namespace.records[id] = record.serialize({includeId:true}); - }, this); - this._didSaveRecords(store, type, records); - }); - }, - - deleteRecords: function(store, type, records) { - var namespace = this._namespaceForType(type); - this._async(function() { - records.forEach(function(record) { - var id = record.get('id'); - delete namespace.records[id]; - }); - this._didSaveRecords(store, type, records); - }); - - }, - - dirtyRecordsForHasManyChange: function(dirtySet, parent, relationship) { - dirtySet.add(parent); - }, - - dirtyRecordsForBelongsToChange: function(dirtySet, child, relationship) { - dirtySet.add(child); - }, + findQuery: function (store, type, query, recordArray) { + var namespace = this._namespaceForType(type); + var results = this.query(namespace.records, query); + return Ember.RSVP.resolve(results); + }, + + query: function (records, query) { + var results = []; + var id, record, property, test, push; + for (id in records) { + record = records[id]; + for (property in query) { + test = query[property]; + push = false; + if (Object.prototype.toString.call(test) === '[object RegExp]') { + push = test.test(record[property]); + } else { + push = record[property] === test; + } + } + if (push) { + results.push(record); + } + } + return results; + }, + + findAll: function (store, type) { + var namespace = this._namespaceForType(type); + var results = []; + for (var id in namespace.records) { + results.push(Ember.copy(namespace.records[id])); + } + return Ember.RSVP.resolve(results); + }, + + createRecord: function (store, type, record) { + var namespace = this._namespaceForType(type); + this._addRecordToNamespace(namespace, record); + this._saveData(); + return Ember.RSVP.resolve(); + }, + + updateRecord: function (store, type, record) { + var namespace = this._namespaceForType(type); + var id = record.get('id'); + namespace.records[id] = record.toJSON({ includeId: true }); + this._saveData(); + return Ember.RSVP.resolve(); + }, + + deleteRecord: function (store, type, record) { + var namespace = this._namespaceForType(type); + var id = record.get('id'); + delete namespace.records[id]; + this._saveData(); + return Ember.RSVP.resolve(); + }, // private - _getNamespace: function() { - return this.namespace || 'DS.LSAdapter'; - }, - - _loadData: function() { - var storage = localStorage.getItem(this._getNamespace()); - this._data = storage ? JSON.parse(storage) : {}; - }, - - _didSaveRecords: function(store, type, records) { - var success = this._saveData(); - if (success) { - store.didSaveRecords(records); - } else { - records.forEach(function(record) { - store.recordWasError(record); - }); - this.trigger('QUOTA_EXCEEDED_ERR', records); - } - }, - - _saveData: function() { - try { - localStorage.setItem(this._getNamespace(), JSON.stringify(this._data)); - return true; - } catch(error) { - if (error.name == 'QUOTA_EXCEEDED_ERR') { - return false; - } else { - throw new Error(error); - } - } - }, - - _namespaceForType: function(type) { - var namespace = type.url || type.toString(); - return this._data[namespace] || ( - this._data[namespace] = {records: {}} - ); - }, - - _addRecordToNamespace: function(namespace, record) { - var data = record.serialize({includeId: true}); - namespace.records[data.id] = data; - }, - - _async: function(callback) { - var _this = this; - setTimeout(function(){ - Ember.run(_this, callback); - }, 1); - } - + _getNamespace: function () { + return this.namespace || 'DS.LSAdapter'; + }, + + _loadData: function () { + var storage = localStorage.getItem(this._getNamespace()); + this._data = storage ? JSON.parse(storage) : {}; + }, + + _saveData: function () { + localStorage.setItem(this._getNamespace(), JSON.stringify(this._data)); + }, + + _namespaceForType: function (type) { + var namespace = type.url || type.toString(); + return this._data[namespace] || ( + this._data[namespace] = {records: {}} + ); + }, + + _addRecordToNamespace: function (namespace, record) { + var data = record.serialize({includeId: true}); + namespace.records[data.id] = data; + } }); - diff --git a/architecture-examples/emberjs/bower_components/ember/ember.js b/architecture-examples/emberjs/bower_components/ember/ember.js index e566b10f98..43bc9af5ff 100644 --- a/architecture-examples/emberjs/bower_components/ember/ember.js +++ b/architecture-examples/emberjs/bower_components/ember/ember.js @@ -1,5 +1,5 @@ -// Version: v1.0.0-rc.1 -// Last commit: 8b061b4 (2013-02-15 12:10:22 -0800) +// Version: v1.0.0 +// Last commit: e2ea0cf (2013-08-31 23:47:39 -0700) (function() { @@ -49,7 +49,14 @@ if (!('MANDATORY_SETTER' in Ember.ENV)) { falsy, an exception will be thrown. */ Ember.assert = function(desc, test) { - if (!test) throw new Error("assertion failed: "+desc); + if (!test) { + Ember.Logger.assert(test, desc); + } + + if (Ember.testing && !test) { + // when testing, ensure test failures when assertions fail + throw new Error("Assertion Failed: " + desc); + } }; @@ -95,12 +102,12 @@ Ember.debug = function(message) { will be displayed. */ Ember.deprecate = function(message, test) { - if (Ember && Ember.TESTING_DEPRECATION) { return; } + if (Ember.TESTING_DEPRECATION) { return; } if (arguments.length === 1) { test = false; } if (test) { return; } - if (Ember && Ember.ENV.RAISE_ON_DEPRECATION) { throw new Error(message); } + if (Ember.ENV.RAISE_ON_DEPRECATION) { throw new Error(message); } var error; @@ -140,6 +147,7 @@ Ember.deprecate = function(message, test) { @method deprecateFunc @param {String} message A description of the deprecation. @param {Function} func The function to be deprecated. + @return {Function} a new function that wrapped the original function with a deprecation warning */ Ember.deprecateFunc = function(message, func) { return function() { @@ -148,10 +156,22 @@ Ember.deprecateFunc = function(message, func) { }; }; + +// Inform the developer about the Ember Inspector if not installed. +if (!Ember.testing) { + if (typeof window !== 'undefined' && window.chrome && 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'); + } + }, false); + } +} + })(); -// Version: v1.0.0-rc.1 -// Last commit: 8b061b4 (2013-02-15 12:10:22 -0800) +// Version: v1.0.0 +// Last commit: e2ea0cf (2013-08-31 23:47:39 -0700) (function() { @@ -168,11 +188,17 @@ var define, requireModule; if (seen[name]) { return seen[name]; } seen[name] = {}; - var mod = registry[name], - deps = mod.deps, - callback = mod.callback, - reified = [], - exports; + var mod, deps, callback, reified, exports; + + mod = registry[name]; + + if (!mod) { + throw new Error("Module '" + name + "' not found."); + } + + deps = mod.deps; + callback = mod.callback; + reified = []; for (var i=0, l=deps.length; i -1; +}; + +// From: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/map +var arrayMap = isNativeFunc(Array.prototype.map) ? Array.prototype.map : function(fun /*, thisp */) { + //"use strict"; + + if (this === void 0 || this === null) { + throw new TypeError(); + } + + var t = Object(this); + var len = t.length >>> 0; + if (typeof fun !== "function") { + throw new TypeError(); + } + + var res = new Array(len); + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (i in t) { + res[i] = fun.call(thisp, t[i], i, t); + } + } + + return res; +}; + +// From: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/foreach +var arrayForEach = isNativeFunc(Array.prototype.forEach) ? Array.prototype.forEach : function(fun /*, thisp */) { + //"use strict"; + + if (this === void 0 || this === null) { + throw new TypeError(); + } + + var t = Object(this); + var len = t.length >>> 0; + if (typeof fun !== "function") { + throw new TypeError(); + } + + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (i in t) { + fun.call(thisp, t[i], i, t); + } + } +}; + +var arrayIndexOf = isNativeFunc(Array.prototype.indexOf) ? Array.prototype.indexOf : function (obj, fromIndex) { + if (fromIndex === null || fromIndex === undefined) { fromIndex = 0; } + else if (fromIndex < 0) { fromIndex = Math.max(0, this.length + fromIndex); } + for (var i = fromIndex, j = this.length; i < j; i++) { + if (this[i] === obj) { return i; } + } + return -1; +}; + +/** + Array polyfills to support ES5 features in older browsers. + + @namespace Ember + @property ArrayPolyfills +*/ +Ember.ArrayPolyfills = { + map: arrayMap, + forEach: arrayForEach, + indexOf: arrayIndexOf +}; + +if (Ember.SHIM_ES5) { + if (!Array.prototype.map) { + Array.prototype.map = arrayMap; + } + + if (!Array.prototype.forEach) { + Array.prototype.forEach = arrayForEach; + } + + if (!Array.prototype.indexOf) { + Array.prototype.indexOf = arrayIndexOf; + } +} + +})(); + + + (function() { /** @module ember-metal @@ -651,7 +983,7 @@ Ember.generateGuid = function generateGuid(obj, prefix) { @method guidFor @for Ember - @param obj {Object} any object, string, number, Element, or primitive + @param {Object} obj any object, string, number, Element, or primitive @return {String} the unique guid for this instance. */ Ember.guidFor = function guidFor(obj) { @@ -660,7 +992,7 @@ Ember.guidFor = function guidFor(obj) { if (obj === undefined) return "(undefined)"; if (obj === null) return "(null)"; - var cache, ret; + var ret; var type = typeof obj; // Don't allow prototype changes to String etc. to change the guidFor @@ -762,7 +1094,7 @@ if (isDefinePropertySimulated) { @param {Object} obj The object to retrieve meta for @param {Boolean} [writable=true] Pass `false` if you do not intend to modify the meta hash, allowing the method to avoid making an unnecessary copy. - @return {Hash} + @return {Object} the meta hash for an object */ Ember.meta = function meta(obj, writable) { @@ -809,6 +1141,7 @@ Ember.setMeta = function setMeta(obj, property, value) { }; /** + @deprecated @private In order to store defaults for a class, a prototype may need to create @@ -841,6 +1174,7 @@ Ember.setMeta = function setMeta(obj, property, value) { shared with its constructor */ Ember.metaPath = function metaPath(obj, path, writable) { + Ember.deprecate("Ember.metaPath is deprecated and will be removed from future releases."); var meta = Ember.meta(obj, writable), keyName, value; for (var i=0, l=path.length; i size ? size : ends; + if (count <= 0) { count = 0; } + + chunk = args.splice(0, size); + chunk = [start, count].concat(chunk); + + start += size; + ends -= count; + + ret = ret.concat(splice.apply(array, chunk)); + } + return ret; + }, + replace: function(array, idx, amt, objects) { if (array.replace) { return array.replace(idx, amt, objects); } else { - var args = Array.prototype.concat.apply([idx, amt], objects); - return array.splice.apply(array, args); + return utils._replace(array, idx, amt, objects); } }, intersection: function(array1, array2) { var intersection = []; - array1.forEach(function(element) { - if (array2.indexOf(element) >= 0) { + utils.forEach(array1, function(element) { + if (utils.indexOf(array2, element) >= 0) { intersection.push(element); } }); @@ -1335,2314 +1850,2205 @@ var utils = Ember.EnumerableUtils = { (function() { -/*jshint newcap:false*/ /** @module ember-metal */ -// NOTE: There is a bug in jshint that doesn't recognize `Object()` without `new` -// as being ok unless both `newcap:false` and not `use strict`. -// https://github.com/jshint/jshint/issues/392 +var META_KEY = Ember.META_KEY, get; -// Testing this is not ideal, but we want to use native functions -// if available, but not to use versions created by libraries like Prototype -var isNativeFunc = function(func) { - // This should probably work in all browsers likely to have ES5 array methods - return func && Function.prototype.toString.call(func).indexOf('[native code]') > -1; -}; +var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; -// From: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/map -var arrayMap = isNativeFunc(Array.prototype.map) ? Array.prototype.map : function(fun /*, thisp */) { - //"use strict"; +var IS_GLOBAL_PATH = /^([A-Z$]|([0-9][A-Z$])).*[\.\*]/; +var HAS_THIS = /^this[\.\*]/; +var FIRST_KEY = /^([^\.\*]+)/; - if (this === void 0 || this === null) { - throw new TypeError(); - } +// .......................................................... +// GET AND SET +// +// If we are on a platform that supports accessors we can use those. +// Otherwise simulate accessors by looking up the property directly on the +// object. - var t = Object(this); - var len = t.length >>> 0; - if (typeof fun !== "function") { - throw new TypeError(); - } +/** + Gets the value of a property on an object. If the property is computed, + the function will be invoked. If the property is not defined but the + object implements the `unknownProperty` method then that will be invoked. - var res = new Array(len); - var thisp = arguments[1]; - for (var i = 0; i < len; i++) { - if (i in t) { - res[i] = fun.call(thisp, t[i], i, t); - } - } + If you plan to run on IE8 and older browsers then you should use this + method anytime you want to retrieve a property on an object that you don't + know for sure is private. (Properties beginning with an underscore '_' + are considered private.) - return res; -}; + On all newer browsers, you only need to use this method to retrieve + properties if the property might not be defined on the object and you want + to respect the `unknownProperty` handler. Otherwise you can ignore this + method. -// From: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/foreach -var arrayForEach = isNativeFunc(Array.prototype.forEach) ? Array.prototype.forEach : function(fun /*, thisp */) { - //"use strict"; + Note that if the object itself is `undefined`, this method will throw + an error. - if (this === void 0 || this === null) { - throw new TypeError(); + @method get + @for Ember + @param {Object} obj The object to retrieve from. + @param {String} keyName The property key to retrieve + @return {Object} the property value or `null`. +*/ +get = function get(obj, keyName) { + // Helpers that operate with 'this' within an #each + if (keyName === '') { + return obj; } - var t = Object(this); - var len = t.length >>> 0; - if (typeof fun !== "function") { - throw new TypeError(); + if (!keyName && 'string'===typeof obj) { + keyName = obj; + obj = null; } - var thisp = arguments[1]; - for (var i = 0; i < len; i++) { - if (i in t) { - fun.call(thisp, t[i], i, t); - } - } -}; + Ember.assert("Cannot call get with "+ keyName +" key.", !!keyName); + Ember.assert("Cannot call get with '"+ keyName +"' on an undefined object.", obj !== undefined); -var arrayIndexOf = isNativeFunc(Array.prototype.indexOf) ? Array.prototype.indexOf : function (obj, fromIndex) { - if (fromIndex === null || fromIndex === undefined) { fromIndex = 0; } - else if (fromIndex < 0) { fromIndex = Math.max(0, this.length + fromIndex); } - for (var i = fromIndex, j = this.length; i < j; i++) { - if (this[i] === obj) { return i; } + if (obj === null || keyName.indexOf('.') !== -1) { + return getPath(obj, keyName); } - return -1; -}; -Ember.ArrayPolyfills = { - map: arrayMap, - forEach: arrayForEach, - indexOf: arrayIndexOf -}; + var meta = obj[META_KEY], desc = meta && meta.descs[keyName], ret; + if (desc) { + return desc.get(obj, keyName); + } else { + if (MANDATORY_SETTER && meta && meta.watching[keyName] > 0) { + ret = meta.values[keyName]; + } else { + ret = obj[keyName]; + } -if (Ember.SHIM_ES5) { - if (!Array.prototype.map) { - Array.prototype.map = arrayMap; - } + if (ret === undefined && + 'object' === typeof obj && !(keyName in obj) && 'function' === typeof obj.unknownProperty) { + return obj.unknownProperty(keyName); + } - if (!Array.prototype.forEach) { - Array.prototype.forEach = arrayForEach; + return ret; } +}; - if (!Array.prototype.indexOf) { - Array.prototype.indexOf = arrayIndexOf; - } +// Currently used only by Ember Data tests +if (Ember.config.overrideAccessors) { + Ember.get = get; + Ember.config.overrideAccessors(); + get = Ember.get; } -})(); - - - -(function() { /** -@module ember-metal -*/ - -/* - JavaScript (before ES6) does not have a Map implementation. Objects, - which are often used as dictionaries, may only have Strings as keys. - - Because Ember has a way to get a unique identifier for every object - via `Ember.guidFor`, we can implement a performant Map with arbitrary - keys. Because it is commonly used in low-level bookkeeping, Map is - implemented as a pure JavaScript object for performance. + @private - This implementation follows the current iteration of the ES6 proposal for - maps (http://wiki.ecmascript.org/doku.php?id=harmony:simple_maps_and_sets), - with two exceptions. First, because we need our implementation to be pleasant - on older browsers, we do not use the `delete` name (using `remove` instead). - Second, as we do not have the luxury of in-VM iteration, we implement a - forEach method for iteration. + 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. - Map is mocked out to look like an Ember object, so you can do - `Ember.Map.create()` for symmetry with other Ember classes. + @method normalizeTuple + @for Ember + @param {Object} target The current target. May be `null`. + @param {String} path A path on the target or a global property path. + @return {Array} a temporary array with the normalized target/path pair. */ -var guidFor = Ember.guidFor, - indexOf = Ember.ArrayPolyfills.indexOf; +var normalizeTuple = Ember.normalizeTuple = function(target, path) { + var hasThis = HAS_THIS.test(path), + isGlobal = !hasThis && IS_GLOBAL_PATH.test(path), + key; -var copy = function(obj) { - var output = {}; + if (!target || isGlobal) target = Ember.lookup; + if (hasThis) path = path.slice(5); - for (var prop in obj) { - if (obj.hasOwnProperty(prop)) { output[prop] = obj[prop]; } + if (target === Ember.lookup) { + key = path.match(FIRST_KEY)[0]; + target = get(target, key); + path = path.slice(key.length+1); } - return output; + // must return some kind of path to be valid else other things will break. + if (!path || path.length===0) throw new Error('Invalid Path'); + + return [ target, path ]; }; -var copyMap = function(original, newObject) { - var keys = original.keys.copy(), - values = copy(original.values); +var getPath = Ember._getPath = function(root, path) { + var hasThis, parts, tuple, idx, len; - newObject.keys = keys; - newObject.values = values; + // If there is no root and path is a key name, return that + // property from the global object. + // E.g. get('Ember') -> Ember + if (root === null && path.indexOf('.') === -1) { return get(Ember.lookup, path); } - return newObject; + // detect complicated paths and normalize them + hasThis = HAS_THIS.test(path); + + if (!root || hasThis) { + tuple = normalizeTuple(root, path); + root = tuple[0]; + path = tuple[1]; + tuple.length = 0; + } + + parts = path.split("."); + len = parts.length; + for (idx = 0; root != null && idx < len; idx++) { + root = get(root, parts[idx], true); + if (root && root.isDestroyed) { return undefined; } + } + return root; }; -/** - This class is used internally by Ember and Ember Data. - Please do not use it at this time. We plan to clean it up - and add many tests soon. +Ember.getWithDefault = function(root, key, defaultValue) { + var value = get(root, key); - @class OrderedSet - @namespace Ember - @constructor - @private -*/ -var OrderedSet = Ember.OrderedSet = function() { - this.clear(); + if (value === undefined) { return defaultValue; } + return value; }; + +Ember.get = get; +Ember.getPath = Ember.deprecateFunc('getPath is deprecated since get now supports paths', Ember.get); + +})(); + + + +(function() { /** - @method create - @static - @return {Ember.OrderedSet} +@module ember-metal */ -OrderedSet.create = function() { - return new OrderedSet(); -}; +var o_create = Ember.create, + metaFor = Ember.meta, + META_KEY = Ember.META_KEY, + a_slice = [].slice, + /* listener flags */ + ONCE = 1, SUSPENDED = 2; -OrderedSet.prototype = { - /** - @method clear - */ - clear: function() { - this.presenceSet = {}; - this.list = []; - }, +/* + The event system uses a series of nested hashes to store listeners on an + object. When a listener is registered, or when an event arrives, these + hashes are consulted to determine which target and action pair to invoke. - /** - @method add - @param obj - */ - add: function(obj) { - var guid = guidFor(obj), - presenceSet = this.presenceSet, - list = this.list; + The hashes are stored in the object's meta hash, and look like this: - if (guid in presenceSet) { return; } + // Object's meta hash + { + listeners: { // variable name: `listenerSet` + "foo:changed": [ // variable name: `actions` + target, method, flags + ] + } + } - presenceSet[guid] = true; - list.push(obj); - }, +*/ - /** - @method remove - @param obj - */ - remove: function(obj) { - var guid = guidFor(obj), - presenceSet = this.presenceSet, - list = this.list; +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; } + } + return index; +} - delete presenceSet[guid]; +function actionsFor(obj, eventName) { + var meta = metaFor(obj, true), + actions; - var index = indexOf.call(list, obj); - if (index > -1) { - list.splice(index, 1); - } - }, + if (!meta.listeners) { meta.listeners = {}; } - /** - @method isEmpty - @return {Boolean} - */ - isEmpty: function() { - return this.list.length === 0; - }, + if (!meta.hasOwnProperty('listeners')) { + // setup inherited copy of the listeners object + meta.listeners = o_create(meta.listeners); + } - /** - @method has - @param obj - @return {Boolean} - */ - has: function(obj) { - var guid = guidFor(obj), - presenceSet = this.presenceSet; + actions = meta.listeners[eventName]; - return guid in presenceSet; - }, + // if there are actions, but the eventName doesn't exist in our listeners, then copy them from the prototype + if (actions && !meta.listeners.hasOwnProperty(eventName)) { + actions = meta.listeners[eventName] = meta.listeners[eventName].slice(); + } else if (!actions) { + actions = meta.listeners[eventName] = []; + } - /** - @method forEach - @param {Function} function - @param target - */ - forEach: function(fn, self) { - // allow mutation during iteration - var list = this.list.slice(); + return actions; +} - for (var i = 0, j = list.length; i < j; i++) { - fn.call(self, list[i]); +function actionsUnion(obj, eventName, otherActions) { + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName]; + + if (!actions) { return; } + for (var i = actions.length - 3; i >= 0; i -= 3) { + var target = actions[i], + method = actions[i+1], + flags = actions[i+2], + actionIndex = indexOf(otherActions, target, method); + + if (actionIndex === -1) { + otherActions.push(target, method, flags); } - }, + } +} - /** - @method toArray - @return {Array} - */ - toArray: function() { - return this.list.slice(); - }, +function actionsDiff(obj, eventName, otherActions) { + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName], + diffActions = []; - /** - @method copy - @return {Ember.OrderedSet} - */ - copy: function() { - var set = new OrderedSet(); + if (!actions) { return; } + for (var i = actions.length - 3; i >= 0; i -= 3) { + var target = actions[i], + method = actions[i+1], + flags = actions[i+2], + actionIndex = indexOf(otherActions, target, method); - set.presenceSet = copy(this.presenceSet); - set.list = this.list.slice(); + if (actionIndex !== -1) { continue; } - return set; + otherActions.push(target, method, flags); + diffActions.push(target, method, flags); } -}; + + return diffActions; +} /** - A Map stores values indexed by keys. Unlike JavaScript's - default Objects, the keys of a Map can be any JavaScript - object. + Add an event listener - Internally, a Map has two data structures: + @method addListener + @for Ember + @param obj + @param {String} eventName + @param {Object|Function} targetOrMethod A target object or a function + @param {Function|String} method A function or the name of a function to be called on `target` + @param {Boolean} once A flag whether a function should only be called once +*/ +function addListener(obj, eventName, target, method, once) { + Ember.assert("You must pass at least an object and event name to Ember.addListener", !!obj && !!eventName); - 1. `keys`: an OrderedSet of all of the existing keys - 2. `values`: a JavaScript Object indexed by the `Ember.guidFor(key)` + if (!method && 'function' === typeof target) { + method = target; + target = null; + } - When a key/value pair is added for the first time, we - add the key to the `keys` OrderedSet, and create or - replace an entry in `values`. When an entry is deleted, - we delete its entry in `keys` and `values`. + var actions = actionsFor(obj, eventName), + actionIndex = indexOf(actions, target, method), + flags = 0; - @class Map - @namespace Ember - @private - @constructor -*/ -var Map = Ember.Map = function() { - this.keys = Ember.OrderedSet.create(); - this.values = {}; -}; + if (once) flags |= ONCE; -/** - @method create - @static -*/ -Map.create = function() { - return new Map(); -}; + if (actionIndex !== -1) { return; } -Map.prototype = { - /** - Retrieve the value associated with a given key. + actions.push(target, method, flags); - @method get - @param {anything} key - @return {anything} the value associated with the key, or `undefined` - */ - get: function(key) { - var values = this.values, - guid = guidFor(key); + if ('function' === typeof obj.didAddListener) { + obj.didAddListener(eventName, target, method); + } +} - return values[guid]; - }, +/** + Remove an event listener - /** - Adds a value to the map. If a value for the given key has already been - provided, the new value will replace the old value. + Arguments should match those passed to `Ember.addListener`. - @method set - @param {anything} key - @param {anything} value - */ - set: function(key, value) { - var keys = this.keys, - values = this.values, - guid = guidFor(key); + @method removeListener + @for Ember + @param obj + @param {String} eventName + @param {Object|Function} targetOrMethod A target object or a function + @param {Function|String} method A function or the name of a function to be called on `target` +*/ +function removeListener(obj, eventName, target, method) { + Ember.assert("You must pass at least an object and event name to Ember.removeListener", !!obj && !!eventName); - keys.add(key); - values[guid] = value; - }, + if (!method && 'function' === typeof target) { + method = target; + target = null; + } - /** - Removes a value from the map for an associated key. + function _removeListener(target, method) { + var actions = actionsFor(obj, eventName), + actionIndex = indexOf(actions, target, method); - @method remove - @param {anything} key - @return {Boolean} true if an item was removed, false otherwise - */ - remove: function(key) { - // don't use ES6 "delete" because it will be annoying - // to use in browsers that are not ES6 friendly; - var keys = this.keys, - values = this.values, - guid = guidFor(key), - value; + // action doesn't exist, give up silently + if (actionIndex === -1) { return; } - if (values.hasOwnProperty(guid)) { - keys.remove(key); - value = values[guid]; - delete values[guid]; - return true; - } else { - return false; + actions.splice(actionIndex, 3); + + if ('function' === typeof obj.didRemoveListener) { + obj.didRemoveListener(eventName, target, method); } - }, + } - /** - Check whether a key is present. + if (method) { + _removeListener(target, method); + } else { + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName]; - @method has - @param {anything} key - @return {Boolean} true if the item was present, false otherwise - */ - has: function(key) { - var values = this.values, - guid = guidFor(key); + if (!actions) { return; } + for (var i = actions.length - 3; i >= 0; i -= 3) { + _removeListener(actions[i], actions[i+1]); + } + } +} - return values.hasOwnProperty(guid); - }, +/** + @private - /** - Iterate over all the keys and values. Calls the function once - for each key, passing in the key and value, in that order. + Suspend listener during callback. - The keys are guaranteed to be iterated over in insertion order. + This should only be used by the target of the event listener + when it is taking an action that would cause the event, e.g. + an object might suspend its property change listener while it is + setting that property. - @method forEach - @param {Function} callback - @param {anything} self if passed, the `this` value inside the - callback. By default, `this` is the map. - */ - forEach: function(callback, self) { - var keys = this.keys, - values = this.values; + @method suspendListener + @for Ember + @param obj + @param {String} eventName + @param {Object|Function} targetOrMethod A target object or a function + @param {Function|String} method A function or the name of a function to be called on `target` + @param {Function} callback +*/ +function suspendListener(obj, eventName, target, method, callback) { + if (!method && 'function' === typeof target) { + method = target; + target = null; + } - keys.forEach(function(key) { - var guid = guidFor(key); - callback.call(self, key, values[guid]); - }); - }, + var actions = actionsFor(obj, eventName), + actionIndex = indexOf(actions, target, method); - /** - @method copy - @return {Ember.Map} - */ - copy: function() { - return copyMap(this, new Map()); + if (actionIndex !== -1) { + actions[actionIndex+2] |= SUSPENDED; // mark the action as suspended } -}; + + function tryable() { return callback.call(target); } + function finalizer() { if (actionIndex !== -1) { actions[actionIndex+2] &= ~SUSPENDED; } } + + return Ember.tryFinally(tryable, finalizer); +} /** - @class MapWithDefault - @namespace Ember - @extends Ember.Map @private - @constructor - @param [options] - @param {anything} [options.defaultValue] -*/ -var MapWithDefault = Ember.MapWithDefault = function(options) { - Map.call(this); - this.defaultValue = options.defaultValue; -}; -/** - @method create - @static - @param [options] - @param {anything} [options.defaultValue] - @return {Ember.MapWithDefault|Ember.Map} If options are passed, returns - `Ember.MapWithDefault` otherwise returns `Ember.Map` + Suspends multiple listeners during a callback. + + + @method suspendListeners + @for Ember + @param obj + @param {Array} eventName Array of event names + @param {Object|Function} targetOrMethod A target object or a function + @param {Function|String} method A function or the name of a function to be called on `target` + @param {Function} callback */ -MapWithDefault.create = function(options) { - if (options) { - return new MapWithDefault(options); - } else { - return new Map(); +function suspendListeners(obj, eventNames, target, method, callback) { + if (!method && 'function' === typeof target) { + method = target; + target = null; } -}; -MapWithDefault.prototype = Ember.create(Map.prototype); - -/** - Retrieve the value associated with a given key. + var suspendedActions = [], + eventName, actions, i, l; - @method get - @param {anything} key - @return {anything} the value associated with the key, or the default value -*/ -MapWithDefault.prototype.get = function(key) { - var hasValue = this.has(key); + for (i=0, l=eventNames.length; i 0) { - ret = meta.values[keyName]; + for (var i = actions.length - 3; i >= 0; i -= 3) { // looping in reverse for once listeners + var target = actions[i], method = actions[i+1], flags = actions[i+2]; + if (!method) { continue; } + if (flags & SUSPENDED) { continue; } + if (flags & ONCE) { removeListener(obj, eventName, target, method); } + if (!target) { target = obj; } + if ('string' === typeof method) { method = target[method]; } + if (params) { + method.apply(target, params); } else { - ret = obj[keyName]; - } - - if (ret === undefined && - 'object' === typeof obj && !(keyName in obj) && 'function' === typeof obj.unknownProperty) { - return obj.unknownProperty(keyName); + method.call(target); } - - return ret; } -}; + return true; +} /** - Sets the value of a property on an object, respecting computed properties - and notifying observers and other listeners of the change. If the - property is not defined but the object implements the `unknownProperty` - 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 `unknownProperty` handler. Otherwise you can ignore this - method. - - @method set + @private + @method hasListeners @for Ember - @param {Object} obj The object to modify. - @param {String} keyName The property key to set - @param {Object} value The value to set - @return {Object} the passed value. + @param obj + @param {String} eventName */ -set = function set(obj, keyName, value, tolerant) { - if (typeof obj === 'string') { - Ember.assert("Path '" + obj + "' must be global if no obj is given.", IS_GLOBAL.test(obj)); - value = keyName; - keyName = obj; - obj = null; - } +function hasListeners(obj, eventName) { + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName]; - if (!obj || keyName.indexOf('.') !== -1) { - return setPath(obj, keyName, value, tolerant); - } + return !!(actions && actions.length); +} - Ember.assert("You need to provide an object and key to `set`.", !!obj && keyName !== undefined); - Ember.assert('calling set on destroyed object', !obj.isDestroyed); +/** + @private + @method listenersFor + @for Ember + @param obj + @param {String} eventName +*/ +function listenersFor(obj, eventName) { + var ret = []; + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName]; - var meta = obj[META_KEY], desc = meta && meta.descs[keyName], - isUnknown, currentValue; - if (desc) { - desc.set(obj, keyName, value); - } else { - isUnknown = 'object' === typeof obj && !(keyName in obj); + if (!actions) { return ret; } - // setUnknownProperty is called if `obj` is an object, - // the property does not already exist, and the - // `setUnknownProperty` method exists on the object - if (isUnknown && 'function' === typeof obj.setUnknownProperty) { - obj.setUnknownProperty(keyName, value); - } else if (meta && meta.watching[keyName] > 0) { - if (MANDATORY_SETTER) { - currentValue = meta.values[keyName]; - } else { - currentValue = obj[keyName]; - } - // only trigger a change if the value has changed - if (value !== currentValue) { - Ember.propertyWillChange(obj, keyName); - if (MANDATORY_SETTER) { - if (currentValue === undefined && !(keyName in obj)) { - Ember.defineProperty(obj, keyName, null, value); // setup mandatory setter - } else { - meta.values[keyName] = value; - } - } else { - obj[keyName] = value; - } - Ember.propertyDidChange(obj, keyName); - } - } else { - obj[keyName] = value; - } + for (var i = 0, l = actions.length; i < l; i += 3) { + var target = actions[i], + method = actions[i+1]; + ret.push([target, method]); } - return value; -}; -// Currently used only by Ember Data tests -if (Ember.config.overrideAccessors) { - Ember.get = get; - Ember.set = set; - Ember.config.overrideAccessors(); - get = Ember.get; - set = Ember.set; + return ret; } -function firstKey(path) { - return path.match(FIRST_KEY)[0]; -} +/** + Define a property as a function that should be executed when + a specified event or events are triggered. -// assumes path is already normalized -function normalizeTuple(target, path) { - var hasThis = HAS_THIS.test(path), - isGlobal = !hasThis && IS_GLOBAL_PATH.test(path), - key; + 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!" - if (!target || isGlobal) target = Ember.lookup; - if (hasThis) path = path.slice(5); + @method on + @for Ember + @param {String} eventNames* + @param {Function} func + @return func +*/ +Ember.on = function(){ + var func = a_slice.call(arguments, -1)[0], + events = a_slice.call(arguments, 0, -1); + func.__ember_listens__ = events; + return func; +}; - if (target === Ember.lookup) { - key = firstKey(path); - target = get(target, key); - path = path.slice(key.length+1); - } +Ember.addListener = addListener; +Ember.removeListener = removeListener; +Ember._suspendListener = suspendListener; +Ember._suspendListeners = suspendListeners; +Ember.sendEvent = sendEvent; +Ember.hasListeners = hasListeners; +Ember.watchedEvents = watchedEvents; +Ember.listenersFor = listenersFor; +Ember.listenersDiff = actionsDiff; +Ember.listenersUnion = actionsUnion; - // must return some kind of path to be valid else other things will break. - if (!path || path.length===0) throw new Error('Invalid Path'); +})(); - return [ target, path ]; -} -function getPath(root, path) { - var hasThis, parts, tuple, idx, len; - // If there is no root and path is a key name, return that - // property from the global object. - // E.g. get('Ember') -> Ember - if (root === null && path.indexOf('.') === -1) { return get(Ember.lookup, path); } +(function() { +var guidFor = Ember.guidFor, + sendEvent = Ember.sendEvent; - // detect complicated paths and normalize them - hasThis = HAS_THIS.test(path); +/* + this.observerSet = { + [senderGuid]: { // variable name: `keySet` + [keyName]: listIndex + } + }, + this.observers = [ + { + sender: obj, + keyName: keyName, + eventName: eventName, + listeners: [ + [target, method, flags] + ] + }, + ... + ] +*/ +var ObserverSet = Ember._ObserverSet = function() { + this.clear(); +}; - if (!root || hasThis) { - tuple = normalizeTuple(root, path); - root = tuple[0]; - path = tuple[1]; - tuple.length = 0; - } +ObserverSet.prototype.add = function(sender, keyName, eventName) { + var observerSet = this.observerSet, + observers = this.observers, + senderGuid = guidFor(sender), + keySet = observerSet[senderGuid], + index; - parts = path.split("."); - len = parts.length; - for (idx=0; root && idx 0 || keyName === 'length', + proto = m.proto, + desc = m.descs[keyName]; -Ember.getWithDefault = function(root, key, defaultValue) { - var value = get(root, key); + if (!watching) { return; } + if (proto === obj) { return; } + if (desc && desc.willChange) { desc.willChange(obj, keyName); } + dependentKeysWillChange(obj, keyName, m); + chainsWillChange(obj, keyName, m); + notifyBeforeObservers(obj, keyName); +} +Ember.propertyWillChange = propertyWillChange; - if (value === undefined) { return defaultValue; } - return value; -}; +/** + This function is called just after an object property has changed. + It will notify any observers and clear caches among other things. + Normally you will not need to call this method directly but if for some + reason you can't directly watch a property you can invoke this method + manually along with `Ember.propertyWillChange()` which you should call just + before the property value changes. -Ember.get = get; -Ember.getPath = Ember.deprecateFunc('getPath is deprecated since get now supports paths', Ember.get); + @method propertyDidChange + @for Ember + @param {Object} obj The object with the property that will change + @param {String} keyName The property key (or path) that will change. + @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]; -Ember.set = set; -Ember.setPath = Ember.deprecateFunc('setPath is deprecated since set now supports paths', Ember.set); + if (proto === obj) { return; } -/** - Error-tolerant form of `Ember.set`. Will not blow up if any part of the - chain is `undefined`, `null`, or destroyed. + // shouldn't this mean that we're watching this key? + if (desc && desc.didChange) { desc.didChange(obj, keyName); } + if (!watching && keyName !== 'length') { return; } - This is primarily used when syncing bindings, which may try to update after - an object has been destroyed. + dependentKeysDidChange(obj, keyName, m); + chainsDidChange(obj, keyName, m, false); + notifyObservers(obj, keyName); +} +Ember.propertyDidChange = propertyDidChange; - @method trySet - @for Ember - @param {Object} obj The object to modify. - @param {String} keyName The property key to set - @param {Object} value The value to set -*/ -Ember.trySet = function(root, path, value) { - return set(root, path, value, true); -}; -Ember.trySetPath = Ember.deprecateFunc('trySetPath has been renamed to trySet', Ember.trySet); +var WILL_SEEN, DID_SEEN; -/** - Returns true if the provided path is global (e.g., `MyApp.fooController.bar`) - instead of local (`foo.bar.baz`). +// called whenever a property is about to change to clear the cache of any dependent keys (and notify those properties of changes, etc...) +function dependentKeysWillChange(obj, depKey, meta) { + if (obj.isDestroying) { return; } - @method isGlobalPath - @for Ember - @private - @param {String} path - @return Boolean -*/ -Ember.isGlobalPath = function(path) { - return IS_GLOBAL.test(path); -}; + var seen = WILL_SEEN, top = !seen; + if (top) { seen = WILL_SEEN = {}; } + iterDeps(propertyWillChange, obj, depKey, seen, meta); + if (top) { WILL_SEEN = null; } +} +// called whenever a property has just changed to update dependent keys +function dependentKeysDidChange(obj, depKey, meta) { + if (obj.isDestroying) { return; } -})(); + var seen = DID_SEEN, top = !seen; + if (top) { seen = DID_SEEN = {}; } + iterDeps(propertyDidChange, obj, depKey, seen, meta); + if (top) { DID_SEEN = null; } +} +function iterDeps(method, obj, depKey, seen, meta) { + var guid = guidFor(obj); + if (!seen[guid]) seen[guid] = {}; + if (seen[guid][depKey]) return; + seen[guid][depKey] = true; + var deps = meta.deps; + deps = deps && deps[depKey]; + if (deps) { + for(var key in deps) { + var desc = meta.descs[key]; + if (desc && desc._suspended === obj) continue; + method(obj, key); + } + } +} -(function() { -/** -@module ember-metal -*/ +function chainsWillChange(obj, keyName, m) { + if (!(m.hasOwnProperty('chainWatchers') && + m.chainWatchers[keyName])) { + return; + } -var GUID_KEY = Ember.GUID_KEY, - META_KEY = Ember.META_KEY, - EMPTY_META = Ember.EMPTY_META, - metaFor = Ember.meta, - o_create = Ember.create, - objectDefineProperty = Ember.platform.defineProperty; + var nodes = m.chainWatchers[keyName], + events = [], + i, l; -var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; + for(i = 0, l = nodes.length; i < l; i++) { + nodes[i].willChange(events); + } -// .......................................................... -// DESCRIPTOR -// + for (i = 0, l = events.length; i < l; i += 2) { + propertyWillChange(events[i], events[i+1]); + } +} -/** - Objects of this type can implement an interface to responds requests to - get and set. The default implementation handles simple properties. +function chainsDidChange(obj, keyName, m, suppressEvents) { + if (!(m.hasOwnProperty('chainWatchers') && + m.chainWatchers[keyName])) { + return; + } - You generally won't need to create or subclass this directly. + var nodes = m.chainWatchers[keyName], + events = suppressEvents ? null : [], + i, l; - @class Descriptor - @namespace Ember - @private - @constructor -*/ -var Descriptor = Ember.Descriptor = function() {}; + for(i = 0, l = nodes.length; i < l; i++) { + nodes[i].didChange(events); + } -// .......................................................... -// DEFINING PROPERTIES API -// + if (suppressEvents) { + return; + } -var MANDATORY_SETTER_FUNCTION = Ember.MANDATORY_SETTER_FUNCTION = function(value) { - Ember.assert("You must use Ember.set() to access this property (of " + this + ")", false); -}; + for (i = 0, l = events.length; i < l; i += 2) { + propertyDidChange(events[i], events[i+1]); + } +} -var DEFAULT_GETTER_FUNCTION = Ember.DEFAULT_GETTER_FUNCTION = function(name) { - return function() { - var meta = this[META_KEY]; - return meta && meta.values[name]; - }; +Ember.overrideChains = function(obj, keyName, m) { + chainsDidChange(obj, keyName, m, true); }; /** - @private + @method beginPropertyChanges + @chainable +*/ +function beginPropertyChanges() { + deferred++; +} - 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. +Ember.beginPropertyChanges = beginPropertyChanges; - Defines a property on an object. This method works much like the ES5 - `Object.defineProperty()` method except that it can also accept computed - properties and other special descriptors. +/** + @method endPropertyChanges +*/ +function endPropertyChanges() { + deferred--; + if (deferred<=0) { + beforeObserverSet.clear(); + observerSet.flush(); + } +} - Normally this method takes only three parameters. However if you pass an - instance of `Ember.Descriptor` as the third param then you can pass an - optional value as the fourth parameter. This is often more efficient than - creating new descriptor hashes for each property. +Ember.endPropertyChanges = endPropertyChanges; - ## Examples +/** + Make a series of property changes together in an + exception-safe way. ```javascript - // ES5 compatible mode - Ember.defineProperty(contact, 'firstName', { - writable: true, - configurable: false, - enumerable: true, - value: 'Charles' + Ember.changeProperties(function() { + obj1.set('foo', mayBlowUpWhenSet); + obj2.set('bar', baz); }); + ``` - // define a simple property - Ember.defineProperty(contact, 'lastName', undefined, 'Jolley'); - - // define a computed property - Ember.defineProperty(contact, 'fullName', Ember.computed(function() { - return this.firstName+' '+this.lastName; - }).property('firstName', 'lastName')); - ``` - - @method defineProperty - @for Ember - @param {Object} obj the object to define this property on. This may be a prototype. - @param {String} keyName the name of the property - @param {Ember.Descriptor} [desc] an instance of `Ember.Descriptor` (typically a - computed property) or an ES5 descriptor. - You must provide this or `data` but not both. - @param {anything} [data] something other than a descriptor, that will - become the explicit value of this property. + @method changeProperties + @param {Function} callback + @param [binding] */ -Ember.defineProperty = function(obj, keyName, desc, data, meta) { - var descs, existingDesc, watching, value; +Ember.changeProperties = function(cb, binding) { + beginPropertyChanges(); + tryFinally(cb, endPropertyChanges, binding); +}; - if (!meta) meta = metaFor(obj); - descs = meta.descs; - existingDesc = meta.descs[keyName]; - watching = meta.watching[keyName] > 0; +function notifyBeforeObservers(obj, keyName) { + if (obj.isDestroying) { return; } - if (existingDesc instanceof Ember.Descriptor) { - existingDesc.teardown(obj, keyName); + var eventName = keyName + ':before', listeners, diff; + if (deferred) { + listeners = beforeObserverSet.add(obj, keyName, eventName); + diff = listenersDiff(obj, eventName, listeners); + sendEvent(obj, eventName, [obj, keyName], diff); + } else { + sendEvent(obj, eventName, [obj, keyName]); } +} - if (desc instanceof Ember.Descriptor) { - value = desc; +function notifyObservers(obj, keyName) { + if (obj.isDestroying) { return; } - descs[keyName] = desc; - if (MANDATORY_SETTER && watching) { - objectDefineProperty(obj, keyName, { - configurable: true, - enumerable: true, - writable: true, - value: undefined // make enumerable - }); - } else { - obj[keyName] = undefined; // make enumerable - } - desc.setup(obj, keyName); + var eventName = keyName + ':change', listeners; + if (deferred) { + listeners = observerSet.add(obj, keyName, eventName); + listenersUnion(obj, eventName, listeners); } else { - descs[keyName] = undefined; // shadow descriptor in proto - if (desc == null) { - value = data; - - if (MANDATORY_SETTER && watching) { - meta.values[keyName] = data; - objectDefineProperty(obj, keyName, { - configurable: true, - enumerable: true, - set: MANDATORY_SETTER_FUNCTION, - get: DEFAULT_GETTER_FUNCTION(keyName) - }); - } else { - obj[keyName] = data; - } - } else { - value = desc; - - // compatibility with ES5 - objectDefineProperty(obj, keyName, desc); - } + sendEvent(obj, eventName, [obj, keyName]); } +} - // if key is being watched, override chains that - // were initialized with the prototype - if (watching) { Ember.overrideChains(obj, keyName, meta); } +})(); - // The `value` passed to the `didDefineProperty` hook is - // either the descriptor or data, whichever was passed. - if (obj.didDefineProperty) { obj.didDefineProperty(obj, keyName, value); } - return this; -}; +(function() { +// META_KEY +// _getPath +// propertyWillChange, propertyDidChange -})(); +var META_KEY = Ember.META_KEY, + MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, + IS_GLOBAL = /^([A-Z$]|([0-9][A-Z$]))/, + getPath = Ember._getPath; +/** + Sets the value of a property on an object, respecting computed properties + and notifying observers and other listeners of the change. If the + 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.) -(function() { -// Ember.tryFinally -/** -@module ember-metal + 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. + @param {String} keyName The property key to set + @param {Object} value The value to set + @return {Object} the passed value. */ +var set = function set(obj, keyName, value, tolerant) { + if (typeof obj === 'string') { + Ember.assert("Path '" + obj + "' must be global if no obj is given.", IS_GLOBAL.test(obj)); + value = keyName; + keyName = obj; + obj = null; + } -var AFTER_OBSERVERS = ':change'; -var BEFORE_OBSERVERS = ':before'; + Ember.assert("Cannot call set with "+ keyName +" key.", !!keyName); -var guidFor = Ember.guidFor; + if (!obj || keyName.indexOf('.') !== -1) { + return setPath(obj, keyName, value, tolerant); + } -var deferred = 0; + Ember.assert("You need to provide an object and key to `set`.", !!obj && keyName !== undefined); + Ember.assert('calling set on destroyed object', !obj.isDestroyed); -/* - this.observerSet = { - [senderGuid]: { // variable name: `keySet` - [keyName]: listIndex + var meta = obj[META_KEY], desc = meta && meta.descs[keyName], + isUnknown, currentValue; + if (desc) { + desc.set(obj, keyName, value); + } else { + isUnknown = 'object' === typeof obj && !(keyName in obj); + + // setUnknownProperty is called if `obj` is an object, + // the property does not already exist, and the + // `setUnknownProperty` method exists on the object + if (isUnknown && 'function' === typeof obj.setUnknownProperty) { + obj.setUnknownProperty(keyName, value); + } else if (meta && meta.watching[keyName] > 0) { + if (MANDATORY_SETTER) { + currentValue = meta.values[keyName]; + } else { + currentValue = obj[keyName]; + } + // only trigger a change if the value has changed + if (value !== currentValue) { + Ember.propertyWillChange(obj, keyName); + if (MANDATORY_SETTER) { + if (currentValue === undefined && !(keyName in obj)) { + Ember.defineProperty(obj, keyName, null, value); // setup mandatory setter + } else { + meta.values[keyName] = value; + } + } else { + obj[keyName] = value; + } + Ember.propertyDidChange(obj, keyName); + } + } else { + obj[keyName] = value; } - }, - this.observers = [ - { - sender: obj, - keyName: keyName, - eventName: eventName, - listeners: [ - [target, method, onceFlag, suspendedFlag] - ] - }, - ... - ] -*/ -function ObserverSet() { - this.clear(); + } + return value; +}; + +// Currently used only by Ember Data tests +if (Ember.config.overrideAccessors) { + Ember.set = set; + Ember.config.overrideAccessors(); + set = Ember.set; } -ObserverSet.prototype.add = function(sender, keyName, eventName) { - var observerSet = this.observerSet, - observers = this.observers, - senderGuid = Ember.guidFor(sender), - keySet = observerSet[senderGuid], - index; +function setPath(root, path, value, tolerant) { + var keyName; - if (!keySet) { - observerSet[senderGuid] = keySet = {}; + // get the last part of the path + keyName = path.slice(path.lastIndexOf('.') + 1); + + // get the first part of the part + path = path.slice(0, path.length-(keyName.length+1)); + + // unless the path is this, look up the first part to + // get the root + if (path !== 'this') { + root = getPath(root, path); } - index = keySet[keyName]; - if (index === undefined) { - index = observers.push({ - sender: sender, - keyName: keyName, - eventName: eventName, - listeners: [] - }) - 1; - keySet[keyName] = index; + + if (!keyName || keyName.length === 0) { + throw new Error('You passed an empty path'); } - return observers[index].listeners; -}; -ObserverSet.prototype.flush = function() { - var observers = this.observers, i, len, observer, sender; - this.clear(); - for (i=0, len=observers.length; i < len; ++i) { - observer = observers[i]; - sender = observer.sender; - if (sender.isDestroying || sender.isDestroyed) { continue; } - Ember.sendEvent(sender, observer.eventName, [sender, observer.keyName], observer.listeners); + if (!root) { + if (tolerant) { return; } + else { throw new Error('Object in path '+path+' could not be found or was destroyed.'); } } -}; -ObserverSet.prototype.clear = function() { - this.observerSet = {}; - this.observers = []; -}; + return set(root, keyName, value); +} -var beforeObserverSet = new ObserverSet(), observerSet = new ObserverSet(); +Ember.set = set; +Ember.setPath = Ember.deprecateFunc('setPath is deprecated since set now supports paths', Ember.set); /** - @method beginPropertyChanges - @chainable + Error-tolerant form of `Ember.set`. Will not blow up if any part of the + chain is `undefined`, `null`, or destroyed. + + This is primarily used when syncing bindings, which may try to update after + an object has been destroyed. + + @method trySet + @for Ember + @param {Object} obj The object to modify. + @param {String} path The property path to set + @param {Object} value The value to set */ -Ember.beginPropertyChanges = function() { - deferred++; +Ember.trySet = function(root, path, value) { + return set(root, path, value, true); }; +Ember.trySetPath = Ember.deprecateFunc('trySetPath has been renamed to trySet', Ember.trySet); + +})(); + + +(function() { /** - @method endPropertyChanges +@module ember-metal */ -Ember.endPropertyChanges = function() { - deferred--; - if (deferred<=0) { - beforeObserverSet.clear(); - observerSet.flush(); - } -}; - -/** - Make a series of property changes together in an - exception-safe way. - ```javascript - Ember.changeProperties(function() { - obj1.set('foo', mayBlowUpWhenSet); - obj2.set('bar', baz); - }); - ``` +/* + JavaScript (before ES6) does not have a Map implementation. Objects, + which are often used as dictionaries, may only have Strings as keys. - @method changeProperties - @param {Function} callback - @param [binding] -*/ -Ember.changeProperties = function(cb, binding){ - Ember.beginPropertyChanges(); - Ember.tryFinally(cb, Ember.endPropertyChanges, binding); -}; + Because Ember has a way to get a unique identifier for every object + via `Ember.guidFor`, we can implement a performant Map with arbitrary + keys. Because it is commonly used in low-level bookkeeping, Map is + implemented as a pure JavaScript object for performance. -/** - Set a list of properties on an object. These properties are set inside - a single `beginPropertyChanges` and `endPropertyChanges` batch, so - observers will be buffered. + This implementation follows the current iteration of the ES6 proposal for + maps (http://wiki.ecmascript.org/doku.php?id=harmony:simple_maps_and_sets), + with two exceptions. First, because we need our implementation to be pleasant + on older browsers, we do not use the `delete` name (using `remove` instead). + Second, as we do not have the luxury of in-VM iteration, we implement a + forEach method for iteration. - @method setProperties - @param target - @param {Hash} properties - @return target + Map is mocked out to look like an Ember object, so you can do + `Ember.Map.create()` for symmetry with other Ember classes. */ -Ember.setProperties = function(self, hash) { - Ember.changeProperties(function(){ - for(var prop in hash) { - if (hash.hasOwnProperty(prop)) Ember.set(self, prop, hash[prop]); - } - }); - return self; -}; - +var set = Ember.set, + guidFor = Ember.guidFor, + indexOf = Ember.ArrayPolyfills.indexOf; -function changeEvent(keyName) { - return keyName+AFTER_OBSERVERS; -} +var copy = function(obj) { + var output = {}; -function beforeEvent(keyName) { - return keyName+BEFORE_OBSERVERS; -} + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { output[prop] = obj[prop]; } + } -/** - @method addObserver - @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); - return this; + return output; }; -Ember.observersFor = function(obj, path) { - return Ember.listenersFor(obj, changeEvent(path)); +var copyMap = function(original, newObject) { + var keys = original.keys.copy(), + values = copy(original.values); + + newObject.keys = keys; + newObject.values = values; + newObject.length = original.length; + + return newObject; }; /** - @method removeObserver - @param obj - @param {String} path - @param {Object|Function} targetOrMethod - @param {Function|String} [method] + This class is used internally by Ember and Ember Data. + Please do not use it at this time. We plan to clean it up + and add many tests soon. + + @class OrderedSet + @namespace Ember + @constructor + @private */ -Ember.removeObserver = function(obj, path, target, method) { - Ember.unwatch(obj, path); - Ember.removeListener(obj, changeEvent(path), target, method); - return this; +var OrderedSet = Ember.OrderedSet = function() { + this.clear(); }; /** - @method addBeforeObserver - @param obj - @param {String} path - @param {Object|Function} targetOrMethod - @param {Function|String} [method] + @method create + @static + @return {Ember.OrderedSet} */ -Ember.addBeforeObserver = function(obj, path, target, method) { - Ember.addListener(obj, beforeEvent(path), target, method); - Ember.watch(obj, path); - return this; +OrderedSet.create = function() { + return new OrderedSet(); }; -// Suspend observer during callback. -// -// This should only be used by the target of the observer -// while it is setting the observed path. -Ember._suspendBeforeObserver = function(obj, path, target, method, callback) { - return Ember._suspendListener(obj, beforeEvent(path), target, method, callback); -}; -Ember._suspendObserver = function(obj, path, target, method, callback) { - return Ember._suspendListener(obj, changeEvent(path), target, method, callback); -}; +OrderedSet.prototype = { + /** + @method clear + */ + clear: function() { + this.presenceSet = {}; + this.list = []; + }, -var map = Ember.ArrayPolyfills.map; + /** + @method add + @param obj + */ + add: function(obj) { + var guid = guidFor(obj), + presenceSet = this.presenceSet, + list = this.list; -Ember._suspendBeforeObservers = function(obj, paths, target, method, callback) { - var events = map.call(paths, beforeEvent); - return Ember._suspendListeners(obj, events, target, method, callback); -}; + if (guid in presenceSet) { return; } -Ember._suspendObservers = function(obj, paths, target, method, callback) { - var events = map.call(paths, changeEvent); - return Ember._suspendListeners(obj, events, target, method, callback); -}; + presenceSet[guid] = true; + list.push(obj); + }, -Ember.beforeObserversFor = function(obj, path) { - return Ember.listenersFor(obj, beforeEvent(path)); -}; + /** + @method remove + @param obj + */ + remove: function(obj) { + var guid = guidFor(obj), + presenceSet = this.presenceSet, + list = this.list; -/** - @method removeBeforeObserver - @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); - return this; -}; + delete presenceSet[guid]; -Ember.notifyBeforeObservers = function(obj, keyName) { - if (obj.isDestroying) { return; } + var index = indexOf.call(list, obj); + if (index > -1) { + list.splice(index, 1); + } + }, - var eventName = beforeEvent(keyName), listeners, listenersDiff; - if (deferred) { - listeners = beforeObserverSet.add(obj, keyName, eventName); - listenersDiff = Ember.listenersDiff(obj, eventName, listeners); - Ember.sendEvent(obj, eventName, [obj, keyName], listenersDiff); - } else { - Ember.sendEvent(obj, eventName, [obj, keyName]); - } -}; + /** + @method isEmpty + @return {Boolean} + */ + isEmpty: function() { + return this.list.length === 0; + }, -Ember.notifyObservers = function(obj, keyName) { - if (obj.isDestroying) { return; } + /** + @method has + @param obj + @return {Boolean} + */ + has: function(obj) { + var guid = guidFor(obj), + presenceSet = this.presenceSet; - var eventName = changeEvent(keyName), listeners; - if (deferred) { - listeners = observerSet.add(obj, keyName, eventName); - Ember.listenersUnion(obj, eventName, listeners); - } else { - Ember.sendEvent(obj, eventName, [obj, keyName]); - } -}; + return guid in presenceSet; + }, -})(); + /** + @method forEach + @param {Function} fn + @param self + */ + forEach: function(fn, self) { + // allow mutation during iteration + var list = this.toArray(); + for (var i = 0, j = list.length; i < j; i++) { + fn.call(self, list[i]); + } + }, + /** + @method toArray + @return {Array} + */ + toArray: function() { + return this.list.slice(); + }, -(function() { -/** -@module ember-metal -*/ + /** + @method copy + @return {Ember.OrderedSet} + */ + copy: function() { + var set = new OrderedSet(); -var guidFor = Ember.guidFor, // utils.js - metaFor = Ember.meta, // utils.js - get = Ember.get, // accessors.js - set = Ember.set, // accessors.js - normalizeTuple = Ember.normalizeTuple, // accessors.js - GUID_KEY = Ember.GUID_KEY, // utils.js - META_KEY = Ember.META_KEY, // utils.js - // circular reference observer depends on Ember.watch - // we should move change events to this file or its own property_events.js - notifyObservers = Ember.notifyObservers, // observer.js - forEach = Ember.ArrayPolyfills.forEach, // array.js - FIRST_KEY = /^([^\.\*]+)/, - IS_PATH = /[\.\*]/; + set.presenceSet = copy(this.presenceSet); + set.list = this.toArray(); -var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, -o_defineProperty = Ember.platform.defineProperty; + return set; + } +}; -function firstKey(path) { - return path.match(FIRST_KEY)[0]; -} +/** + A Map stores values indexed by keys. Unlike JavaScript's + default Objects, the keys of a Map can be any JavaScript + object. -// returns true if the passed path is just a keyName -function isKeyName(path) { - return path==='*' || !IS_PATH.test(path); -} - -// .......................................................... -// DEPENDENT KEYS -// + Internally, a Map has two data structures: -function iterDeps(method, obj, depKey, seen, meta) { + 1. `keys`: an OrderedSet of all of the existing keys + 2. `values`: a JavaScript Object indexed by the `Ember.guidFor(key)` - var guid = guidFor(obj); - if (!seen[guid]) seen[guid] = {}; - if (seen[guid][depKey]) return; - seen[guid][depKey] = true; + When a key/value pair is added for the first time, we + add the key to the `keys` OrderedSet, and create or + replace an entry in `values`. When an entry is deleted, + we delete its entry in `keys` and `values`. - var deps = meta.deps; - deps = deps && deps[depKey]; - if (deps) { - for(var key in deps) { - var desc = meta.descs[key]; - if (desc && desc._suspended === obj) continue; - method(obj, key); - } - } -} + @class Map + @namespace Ember + @private + @constructor +*/ +var Map = Ember.Map = function() { + this.keys = Ember.OrderedSet.create(); + this.values = {}; +}; +/** + @method create + @static +*/ +Map.create = function() { + return new Map(); +}; -var WILL_SEEN, DID_SEEN; +Map.prototype = { + /** + This property will change as the number of objects in the map changes. + + @property length + @type number + @default 0 + */ + length: 0, + + + /** + Retrieve the value associated with a given key. -// called whenever a property is about to change to clear the cache of any dependent keys (and notify those properties of changes, etc...) -function dependentKeysWillChange(obj, depKey, meta) { - if (obj.isDestroying) { return; } + @method get + @param {*} key + @return {*} the value associated with the key, or `undefined` + */ + get: function(key) { + var values = this.values, + guid = guidFor(key); - var seen = WILL_SEEN, top = !seen; - if (top) { seen = WILL_SEEN = {}; } - iterDeps(propertyWillChange, obj, depKey, seen, meta); - if (top) { WILL_SEEN = null; } -} + return values[guid]; + }, -// called whenever a property has just changed to update dependent keys -function dependentKeysDidChange(obj, depKey, meta) { - if (obj.isDestroying) { return; } + /** + Adds a value to the map. If a value for the given key has already been + provided, the new value will replace the old value. - var seen = DID_SEEN, top = !seen; - if (top) { seen = DID_SEEN = {}; } - iterDeps(propertyDidChange, obj, depKey, seen, meta); - if (top) { DID_SEEN = null; } -} + @method set + @param {*} key + @param {*} value + */ + set: function(key, value) { + var keys = this.keys, + values = this.values, + guid = guidFor(key); -// .......................................................... -// CHAIN -// + keys.add(key); + values[guid] = value; + set(this, 'length', keys.list.length); + }, -function addChainWatcher(obj, keyName, node) { - if (!obj || ('object' !== typeof obj)) { return; } // nothing to do + /** + Removes a value from the map for an associated key. - var m = metaFor(obj), nodes = m.chainWatchers; + @method remove + @param {*} key + @return {Boolean} true if an item was removed, false otherwise + */ + remove: function(key) { + // don't use ES6 "delete" because it will be annoying + // to use in browsers that are not ES6 friendly; + var keys = this.keys, + values = this.values, + guid = guidFor(key); - if (!m.hasOwnProperty('chainWatchers')) { - nodes = m.chainWatchers = {}; - } + if (values.hasOwnProperty(guid)) { + keys.remove(key); + delete values[guid]; + set(this, 'length', keys.list.length); + return true; + } else { + return false; + } + }, - if (!nodes[keyName]) { nodes[keyName] = []; } - nodes[keyName].push(node); - Ember.watch(obj, keyName); -} + /** + Check whether a key is present. -function removeChainWatcher(obj, keyName, node) { - if (!obj || 'object' !== typeof obj) { return; } // nothing to do + @method has + @param {*} key + @return {Boolean} true if the item was present, false otherwise + */ + has: function(key) { + var values = this.values, + guid = guidFor(key); - var m = metaFor(obj, false); - if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do + return values.hasOwnProperty(guid); + }, - var nodes = m.chainWatchers; + /** + Iterate over all the keys and values. Calls the function once + for each key, passing in the key and value, in that order. - if (nodes[keyName]) { - nodes = nodes[keyName]; - for (var i = 0, l = nodes.length; i < l; i++) { - if (nodes[i] === node) { nodes.splice(i, 1); } - } - } - Ember.unwatch(obj, keyName); -} + The keys are guaranteed to be iterated over in insertion order. -var pendingQueue = []; + @method forEach + @param {Function} callback + @param {*} self if passed, the `this` value inside the + callback. By default, `this` is the map. + */ + forEach: function(callback, self) { + var keys = this.keys, + values = this.values; -// attempts to add the pendingQueue chains again. If some of them end up -// back in the queue and reschedule is true, schedules a timeout to try -// again. -function flushPendingChains() { - if (pendingQueue.length === 0) { return; } // nothing to do + keys.forEach(function(key) { + var guid = guidFor(key); + callback.call(self, key, values[guid]); + }); + }, - var queue = pendingQueue; - pendingQueue = []; + /** + @method copy + @return {Ember.Map} + */ + copy: function() { + return copyMap(this, new Map()); + } +}; - forEach.call(queue, function(q) { q[0].add(q[1]); }); +/** + @class MapWithDefault + @namespace Ember + @extends Ember.Map + @private + @constructor + @param [options] + @param {*} [options.defaultValue] +*/ +var MapWithDefault = Ember.MapWithDefault = function(options) { + Map.call(this); + this.defaultValue = options.defaultValue; +}; - Ember.warn('Watching an undefined global, Ember expects watched globals to be setup by the time the run loop is flushed, check for typos', pendingQueue.length === 0); -} +/** + @method create + @static + @param [options] + @param {*} [options.defaultValue] + @return {Ember.MapWithDefault|Ember.Map} If options are passed, returns + `Ember.MapWithDefault` otherwise returns `Ember.Map` +*/ +MapWithDefault.create = function(options) { + if (options) { + return new MapWithDefault(options); + } else { + return new Map(); + } +}; -function isProto(pvalue) { - return metaFor(pvalue, false).proto === pvalue; -} +MapWithDefault.prototype = Ember.create(Map.prototype); -// A ChainNode watches a single key on an object. If you provide a starting -// value for the key then the node won't actually watch it. For a root node -// pass null for parent and key and object for value. -var ChainNode = function(parent, key, value) { - var obj; - this._parent = parent; - this._key = key; +/** + Retrieve the value associated with a given key. - // _watching is true when calling get(this._parent, this._key) will - // return the value of this node. - // - // It is false for the root of a chain (because we have no parent) - // and for global paths (because the parent node is the object with - // the observer on it) - this._watching = value===undefined; + @method get + @param {*} key + @return {*} the value associated with the key, or the default value +*/ +MapWithDefault.prototype.get = function(key) { + var hasValue = this.has(key); - this._value = value; - this._paths = {}; - if (this._watching) { - this._object = parent.value(); - if (this._object) { addChainWatcher(this._object, this._key, this); } + if (hasValue) { + return Map.prototype.get.call(this, key); + } else { + var defaultValue = this.defaultValue(key); + this.set(key, defaultValue); + return defaultValue; } +}; - // Special-case: the EachProxy relies on immediate evaluation to - // establish its observers. - // - // TODO: Replace this with an efficient callback that the EachProxy - // can implement. - if (this._parent && this._parent._key === '@each') { - this.value(); - } +/** + @method copy + @return {Ember.MapWithDefault} +*/ +MapWithDefault.prototype.copy = function() { + return copyMap(this, new MapWithDefault({ + defaultValue: this.defaultValue + })); }; -var ChainNodePrototype = ChainNode.prototype; +})(); -ChainNodePrototype.value = function() { - if (this._value === undefined && this._watching) { - var obj = this._parent.value(); - this._value = (obj && !isProto(obj)) ? get(obj, this._key) : undefined; - } - return this._value; -}; -ChainNodePrototype.destroy = function() { - if (this._watching) { - var obj = this._object; - if (obj) { removeChainWatcher(obj, this._key, this); } - this._watching = false; // so future calls do nothing - } -}; -// copies a top level object only -ChainNodePrototype.copy = function(obj) { - var ret = new ChainNode(null, null, obj), - paths = this._paths, path; - for (path in paths) { - if (paths[path] <= 0) { continue; } // this check will also catch non-number vals. - ret.add(path); - } - return ret; -}; +(function() { +/** +@module ember-metal +*/ -// called on the root node of a chain to setup watchers on the specified -// path. -ChainNodePrototype.add = function(path) { - var obj, tuple, key, src, paths; +var META_KEY = Ember.META_KEY, + metaFor = Ember.meta, + objectDefineProperty = Ember.platform.defineProperty; - paths = this._paths; - paths[path] = (paths[path] || 0) + 1; +var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; - obj = this.value(); - tuple = normalizeTuple(obj, path); +// .......................................................... +// DESCRIPTOR +// - // the path was a local path - if (tuple[0] && tuple[0] === obj) { - path = tuple[1]; - key = firstKey(path); - path = path.slice(key.length+1); +/** + Objects of this type can implement an interface to respond to requests to + get and set. The default implementation handles simple properties. - // global path, but object does not exist yet. - // put into a queue and try to connect later. - } else if (!tuple[0]) { - pendingQueue.push([this, path]); - tuple.length = 0; - return; + You generally won't need to create or subclass this directly. - // global path, and object already exists - } else { - src = tuple[0]; - key = path.slice(0, 0-(tuple[1].length+1)); - path = tuple[1]; - } + @class Descriptor + @namespace Ember + @private + @constructor +*/ +Ember.Descriptor = function() {}; - tuple.length = 0; - this.chain(key, path, src); +// .......................................................... +// DEFINING PROPERTIES API +// + +var MANDATORY_SETTER_FUNCTION = Ember.MANDATORY_SETTER_FUNCTION = function(value) { + Ember.assert("You must use Ember.set() to access this property (of " + this + ")", false); }; -// called on the root node of a chain to teardown watcher on the specified -// path -ChainNodePrototype.remove = function(path) { - var obj, tuple, key, src, paths; +var DEFAULT_GETTER_FUNCTION = Ember.DEFAULT_GETTER_FUNCTION = function(name) { + return function() { + var meta = this[META_KEY]; + return meta && meta.values[name]; + }; +}; - paths = this._paths; - if (paths[path] > 0) { paths[path]--; } +/** + @private - obj = this.value(); - tuple = normalizeTuple(obj, path); - if (tuple[0] === obj) { - path = tuple[1]; - key = firstKey(path); - path = path.slice(key.length+1); - } else { - src = tuple[0]; - key = path.slice(0, 0-(tuple[1].length+1)); - path = tuple[1]; - } + 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. - tuple.length = 0; - this.unchain(key, path); -}; + Defines a property on an object. This method works much like the ES5 + `Object.defineProperty()` method except that it can also accept computed + properties and other special descriptors. -ChainNodePrototype.count = 0; + Normally this method takes only three parameters. However if you pass an + instance of `Ember.Descriptor` as the third param then you can pass an + optional value as the fourth parameter. This is often more efficient than + creating new descriptor hashes for each property. -ChainNodePrototype.chain = function(key, path, src) { - var chains = this._chains, node; - if (!chains) { chains = this._chains = {}; } + ## Examples - node = chains[key]; - if (!node) { node = chains[key] = new ChainNode(this, key, src); } - node.count++; // count chains... + ```javascript + // ES5 compatible mode + Ember.defineProperty(contact, 'firstName', { + writable: true, + configurable: false, + enumerable: true, + value: 'Charles' + }); - // chain rest of path if there is one - if (path && path.length>0) { - key = firstKey(path); - path = path.slice(key.length+1); - node.chain(key, path); // NOTE: no src means it will observe changes... - } -}; + // define a simple property + Ember.defineProperty(contact, 'lastName', undefined, 'Jolley'); -ChainNodePrototype.unchain = function(key, path) { - var chains = this._chains, node = chains[key]; + // define a computed property + Ember.defineProperty(contact, 'fullName', Ember.computed(function() { + return this.firstName+' '+this.lastName; + }).property('firstName', 'lastName')); + ``` - // unchain rest of path first... - if (path && path.length>1) { - key = firstKey(path); - path = path.slice(key.length+1); - node.unchain(key, path); - } + @method defineProperty + @for Ember + @param {Object} obj the object to define this property on. This may be a prototype. + @param {String} keyName the name of the property + @param {Ember.Descriptor} [desc] an instance of `Ember.Descriptor` (typically a + computed property) or an ES5 descriptor. + You must provide this or `data` but not both. + @param {*} [data] something other than a descriptor, that will + become the explicit value of this property. +*/ +Ember.defineProperty = function(obj, keyName, desc, data, meta) { + var descs, existingDesc, watching, value; - // delete node if needed. - node.count--; - if (node.count<=0) { - delete chains[node._key]; - node.destroy(); + if (!meta) meta = metaFor(obj); + descs = meta.descs; + existingDesc = meta.descs[keyName]; + watching = meta.watching[keyName] > 0; + + if (existingDesc instanceof Ember.Descriptor) { + existingDesc.teardown(obj, keyName); } -}; + if (desc instanceof Ember.Descriptor) { + value = desc; -ChainNodePrototype.willChange = function() { - var chains = this._chains; - if (chains) { - for(var key in chains) { - if (!chains.hasOwnProperty(key)) { continue; } - chains[key].willChange(); + descs[keyName] = desc; + if (MANDATORY_SETTER && watching) { + objectDefineProperty(obj, keyName, { + configurable: true, + enumerable: true, + writable: true, + value: undefined // make enumerable + }); + } else { + obj[keyName] = undefined; // make enumerable + } + } else { + descs[keyName] = undefined; // shadow descriptor in proto + if (desc == null) { + value = data; + + if (MANDATORY_SETTER && watching) { + meta.values[keyName] = data; + objectDefineProperty(obj, keyName, { + configurable: true, + enumerable: true, + set: MANDATORY_SETTER_FUNCTION, + get: DEFAULT_GETTER_FUNCTION(keyName) + }); + } else { + obj[keyName] = data; + } + } else { + value = desc; + + // compatibility with ES5 + objectDefineProperty(obj, keyName, desc); } } - if (this._parent) { this._parent.chainWillChange(this, this._key, 1); } -}; + // if key is being watched, override chains that + // were initialized with the prototype + if (watching) { Ember.overrideChains(obj, keyName, meta); } -ChainNodePrototype.chainWillChange = function(chain, path, depth) { - if (this._key) { path = this._key + '.' + path; } + // The `value` passed to the `didDefineProperty` hook is + // either the descriptor or data, whichever was passed. + if (obj.didDefineProperty) { obj.didDefineProperty(obj, keyName, value); } - if (this._parent) { - this._parent.chainWillChange(this, path, depth+1); - } else { - if (depth > 1) { Ember.propertyWillChange(this.value(), path); } - path = 'this.' + path; - if (this._paths[path] > 0) { Ember.propertyWillChange(this.value(), path); } - } + return this; }; -ChainNodePrototype.chainDidChange = function(chain, path, depth) { - if (this._key) { path = this._key + '.' + path; } - if (this._parent) { - this._parent.chainDidChange(this, path, depth+1); - } else { - if (depth > 1) { Ember.propertyDidChange(this.value(), path); } - path = 'this.' + path; - if (this._paths[path] > 0) { Ember.propertyDidChange(this.value(), path); } - } -}; -ChainNodePrototype.didChange = function(suppressEvent) { - // invalidate my own value first. - if (this._watching) { - var obj = this._parent.value(); - if (obj !== this._object) { - removeChainWatcher(this._object, this._key, this); - this._object = obj; - addChainWatcher(obj, this._key, this); - } - this._value = undefined; +})(); - // Special-case: the EachProxy relies on immediate evaluation to - // establish its observers. - if (this._parent && this._parent._key === '@each') - this.value(); - } - // then notify chains... - var chains = this._chains; - if (chains) { - for(var key in chains) { - if (!chains.hasOwnProperty(key)) { continue; } - chains[key].didChange(suppressEvent); - } - } - if (suppressEvent) { return; } +(function() { +var get = Ember.get; - // and finally tell parent about my path changing... - if (this._parent) { this._parent.chainDidChange(this, this._key, 1); } -}; +/** + To get multiple properties at once, call `Ember.getProperties` + with an object followed by a list of strings or an array: -// 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; - if (!ret) { - ret = m.chains = new ChainNode(null, null, obj); - } else if (ret.value() !== obj) { - ret = m.chains = ret.copy(obj); + ```javascript + Ember.getProperties(record, 'firstName', 'lastName', 'zipCode'); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } + ``` + + is equivalent to: + + ```javascript + Ember.getProperties(record, ['firstName', 'lastName', 'zipCode']); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } + ``` + + @method getProperties + @param obj + @param {String...|Array} list of keys to get + @return {Hash} +*/ +Ember.getProperties = function(obj) { + var ret = {}, + propertyNames = arguments, + i = 1; + + if (arguments.length === 2 && Ember.typeOf(arguments[1]) === 'array') { + i = 0; + propertyNames = arguments[1]; + } + for(var len = propertyNames.length; i < len; i++) { + ret[propertyNames[i]] = get(obj, propertyNames[i]); } return ret; -} - -Ember.overrideChains = function(obj, keyName, m) { - chainsDidChange(obj, keyName, m, true); }; -function chainsWillChange(obj, keyName, m, arg) { - if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do +})(); - var nodes = m.chainWatchers; - nodes = nodes[keyName]; - if (!nodes) { return; } - for(var i = 0, l = nodes.length; i < l; i++) { - nodes[i].willChange(arg); - } -} +(function() { +var changeProperties = Ember.changeProperties, + set = Ember.set; -function chainsDidChange(obj, keyName, m, arg) { - if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do +/** + Set a list of properties on an object. These properties are set inside + a single `beginPropertyChanges` and `endPropertyChanges` batch, so + observers will be buffered. - var nodes = m.chainWatchers; + ```javascript + anObject.setProperties({ + firstName: "Stanley", + lastName: "Stuart", + age: "21" + }) + ``` - nodes = nodes[keyName]; - if (!nodes) { return; } + @method setProperties + @param self + @param {Object} hash + @return self +*/ +Ember.setProperties = function(self, hash) { + changeProperties(function() { + for(var prop in hash) { + if (hash.hasOwnProperty(prop)) { set(self, prop, hash[prop]); } + } + }); + return self; +}; - // looping in reverse because the chainWatchers array can be modified inside didChange - for (var i = nodes.length - 1; i >= 0; i--) { - nodes[i].didChange(arg); - } -} +})(); -// .......................................................... -// WATCH -// -/** - @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()` +(function() { +var metaFor = Ember.meta, // utils.js + typeOf = Ember.typeOf, // utils.js + MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, + o_defineProperty = Ember.platform.defineProperty; - @method watch - @for Ember - @param obj - @param {String} keyName -*/ -Ember.watch = function(obj, keyName) { +Ember.watchKey = function(obj, keyName) { // can't watch length on Array - it is special... - if (keyName === 'length' && Ember.typeOf(obj) === 'array') { return this; } + if (keyName === 'length' && typeOf(obj) === 'array') { return; } - var m = metaFor(obj), watching = m.watching, desc; + var m = metaFor(obj), watching = m.watching; // activate watching first time if (!watching[keyName]) { watching[keyName] = 1; - if (isKeyName(keyName)) { - desc = m.descs[keyName]; - if (desc && desc.willWatch) { desc.willWatch(obj, keyName); } - - if ('function' === typeof obj.willWatchProperty) { - obj.willWatchProperty(keyName); - } - if (MANDATORY_SETTER && keyName in obj) { - m.values[keyName] = obj[keyName]; - o_defineProperty(obj, keyName, { - configurable: true, - enumerable: true, - set: Ember.MANDATORY_SETTER_FUNCTION, - get: Ember.DEFAULT_GETTER_FUNCTION(keyName) - }); - } - } else { - chainsFor(obj).add(keyName); + if ('function' === typeof obj.willWatchProperty) { + obj.willWatchProperty(keyName); } - } else { + if (MANDATORY_SETTER && keyName in obj) { + m.values[keyName] = obj[keyName]; + o_defineProperty(obj, keyName, { + configurable: true, + enumerable: true, + set: Ember.MANDATORY_SETTER_FUNCTION, + get: Ember.DEFAULT_GETTER_FUNCTION(keyName) + }); + } + } else { watching[keyName] = (watching[keyName] || 0) + 1; } - return this; -}; - -Ember.isWatching = function isWatching(obj, key) { - var meta = obj[META_KEY]; - return (meta && meta.watching[key]) > 0; }; -Ember.watch.flushPending = flushPendingChains; - -Ember.unwatch = function(obj, keyName) { - // can't watch length on Array - it is special... - if (keyName === 'length' && Ember.typeOf(obj) === 'array') { return this; } - var m = metaFor(obj), watching = m.watching, desc; +Ember.unwatchKey = function(obj, keyName) { + var m = metaFor(obj), watching = m.watching; if (watching[keyName] === 1) { watching[keyName] = 0; - if (isKeyName(keyName)) { - desc = m.descs[keyName]; - if (desc && desc.didUnwatch) { desc.didUnwatch(obj, keyName); } - - if ('function' === typeof obj.didUnwatchProperty) { - obj.didUnwatchProperty(keyName); - } - - if (MANDATORY_SETTER && keyName in obj) { - o_defineProperty(obj, keyName, { - configurable: true, - enumerable: true, - writable: true, - value: m.values[keyName] - }); - delete m.values[keyName]; - } - } else { - chainsFor(obj).remove(keyName); + if ('function' === typeof obj.didUnwatchProperty) { + obj.didUnwatchProperty(keyName); } - } else if (watching[keyName]>1) { + if (MANDATORY_SETTER && keyName in obj) { + o_defineProperty(obj, keyName, { + configurable: true, + enumerable: true, + writable: true, + value: m.values[keyName] + }); + delete m.values[keyName]; + } + } else if (watching[keyName] > 1) { watching[keyName]--; } - - return this; }; -/** - @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. - - @method rewatch - @for Ember - @param obj -*/ -Ember.rewatch = function(obj) { - var m = metaFor(obj, false), chains = m.chains; - - // make sure the object has its own guid. - if (GUID_KEY in obj && !obj.hasOwnProperty(GUID_KEY)) { - Ember.generateGuid(obj, 'ember'); - } - - // make sure any chained watchers update. - if (chains && chains.value() !== obj) { - m.chains = chains.copy(obj); - } +})(); - return this; -}; -Ember.finishChains = function(obj) { - var m = metaFor(obj, false), chains = m.chains; - if (chains) { - if (chains.value() !== obj) { - m.chains = chains = chains.copy(obj); - } - chains.didChange(true); - } -}; -// .......................................................... -// PROPERTY CHANGES -// +(function() { +var metaFor = Ember.meta, // utils.js + get = Ember.get, // property_get.js + normalizeTuple = Ember.normalizeTuple, // property_get.js + forEach = Ember.ArrayPolyfills.forEach, // array.js + warn = Ember.warn, + watchKey = Ember.watchKey, + unwatchKey = Ember.unwatchKey, + FIRST_KEY = /^([^\.\*]+)/; -/** - This function is called just before an object property is about to change. - It will notify any before observers and prepare caches among other things. +function firstKey(path) { + return path.match(FIRST_KEY)[0]; +} - Normally you will not need to call this method directly but if for some - reason you can't directly watch a property you can invoke this method - manually along with `Ember.propertyDidChange()` which you should call just - after the property value changes. +var pendingQueue = []; - @method propertyWillChange - @for Ember - @param {Object} obj The object with the property that will change - @param {String} keyName The property key (or path) that will change. - @return {void} -*/ -function propertyWillChange(obj, keyName, value) { - var m = metaFor(obj, false), - watching = m.watching[keyName] > 0 || keyName === 'length', - proto = m.proto, - desc = m.descs[keyName]; +// attempts to add the pendingQueue chains again. If some of them end up +// back in the queue and reschedule is true, schedules a timeout to try +// again. +Ember.flushPendingChains = function() { + if (pendingQueue.length === 0) { return; } // nothing to do - if (!watching) { return; } - if (proto === obj) { return; } - if (desc && desc.willChange) { desc.willChange(obj, keyName); } - dependentKeysWillChange(obj, keyName, m); - chainsWillChange(obj, keyName, m); - Ember.notifyBeforeObservers(obj, keyName); -} + var queue = pendingQueue; + pendingQueue = []; -Ember.propertyWillChange = propertyWillChange; + forEach.call(queue, function(q) { q[0].add(q[1]); }); -/** - This function is called just after an object property has changed. - It will notify any observers and clear caches among other things. + warn('Watching an undefined global, Ember expects watched globals to be setup by the time the run loop is flushed, check for typos', pendingQueue.length === 0); +}; - Normally you will not need to call this method directly but if for some - reason you can't directly watch a property you can invoke this method - manually along with `Ember.propertyWilLChange()` which you should call just - before the property value changes. - @method propertyDidChange - @for Ember - @param {Object} obj The object with the property that will change - @param {String} keyName The property key (or path) that will change. - @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]; +function addChainWatcher(obj, keyName, node) { + if (!obj || ('object' !== typeof obj)) { return; } // nothing to do - if (proto === obj) { return; } + var m = metaFor(obj), nodes = m.chainWatchers; - // shouldn't this mean that we're watching this key? - if (desc && desc.didChange) { desc.didChange(obj, keyName); } - if (!watching && keyName !== 'length') { return; } + if (!m.hasOwnProperty('chainWatchers')) { + nodes = m.chainWatchers = {}; + } - dependentKeysDidChange(obj, keyName, m); - chainsDidChange(obj, keyName, m); - Ember.notifyObservers(obj, keyName); + if (!nodes[keyName]) { nodes[keyName] = []; } + nodes[keyName].push(node); + watchKey(obj, keyName); } -Ember.propertyDidChange = propertyDidChange; +var removeChainWatcher = Ember.removeChainWatcher = function(obj, keyName, node) { + if (!obj || 'object' !== typeof obj) { return; } // nothing to do -var NODE_STACK = []; + var m = metaFor(obj, false); + if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do -/** - Tears down the meta on an object so that it can be garbage collected. - Multiple calls will have no effect. + var nodes = m.chainWatchers; - @method destroy - @for Ember - @param {Object} obj the object to destroy - @return {void} -*/ -Ember.destroy = function (obj) { - var meta = obj[META_KEY], node, nodes, key, nodeObject; - if (meta) { - obj[META_KEY] = null; - // remove chainWatchers to remove circular references that would prevent GC - node = meta.chains; - if (node) { - NODE_STACK.push(node); - // process tree - while (NODE_STACK.length > 0) { - node = NODE_STACK.pop(); - // push children - nodes = node._chains; - if (nodes) { - for (key in nodes) { - if (nodes.hasOwnProperty(key)) { - NODE_STACK.push(nodes[key]); - } - } - } - // remove chainWatcher in node object - if (node._watching) { - nodeObject = node._object; - if (nodeObject) { - removeChainWatcher(nodeObject, node._key, node); - } - } - } + if (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); }; -})(); - +// A ChainNode watches a single key on an object. If you provide a starting +// value for the key then the node won't actually watch it. For a root node +// pass null for parent and key and object for value. +var ChainNode = Ember._ChainNode = function(parent, key, value) { + this._parent = parent; + this._key = key; + // _watching is true when calling get(this._parent, this._key) will + // return the value of this node. + // + // It is false for the root of a chain (because we have no parent) + // and for global paths (because the parent node is the object with + // the observer on it) + this._watching = value===undefined; -(function() { -/** -@module ember-metal -*/ + this._value = value; + this._paths = {}; + if (this._watching) { + this._object = parent.value(); + if (this._object) { addChainWatcher(this._object, this._key, this); } + } -Ember.warn("The CP_DEFAULT_CACHEABLE flag has been removed and computed properties are always cached by default. Use `volatile` if you don't want caching.", Ember.ENV.CP_DEFAULT_CACHEABLE !== false); + // Special-case: the EachProxy relies on immediate evaluation to + // establish its observers. + // + // TODO: Replace this with an efficient callback that the EachProxy + // can implement. + if (this._parent && this._parent._key === '@each') { + this.value(); + } +}; +var ChainNodePrototype = ChainNode.prototype; -var get = Ember.get, - set = Ember.set, - metaFor = Ember.meta, - guidFor = Ember.guidFor, - a_slice = [].slice, - o_create = Ember.create, - META_KEY = Ember.META_KEY, - watch = Ember.watch, - unwatch = Ember.unwatch; +function lazyGet(obj, key) { + if (!obj) return undefined; -// .......................................................... -// DEPENDENT KEYS -// + var meta = metaFor(obj, false); + // check if object meant only to be a prototype + if (meta.proto === obj) return undefined; -// data structure: -// meta.deps = { -// 'depKey': { -// 'keyName': count, -// } -// } + if (key === "@each") return get(obj, key); -/* - This function returns a map of unique dependencies for a - given object and key. -*/ -function keysForDep(obj, depsMeta, depKey) { - var keys = depsMeta[depKey]; - if (!keys) { - // if there are no dependencies yet for a the given key - // create a new empty list of dependencies for the key - keys = depsMeta[depKey] = {}; - } else if (!depsMeta.hasOwnProperty(depKey)) { - // otherwise if the dependency list is inherited from - // a superclass, clone the hash - keys = depsMeta[depKey] = o_create(keys); + // if a CP only return cached value + var desc = meta.descs[key]; + if (desc && desc._cacheable) { + if (key in meta.cache) { + return meta.cache[key]; + } else { + return undefined; + } } - return keys; -} -/* return obj[META_KEY].deps */ -function metaForDeps(obj, meta) { - var deps = meta.deps; - // If the current object has no dependencies... - if (!deps) { - // initialize the dependencies with a pointer back to - // the current object - deps = meta.deps = {}; - } else if (!meta.hasOwnProperty('deps')) { - // otherwise if the dependencies are inherited from the - // object's superclass, clone the deps - deps = meta.deps = o_create(deps); - } - return deps; + return get(obj, key); } -function addDependentKeys(desc, obj, keyName, meta) { - // the descriptor has a list of dependent keys, so - // add all of its dependent keys. - var depKeys = desc._dependentKeys, depsMeta, idx, len, depKey, keys; - if (!depKeys) return; - - depsMeta = metaForDeps(obj, meta); - - for(idx = 0, len = depKeys.length; idx < len; idx++) { - depKey = depKeys[idx]; - // Lookup keys meta for depKey - keys = keysForDep(obj, depsMeta, depKey); - // Increment the number of times depKey depends on keyName. - keys[keyName] = (keys[keyName] || 0) + 1; - // Watch the depKey - watch(obj, depKey); +ChainNodePrototype.value = function() { + if (this._value === undefined && this._watching) { + var obj = this._parent.value(); + this._value = lazyGet(obj, this._key); } -} - -function removeDependentKeys(desc, obj, keyName, meta) { - // the descriptor has a list of dependent keys, so - // add all of its dependent keys. - var depKeys = desc._dependentKeys, depsMeta, idx, len, depKey, keys; - if (!depKeys) return; - - depsMeta = metaForDeps(obj, meta); + return this._value; +}; - for(idx = 0, len = depKeys.length; idx < len; idx++) { - depKey = depKeys[idx]; - // Lookup keys meta for depKey - keys = keysForDep(obj, depsMeta, depKey); - // Increment the number of times depKey depends on keyName. - keys[keyName] = (keys[keyName] || 0) - 1; - // Watch the depKey - unwatch(obj, depKey); +ChainNodePrototype.destroy = function() { + if (this._watching) { + var obj = this._object; + if (obj) { removeChainWatcher(obj, this._key, this); } + this._watching = false; // so future calls do nothing } -} - -// .......................................................... -// COMPUTED PROPERTY -// +}; -/** - @class ComputedProperty - @namespace Ember - @extends Ember.Descriptor - @constructor -*/ -function ComputedProperty(func, opts) { - this.func = func; - this._cacheable = (opts && opts.cacheable !== undefined) ? opts.cacheable : true; - this._dependentKeys = opts && opts.dependentKeys; -} +// copies a top level object only +ChainNodePrototype.copy = function(obj) { + var ret = new ChainNode(null, null, obj), + paths = this._paths, path; + for (path in paths) { + if (paths[path] <= 0) { continue; } // this check will also catch non-number vals. + ret.add(path); + } + return ret; +}; -Ember.ComputedProperty = ComputedProperty; -ComputedProperty.prototype = new Ember.Descriptor(); +// called on the root node of a chain to setup watchers on the specified +// path. +ChainNodePrototype.add = function(path) { + var obj, tuple, key, src, paths; -var ComputedPropertyPrototype = ComputedProperty.prototype; + paths = this._paths; + paths[path] = (paths[path] || 0) + 1; -/** - Call on a computed property to set it into cacheable mode. When in this - mode the computed property will automatically cache the return value of - your function until one of the dependent keys changes. + obj = this.value(); + tuple = normalizeTuple(obj, path); - ```javascript - MyApp.president = Ember.Object.create({ - fullName: function() { - return this.get('firstName') + ' ' + this.get('lastName'); + // the path was a local path + if (tuple[0] && tuple[0] === obj) { + path = tuple[1]; + key = firstKey(path); + path = path.slice(key.length+1); - // After calculating the value of this function, Ember will - // return that value without re-executing this function until - // one of the dependent properties change. - }.property('firstName', 'lastName') - }); - ``` + // global path, but object does not exist yet. + // put into a queue and try to connect later. + } else if (!tuple[0]) { + pendingQueue.push([this, path]); + tuple.length = 0; + return; - Properties are cacheable by default. + // global path, and object already exists + } else { + src = tuple[0]; + key = path.slice(0, 0-(tuple[1].length+1)); + path = tuple[1]; + } - @method cacheable - @param {Boolean} aFlag optional set to `false` to disable caching - @chainable -*/ -ComputedPropertyPrototype.cacheable = function(aFlag) { - this._cacheable = aFlag !== false; - return this; + tuple.length = 0; + this.chain(key, path, src); }; -/** - Call on a computed property to set it into non-cached mode. When in this - mode the computed property will not automatically cache the return value. +// called on the root node of a chain to teardown watcher on the specified +// path +ChainNodePrototype.remove = function(path) { + var obj, tuple, key, src, paths; - ```javascript - MyApp.outsideService = Ember.Object.create({ - value: function() { - return OutsideService.getValue(); - }.property().volatile() - }); - ``` + paths = this._paths; + if (paths[path] > 0) { paths[path]--; } - @method volatile - @chainable -*/ -ComputedPropertyPrototype.volatile = function() { - return this.cacheable(false); + obj = this.value(); + tuple = normalizeTuple(obj, path); + if (tuple[0] === obj) { + path = tuple[1]; + key = firstKey(path); + path = path.slice(key.length+1); + } else { + src = tuple[0]; + key = path.slice(0, 0-(tuple[1].length+1)); + path = tuple[1]; + } + + tuple.length = 0; + this.unchain(key, path); }; -/** - Sets the dependent keys on this computed property. Pass any number of - arguments containing key paths that this computed property depends on. +ChainNodePrototype.count = 0; - ```javascript - MyApp.president = Ember.Object.create({ - fullName: Ember.computed(function() { - return this.get('firstName') + ' ' + this.get('lastName'); +ChainNodePrototype.chain = function(key, path, src) { + var chains = this._chains, node; + if (!chains) { chains = this._chains = {}; } - // Tell Ember that this computed property depends on firstName - // and lastName - }).property('firstName', 'lastName') - }); - ``` + node = chains[key]; + if (!node) { node = chains[key] = new ChainNode(this, key, src); } + node.count++; // count chains... - @method property - @param {String} path* zero or more property paths - @chainable -*/ -ComputedPropertyPrototype.property = function() { - var args = []; - for (var i = 0, l = arguments.length; i < l; i++) { - args.push(arguments[i]); + // chain rest of path if there is one + if (path && path.length>0) { + key = firstKey(path); + path = path.slice(key.length+1); + node.chain(key, path); // NOTE: no src means it will observe changes... } - this._dependentKeys = args; - return this; }; -/** - 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. - - You can pass a hash of these values to a computed property like this: +ChainNodePrototype.unchain = function(key, path) { + var chains = this._chains, node = chains[key]; - ``` - person: function() { - var personId = this.get('personId'); - return App.Person.create({ id: personId }); - }.property().meta({ type: App.Person }) - ``` - - The hash that you pass to the `meta()` function will be saved on the - computed property descriptor under the `_meta` key. Ember runtime - exposes a public API for retrieving these values from classes, - via the `metaForProperty()` function. - - @method meta - @param {Hash} meta - @chainable -*/ - -ComputedPropertyPrototype.meta = function(meta) { - if (arguments.length === 0) { - return this._meta || {}; - } else { - this._meta = meta; - return this; + // unchain rest of path first... + if (path && path.length>1) { + key = firstKey(path); + path = path.slice(key.length+1); + node.unchain(key, path); } -}; -/* impl descriptor API */ -ComputedPropertyPrototype.willWatch = function(obj, keyName) { - // watch already creates meta for this instance - var meta = obj[META_KEY]; - Ember.assert('watch should have setup meta to be writable', meta.source === obj); - if (!(keyName in meta.cache)) { - addDependentKeys(this, obj, keyName, meta); + // delete node if needed. + node.count--; + if (node.count<=0) { + delete chains[node._key]; + node.destroy(); } + }; -ComputedPropertyPrototype.didUnwatch = function(obj, keyName) { - var meta = obj[META_KEY]; - Ember.assert('unwatch should have setup meta to be writable', meta.source === obj); - if (!(keyName in meta.cache)) { - // unwatch already creates meta for this instance - removeDependentKeys(this, obj, keyName, meta); +ChainNodePrototype.willChange = function(events) { + var chains = this._chains; + if (chains) { + for(var key in chains) { + if (!chains.hasOwnProperty(key)) { continue; } + chains[key].willChange(events); + } } + + if (this._parent) { this._parent.chainWillChange(this, this._key, 1, events); } }; -/* impl descriptor API */ -ComputedPropertyPrototype.didChange = function(obj, keyName) { - // _suspended is set via a CP.set to ensure we don't clear - // the cached value set by the setter - if (this._cacheable && this._suspended !== obj) { - var meta = metaFor(obj); - if (keyName in meta.cache) { - delete meta.cache[keyName]; - if (!meta.watching[keyName]) { - removeDependentKeys(this, obj, keyName, meta); - } +ChainNodePrototype.chainWillChange = function(chain, path, depth, events) { + if (this._key) { path = this._key + '.' + path; } + + if (this._parent) { + this._parent.chainWillChange(this, path, depth+1, events); + } else { + if (depth > 1) { + events.push(this.value(), path); + } + path = 'this.' + path; + if (this._paths[path] > 0) { + events.push(this.value(), path); } } }; -/* impl descriptor API */ -ComputedPropertyPrototype.get = function(obj, keyName) { - var ret, cache, meta; - if (this._cacheable) { - meta = metaFor(obj); - cache = meta.cache; - if (keyName in cache) { return cache[keyName]; } - ret = cache[keyName] = this.func.call(obj, keyName); - if (!meta.watching[keyName]) { - addDependentKeys(this, obj, keyName, meta); - } +ChainNodePrototype.chainDidChange = function(chain, path, depth, events) { + if (this._key) { path = this._key + '.' + path; } + if (this._parent) { + this._parent.chainDidChange(this, path, depth+1, events); } else { - ret = this.func.call(obj, keyName); + if (depth > 1) { + events.push(this.value(), path); + } + path = 'this.' + path; + if (this._paths[path] > 0) { + events.push(this.value(), path); + } } - return ret; }; -/* impl descriptor API */ -ComputedPropertyPrototype.set = function(obj, keyName, value) { - var cacheable = this._cacheable, - func = this.func, - meta = metaFor(obj, cacheable), - watched = meta.watching[keyName], - oldSuspended = this._suspended, - hadCachedValue = false, - cache = meta.cache, - cachedValue, ret; +ChainNodePrototype.didChange = function(events) { + // invalidate my own value first. + if (this._watching) { + var obj = this._parent.value(); + if (obj !== this._object) { + removeChainWatcher(this._object, this._key, this); + this._object = obj; + addChainWatcher(obj, this._key, this); + } + this._value = undefined; - this._suspended = obj; + // Special-case: the EachProxy relies on immediate evaluation to + // establish its observers. + if (this._parent && this._parent._key === '@each') + this.value(); + } - try { - if (cacheable && cache.hasOwnProperty(keyName)) { - cachedValue = cache[keyName]; - hadCachedValue = true; + // then notify chains... + var chains = this._chains; + if (chains) { + for(var key in chains) { + if (!chains.hasOwnProperty(key)) { continue; } + chains[key].didChange(events); } + } - // Check if the CP has been wrapped - if (func.wrappedFunction) { func = func.wrappedFunction; } + // if no events are passed in then we only care about the above wiring update + if (events === null) { return; } - // For backwards-compatibility with computed properties - // that check for arguments.length === 2 to determine if - // they are being get or set, only pass the old cached - // value if the computed property opts into a third - // argument. - if (func.length === 3) { - ret = func.call(obj, keyName, value, cachedValue); - } else if (func.length === 2) { - ret = func.call(obj, keyName, value); - } else { - Ember.defineProperty(obj, keyName, null, cachedValue); - Ember.set(obj, keyName, value); - return; + // and finally tell parent about my path changing... + if (this._parent) { this._parent.chainDidChange(this, this._key, 1, events); } +}; + +Ember.finishChains = function(obj) { + var m = metaFor(obj, false), chains = m.chains; + if (chains) { + if (chains.value() !== obj) { + m.chains = chains = chains.copy(obj); } + chains.didChange(null); + } +}; - if (hadCachedValue && cachedValue === ret) { return; } +})(); - if (watched) { Ember.propertyWillChange(obj, keyName); } - if (hadCachedValue) { - delete cache[keyName]; - } - if (cacheable) { - if (!watched && !hadCachedValue) { - addDependentKeys(this, obj, keyName, meta); - } - cache[keyName] = ret; - } +(function() { +var metaFor = Ember.meta, // utils.js + typeOf = Ember.typeOf, // utils.js + ChainNode = Ember._ChainNode; // chains.js - if (watched) { Ember.propertyDidChange(obj, keyName); } - } finally { - this._suspended = oldSuspended; +// 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; + if (!ret) { + ret = m.chains = new ChainNode(null, null, obj); + } else if (ret.value() !== obj) { + ret = m.chains = ret.copy(obj); } return ret; -}; +} -/* called when property is defined */ -ComputedPropertyPrototype.setup = function(obj, keyName) { - var meta = obj[META_KEY]; - if (meta && meta.watching[keyName]) { - addDependentKeys(this, obj, keyName, metaFor(obj)); +Ember.watchPath = function(obj, keyPath) { + // can't watch length on Array - it is special... + if (keyPath === 'length' && typeOf(obj) === 'array') { return; } + + var m = metaFor(obj), watching = m.watching; + + if (!watching[keyPath]) { // activate watching first time + watching[keyPath] = 1; + chainsFor(obj).add(keyPath); + } else { + watching[keyPath] = (watching[keyPath] || 0) + 1; } }; -/* called before property is overridden */ -ComputedPropertyPrototype.teardown = function(obj, keyName) { - var meta = metaFor(obj); +Ember.unwatchPath = function(obj, keyPath) { + var m = metaFor(obj), watching = m.watching; - if (meta.watching[keyName] || keyName in meta.cache) { - removeDependentKeys(this, obj, keyName, meta); + if (watching[keyPath] === 1) { + watching[keyPath] = 0; + chainsFor(obj).remove(keyPath); + } else if (watching[keyPath] > 1) { + watching[keyPath]--; } +}; +})(); - if (this._cacheable) { delete meta.cache[keyName]; } - return null; // no value to restore -}; +(function() { +/** +@module ember-metal +*/ + +var metaFor = Ember.meta, // utils.js + GUID_KEY = Ember.GUID_KEY, // utils.js + META_KEY = Ember.META_KEY, // utils.js + removeChainWatcher = Ember.removeChainWatcher, + watchKey = Ember.watchKey, // watch_key.js + unwatchKey = Ember.unwatchKey, + watchPath = Ember.watchPath, // watch_path.js + unwatchPath = Ember.unwatchPath, + typeOf = Ember.typeOf, // utils.js + generateGuid = Ember.generateGuid, + IS_PATH = /[\.\*]/; + +// returns true if the passed path is just a keyName +function isKeyName(path) { + return path==='*' || !IS_PATH.test(path); +} /** - This helper returns a new property descriptor that wraps the passed - computed property function. You can use this helper to define properties - with mixins or via `Ember.defineProperty()`. + @private - The function you pass will be used to both get and set property values. - The function should accept two parameters, key and value. If value is not - undefined you should set the value first. In either case return the - current value of the property. + 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()` - @method computed + @method watch @for Ember - @param {Function} func The computed property function. - @return {Ember.ComputedProperty} property descriptor instance + @param obj + @param {String} keyName */ -Ember.computed = function(func) { - var args; +Ember.watch = function(obj, keyPath) { + // can't watch length on Array - it is special... + if (keyPath === 'length' && typeOf(obj) === 'array') { return; } - if (arguments.length > 1) { - args = a_slice.call(arguments, 0, -1); - func = a_slice.call(arguments, -1)[0]; + if (isKeyName(keyPath)) { + watchKey(obj, keyPath); + } else { + watchPath(obj, keyPath); } +}; - var cp = new ComputedProperty(func); +Ember.isWatching = function isWatching(obj, key) { + var meta = obj[META_KEY]; + return (meta && meta.watching[key]) > 0; +}; - if (args) { - cp.property.apply(cp, args); - } +Ember.watch.flushPending = Ember.flushPendingChains; - return cp; +Ember.unwatch = function(obj, keyPath) { + // can't watch length on Array - it is special... + if (keyPath === 'length' && typeOf(obj) === 'array') { return; } + + if (isKeyName(keyPath)) { + unwatchKey(obj, keyPath); + } else { + unwatchPath(obj, keyPath); + } }; /** - Returns the cached value for a property, if one exists. - This can be useful for peeking at the value of a computed - property that is generated lazily, without accidentally causing - it to be created. + @private - @method cacheFor + 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. + + @method rewatch @for Ember - @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 + @param obj */ -Ember.cacheFor = function cacheFor(obj, key) { - var cache = metaFor(obj, false).cache; +Ember.rewatch = function(obj) { + var m = metaFor(obj, false), chains = m.chains; - if (cache && key in cache) { - return cache[key]; + // make sure the object has its own guid. + if (GUID_KEY in obj && !obj.hasOwnProperty(GUID_KEY)) { + generateGuid(obj, 'ember'); } -}; -/** - @method computed.not - @for Ember - @param {String} dependentKey -*/ -Ember.computed.not = function(dependentKey) { - return Ember.computed(dependentKey, function(key) { - return !get(this, dependentKey); - }); + // make sure any chained watchers update. + if (chains && chains.value() !== obj) { + m.chains = chains.copy(obj); + } }; -/** - @method computed.empty - @for Ember - @param {String} dependentKey -*/ -Ember.computed.empty = function(dependentKey) { - return Ember.computed(dependentKey, function(key) { - var val = get(this, dependentKey); - return val === undefined || val === null || val === '' || (Ember.isArray(val) && get(val, 'length') === 0); - }); -}; +var NODE_STACK = []; /** - @method computed.bool - @for Ember - @param {String} dependentKey -*/ -Ember.computed.bool = function(dependentKey) { - return Ember.computed(dependentKey, function(key) { - return !!get(this, dependentKey); - }); -}; + Tears down the meta on an object so that it can be garbage collected. + Multiple calls will have no effect. -/** - @method computed.alias + @method destroy @for Ember - @param {String} dependentKey + @param {Object} obj the object to destroy + @return {void} */ -Ember.computed.alias = function(dependentKey) { - return Ember.computed(dependentKey, function(key, value){ - if (arguments.length === 1) { - return get(this, dependentKey); - } else { - set(this, dependentKey, value); - return value; +Ember.destroy = function (obj) { + var meta = obj[META_KEY], node, nodes, key, nodeObject; + if (meta) { + obj[META_KEY] = null; + // remove chainWatchers to remove circular references that would prevent GC + node = meta.chains; + if (node) { + NODE_STACK.push(node); + // process tree + while (NODE_STACK.length > 0) { + node = NODE_STACK.pop(); + // push children + nodes = node._chains; + if (nodes) { + for (key in nodes) { + if (nodes.hasOwnProperty(key)) { + NODE_STACK.push(nodes[key]); + } + } + } + // remove chainWatcher in node object + if (node._watching) { + nodeObject = node._object; + if (nodeObject) { + removeChainWatcher(nodeObject, node._key, node); + } + } + } } - }); + } }; })(); @@ -3654,5202 +4060,5411 @@ Ember.computed.alias = function(dependentKey) { @module ember-metal */ -var o_create = Ember.create, - metaFor = Ember.meta, - metaPath = Ember.metaPath, - META_KEY = Ember.META_KEY; +Ember.warn("The CP_DEFAULT_CACHEABLE flag has been removed and computed properties are always cached by default. Use `volatile` if you don't want caching.", Ember.ENV.CP_DEFAULT_CACHEABLE !== false); -/* - The event system uses a series of nested hashes to store listeners on an - object. When a listener is registered, or when an event arrives, these - hashes are consulted to determine which target and action pair to invoke. - The hashes are stored in the object's meta hash, and look like this: +var get = Ember.get, + set = Ember.set, + metaFor = Ember.meta, + a_slice = [].slice, + o_create = Ember.create, + META_KEY = Ember.META_KEY, + watch = Ember.watch, + unwatch = Ember.unwatch; - // Object's meta hash - { - listeners: { // variable name: `listenerSet` - "foo:changed": [ // variable name: `actions` - [target, method, onceFlag, suspendedFlag] - ] - } - } +// .......................................................... +// DEPENDENT KEYS +// -*/ +// data structure: +// meta.deps = { +// 'depKey': { +// 'keyName': count, +// } +// } -function indexOf(array, target, method) { - var index = -1; - for (var i = 0, l = array.length; i < l; i++) { - if (target === array[i][0] && method === array[i][1]) { index = i; break; } +/* + This function returns a map of unique dependencies for a + given object and key. +*/ +function keysForDep(depsMeta, depKey) { + var keys = depsMeta[depKey]; + if (!keys) { + // if there are no dependencies yet for a the given key + // create a new empty list of dependencies for the key + keys = depsMeta[depKey] = {}; + } else if (!depsMeta.hasOwnProperty(depKey)) { + // otherwise if the dependency list is inherited from + // a superclass, clone the hash + keys = depsMeta[depKey] = o_create(keys); } - return index; + return keys; } -function actionsFor(obj, eventName) { - var meta = metaFor(obj, true), - actions; - - if (!meta.listeners) { meta.listeners = {}; } +function metaForDeps(meta) { + return keysForDep(meta, 'deps'); +} - if (!meta.hasOwnProperty('listeners')) { - // setup inherited copy of the listeners object - meta.listeners = o_create(meta.listeners); - } +function addDependentKeys(desc, obj, keyName, meta) { + // the descriptor has a list of dependent keys, so + // add all of its dependent keys. + var depKeys = desc._dependentKeys, depsMeta, idx, len, depKey, keys; + if (!depKeys) return; - actions = meta.listeners[eventName]; + depsMeta = metaForDeps(meta); - // if there are actions, but the eventName doesn't exist in our listeners, then copy them from the prototype - if (actions && !meta.listeners.hasOwnProperty(eventName)) { - actions = meta.listeners[eventName] = meta.listeners[eventName].slice(); - } else if (!actions) { - actions = meta.listeners[eventName] = []; + for(idx = 0, len = depKeys.length; idx < len; idx++) { + depKey = depKeys[idx]; + // Lookup keys meta for depKey + keys = keysForDep(depsMeta, depKey); + // Increment the number of times depKey depends on keyName. + keys[keyName] = (keys[keyName] || 0) + 1; + // Watch the depKey + watch(obj, depKey); } - - return actions; } -function actionsUnion(obj, eventName, otherActions) { - var meta = obj[META_KEY], - actions = meta && meta.listeners && meta.listeners[eventName]; +function removeDependentKeys(desc, obj, keyName, meta) { + // the descriptor has a list of dependent keys, so + // add all of its dependent keys. + var depKeys = desc._dependentKeys, depsMeta, idx, len, depKey, keys; + if (!depKeys) return; - if (!actions) { return; } - for (var i = actions.length - 1; i >= 0; i--) { - var target = actions[i][0], - method = actions[i][1], - once = actions[i][2], - suspended = actions[i][3], - actionIndex = indexOf(otherActions, target, method); + depsMeta = metaForDeps(meta); - if (actionIndex === -1) { - otherActions.push([target, method, once, suspended]); - } + for(idx = 0, len = depKeys.length; idx < len; idx++) { + depKey = depKeys[idx]; + // Lookup keys meta for depKey + keys = keysForDep(depsMeta, depKey); + // Increment the number of times depKey depends on keyName. + keys[keyName] = (keys[keyName] || 0) - 1; + // Watch the depKey + unwatch(obj, depKey); } } -function actionsDiff(obj, eventName, otherActions) { - var meta = obj[META_KEY], - actions = meta && meta.listeners && meta.listeners[eventName], - diffActions = []; +// .......................................................... +// COMPUTED PROPERTY +// - if (!actions) { return; } - for (var i = actions.length - 1; i >= 0; i--) { - var target = actions[i][0], - method = actions[i][1], - once = actions[i][2], - suspended = actions[i][3], - actionIndex = indexOf(otherActions, target, method); +/** + A computed property transforms an objects function into a property. - if (actionIndex !== -1) { continue; } + By default the function backing the computed property will only be called + once and the result will be cached. You can specify various properties + that your computed property is dependent on. This will force the cached + result to be recomputed if the dependencies are modified. - otherActions.push([target, method, once, suspended]); - diffActions.push([target, method, once, suspended]); - } + In the following example we declare a computed property (by calling + `.property()` on the fullName function) and setup the properties + dependencies (depending on firstName and lastName). The fullName function + will be called once (regardless of how many times it is accessed) as long + as it's dependencies have not been changed. Once firstName or lastName are updated + any future calls (or anything bound) to fullName will incorporate the new + values. - return diffActions; -} + ```javascript + Person = Ember.Object.extend({ + // these will be supplied by `create` + firstName: null, + lastName: null, -/** - Add an event listener + fullName: function() { + var firstName = this.get('firstName'); + var lastName = this.get('lastName'); - @method addListener - @for Ember - @param obj - @param {String} eventName - @param {Object|Function} targetOrMethod A target object or a function - @param {Function|String} method A function or the name of a function to be called on `target` -*/ -function addListener(obj, eventName, target, method, once) { - Ember.assert("You must pass at least an object and event name to Ember.addListener", !!obj && !!eventName); + return firstName + ' ' + lastName; + }.property('firstName', 'lastName') + }); - if (!method && 'function' === typeof target) { - method = target; - target = null; - } + var tom = Person.create({ + firstName: "Tom", + lastName: "Dale" + }); - var actions = actionsFor(obj, eventName), - actionIndex = indexOf(actions, target, method); + tom.get('fullName') // "Tom Dale" + ``` - if (actionIndex !== -1) { return; } + You can also define what Ember should do when setting a computed property. + If you try to set a computed property, it will be invoked with the key and + value you want to set it to. You can also accept the previous value as the + third parameter. - actions.push([target, method, once, undefined]); + ```javascript - if ('function' === typeof obj.didAddListener) { - obj.didAddListener(eventName, target, method); - } -} + Person = Ember.Object.extend({ + // these will be supplied by `create` + firstName: null, + lastName: null, -/** - Remove an event listener + fullName: function(key, value, oldValue) { + // getter + if (arguments.length === 1) { + var firstName = this.get('firstName'); + var lastName = this.get('lastName'); - Arguments should match those passed to {{#crossLink "Ember/addListener"}}{{/crossLink}} + return firstName + ' ' + lastName; - @method removeListener - @for Ember - @param obj - @param {String} eventName - @param {Object|Function} targetOrMethod A target object or a function - @param {Function|String} method A function or the name of a function to be called on `target` -*/ -function removeListener(obj, eventName, target, method) { - Ember.assert("You must pass at least an object and event name to Ember.removeListener", !!obj && !!eventName); + // setter + } else { + var name = value.split(" "); - if (!method && 'function' === typeof target) { - method = target; - target = null; - } + this.set('firstName', name[0]); + this.set('lastName', name[1]); - function _removeListener(target, method, once) { - var actions = actionsFor(obj, eventName), - actionIndex = indexOf(actions, target, method); + return value; + } + }.property('firstName', 'lastName') + }); - // action doesn't exist, give up silently - if (actionIndex === -1) { return; } + var person = Person.create(); + person.set('fullName', "Peter Wagenet"); + person.get('firstName') // Peter + person.get('lastName') // Wagenet + ``` - actions.splice(actionIndex, 1); + @class ComputedProperty + @namespace Ember + @extends Ember.Descriptor + @constructor +*/ +function ComputedProperty(func, opts) { + this.func = func; - if ('function' === typeof obj.didRemoveListener) { - obj.didRemoveListener(eventName, target, method); - } - } + this._cacheable = (opts && opts.cacheable !== undefined) ? opts.cacheable : true; + this._dependentKeys = opts && opts.dependentKeys; + this._readOnly = opts && (opts.readOnly !== undefined || !!opts.readOnly); +} - if (method) { - _removeListener(target, method); - } else { - var meta = obj[META_KEY], - actions = meta && meta.listeners && meta.listeners[eventName]; +Ember.ComputedProperty = ComputedProperty; +ComputedProperty.prototype = new Ember.Descriptor(); - if (!actions) { return; } - for (var i = actions.length - 1; i >= 0; i--) { - _removeListener(actions[i][0], actions[i][1]); - } - } -} +var ComputedPropertyPrototype = ComputedProperty.prototype; /** - @private + Properties are cacheable by default. Computed property will automatically + cache the return value of your function until one of the dependent keys changes. - Suspend listener during callback. + Call `volatile()` to set it into non-cached mode. When in this mode + the computed property will not automatically cache the return value. - This should only be used by the target of the event listener - when it is taking an action that would cause the event, e.g. - an object might suspend its property change listener while it is - setting that property. + However, if a property is properly observable, there is no reason to disable + caching. - @method suspendListener - @for Ember - @param obj - @param {String} eventName - @param {Object|Function} targetOrMethod A target object or a function - @param {Function|String} method A function or the name of a function to be called on `target` - @param {Function} callback + @method cacheable + @param {Boolean} aFlag optional set to `false` to disable caching + @return {Ember.ComputedProperty} this + @chainable */ -function suspendListener(obj, eventName, target, method, callback) { - if (!method && 'function' === typeof target) { - method = target; - target = null; - } - - var actions = actionsFor(obj, eventName), - actionIndex = indexOf(actions, target, method), - action; +ComputedPropertyPrototype.cacheable = function(aFlag) { + this._cacheable = aFlag !== false; + return this; +}; - if (actionIndex !== -1) { - action = actions[actionIndex].slice(); // copy it, otherwise we're modifying a shared object - action[3] = true; // mark the action as suspended - actions[actionIndex] = action; // replace the shared object with our copy - } +/** + Call on a computed property to set it into non-cached mode. When in this + mode the computed property will not automatically cache the return value. - function tryable() { return callback.call(target); } - function finalizer() { if (action) { action[3] = undefined; } } + ```javascript + MyApp.outsideService = Ember.Object.create({ + value: function() { + return OutsideService.getValue(); + }.property().volatile() + }); + ``` - return Ember.tryFinally(tryable, finalizer); -} + @method volatile + @return {Ember.ComputedProperty} this + @chainable +*/ +ComputedPropertyPrototype.volatile = function() { + return this.cacheable(false); +}; /** - @private + Call on a computed property to set it into read-only mode. When in this + mode the computed property will throw an error when set. - Suspend listener during callback. + ```javascript + MyApp.person = Ember.Object.create({ + guid: function() { + return 'guid-guid-guid'; + }.property().readOnly() + }); - This should only be used by the target of the event listener - when it is taking an action that would cause the event, e.g. - an object might suspend its property change listener while it is - setting that property. + MyApp.person.set('guid', 'new-guid'); // will throw an exception + ``` - @method suspendListener - @for Ember - @param obj - @param {Array} eventName Array of event names - @param {Object|Function} targetOrMethod A target object or a function - @param {Function|String} method A function or the name of a function to be called on `target` - @param {Function} callback + @method readOnly + @return {Ember.ComputedProperty} this + @chainable */ -function suspendListeners(obj, eventNames, target, method, callback) { - if (!method && 'function' === typeof target) { - method = target; - target = null; - } - - var suspendedActions = [], - eventName, actions, action, i, l; +ComputedPropertyPrototype.readOnly = function(readOnly) { + this._readOnly = readOnly === undefined || !!readOnly; + return this; +}; - for (i=0, l=eventNames.length; i= 0; i--) { // looping in reverse for once listeners - if (!actions[i] || actions[i][3] === true) { continue; } - var target = actions[i][0], - method = actions[i][1], - once = actions[i][2]; + var tom = Person.create({ + firstName: "Tom", + lastName: "Dale" + }); - if (once) { removeListener(obj, eventName, target, method); } - if (!target) { target = obj; } - if ('string' === typeof method) { method = target[method]; } - if (params) { - method.apply(target, params); - } else { - method.apply(target); - } + tom.get('fullName') // "Tom Dale" + ``` + + @method get + @param {String} keyName The key being accessed. + @return {Object} The return value of the function backing the CP. +*/ +ComputedPropertyPrototype.get = function(obj, keyName) { + var ret, cache, meta, chainNodes; + if (this._cacheable) { + meta = metaFor(obj); + cache = meta.cache; + if (keyName in cache) { return cache[keyName]; } + ret = cache[keyName] = this.func.call(obj, keyName); + chainNodes = meta.chainWatchers && meta.chainWatchers[keyName]; + if (chainNodes) { finishChains(chainNodes); } + addDependentKeys(this, obj, keyName, meta); + } else { + ret = this.func.call(obj, keyName); } - return true; -} + return ret; +}; /** - @private - @method hasListeners - @for Ember - @param obj - @param {String} eventName -*/ -function hasListeners(obj, eventName) { - var meta = obj[META_KEY], - actions = meta && meta.listeners && meta.listeners[eventName]; + Set the value of a computed property. If the function that backs your + computed property does not accept arguments then the default action for + setting would be to define the property on the current object, and set + the value of the property to the value being set. - return !!(actions && actions.length); -} + Generally speaking if you intend for your computed property to be set + your backing function should accept either two or three arguments. -/** - @private - @method listenersFor - @for Ember - @param obj - @param {String} eventName + @method set + @param {String} keyName The key being accessed. + @param {Object} newValue The new value being assigned. + @param {String} oldValue The old value being replaced. + @return {Object} The return value of the function backing the CP. */ -function listenersFor(obj, eventName) { - var ret = []; - var meta = obj[META_KEY], - actions = meta && meta.listeners && meta.listeners[eventName]; - - if (!actions) { return ret; } +ComputedPropertyPrototype.set = function(obj, keyName, value) { + var cacheable = this._cacheable, + func = this.func, + meta = metaFor(obj, cacheable), + watched = meta.watching[keyName], + oldSuspended = this._suspended, + hadCachedValue = false, + cache = meta.cache, + funcArgLength, cachedValue, ret; - for (var i = 0, l = actions.length; i < l; i++) { - var target = actions[i][0], - method = actions[i][1]; - ret.push([target, method]); + if (this._readOnly) { + throw new Error('Cannot Set: ' + keyName + ' on: ' + obj.toString() ); } - return ret; -} - -Ember.addListener = addListener; -Ember.removeListener = removeListener; -Ember._suspendListener = suspendListener; -Ember._suspendListeners = suspendListeners; -Ember.sendEvent = sendEvent; -Ember.hasListeners = hasListeners; -Ember.watchedEvents = watchedEvents; -Ember.listenersFor = listenersFor; -Ember.listenersDiff = actionsDiff; -Ember.listenersUnion = actionsUnion; - -})(); + this._suspended = obj; + try { + if (cacheable && cache.hasOwnProperty(keyName)) { + cachedValue = cache[keyName]; + hadCachedValue = true; + } -(function() { -// Ember.Logger -// Ember.watch.flushPending -// Ember.beginPropertyChanges, Ember.endPropertyChanges -// Ember.guidFor, Ember.tryFinally + // Check if the CP has been wrapped. If if has, use the + // length from the wrapped function. + funcArgLength = (func.wrappedFunction ? func.wrappedFunction.length : func.length); -/** -@module ember-metal -*/ + // For backwards-compatibility with computed properties + // that check for arguments.length === 2 to determine if + // they are being get or set, only pass the old cached + // value if the computed property opts into a third + // argument. + if (funcArgLength === 3) { + ret = func.call(obj, keyName, value, cachedValue); + } else if (funcArgLength === 2) { + ret = func.call(obj, keyName, value); + } else { + Ember.defineProperty(obj, keyName, null, cachedValue); + Ember.set(obj, keyName, value); + return; + } -// .......................................................... -// HELPERS -// + if (hadCachedValue && cachedValue === ret) { return; } -var slice = [].slice, - forEach = Ember.ArrayPolyfills.forEach; + if (watched) { Ember.propertyWillChange(obj, keyName); } -// invokes passed params - normalizing so you can pass target/func, -// target/string or just func -function invoke(target, method, args, ignore) { + if (hadCachedValue) { + delete cache[keyName]; + } - if (method === undefined) { - method = target; - target = undefined; - } + if (cacheable) { + if (!hadCachedValue) { + addDependentKeys(this, obj, keyName, meta); + } + cache[keyName] = ret; + } - if ('string' === typeof method) { method = target[method]; } - if (args && ignore > 0) { - args = args.length > ignore ? slice.call(args, ignore) : null; + if (watched) { Ember.propertyDidChange(obj, keyName); } + } finally { + this._suspended = oldSuspended; } + return ret; +}; - return Ember.handleErrors(function() { - // IE8's Function.prototype.apply doesn't accept undefined/null arguments. - return method.apply(target || this, args || []); - }, this); -} - - -// .......................................................... -// RUNLOOP -// +/* called before property is overridden */ +ComputedPropertyPrototype.teardown = function(obj, keyName) { + var meta = metaFor(obj); -var timerMark; // used by timers... + if (keyName in meta.cache) { + removeDependentKeys(this, obj, keyName, meta); + } -/** -Ember RunLoop (Private) + if (this._cacheable) { delete meta.cache[keyName]; } -@class RunLoop -@namespace Ember -@private -@constructor -*/ -var RunLoop = function(prev) { - this._prev = prev || null; - this.onceTimers = {}; + return null; // no value to restore }; -RunLoop.prototype = { - /** - @method end - */ - end: function() { - this.flush(); - }, - /** - @method prev - */ - prev: function() { - return this._prev; - }, +/** + This helper returns a new property descriptor that wraps the passed + computed property function. You can use this helper to define properties + with mixins or via `Ember.defineProperty()`. - // .......................................................... - // Delayed Actions - // + The function you pass will be used to both get and set property values. + The function should accept two parameters, key and value. If value is not + undefined you should set the value first. In either case return the + current value of the property. + @method computed + @for Ember + @param {Function} func The computed property function. + @return {Ember.ComputedProperty} property descriptor instance +*/ +Ember.computed = function(func) { + var args; - /** - @method schedule - @param {String} queueName - @param target - @param method - */ - schedule: function(queueName, target, method) { - var queues = this._queues, queue; - if (!queues) { queues = this._queues = {}; } - queue = queues[queueName]; - if (!queue) { queue = queues[queueName] = []; } + if (arguments.length > 1) { + args = a_slice.call(arguments, 0, -1); + func = a_slice.call(arguments, -1)[0]; + } - var args = arguments.length > 3 ? slice.call(arguments, 3) : null; - queue.push({ target: target, method: method, args: args }); - return this; - }, + if (typeof func !== "function") { + throw new Error("Computed Property declared without a property function"); + } - /** - @method flush - @param {String} queueName - */ - flush: function(queueName) { - var queueNames, idx, len, queue, log; + var cp = new ComputedProperty(func); - if (!this._queues) { return this; } // nothing to do + if (args) { + cp.property.apply(cp, args); + } - function iter(item) { - invoke(item.target, item.method, item.args); - } + return cp; +}; - function tryable() { - forEach.call(queue, iter); - } +/** + Returns the cached value for a property, if one exists. + This can be useful for peeking at the value of a computed + property that is generated lazily, without accidentally causing + it to be created. - Ember.watch.flushPending(); // make sure all chained watchers are setup + @method cacheFor + @for Ember + @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 +*/ +Ember.cacheFor = function cacheFor(obj, key) { + var cache = metaFor(obj, false).cache; - if (queueName) { - while (this._queues && (queue = this._queues[queueName])) { - this._queues[queueName] = null; + if (cache && key in cache) { + return cache[key]; + } +}; - // the sync phase is to allow property changes to propagate. don't - // invoke observers until that is finished. - if (queueName === 'sync') { - log = Ember.LOG_BINDINGS; - if (log) { Ember.Logger.log('Begin: Flush Sync Queue'); } +function getProperties(self, propertyNames) { + var ret = {}; + for(var i = 0; i < propertyNames.length; i++) { + ret[propertyNames[i]] = get(self, propertyNames[i]); + } + return ret; +} - Ember.beginPropertyChanges(); +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); + }); + }; +} - Ember.tryFinally(tryable, Ember.endPropertyChanges); +function registerComputedWithProperties(name, macro) { + Ember.computed[name] = function() { + var properties = a_slice.call(arguments); - if (log) { Ember.Logger.log('End: Flush Sync Queue'); } + var computed = Ember.computed(function() { + return macro.apply(this, [getProperties(this, properties)]); + }); - } else { - forEach.call(queue, iter); - } - } + return computed.property.apply(computed, properties); + }; +} - } else { - queueNames = Ember.run.queues; - len = queueNames.length; - idx = 0; +/** + A computed property that returns true of the value of the dependent + property is null, an empty string, empty array, or empty function. - outerloop: - while (idx < len) { - queueName = queueNames[idx]; - queue = this._queues && this._queues[queueName]; - delete this._queues[queueName]; + Note: When using `Ember.computed.empty` to watch an array make sure to + use the `array.length` syntax so the computed can subscribe to transitions + from empty to non-empty states. - if (queue) { - // the sync phase is to allow property changes to propagate. don't - // invoke observers until that is finished. - if (queueName === 'sync') { - log = Ember.LOG_BINDINGS; - if (log) { Ember.Logger.log('Begin: Flush Sync Queue'); } + Example - Ember.beginPropertyChanges(); + ```javascript + var ToDoList = Ember.Object.extend({ + done: Ember.computed.empty('todos.length') + }); + var todoList = ToDoList.create({todos: ['Unit Test', 'Documentation', 'Release']}); + todoList.get('done'); // false + todoList.get('todos').clear(); // [] + todoList.get('done'); // true + ``` - Ember.tryFinally(tryable, Ember.endPropertyChanges); + @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)); +}); - if (log) { Ember.Logger.log('End: Flush Sync Queue'); } - } else { - forEach.call(queue, iter); - } - } +/** + A computed property that returns true of the value of the dependent + property is NOT null, an empty string, empty array, or empty function. - // Loop through prior queues - for (var i = 0; i <= idx; i++) { - if (this._queues && this._queues[queueNames[i]]) { - // Start over at the first queue with contents - idx = i; - continue outerloop; - } - } + Example - idx++; - } - } + ```javascript + var Hampster = 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 + ``` - timerMark = null; + @method computed.notEmpty + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which returns true if + original value for property is not empty. +*/ +registerComputed('notEmpty', function(dependentKey) { + return !Ember.isEmpty(get(this, dependentKey)); +}); - return this; - } +/** + A computed property that returns true of the value of the dependent + property is null or undefined. This avoids errors from JSLint complaining + about use of ==, which can be technically confusing. -}; + Example -Ember.RunLoop = RunLoop; + ```javascript + var Hampster = 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 + ``` -// .......................................................... -// Ember.run - this is ideally the only public API the dev sees -// + @method computed.none + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which + returns true if original value for property is null or undefined. +*/ +registerComputed('none', function(dependentKey) { + return Ember.isNone(get(this, dependentKey)); +}); /** - Runs the passed target and method inside of a RunLoop, ensuring any - deferred actions including bindings and views updates are flushed at the - end. + A computed property that returns the inverse boolean value + of the original value for the dependent property. - Normally you should not need to invoke this method yourself. However if - you are implementing raw event handlers when interfacing with other - libraries or plugins, you should probably wrap all of your code inside this - call. + Example ```javascript - Ember.run(function(){ - // code to be execute within a RunLoop + var User = Ember.Object.extend({ + isAnonymous: Ember.computed.not('loggedIn') }); + var user = User.create({loggedIn: false}); + user.get('isAnonymous'); // false + user.set('loggedIn', true); + user.get('isAnonymous'); // true ``` - @class run - @namespace Ember - @static - @constructor - @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. + @method computed.not + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which returns + inverse of the original value for property */ -Ember.run = function(target, method) { - var loop, - args = arguments; - run.begin(); - - function tryable() { - if (target || method) { - return invoke(target, method, args, 2); - } - } +registerComputed('not', function(dependentKey) { + return !get(this, dependentKey); +}); - return Ember.tryFinally(tryable, run.end); -}; +/** + A computed property that converts the provided dependent property + into a boolean value. -var run = Ember.run; + ```javascript + var Hampster = 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 + ``` + @method computed.bool + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which convert + to boolean the original value for property +*/ +registerComputed('bool', function(dependentKey) { + return !!get(this, dependentKey); +}); /** - Begins a new RunLoop. Any deferred actions invoked after the begin will - be buffered until you invoke a matching call to `Ember.run.end()`. This is - an lower-level way to use a RunLoop instead of using `Ember.run()`. + A computed property which matches the original value for the + dependent property against a given RegExp, returning `true` + if they values matches the RegExp and `false` if it does not. + + Example ```javascript - Ember.run.begin(); - // code to be execute within a RunLoop - Ember.run.end(); + var User = Ember.Object.extend({ + hasValidEmail: Ember.computed.match('email', /^.+@.+\..+$/) + }); + var user = User.create({loggedIn: false}); + user.get('hasValidEmail'); // false + user.set('email', ''); + user.get('hasValidEmail'); // false + user.set('email', 'ember_hampster@example.com'); + user.get('hasValidEmail'); // true ``` - @method begin - @return {void} + @method computed.match + @for Ember + @param {String} dependentKey + @param {RegExp} regexp + @return {Ember.ComputedProperty} computed property which match + the original value for property against a given RegExp */ -Ember.run.begin = function() { - run.currentRunLoop = new RunLoop(run.currentRunLoop); -}; +registerComputed('match', function(dependentKey, regexp) { + var value = get(this, dependentKey); + return typeof value === 'string' ? !!value.match(regexp) : false; +}); /** - Ends a RunLoop. This must be called sometime after you call - `Ember.run.begin()` to flush any deferred actions. This is a lower-level way - to use a RunLoop instead of using `Ember.run()`. + A computed property that returns true if the provided dependent property + is equal to the given value. + + Example ```javascript - Ember.run.begin(); - // code to be execute within a RunLoop - Ember.run.end(); + var Hampster = Ember.Object.extend({ + napTime: Ember.computed.equal('state', 'sleepy') + }); + var hampster = Hampster.create(); + hampster.get('napTime'); // false + hampster.set('state', 'sleepy'); + hampster.get('napTime'); // false + hampster.set('state', 'hungry'); + hampster.get('napTime'); // false ``` - @method end - @return {void} + @method computed.equal + @for Ember + @param {String} dependentKey + @param {String|Number|Object} value + @return {Ember.ComputedProperty} computed property which returns true if + the original value for property is equal to the given value. */ -Ember.run.end = function() { - Ember.assert('must have a current run loop', run.currentRunLoop); +registerComputed('equal', function(dependentKey, value) { + return get(this, dependentKey) === value; +}); - function tryable() { run.currentRunLoop.end(); } - function finalizer() { run.currentRunLoop = run.currentRunLoop.prev(); } +/** + A computed property that returns true if the provied dependent property + is greater than the provided value. - Ember.tryFinally(tryable, finalizer); -}; + Example -/** - Array of named queues. This array determines the order in which queues - are flushed at the end of the RunLoop. You can define your own queues by - simply adding the queue name to this array. Normally you should not need - to inspect or modify this property. + ```javascript + var Hampster = 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 + ``` - @property queues - @type Array - @default ['sync', 'actions', 'destroy', 'timers'] + @method computed.gt + @for Ember + @param {String} dependentKey + @param {Number} value + @return {Ember.ComputedProperty} computed property which returns true if + the original value for property is greater then given value. */ -Ember.run.queues = ['sync', 'actions', 'destroy', 'timers']; +registerComputed('gt', function(dependentKey, value) { + return get(this, dependentKey) > value; +}); /** - Adds the passed target/method and any optional arguments to the named - queue to be executed at the end of the RunLoop. If you have not already - started a RunLoop when calling this method one will be started for you - automatically. + A computed property that returns true if the provided dependent property + is greater than or equal to the provided value. - At the end of a RunLoop, any methods scheduled in this way will be invoked. - Methods will be invoked in an order matching the named queues defined in - the `run.queues` property. + Example ```javascript - Ember.run.schedule('timers', this, function(){ - // this will be executed at the end of the RunLoop, when timers are run - console.log("scheduled on timers queue"); + var Hampster = 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 + ``` - Ember.run.schedule('sync', this, function(){ - // this will be executed at the end of the RunLoop, when bindings are synced - console.log("scheduled on sync queue"); - }); - - // Note the functions will be run in order based on the run queues order. Output would be: - // scheduled on sync queue - // scheduled on timers queue - ``` - - @method schedule - @param {String} queue The name of the queue to schedule against. - Default queues are 'sync' and 'actions' - @param {Object} [target] target object to use as the context when invoking a method. - @param {String|Function} method The method to invoke. If you pass a string it - will be resolved on the target object at the time the scheduled item is - invoked allowing you to change the target function. - @param {Object} [arguments*] Optional arguments to be passed to the queued method. - @return {void} + @method computed.gte + @for Ember + @param {String} dependentKey + @param {Number} value + @return {Ember.ComputedProperty} computed property which returns true if + the original value for property is greater or equal then given value. */ -Ember.run.schedule = function(queue, target, method) { - var loop = run.autorun(); - loop.schedule.apply(loop, arguments); -}; - -var scheduledAutorun; -function autorun() { - scheduledAutorun = null; - if (run.currentRunLoop) { run.end(); } -} +registerComputed('gte', function(dependentKey, value) { + return get(this, dependentKey) >= value; +}); -// Used by global test teardown -Ember.run.hasScheduledTimers = function() { - return !!(scheduledAutorun || scheduledLater || scheduledNext); -}; +/** + A computed property that returns true if the provided dependent property + is less than the provided value. -// Used by global test teardown -Ember.run.cancelTimers = function () { - if (scheduledAutorun) { - clearTimeout(scheduledAutorun); - scheduledAutorun = null; - } - if (scheduledLater) { - clearTimeout(scheduledLater); - scheduledLater = null; - } - if (scheduledNext) { - clearTimeout(scheduledNext); - scheduledNext = null; - } - timers = {}; -}; + Example -/** - Begins a new RunLoop if necessary and schedules a timer to flush the - RunLoop at a later time. This method is used by parts of Ember to - ensure the RunLoop always finishes. You normally do not need to call this - method directly. Instead use `Ember.run()` + ```javascript + var Hampster = 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 + ``` - @method autorun - @example - Ember.run.autorun(); - @return {Ember.RunLoop} the new current RunLoop + @method computed.lt + @for Ember + @param {String} dependentKey + @param {Number} value + @return {Ember.ComputedProperty} computed property which returns true if + the original value for property is less then given value. */ -Ember.run.autorun = function() { - if (!run.currentRunLoop) { - Ember.assert("You have turned on testing mode, which disabled the run-loop's autorun. You will need to wrap any code with asynchronous side-effects in an Ember.run", !Ember.testing); +registerComputed('lt', function(dependentKey, value) { + return get(this, dependentKey) < value; +}); - run.begin(); +/** + A computed property that returns true if the provided dependent property + is less than or equal to the provided value. - if (!scheduledAutorun) { - scheduledAutorun = setTimeout(autorun, 1); - } - } + Example - return run.currentRunLoop; -}; + ```javascript + var Hampster = 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 + ``` + + @method computed.lte + @for Ember + @param {String} dependentKey + @param {Number} value + @return {Ember.ComputedProperty} computed property which returns true if + the original value for property is less or equal then given value. +*/ +registerComputed('lte', function(dependentKey, value) { + return get(this, dependentKey) <= value; +}); /** - Immediately flushes any events scheduled in the 'sync' queue. Bindings - use this queue so this method is a useful way to immediately force all - bindings in the application to sync. + A computed property that performs a logical `and` on the + original values for the provided dependent properties. - You should call this method anytime you need any changed state to propagate - throughout the app immediately without repainting the UI. + + Example ```javascript - Ember.run.sync(); + var Hampster = 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 ``` - @method sync - @return {void} + @method computed.and + @for Ember + @param {String} dependentKey, [dependentKey...] + @return {Ember.ComputedProperty} computed property which performs + a logical `and` on the values of all the original values for properties. */ -Ember.run.sync = function() { - run.autorun(); - run.currentRunLoop.flush('sync'); -}; - -// .......................................................... -// TIMERS -// - -var timers = {}; // active timers... - -var scheduledLater; -function invokeLaterTimers() { - scheduledLater = null; - var now = (+ new Date()), earliest = -1; - for (var key in timers) { - if (!timers.hasOwnProperty(key)) { continue; } - var timer = timers[key]; - if (timer && timer.expires) { - if (now >= timer.expires) { - delete timers[key]; - invoke(timer.target, timer.method, timer.args, 2); - } else { - if (earliest<0 || (timer.expires < earliest)) earliest=timer.expires; - } +registerComputedWithProperties('and', function(properties) { + for (var key in properties) { + if (properties.hasOwnProperty(key) && !properties[key]) { + return false; } } - - // schedule next timeout to fire... - if (earliest > 0) { scheduledLater = setTimeout(invokeLaterTimers, earliest-(+ new Date())); } -} + return true; +}); /** - Invokes the passed target/method and optional arguments after a specified - period if time. The last parameter of this method must always be a number - of milliseconds. + A computed property that which performs a logical `or` on the + original values for the provided dependent properties. - You should use this method whenever you need to run some action after a - period of time instead of using `setTimeout()`. This method will ensure that - items that expire during the same script execution cycle all execute - together, which is often more efficient than using a real setTimeout. + Example ```javascript - Ember.run.later(myContext, function(){ - // code here will execute within a RunLoop in about 500ms with this == myContext - }, 500); + var Hampster = 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 ``` - @method later - @param {Object} [target] target of method to invoke - @param {Function|String} method The method to invoke. - 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. - @param {Number} wait - Number of milliseconds to wait. - @return {String} a string you can use to cancel the timer in - {{#crossLink "Ember/run.cancel"}}{{/crossLink}} later. + @method computed.or + @for Ember + @param {String} dependentKey, [dependentKey...] + @return {Ember.ComputedProperty} computed property which performs + a logical `or` on the values of all the original values for properties. */ -Ember.run.later = function(target, method) { - var args, expires, timer, guid, wait; - - // setTimeout compatibility... - if (arguments.length===2 && 'function' === typeof target) { - wait = method; - method = target; - target = undefined; - args = [target, method]; - } else { - args = slice.call(arguments); - wait = args.pop(); +registerComputedWithProperties('or', function(properties) { + for (var key in properties) { + if (properties.hasOwnProperty(key) && properties[key]) { + return true; + } } + return false; +}); - expires = (+ new Date()) + wait; - timer = { target: target, method: method, expires: expires, args: args }; - guid = Ember.guidFor(timer); - timers[guid] = timer; - run.once(timers, invokeLaterTimers); - return guid; -}; - -function invokeOnceTimer(guid, onceTimers) { - if (onceTimers[this.tguid]) { delete onceTimers[this.tguid][this.mguid]; } - if (timers[guid]) { invoke(this.target, this.method, this.args); } - delete timers[guid]; -} - -function scheduleOnce(queue, target, method, args) { - var tguid = Ember.guidFor(target), - mguid = Ember.guidFor(method), - onceTimers = run.autorun().onceTimers, - guid = onceTimers[tguid] && onceTimers[tguid][mguid], - timer; +/** + A computed property that returns the first truthy value + from a list of dependent properties. - if (guid && timers[guid]) { - timers[guid].args = args; // replace args - } else { - timer = { - target: target, - method: method, - args: args, - tguid: tguid, - mguid: mguid - }; + Example - guid = Ember.guidFor(timer); - timers[guid] = timer; - if (!onceTimers[tguid]) { onceTimers[tguid] = {}; } - onceTimers[tguid][mguid] = guid; // so it isn't scheduled more than once + ```javascript + var Hampster = 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' + ``` - run.schedule(queue, timer, invokeOnceTimer, guid, onceTimers); + @method computed.any + @for Ember + @param {String} dependentKey, [dependentKey...] + @return {Ember.ComputedProperty} computed property which returns + the first truthy value of given list of properties. +*/ +registerComputedWithProperties('any', function(properties) { + for (var key in properties) { + if (properties.hasOwnProperty(key) && properties[key]) { + return properties[key]; + } } - - return guid; -} + return null; +}); /** - Schedules an item to run one time during the current RunLoop. Calling - this method with the same target/method combination will have no effect. + A computed property that returns the array of values + for the provided dependent properties. - Note that although you can pass optional arguments these will not be - considered when looking for duplicates. New arguments will replace previous - calls. + Example ```javascript - Ember.run(function(){ - var doFoo = function() { foo(); } - Ember.run.once(myContext, doFoo); - Ember.run.once(myContext, doFoo); - // doFoo will only be executed once at the end of the RunLoop + var Hampster = Ember.Object.extend({ + clothes: Ember.computed.map('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'] ``` - @method once - @param {Object} [target] target of method to invoke - @param {Function|String} method The method to invoke. - 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 -*/ -Ember.run.once = function(target, method) { - return scheduleOnce('actions', target, method, slice.call(arguments, 2)); -}; - -Ember.run.scheduleOnce = function(queue, target, method, args) { - return scheduleOnce(queue, target, method, slice.call(arguments, 3)); -}; - -var scheduledNext; -function invokeNextTimers() { - scheduledNext = null; - for(var key in timers) { - if (!timers.hasOwnProperty(key)) { continue; } - var timer = timers[key]; - if (timer.next) { - delete timers[key]; - invoke(timer.target, timer.method, timer.args, 2); + @method computed.map + @for Ember + @param {String} dependentKey, [dependentKey...] + @return {Ember.ComputedProperty} computed property which maps + values of all passed properties in to an array. +*/ +registerComputedWithProperties('collect', function(properties) { + var res = []; + for (var key in properties) { + if (properties.hasOwnProperty(key)) { + if (Ember.isNone(properties[key])) { + res.push(null); + } else { + res.push(properties[key]); + } } } -} + return res; +}); /** - Schedules an item to run after control has been returned to the system. - This is often equivalent to calling `setTimeout(function() {}, 1)`. + Creates a new property that is an alias for another property + on an object. Calls to `get` or `set` this property behave as + though they were called on the original property. ```javascript - Ember.run.next(myContext, function(){ - // code to be executed in the next RunLoop, which will be scheduled after the current one + Person = Ember.Object.extend({ + name: 'Alex Matchneer', + nomen: Ember.computed.alias('name') }); - ``` - - @method next - @param {Object} [target] target of method to invoke - @param {Function|String} method The method to invoke. - 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 -*/ -Ember.run.next = function(target, method) { - var guid, - timer = { - target: target, - method: method, - args: slice.call(arguments), - next: true - }; - guid = Ember.guidFor(timer); - timers[guid] = timer; + alex = Person.create(); + alex.get('nomen'); // 'Alex Matchneer' + alex.get('name'); // 'Alex Matchneer' - if (!scheduledNext) { scheduledNext = setTimeout(invokeNextTimers, 1); } - return guid; + alex.set('nomen', '@machty'); + alex.get('name'); // '@machty' + ``` + @method computed.alias + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which creates an + alias to the original value for property. +*/ +Ember.computed.alias = function(dependentKey) { + return Ember.computed(dependentKey, function(key, value) { + if (arguments.length > 1) { + set(this, dependentKey, value); + return value; + } else { + return get(this, dependentKey); + } + }); }; /** - Cancels a scheduled item. Must be a value returned by `Ember.run.later()`, - `Ember.run.once()`, or `Ember.run.next()`. + Where `computed.alias` aliases `get` and `set`, and allows for bidirectional + data flow, `computed.oneWay` only provides an aliased `get`. The `set` will + not mutate the upstream property, rather causes the current property to + become the value set. This causes the downstream property to permentantly + diverge from the upstream property. + + Example ```javascript - var runNext = Ember.run.next(myContext, function(){ - // will not be executed + User = Ember.Object.extend({ + firstName: null, + lastName: null, + nickName: Ember.computed.oneWay('firstName') }); - Ember.run.cancel(runNext); - - var runLater = Ember.run.later(myContext, function(){ - // will not be executed - }, 500); - Ember.run.cancel(runLater); - var runOnce = Ember.run.once(myContext, function(){ - // will not be executed + user = User.create({ + firstName: 'Teddy', + lastName: 'Zeenny' }); - Ember.run.cancel(runOnce); + + user.get('nickName'); + # 'Teddy' + + user.set('nickName', 'TeddyBear'); + # 'TeddyBear' + + user.get('firstName'); + # 'Teddy' ``` - @method cancel - @param {Object} timer Timer object to cancel - @return {void} + @method computed.oneWay + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which creates an + one way computed property to the original value for property. */ -Ember.run.cancel = function(timer) { - delete timers[timer]; +Ember.computed.oneWay = function(dependentKey) { + return Ember.computed(dependentKey, function() { + return get(this, dependentKey); + }); }; -})(); - - -(function() { -// Ember.Logger -// get, set, trySet -// guidFor, isArray, meta -// addObserver, removeObserver -// Ember.run.schedule /** -@module ember-metal -*/ + A computed property that acts like a standard getter and setter, + but retruns the value at the provided `defaultPath` if the + property itself has not been set to a value -// .......................................................... -// CONSTANTS -// + Example -/** - Debug parameter you can turn on. This will log all bindings that fire to - the console. This should be disabled in production code. Note that you - can also enable this from the console or temporarily. + ```javascript + var Hampster = 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' + ``` - @property LOG_BINDINGS + @method computed.defaultTo @for Ember - @type Boolean - @default false + @param {String} defaultPath + @return {Ember.ComputedProperty} computed property which acts like + a standard getter and setter, but defaults to the value from `defaultPath`. */ -Ember.LOG_BINDINGS = false || !!Ember.ENV.LOG_BINDINGS; +Ember.computed.defaultTo = function(defaultPath) { + return Ember.computed(function(key, newValue, cachedValue) { + if (arguments.length === 1) { + return cachedValue != null ? cachedValue : get(this, defaultPath); + } + return newValue != null ? newValue : get(this, defaultPath); + }); +}; -var get = Ember.get, - set = Ember.set, - guidFor = Ember.guidFor, - isGlobalPath = Ember.isGlobalPath; -function getWithGlobals(obj, path) { - return get(isGlobalPath(path) ? Ember.lookup : obj, path); -} +})(); -// .......................................................... -// BINDING -// -var Binding = function(toPath, fromPath) { - this._direction = 'fwd'; - this._from = fromPath; - this._to = toPath; - this._directionMap = Ember.Map.create(); -}; +(function() { +// Ember.tryFinally /** -@class Binding -@namespace Ember +@module ember-metal */ -Binding.prototype = { - /** - This copies the Binding so it can be connected to another object. +var AFTER_OBSERVERS = ':change'; +var BEFORE_OBSERVERS = ':before'; - @method copy - @return {Ember.Binding} - */ - copy: function () { - var copy = new Binding(this._to, this._from); - if (this._oneWay) { copy._oneWay = true; } - return copy; - }, +function changeEvent(keyName) { + return keyName+AFTER_OBSERVERS; +} - // .......................................................... - // CONFIG - // +function beforeEvent(keyName) { + return keyName+BEFORE_OBSERVERS; +} - /** - This will set `from` property path to the specified value. It will not - attempt to resolve this property path to an actual object until you - connect the binding. +/** + @method addObserver + @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); + return this; +}; - The binding will search for the property path starting at the root object - you pass when you `connect()` the binding. It follows the same rules as - `get()` - see that method for more information. +Ember.observersFor = function(obj, path) { + return Ember.listenersFor(obj, changeEvent(path)); +}; - @method from - @param {String} propertyPath the property path to connect to - @return {Ember.Binding} `this` - */ - from: function(path) { - this._from = path; - return this; - }, +/** + @method removeObserver + @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); + return this; +}; - /** - This will set the `to` property path to the specified value. It will not - attempt to resolve this property path to an actual object until you - connect the binding. +/** + @method addBeforeObserver + @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); + return this; +}; - The binding will search for the property path starting at the root object - you pass when you `connect()` the binding. It follows the same rules as - `get()` - see that method for more information. +// Suspend observer during callback. +// +// This should only be used by the target of the observer +// while it is setting the observed path. +Ember._suspendBeforeObserver = function(obj, path, target, method, callback) { + return Ember._suspendListener(obj, beforeEvent(path), target, method, callback); +}; - @method to - @param {String|Tuple} propertyPath A property path or tuple - @return {Ember.Binding} `this` - */ - to: function(path) { - this._to = path; - return this; - }, +Ember._suspendObserver = function(obj, path, target, method, callback) { + return Ember._suspendListener(obj, changeEvent(path), target, method, callback); +}; - /** - Configures the binding as one way. A one-way binding will relay changes - on the `from` side to the `to` side, but not the other way around. This - means that if you change the `to` side directly, the `from` side may have - a different value. +var map = Ember.ArrayPolyfills.map; - @method oneWay - @return {Ember.Binding} `this` - */ - oneWay: function() { - this._oneWay = true; - return this; - }, +Ember._suspendBeforeObservers = function(obj, paths, target, method, callback) { + var events = map.call(paths, beforeEvent); + return Ember._suspendListeners(obj, events, target, method, callback); +}; - toString: function() { - var oneWay = this._oneWay ? '[oneWay]' : ''; - return "Ember.Binding<" + guidFor(this) + ">(" + this._from + " -> " + this._to + ")" + oneWay; - }, +Ember._suspendObservers = function(obj, paths, target, method, callback) { + var events = map.call(paths, changeEvent); + return Ember._suspendListeners(obj, events, target, method, callback); +}; - // .......................................................... - // CONNECT AND SYNC - // +Ember.beforeObserversFor = function(obj, path) { + return Ember.listenersFor(obj, beforeEvent(path)); +}; - /** - Attempts to connect this binding instance so that it can receive and relay - changes. This method will raise an exception if you have not set the - from/to properties yet. +/** + @method removeBeforeObserver + @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); + return this; +}; +})(); - @method connect - @param {Object} obj The root object for this binding. - @return {Ember.Binding} `this` - */ - connect: function(obj) { - Ember.assert('Must pass a valid object to Ember.Binding.connect()', !!obj); - var fromPath = this._from, toPath = this._to; - Ember.trySet(obj, toPath, getWithGlobals(obj, fromPath)); - // add an observer on the object to be notified when the binding should be updated - Ember.addObserver(obj, fromPath, this, this.fromDidChange); +(function() { +define("backburner/queue", + ["exports"], + function(__exports__) { + "use strict"; + function Queue(daq, name, options) { + this.daq = daq; + this.name = name; + this.options = options; + this._queue = []; + } + + Queue.prototype = { + daq: null, + name: null, + options: null, + _queue: null, + + push: function(target, method, args, stack) { + var queue = this._queue; + queue.push(target, method, args, stack); + return {queue: this, target: target, method: method}; + }, - // if the binding is a two-way binding, also set up an observer on the target - if (!this._oneWay) { Ember.addObserver(obj, toPath, this, this.toDidChange); } + pushUnique: function(target, method, args, stack) { + var queue = this._queue, currentTarget, currentMethod, i, l; - this._readyToSync = true; + for (i = 0, l = queue.length; i < l; i += 4) { + currentTarget = queue[i]; + currentMethod = queue[i+1]; - return this; - }, + if (currentTarget === target && currentMethod === method) { + queue[i+2] = args; // replace args + queue[i+3] = stack; // replace stack + return {queue: this, target: target, method: method}; // TODO: test this code path + } + } - /** - Disconnects the binding instance. Changes will no longer be relayed. You - will not usually need to call this method. + this._queue.push(target, method, args, stack); + return {queue: this, target: target, method: method}; + }, - @method disconnect - @param {Object} obj The root object you passed when connecting the binding. - @return {Ember.Binding} `this` - */ - disconnect: function(obj) { - Ember.assert('Must pass a valid object to Ember.Binding.disconnect()', !!obj); + // TODO: remove me, only being used for Ember.run.sync + flush: function() { + var queue = this._queue, + options = this.options, + before = options && options.before, + after = options && options.after, + target, method, args, stack, i, l = queue.length; + + if (l && before) { before(); } + for (i = 0; i < l; i += 4) { + target = queue[i]; + method = queue[i+1]; + args = queue[i+2]; + stack = queue[i+3]; // Debugging assistance + + // TODO: error handling + if (args && args.length > 0) { + method.apply(target, args); + } else { + method.call(target); + } + } + if (l && after) { after(); } - var twoWay = !this._oneWay; + // check if new items have been added + if (queue.length > l) { + this._queue = queue.slice(l); + this.flush(); + } else { + this._queue.length = 0; + } + }, - // remove an observer on the object so we're no longer notified of - // changes that should update bindings. - Ember.removeObserver(obj, this._from, this, this.fromDidChange); + cancel: function(actionToCancel) { + var queue = this._queue, currentTarget, currentMethod, i, l; - // if the binding is two-way, remove the observer from the target as well - if (twoWay) { Ember.removeObserver(obj, this._to, this, this.toDidChange); } + for (i = 0, l = queue.length; i < l; i += 4) { + currentTarget = queue[i]; + currentMethod = queue[i+1]; - this._readyToSync = false; // disable scheduled syncs... - return this; - }, + if (currentTarget === actionToCancel.target && currentMethod === actionToCancel.method) { + queue.splice(i, 4); + return true; + } + } - // .......................................................... - // PRIVATE - // + // if not found in current queue + // could be in the queue that is being flushed + queue = this._queueBeingFlushed; + if (!queue) { + return; + } + for (i = 0, l = queue.length; i < l; i += 4) { + currentTarget = queue[i]; + currentMethod = queue[i+1]; + + if (currentTarget === actionToCancel.target && currentMethod === actionToCancel.method) { + // don't mess with array during flush + // just nullify the method + queue[i+1] = null; + return true; + } + } + } + }; - /* called when the from side changes */ - fromDidChange: function(target) { - this._scheduleSync(target, 'fwd'); - }, - /* called when the to side changes */ - toDidChange: function(target) { - this._scheduleSync(target, 'back'); - }, + __exports__.Queue = Queue; + }); - _scheduleSync: function(obj, dir) { - var directionMap = this._directionMap; - var existingDir = directionMap.get(obj); +define("backburner/deferred_action_queues", + ["backburner/queue","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Queue = __dependency1__.Queue; - // if we haven't scheduled the binding yet, schedule it - if (!existingDir) { - Ember.run.schedule('sync', this, this._sync, obj); - directionMap.set(obj, dir); - } + function DeferredActionQueues(queueNames, options) { + var queues = this.queues = {}; + this.queueNames = queueNames = queueNames || []; - // If both a 'back' and 'fwd' sync have been scheduled on the same object, - // default to a 'fwd' sync so that it remains deterministic. - if (existingDir === 'back' && dir === 'fwd') { - directionMap.set(obj, 'fwd'); + var queueName; + for (var i = 0, l = queueNames.length; i < l; i++) { + queueName = queueNames[i]; + queues[queueName] = new Queue(this, queueName, options[queueName]); + } } - }, - _sync: function(obj) { - var log = Ember.LOG_BINDINGS; + DeferredActionQueues.prototype = { + queueNames: null, + queues: null, - // don't synchronize destroyed objects or disconnected bindings - if (obj.isDestroyed || !this._readyToSync) { return; } + schedule: function(queueName, target, method, args, onceFlag, stack) { + var queues = this.queues, + queue = queues[queueName]; - // get the direction of the binding for the object we are - // synchronizing from - var directionMap = this._directionMap; - var direction = directionMap.get(obj); + if (!queue) { throw new Error("You attempted to schedule an action in a queue (" + queueName + ") that doesn't exist"); } - var fromPath = this._from, toPath = this._to; + if (onceFlag) { + return queue.pushUnique(target, method, args, stack); + } else { + return queue.push(target, method, args, stack); + } + }, - directionMap.remove(obj); + flush: function() { + var queues = this.queues, + queueNames = this.queueNames, + queueName, queue, queueItems, priorQueueNameIndex, + queueNameIndex = 0, numberOfQueues = queueNames.length; + + outerloop: + while (queueNameIndex < numberOfQueues) { + queueName = queueNames[queueNameIndex]; + queue = queues[queueName]; + queueItems = queue._queueBeingFlushed = queue._queue.slice(); + queue._queue = []; + + var options = queue.options, + before = options && options.before, + after = options && options.after, + target, method, args, stack, + queueIndex = 0, numberOfQueueItems = queueItems.length; + + if (numberOfQueueItems && before) { before(); } + while (queueIndex < numberOfQueueItems) { + target = queueItems[queueIndex]; + method = queueItems[queueIndex+1]; + args = queueItems[queueIndex+2]; + stack = queueItems[queueIndex+3]; // Debugging assistance + + if (typeof method === 'string') { method = target[method]; } + + // method could have been nullified / canceled during flush + if (method) { + // TODO: error handling + if (args && args.length > 0) { + method.apply(target, args); + } else { + method.call(target); + } + } - // if we're synchronizing from the remote object... - if (direction === 'fwd') { - var fromValue = getWithGlobals(obj, this._from); - if (log) { - Ember.Logger.log(' ', this.toString(), '->', fromValue, obj); - } - if (this._oneWay) { - Ember.trySet(obj, toPath, fromValue); - } else { - Ember._suspendObserver(obj, toPath, this, this.toDidChange, function () { - Ember.trySet(obj, toPath, fromValue); - }); + queueIndex += 4; + } + queue._queueBeingFlushed = null; + if (numberOfQueueItems && after) { after(); } + + if ((priorQueueNameIndex = indexOfPriorQueueWithActions(this, queueNameIndex)) !== -1) { + queueNameIndex = priorQueueNameIndex; + continue outerloop; + } + + queueNameIndex++; + } } - // if we're synchronizing *to* the remote object - } else if (direction === 'back') { - var toValue = get(obj, this._to); - if (log) { - Ember.Logger.log(' ', this.toString(), '<-', toValue, obj); + }; + + function indexOfPriorQueueWithActions(daq, currentQueueIndex) { + var queueName, queue; + + for (var i = 0, l = currentQueueIndex; i <= l; i++) { + queueName = daq.queueNames[i]; + queue = daq.queues[queueName]; + if (queue._queue.length) { return i; } } - Ember._suspendObserver(obj, fromPath, this, this.fromDidChange, function () { - Ember.trySet(Ember.isGlobalPath(fromPath) ? Ember.lookup : obj, fromPath, toValue); - }); + + return -1; } - } -}; -function mixinProperties(to, from) { - for (var key in from) { - if (from.hasOwnProperty(key)) { - to[key] = from[key]; + __exports__.DeferredActionQueues = DeferredActionQueues; + }); + +define("backburner", + ["backburner/deferred_action_queues","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var DeferredActionQueues = __dependency1__.DeferredActionQueues; + + var slice = [].slice, + pop = [].pop, + throttlers = [], + debouncees = [], + timers = [], + autorun, laterTimer, laterTimerExpiresAt, + global = this; + + function Backburner(queueNames, options) { + this.queueNames = queueNames; + this.options = options || {}; + if (!this.options.defaultQueue) { + this.options.defaultQueue = queueNames[0]; + } + this.instanceStack = []; } - } -} -mixinProperties(Binding, { + Backburner.prototype = { + queueNames: null, + options: null, + currentInstance: null, + instanceStack: null, - /** - See {{#crossLink "Ember.Binding/from"}}{{/crossLink}} + begin: function() { + var onBegin = this.options && this.options.onBegin, + previousInstance = this.currentInstance; - @method from - @static - */ - from: function() { - var C = this, binding = new C(); - return binding.from.apply(binding, arguments); - }, + if (previousInstance) { + this.instanceStack.push(previousInstance); + } - /** - See {{#crossLink "Ember.Binding/to"}}{{/crossLink}} + this.currentInstance = new DeferredActionQueues(this.queueNames, this.options); + if (onBegin) { + onBegin(this.currentInstance, previousInstance); + } + }, - @method to - @static - */ - to: function() { - var C = this, binding = new C(); - return binding.to.apply(binding, arguments); - }, + end: function() { + var onEnd = this.options && this.options.onEnd, + currentInstance = this.currentInstance, + nextInstance = null; - /** - Creates a new Binding instance and makes it apply in a single direction. - A one-way binding will relay changes on the `from` side object (supplied - as the `from` argument) the `to` side, but not the other way around. - This means that if you change the "to" side directly, the "from" side may have - a different value. + try { + currentInstance.flush(); + } finally { + this.currentInstance = null; - See {{#crossLink "Binding/oneWay"}}{{/crossLink}} + if (this.instanceStack.length) { + nextInstance = this.instanceStack.pop(); + this.currentInstance = nextInstance; + } - @method oneWay - @param {String} from from path. - @param {Boolean} [flag] (Optional) passing nothing here will make the - binding `oneWay`. You can instead pass `false` to disable `oneWay`, making the - binding two way again. - */ - oneWay: function(from, flag) { - var C = this, binding = new C(null, from); - return binding.oneWay(flag); - } + if (onEnd) { + onEnd(currentInstance, nextInstance); + } + } + }, -}); + run: function(target, method /*, args */) { + var ret; + this.begin(); -/** - An `Ember.Binding` connects the properties of two objects so that whenever - the value of one property changes, the other property will be changed also. + if (!method) { + method = target; + target = null; + } - ## Automatic Creation of Bindings with `/^*Binding/`-named Properties + if (typeof method === 'string') { + method = target[method]; + } - You do not usually create Binding objects directly but instead describe - bindings in your class or object definition using automatic binding - detection. + // Prevent Safari double-finally. + var finallyAlreadyCalled = false; + try { + if (arguments.length > 2) { + ret = method.apply(target, slice.call(arguments, 2)); + } else { + ret = method.call(target); + } + } finally { + if (!finallyAlreadyCalled) { + finallyAlreadyCalled = true; + this.end(); + } + } + return ret; + }, - Properties ending in a `Binding` suffix will be converted to `Ember.Binding` - instances. The value of this property should be a string representing a path - to another object or a custom binding instanced created using Binding helpers - (see "Customizing Your Bindings"): + defer: function(queueName, target, method /* , args */) { + if (!method) { + method = target; + target = null; + } - ``` - valueBinding: "MyApp.someController.title" - ``` + if (typeof method === 'string') { + method = target[method]; + } - This will create a binding from `MyApp.someController.title` to the `value` - property of your object instance automatically. Now the two values will be - kept in sync. + var stack = this.DEBUG ? new Error().stack : 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); + }, - ## One Way Bindings + deferOnce: function(queueName, target, method /* , args */) { + if (!method) { + method = target; + target = null; + } - One especially useful binding customization you can use is the `oneWay()` - helper. This helper tells Ember that you are only interested in - receiving changes on the object you are binding from. For example, if you - are binding to a preference and you want to be notified if the preference - has changed, but your object will not be changing the preference itself, you - could do: + if (typeof method === 'string') { + method = target[method]; + } - ``` - bigTitlesBinding: Ember.Binding.oneWay("MyApp.preferencesController.bigTitles") - ``` + var stack = this.DEBUG ? new Error().stack : 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); + }, - This way if the value of `MyApp.preferencesController.bigTitles` changes the - `bigTitles` property of your object will change also. However, if you - change the value of your `bigTitles` property, it will not update the - `preferencesController`. + setTimeout: function() { + var self = this, + wait = pop.call(arguments), + target = arguments[0], + method = arguments[1], + executeAt = (+new Date()) + wait; - One way bindings are almost twice as fast to setup and twice as fast to - execute because the binding only has to worry about changes to one side. + if (!method) { + method = target; + target = null; + } - You should consider using one way bindings anytime you have an object that - may be created frequently and you do not intend to change a property; only - to monitor it for changes. (such as in the example above). + if (typeof method === 'string') { + method = target[method]; + } - ## Adding Bindings Manually + var fn, args; + if (arguments.length > 2) { + args = slice.call(arguments, 2); - All of the examples above show you how to configure a custom binding, but the - result of these customizations will be a binding template, not a fully active - Binding instance. The binding will actually become active only when you - instantiate the object the binding belongs to. It is useful however, to - understand what actually happens when the binding is activated. + fn = function() { + method.apply(target, args); + }; + } else { + fn = function() { + method.call(target); + }; + } - For a binding to function it must have at least a `from` property and a `to` - property. The `from` property path points to the object/key that you want to - bind from while the `to` path points to the object/key you want to bind to. + // find position to insert - TODO: binary search + var i, l; + for (i = 0, l = timers.length; i < l; i += 2) { + if (executeAt < timers[i]) { break; } + } - When you define a custom binding, you are usually describing the property - you want to bind from (such as `MyApp.someController.value` in the examples - above). When your object is created, it will automatically assign the value - you want to bind `to` based on the name of your binding key. In the - examples above, during init, Ember objects will effectively call - something like this on your binding: + timers.splice(i, 0, executeAt, fn); - ```javascript - binding = Ember.Binding.from(this.valueBinding).to("value"); - ``` + if (laterTimer && laterTimerExpiresAt < executeAt) { return fn; } - This creates a new binding instance based on the template you provide, and - sets the to path to the `value` property of the new object. Now that the - binding is fully configured with a `from` and a `to`, it simply needs to be - connected to become active. This is done through the `connect()` method: + if (laterTimer) { + clearTimeout(laterTimer); + laterTimer = null; + } + laterTimer = global.setTimeout(function() { + executeTimers(self); + laterTimer = null; + laterTimerExpiresAt = null; + }, wait); + laterTimerExpiresAt = executeAt; + + return fn; + }, - ```javascript - binding.connect(this); - ``` + throttle: function(target, method /* , args, wait */) { + var self = this, + args = arguments, + wait = pop.call(args), + throttler; - Note that when you connect a binding you pass the object you want it to be - connected to. This object will be used as the root for both the from and - to side of the binding when inspecting relative paths. This allows the - binding to be automatically inherited by subclassed objects as well. + for (var i = 0, l = throttlers.length; i < l; i++) { + throttler = throttlers[i]; + if (throttler[0] === target && throttler[1] === method) { return; } // do nothing + } - Now that the binding is connected, it will observe both the from and to side - and relay changes. + var timer = global.setTimeout(function() { + self.run.apply(self, args); - If you ever needed to do so (you almost never will, but it is useful to - understand this anyway), you could manually create an active binding by - using the `Ember.bind()` helper method. (This is the same method used by - to setup your bindings on objects): + // 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; + } + } - ```javascript - Ember.bind(MyApp.anotherObject, "value", "MyApp.someController.value"); - ``` + if (index > -1) { throttlers.splice(index, 1); } + }, wait); - Both of these code fragments have the same effect as doing the most friendly - form of binding creation like so: + throttlers.push([target, method, timer]); + }, - ```javascript - MyApp.anotherObject = Ember.Object.create({ - valueBinding: "MyApp.someController.value", + debounce: function(target, method /* , args, wait, [immediate] */) { + var self = this, + args = arguments, + immediate = pop.call(args), + wait, + index, + debouncee; + + if (typeof immediate === "number") { + wait = immediate; + immediate = false; + } else { + wait = pop.call(args); + } - // OTHER CODE FOR THIS OBJECT... - }); - ``` + // Remove debouncee + index = findDebouncee(target, method); - Ember's built in binding creation method makes it easy to automatically - create bindings for you. You should always use the highest-level APIs - available, even if you understand how it works underneath. + if (index !== -1) { + debouncee = debouncees[index]; + debouncees.splice(index, 1); + clearTimeout(debouncee[2]); + } - @class Binding - @namespace Ember - @since Ember 0.9 -*/ -Ember.Binding = Binding; + var timer = global.setTimeout(function() { + if (!immediate) { + self.run.apply(self, args); + } + index = findDebouncee(target, method); + if (index) { + debouncees.splice(index, 1); + } + }, wait); + if (immediate && index === -1) { + self.run.apply(self, args); + } -/** - Global helper method to create a new binding. Just pass the root object - along with a `to` and `from` path to create and connect the binding. + debouncees.push([target, method, timer]); + }, - @method bind - @for Ember - @param {Object} obj The root object of the transform. - @param {String} to The path to the 'to' side of the binding. - Must be relative to obj. - @param {String} from The path to the 'from' side of the binding. - Must be relative to obj or a global path. - @return {Ember.Binding} binding instance -*/ -Ember.bind = function(obj, to, from) { - return new Ember.Binding(to, from).connect(obj); -}; + cancelTimers: function() { + var i, len; -/** - @method oneWay - @for Ember - @param {Object} obj The root object of the transform. - @param {String} to The path to the 'to' side of the binding. - Must be relative to obj. - @param {String} from The path to the 'from' side of the binding. - Must be relative to obj or a global path. - @return {Ember.Binding} binding instance -*/ -Ember.oneWay = function(obj, to, from) { - return new Ember.Binding(to, from).oneWay().connect(obj); -}; + for (i = 0, len = throttlers.length; i < len; i++) { + clearTimeout(throttlers[i][2]); + } + throttlers = []; -})(); + for (i = 0, len = debouncees.length; i < len; i++) { + clearTimeout(debouncees[i][2]); + } + debouncees = []; + if (laterTimer) { + clearTimeout(laterTimer); + laterTimer = null; + } + timers = []; + if (autorun) { + clearTimeout(autorun); + autorun = null; + } + }, -(function() { -/** -@module ember-metal -*/ + hasTimers: function() { + return !!timers.length || autorun; + }, -var Mixin, REQUIRED, Alias, - a_map = Ember.ArrayPolyfills.map, - a_indexOf = Ember.ArrayPolyfills.indexOf, - a_forEach = Ember.ArrayPolyfills.forEach, - a_slice = [].slice, - EMPTY_META = {}, // dummy for non-writable meta - o_create = Ember.create, - defineProperty = Ember.defineProperty, - guidFor = Ember.guidFor; + cancel: function(timer) { + if (timer && typeof timer === '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 + 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 { + return; // timer was null or not a timer + } + } + }; -function mixinsMeta(obj) { - var m = Ember.meta(obj, true), ret = m.mixins; - if (!ret) { - ret = m.mixins = {}; - } else if (!m.hasOwnProperty('mixins')) { - ret = m.mixins = o_create(ret); - } - return ret; -} + Backburner.prototype.schedule = Backburner.prototype.defer; + Backburner.prototype.scheduleOnce = Backburner.prototype.deferOnce; + Backburner.prototype.later = Backburner.prototype.setTimeout; -function initMixin(mixin, args) { - if (args && args.length > 0) { - mixin.mixins = a_map.call(args, function(x) { - if (x instanceof Mixin) { return x; } + function createAutorun(backburner) { + backburner.begin(); + autorun = global.setTimeout(function() { + autorun = null; + backburner.end(); + }); + } - // Note: Manually setup a primitive mixin here. This is the only - // way to actually get a primitive mixin. This way normal creation - // of mixins will give you combined mixins... - var mixin = new Mixin(); - mixin.properties = x; - return mixin; - }); - } - return mixin; -} + function executeTimers(self) { + var now = +new Date(), + time, fns, i, l; -function isMethod(obj) { - return 'function' === typeof obj && - obj.isMethod !== false && - obj !== Boolean && obj !== Object && obj !== Number && obj !== Array && obj !== Date && obj !== String; -} + self.run(function() { + // TODO: binary search + for (i = 0, l = timers.length; i < l; i += 2) { + time = timers[i]; + if (time > now) { break; } + } -var CONTINUE = {}; + fns = timers.splice(0, i); -function mixinProperties(mixinsMeta, mixin) { - var guid; + for (i = 1, l = fns.length; i < l; i += 2) { + self.schedule(self.options.defaultQueue, null, fns[i]); + } + }); - if (mixin instanceof Mixin) { - guid = guidFor(mixin); - if (mixinsMeta[guid]) { return CONTINUE; } - mixinsMeta[guid] = mixin; - return mixin.properties; - } else { - return mixin; // apply anonymous mixin properties - } -} + if (timers.length) { + laterTimer = global.setTimeout(function() { + executeTimers(self); + laterTimer = null; + laterTimerExpiresAt = null; + }, timers[0] - now); + laterTimerExpiresAt = timers[0]; + } + } -function concatenatedProperties(props, values, base) { - var concats; + function findDebouncee(target, method) { + var debouncee, + index = -1; - // reset before adding each new mixin to pickup concats from previous - concats = values.concatenatedProperties || base.concatenatedProperties; - if (props.concatenatedProperties) { - concats = concats ? concats.concat(props.concatenatedProperties) : props.concatenatedProperties; - } + for (var i = 0, l = debouncees.length; i < l; i++) { + debouncee = debouncees[i]; + if (debouncee[0] === target && debouncee[1] === method) { + index = i; + break; + } + } - return concats; -} + return index; + } -function giveDescriptorSuper(meta, key, property, values, descs) { - var superProperty; - // Computed properties override methods, and do not call super to them - if (values[key] === undefined) { - // Find the original descriptor in a parent mixin - superProperty = descs[key]; - } + __exports__.Backburner = Backburner; + }); +})(); - // If we didn't find the original descriptor in a parent mixin, find - // it on the original object. - superProperty = superProperty || meta.descs[key]; - if (!superProperty || !(superProperty instanceof Ember.ComputedProperty)) { - return property; - } - // Since multiple mixins may inherit from the same parent, we need - // to clone the computed property so that other mixins do not receive - // the wrapped version. - property = o_create(property); - property.func = Ember.wrap(property.func, superProperty.func); +(function() { +var onBegin = function(current) { + Ember.run.currentRunLoop = current; +}; - return property; -} +var onEnd = function(current, next) { + Ember.run.currentRunLoop = next; +}; -function giveMethodSuper(obj, key, method, values, descs) { - var superMethod; +var Backburner = requireModule('backburner').Backburner, + backburner = new Backburner(['sync', 'actions', 'destroy'], { + sync: { + before: Ember.beginPropertyChanges, + after: Ember.endPropertyChanges + }, + defaultQueue: 'actions', + onBegin: onBegin, + onEnd: onEnd + }), + slice = [].slice; - // Methods overwrite computed properties, and do not call super to them. - if (descs[key] === undefined) { - // Find the original method in a parent mixin - superMethod = values[key]; - } +// .......................................................... +// Ember.run - this is ideally the only public API the dev sees +// - // If we didn't find the original value in a parent mixin, find it in - // the original object - superMethod = superMethod || obj[key]; +/** + Runs the passed target and method inside of a RunLoop, ensuring any + deferred actions including bindings and views updates are flushed at the + end. - // Only wrap the new method if the original method was a function - if ('function' !== typeof superMethod) { - return method; - } + Normally you should not need to invoke this method yourself. However if + you are implementing raw event handlers when interfacing with other + libraries or plugins, you should probably wrap all of your code inside this + call. - return Ember.wrap(method, superMethod); -} + ```javascript + Ember.run(function() { + // code to be execute within a RunLoop + }); + ``` -function applyConcatenatedProperties(obj, key, value, values) { - var baseValue = values[key] || obj[key]; + @class run + @namespace Ember + @static + @constructor + @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. +*/ +Ember.run = function(target, method) { + var ret; - if (baseValue) { - if ('function' === typeof baseValue.concat) { - return baseValue.concat(value); - } else { - return Ember.makeArray(baseValue).concat(value); + if (Ember.onerror) { + try { + ret = backburner.run.apply(backburner, arguments); + } catch (e) { + Ember.onerror(e); } } else { - return Ember.makeArray(value); + ret = backburner.run.apply(backburner, arguments); } -} - -function addNormalizedProperty(base, key, value, meta, descs, values, concats) { - if (value instanceof Ember.Descriptor) { - if (value === REQUIRED && descs[key]) { return CONTINUE; } - - // Wrap descriptor function to implement - // _super() if needed - if (value.func) { - value = giveDescriptorSuper(meta, key, value, values, descs); - } - - descs[key] = value; - values[key] = undefined; - } else { - // impl super if needed... - if (isMethod(value)) { - value = giveMethodSuper(base, key, value, values, descs); - } else if ((concats && a_indexOf.call(concats, key) >= 0) || key === 'concatenatedProperties') { - value = applyConcatenatedProperties(base, key, value, values); - } - descs[key] = undefined; - values[key] = value; - } -} + return ret; +}; -function mergeMixins(mixins, m, descs, values, base) { - var mixin, props, key, concats, meta; +/** - function removeKeys(keyName) { - delete descs[keyName]; - delete values[keyName]; - } + 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. - for(var i=0, l=mixins.length; i= 0) { - if (_detect(mixins[loc], targetMixin, seen)) { return true; } - } - return false; -} + // less than 150ms passes -/** - @method detect - @param obj - @return {Boolean} -*/ -MixinPrototype.detect = function(obj) { - if (!obj) { return false; } - if (obj instanceof Mixin) { return _detect(obj, this, {}); } - var mixins = Ember.meta(obj, false).mixins; - if (mixins) { - return !!mixins[guidFor(this)]; - } - return false; -}; - -MixinPrototype.without = function() { - var ret = new Mixin(this); - ret._without = a_slice.call(arguments); - return ret; -}; - -function _keys(ret, mixin, seen) { - if (seen[guidFor(mixin)]) { return; } - seen[guidFor(mixin)] = true; - - if (mixin.properties) { - var props = mixin.properties; - for (var key in props) { - if (props.hasOwnProperty(key)) { ret[key] = true; } - } - } else if (mixin.mixins) { - a_forEach.call(mixin.mixins, function(x) { _keys(ret, x, seen); }); - } -} - -MixinPrototype.keys = function() { - var keys = {}, seen = {}, ret = []; - _keys(keys, this, seen); - for(var key in keys) { - if (keys.hasOwnProperty(key)) { ret.push(key); } - } - return ret; -}; - -// returns the mixins currently applied to the specified object -// TODO: Make Ember.mixin -Mixin.mixins = function(obj) { - var mixins = Ember.meta(obj, false).mixins, ret = []; - - if (!mixins) { return ret; } + Ember.run.debounce(myContext, myFunc, 150); - for (var key in mixins) { - var mixin = mixins[key]; - - // skip primitive mixins since these are always anonymous - if (!mixin.properties) { ret.push(mixin); } - } + // 150ms passes + // myFunc is invoked with context myContext + // console logs 'debounce ran.' one time. + ``` - return ret; + @method debounce + @param {Object} [target] target of method to invoke + @param {Function|String} method The 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*] 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} +*/ +Ember.run.debounce = function() { + return backburner.debounce.apply(backburner, arguments); }; -REQUIRED = new Ember.Descriptor(); -REQUIRED.toString = function() { return '(Required Property)'; }; - /** - Denotes a required property for a mixin + Ensure that the target method is never called more frequently than + the specified spacing period. - @method required - @for Ember -*/ -Ember.required = function() { - return REQUIRED; -}; + ```javascript + var myFunc = function() { console.log(this.name + ' ran.'); }; + var myContext = {name: 'throttle'}; -Alias = function(methodName) { - this.methodName = methodName; -}; -Alias.prototype = new Ember.Descriptor(); + Ember.run.throttle(myContext, myFunc, 150); -/** - Makes a property or method available via an additional name. + // 50ms passes + Ember.run.throttle(myContext, myFunc, 150); - ```javascript - App.PaintSample = Ember.Object.extend({ - color: 'red', - colour: Ember.alias('color'), - name: function(){ - return "Zed"; - }, - moniker: Ember.alias("name") - }); + // 50ms passes + Ember.run.throttle(myContext, myFunc, 150); - var paintSample = App.PaintSample.create() - paintSample.get('colour'); // 'red' - paintSample.moniker(); // 'Zed' + // 50ms passes + Ember.run.throttle(myContext, myFunc, 150); + + // 150ms passes + // myFunc is invoked with context myContext + // console logs 'throttle ran.' twice, 150ms apart. ``` - @method alias - @for Ember - @param {String} methodName name of the method or property to alias - @return {Ember.Descriptor} - @deprecated Use `Ember.aliasMethod` or `Ember.computed.alias` instead + @method throttle + @param {Object} [target] target of method to invoke + @param {Function|String} method The 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*] Optional arguments to pass to the timeout. + @param {Number} spacing Number of milliseconds to space out requests. + @return {void} */ -Ember.alias = function(methodName) { - return new Alias(methodName); +Ember.run.throttle = function() { + return backburner.throttle.apply(backburner, arguments); }; -Ember.deprecateFunc("Ember.alias is deprecated. Please use Ember.aliasMethod or Ember.computed.alias instead.", Ember.alias); +// Make sure it's not an autorun during testing +function checkAutoRun() { + if (!Ember.run.currentRunLoop) { + Ember.assert("You have turned on testing mode, which disabled the run-loop's autorun. You will need to wrap any code with asynchronous side-effects in an Ember.run", !Ember.testing); + } +} -/** - Makes a method available via an additional name. +})(); - ```javascript - App.Person = Ember.Object.extend({ - name: function(){ - return 'Tomhuda Katzdale'; - }, - moniker: Ember.aliasMethod('name') - }); - var goodGuy = App.Person.create() - ``` - @method aliasMethod - @for Ember - @param {String} methodName name of the method to alias - @return {Ember.Descriptor} +(function() { +// Ember.Logger +// get +// set +// guidFor, meta +// addObserver, removeObserver +// Ember.run.schedule +/** +@module ember-metal */ -Ember.aliasMethod = function(methodName) { - return new Alias(methodName); -}; // .......................................................... -// OBSERVER HELPER +// CONSTANTS // /** - @method observer - @for Ember - @param {Function} func - @param {String} propertyNames* - @return func -*/ -Ember.observer = function(func) { - var paths = a_slice.call(arguments, 1); - func.__ember_observes__ = paths; - return func; -}; + Debug parameter you can turn on. This will log all bindings that fire to + the console. This should be disabled in production code. Note that you + can also enable this from the console or temporarily. -// If observers ever become asynchronous, Ember.immediateObserver -// must remain synchronous. -/** - @method immediateObserver + @property LOG_BINDINGS @for Ember - @param {Function} func - @param {String} propertyNames* - @return func + @type Boolean + @default false */ -Ember.immediateObserver = function() { - for (var i=0, l=arguments.length; i(" + this._from + " -> " + this._to + ")" + oneWay; + }, - return callbacks; - }; + // .......................................................... + // CONNECT AND SYNC + // - var EventTarget = { - mixin: function(object) { - object.on = this.on; - object.off = this.off; - object.trigger = this.trigger; - return object; - }, + /** + Attempts to connect this binding instance so that it can receive and relay + changes. This method will raise an exception if you have not set the + from/to properties yet. - on: function(eventNames, callback, binding) { - var allCallbacks = callbacksFor(this), callbacks, eventName; - eventNames = eventNames.split(/\s+/); - binding = binding || this; + @method connect + @param {Object} obj The root object for this binding. + @return {Ember.Binding} `this` + */ + connect: function(obj) { + Ember.assert('Must pass a valid object to Ember.Binding.connect()', !!obj); - while (eventName = eventNames.shift()) { - callbacks = allCallbacks[eventName]; + var fromPath = this._from, toPath = this._to; + Ember.trySet(obj, toPath, getWithGlobals(obj, fromPath)); - if (!callbacks) { - callbacks = allCallbacks[eventName] = []; - } + // add an observer on the object to be notified when the binding should be updated + Ember.addObserver(obj, fromPath, this, this.fromDidChange); - if (indexOf(callbacks, callback) === -1) { - callbacks.push([callback, binding]); - } - } - }, + // if the binding is a two-way binding, also set up an observer on the target + if (!this._oneWay) { Ember.addObserver(obj, toPath, this, this.toDidChange); } - off: function(eventNames, callback) { - var allCallbacks = callbacksFor(this), callbacks, eventName, index; - eventNames = eventNames.split(/\s+/); + this._readyToSync = true; - while (eventName = eventNames.shift()) { - if (!callback) { - allCallbacks[eventName] = []; - continue; - } + return this; + }, - callbacks = allCallbacks[eventName]; + /** + Disconnects the binding instance. Changes will no longer be relayed. You + will not usually need to call this method. - index = indexOf(callbacks, callback); + @method disconnect + @param {Object} obj The root object you passed when connecting the binding. + @return {Ember.Binding} `this` + */ + disconnect: function(obj) { + Ember.assert('Must pass a valid object to Ember.Binding.disconnect()', !!obj); - if (index !== -1) { callbacks.splice(index, 1); } - } - }, + var twoWay = !this._oneWay; - trigger: function(eventName, options) { - var allCallbacks = callbacksFor(this), - callbacks, callbackTuple, callback, binding, event; + // remove an observer on the object so we're no longer notified of + // changes that should update bindings. + Ember.removeObserver(obj, this._from, this, this.fromDidChange); - if (callbacks = allCallbacks[eventName]) { - // Don't cache the callbacks.length since it may grow - for (var i=0; i', fromValue, obj); + } + if (this._oneWay) { + Ember.trySet(obj, toPath, fromValue); + } else { + Ember._suspendObserver(obj, toPath, this, this.toDidChange, function () { + Ember.trySet(obj, toPath, fromValue); }); - - return thenPromise; - }, - - resolve: function(value) { - resolve(this, value); - - this.resolve = noop; - this.reject = noop; - }, - - reject: function(value) { - reject(this, value); - - this.resolve = noop; - this.reject = noop; } - }; - - function resolve(promise, value) { - RSVP.async(function() { - promise.trigger('promise:resolved', { detail: value }); - promise.isResolved = true; - promise.resolvedValue = value; + // if we're synchronizing *to* the remote object + } else if (direction === 'back') { + var toValue = get(obj, this._to); + if (log) { + Ember.Logger.log(' ', this.toString(), '<-', toValue, obj); + } + Ember._suspendObserver(obj, fromPath, this, this.fromDidChange, function () { + Ember.trySet(Ember.isGlobalPath(fromPath) ? Ember.lookup : obj, fromPath, toValue); }); } + } - function reject(promise, value) { - RSVP.async(function() { - promise.trigger('promise:failed', { detail: value }); - promise.isRejected = true; - promise.rejectedValue = value; - }); - } - - function all(promises) { - var i, results = []; - var allPromise = new Promise(); - var remaining = promises.length; - - if (remaining === 0) { - allPromise.resolve([]); - } - - var resolver = function(index) { - return function(value) { - resolve(index, value); - }; - }; - - var resolve = function(index, value) { - results[index] = value; - if (--remaining === 0) { - allPromise.resolve(results); - } - }; - - var reject = function(error) { - allPromise.reject(error); - }; +}; - for (i = 0; i < remaining; i++) { - promises[i].then(resolver(i), reject); - } - return allPromise; +function mixinProperties(to, from) { + for (var key in from) { + if (from.hasOwnProperty(key)) { + to[key] = from[key]; } + } +} - EventTarget.mixin(Promise.prototype); - - RSVP = { async: async, Promise: Promise, Event: Event, EventTarget: EventTarget, all: all, raiseOnUncaughtExceptions: true }; - return RSVP; - }); - -})(); +mixinProperties(Binding, { -(function() { -define("container", - [], - function() { + /* + See `Ember.Binding.from`. - var objectCreate = Object.create || function(parent) { - function F() {} - F.prototype = parent; - return new F(); - }; + @method from + @static + */ + from: function() { + var C = this, binding = new C(); + return binding.from.apply(binding, arguments); + }, - function InheritingDict(parent) { - this.parent = parent; - this.dict = {}; - } + /* + See `Ember.Binding.to`. - InheritingDict.prototype = { - get: function(key) { - var dict = this.dict; + @method to + @static + */ + to: function() { + var C = this, binding = new C(); + return binding.to.apply(binding, arguments); + }, - if (dict.hasOwnProperty(key)) { - return dict[key]; - } + /** + Creates a new Binding instance and makes it apply in a single direction. + A one-way binding will relay changes on the `from` side object (supplied + as the `from` argument) the `to` side, but not the other way around. + This means that if you change the "to" side directly, the "from" side may have + a different value. - if (this.parent) { - return this.parent.get(key); - } - }, + See `Binding.oneWay`. - set: function(key, value) { - this.dict[key] = value; - }, + @method oneWay + @param {String} from from path. + @param {Boolean} [flag] (Optional) passing nothing here will make the + binding `oneWay`. You can instead pass `false` to disable `oneWay`, making the + binding two way again. + @return {Ember.Binding} `this` + */ + oneWay: function(from, flag) { + var C = this, binding = new C(null, from); + return binding.oneWay(flag); + } - has: function(key) { - var dict = this.dict; +}); - if (dict.hasOwnProperty(key)) { - return true; - } +/** + An `Ember.Binding` connects the properties of two objects so that whenever + the value of one property changes, the other property will be changed also. - if (this.parent) { - return this.parent.has(key); - } + ## Automatic Creation of Bindings with `/^*Binding/`-named Properties - return false; - }, + You do not usually create Binding objects directly but instead describe + bindings in your class or object definition using automatic binding + detection. - eachLocal: function(callback, binding) { - var dict = this.dict; + Properties ending in a `Binding` suffix will be converted to `Ember.Binding` + instances. The value of this property should be a string representing a path + to another object or a custom binding instanced created using Binding helpers + (see "One Way Bindings"): - for (var prop in dict) { - if (dict.hasOwnProperty(prop)) { - callback.call(binding, prop, dict[prop]); - } - } - } - }; + ``` + valueBinding: "MyApp.someController.title" + ``` - function Container(parent) { - this.parent = parent; - this.children = []; + This will create a binding from `MyApp.someController.title` to the `value` + property of your object instance automatically. Now the two values will be + kept in sync. - this.resolver = parent && parent.resolver || function() {}; - this.registry = new InheritingDict(parent && parent.registry); - this.cache = new InheritingDict(parent && parent.cache); - this.typeInjections = new InheritingDict(parent && parent.typeInjections); - this.injections = {}; - this._options = new InheritingDict(parent && parent._options); - this._typeOptions = new InheritingDict(parent && parent._typeOptions); - } + ## One Way Bindings - Container.prototype = { - child: function() { - var container = new Container(this); - this.children.push(container); - return container; - }, + One especially useful binding customization you can use is the `oneWay()` + helper. This helper tells Ember that you are only interested in + receiving changes on the object you are binding from. For example, if you + are binding to a preference and you want to be notified if the preference + has changed, but your object will not be changing the preference itself, you + could do: - set: function(object, key, value) { - object[key] = value; - }, + ``` + bigTitlesBinding: Ember.Binding.oneWay("MyApp.preferencesController.bigTitles") + ``` - register: function(type, name, factory, options) { - var fullName; + This way if the value of `MyApp.preferencesController.bigTitles` changes the + `bigTitles` property of your object will change also. However, if you + change the value of your `bigTitles` property, it will not update the + `preferencesController`. + One way bindings are almost twice as fast to setup and twice as fast to + execute because the binding only has to worry about changes to one side. - if (type.indexOf(':') !== -1){ - options = factory; - factory = name; - fullName = type; - } else { - Ember.deprecate('register("'+type +'", "'+ name+'") is now deprecated in-favour of register("'+type+':'+name+'");', true); - fullName = type + ":" + name; - } + You should consider using one way bindings anytime you have an object that + may be created frequently and you do not intend to change a property; only + to monitor it for changes (such as in the example above). - this.registry.set(fullName, factory); - this._options.set(fullName, options || {}); - }, + ## Adding Bindings Manually - resolve: function(fullName) { - return this.resolver(fullName) || this.registry.get(fullName); - }, + All of the examples above show you how to configure a custom binding, but the + result of these customizations will be a binding template, not a fully active + Binding instance. The binding will actually become active only when you + instantiate the object the binding belongs to. It is useful however, to + understand what actually happens when the binding is activated. - lookup: function(fullName) { - if (this.cache.has(fullName)) { - return this.cache.get(fullName); - } + For a binding to function it must have at least a `from` property and a `to` + property. The `from` property path points to the object/key that you want to + bind from while the `to` path points to the object/key you want to bind to. - var value = instantiate(this, fullName); + When you define a custom binding, you are usually describing the property + you want to bind from (such as `MyApp.someController.value` in the examples + above). When your object is created, it will automatically assign the value + you want to bind `to` based on the name of your binding key. In the + examples above, during init, Ember objects will effectively call + something like this on your binding: - if (!value) { return; } + ```javascript + binding = Ember.Binding.from(this.valueBinding).to("value"); + ``` - if (isSingleton(this, fullName)) { - this.cache.set(fullName, value); - } + This creates a new binding instance based on the template you provide, and + sets the to path to the `value` property of the new object. Now that the + binding is fully configured with a `from` and a `to`, it simply needs to be + connected to become active. This is done through the `connect()` method: - return value; - }, + ```javascript + binding.connect(this); + ``` - has: function(fullName) { - if (this.cache.has(fullName)) { - return true; - } + Note that when you connect a binding you pass the object you want it to be + connected to. This object will be used as the root for both the from and + to side of the binding when inspecting relative paths. This allows the + binding to be automatically inherited by subclassed objects as well. - return !!factoryFor(this, fullName); - }, + Now that the binding is connected, it will observe both the from and to side + and relay changes. - optionsForType: function(type, options) { - if (this.parent) { illegalChildOperation('optionsForType'); } + If you ever needed to do so (you almost never will, but it is useful to + understand this anyway), you could manually create an active binding by + using the `Ember.bind()` helper method. (This is the same method used by + to setup your bindings on objects): - this._typeOptions.set(type, options); - }, + ```javascript + Ember.bind(MyApp.anotherObject, "value", "MyApp.someController.value"); + ``` - options: function(type, options) { - this.optionsForType(type, options); - }, + Both of these code fragments have the same effect as doing the most friendly + form of binding creation like so: - typeInjection: function(type, property, fullName) { - if (this.parent) { illegalChildOperation('typeInjection'); } + ```javascript + MyApp.anotherObject = Ember.Object.create({ + valueBinding: "MyApp.someController.value", - var injections = this.typeInjections.get(type); - if (!injections) { - injections = []; - this.typeInjections.set(type, injections); - } - injections.push({ property: property, fullName: fullName }); - }, + // OTHER CODE FOR THIS OBJECT... + }); + ``` - injection: function(factoryName, property, injectionName) { - if (this.parent) { illegalChildOperation('injection'); } + Ember's built in binding creation method makes it easy to automatically + create bindings for you. You should always use the highest-level APIs + available, even if you understand how it works underneath. - if (factoryName.indexOf(':') === -1) { - return this.typeInjection(factoryName, property, injectionName); - } + @class Binding + @namespace Ember + @since Ember 0.9 +*/ +Ember.Binding = Binding; - var injections = this.injections[factoryName] = this.injections[factoryName] || []; - injections.push({ property: property, fullName: injectionName }); - }, - destroy: function() { - this.isDestroyed = true; +/** + Global helper method to create a new binding. Just pass the root object + along with a `to` and `from` path to create and connect the binding. - for (var i=0, l=this.children.length; i 0) { + mixin.mixins = a_map.call(args, function(x) { + if (x instanceof Mixin) { return x; } - function buildInjections(container, injections) { - var hash = {}; + // Note: Manually setup a primitive mixin here. This is the only + // way to actually get a primitive mixin. This way normal creation + // of mixins will give you combined mixins... + var mixin = new Mixin(); + mixin.properties = x; + return mixin; + }); + } + return mixin; +} - if (!injections) { return hash; } +function isMethod(obj) { + return 'function' === typeof obj && + obj.isMethod !== false && + obj !== Boolean && obj !== Object && obj !== Number && obj !== Array && obj !== Date && obj !== String; +} - var injection, lookup; +var CONTINUE = {}; - for (var i=0, l=injections.length; i= 0) || + key === 'concatenatedProperties' || + key === 'mergedProperties') { + value = applyConcatenatedProperties(base, key, value, values); + } else if ((mergings && a_indexOf.call(mergings, key) >= 0)) { + value = applyMergedProperties(base, key, value, values); + } - Use this instead of the built-in `typeof` to get the type of an item. - It will return the same result across all browsers and includes a bit - more detail. Here is what will be returned: + descs[key] = undefined; + values[key] = value; + } +} - | Return Value | Meaning | - |---------------|------------------------------------------------------| - | 'string' | String primitive | - | 'number' | Number primitive | - | 'boolean' | Boolean primitive | - | 'null' | Null value | - | 'undefined' | Undefined value | - | 'function' | A function | - | 'array' | An instance of Array | - | 'class' | A Ember class (created using Ember.Object.extend()) | - | 'instance' | A Ember object instance | - | 'error' | An instance of the Error object | - | 'object' | A JavaScript object not inheriting from Ember.Object | +function mergeMixins(mixins, m, descs, values, base, keys) { + var mixin, props, key, concats, mergings, meta; - Examples: + function removeKeys(keyName) { + delete descs[keyName]; + delete values[keyName]; + } - ```javascript - Ember.typeOf(); // 'undefined' - Ember.typeOf(null); // 'null' - Ember.typeOf(undefined); // 'undefined' - Ember.typeOf('michael'); // 'string' - Ember.typeOf(101); // 'number' - Ember.typeOf(true); // 'boolean' - Ember.typeOf(Ember.makeArray); // 'function' - Ember.typeOf([1,2,90]); // 'array' - Ember.typeOf(Ember.Object.extend()); // 'class' - Ember.typeOf(Ember.Object.create()); // 'instance' - Ember.typeOf(new Error('teamocil')); // 'error' + for(var i=0, l=mixins.length; i w. -*/ -Ember.compare = function compare(v, w) { - if (v === w) { return 0; } + // Go through all mixins and hashes passed in, and: + // + // * Handle concatenated properties + // * Handle merged properties + // * Set up _super wrapping if necessary + // * Set up computed property descriptors + // * Copying `toString` in broken browsers + mergeMixins(mixins, mixinsMeta(obj), descs, values, obj, keys); - var type1 = Ember.typeOf(v); - var type2 = Ember.typeOf(w); + for(var i = 0, l = keys.length; i < l; i++) { + key = keys[i]; + if (key === 'constructor' || !values.hasOwnProperty(key)) { continue; } - var Comparable = Ember.Comparable; - if (Comparable) { - if (type1==='instance' && Comparable.detect(v.constructor)) { - return v.constructor.compare(v, w); - } + desc = descs[key]; + value = values[key]; - if (type2 === 'instance' && Comparable.detect(w.constructor)) { - return 1-w.constructor.compare(w, v); - } - } + if (desc === REQUIRED) { continue; } - // 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; + while (desc && desc instanceof Alias) { + var followed = followAlias(obj, desc, m, descs, values); + desc = followed.desc; + value = followed.value; } - // We no longer need Ember.ORDER_DEFINITION. - delete Ember.ORDER_DEFINITION; + if (desc === undefined && value === undefined) { continue; } + + replaceObserversAndListeners(obj, key, value); + detectBinding(obj, key, value, m); + defineProperty(obj, key, desc, value, m); } - var type1Index = mapping[type1]; - var type2Index = mapping[type2]; + if (!partial) { // don't apply to prototype + finishPartial(obj, m); + } - if (type1Index < type2Index) { return -1; } - if (type1Index > type2Index) { return 1; } + return obj; +} - // 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; +/** + @method mixin + @for Ember + @param obj + @param mixins* + @return obj +*/ +Ember.mixin = function(obj) { + var args = a_slice.call(arguments, 1); + applyMixin(obj, args, false); + return obj; +}; - case 'string': - var comp = v.localeCompare(w); - if (comp < 0) { return -1; } - if (comp > 0) { return 1; } - return 0; +/** + The `Ember.Mixin` class allows you to create mixins, whose properties can be + added to other classes. For instance, - 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; } - - // 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; - - case 'instance': - if (Ember.Comparable && Ember.Comparable.detect(v)) { - return v.compare(v, w); - } - return 0; + ```javascript + App.Editable = Ember.Mixin.create({ + edit: function() { + console.log('starting to edit'); + this.set('isEditing', true); + }, + isEditing: false + }); - case 'date': - var vNum = v.getTime(); - var wNum = w.getTime(); - if (vNum < wNum) { return -1; } - if (vNum > wNum) { return 1; } - return 0; + // Mix mixins into classes by passing them as the first arguments to + // .extend. + App.CommentView = Ember.View.extend(App.Editable, { + template: Ember.Handlebars.compile('{{#if view.isEditing}}...{{else}}...{{/if}}') + }); - default: - return 0; - } -}; + commentView = App.CommentView.create(); + commentView.edit(); // outputs 'starting to edit' + ``` -function _copy(obj, deep, seen, copies) { - var ret, loc, key; + Note that Mixins are created with `Ember.Mixin.create`, not + `Ember.Mixin.extend`. - // primitive data types are immutable, just return them. - if ('object' !== typeof obj || obj===null) return obj; + Note that mixins extend a constructor's prototype so arrays and object literals + defined as properties will be shared amongst objects that implement the mixin. + If you want to define an property in a mixin that is not shared, you can define + it either as a computed property or have it be created on initialization of the object. - // avoid cyclical loops - if (deep && (loc=indexOf(seen, obj))>=0) return copies[loc]; + ```javascript + //filters array will be shared amongst any object implementing mixin + App.Filterable = Ember.Mixin.create({ + filters: Ember.A() + }); - Ember.assert('Cannot clone an Ember.Object that does not implement Ember.Copyable', !(obj instanceof Ember.Object) || (Ember.Copyable && Ember.Copyable.detect(obj))); + //filters will be a separate array for every object implementing the mixin + App.Filterable = Ember.Mixin.create({ + filters: Ember.computed(function(){return Ember.A();}) + }); - // 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); + //filters will be created as a separate array during the object's initialization + App.Filterable = Ember.Mixin.create({ + init: function() { + this._super(); + this.set("filters", Ember.A()); } - } 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; + }); + ``` - // Prevents browsers that don't respect non-enumerability from - // copying internal Ember properties - if (key.substring(0,2) === '__') continue; + @class Mixin + @namespace Ember +*/ +Ember.Mixin = function() { return initMixin(this, arguments); }; - ret[key] = deep ? _copy(obj[key], deep, seen, copies) : obj[key]; - } - } +Mixin = Ember.Mixin; - if (deep) { - seen.push(obj); - copies.push(ret); - } +Mixin.prototype = { + properties: null, + mixins: null, + ownerConstructor: null +}; - return ret; -} +Mixin._apply = applyMixin; -/** - 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). +Mixin.applyPartial = function(obj) { + var args = a_slice.call(arguments, 1); + return applyMixin(obj, args, true); +}; - If the passed object implements the `clone()` method, then this function - will simply call that method and return the result. +Mixin.finishPartial = finishPartial; - @method copy - @for Ember - @param {Object} object 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); -}; +Ember.anyUnprocessedMixins = false; /** - Convenience method to inspect an object. This method will attempt to - convert the object into a useful string description. + @method create + @static + @param arguments* +*/ +Mixin.create = function() { + Ember.anyUnprocessedMixins = true; + var M = this; + return initMixin(new M(), arguments); +}; - It is a pretty simple implementation. If you want something more robust, - use something like JSDump: https://github.com/NV/jsDump +var MixinPrototype = Mixin.prototype; - @method inspect - @for Ember - @param {Object} obj The object you want to inspect. - @return {String} A description of the object +/** + @method reopen + @param arguments* */ -Ember.inspect = function(obj) { - if (typeof obj !== 'object' || obj === null) { - return obj + ''; +MixinPrototype.reopen = function() { + var mixin, tmp; + + if (this.properties) { + mixin = Mixin.create(); + mixin.properties = this.properties; + delete this.properties; + this.mixins = [mixin]; + } else if (!this.mixins) { + this.mixins = []; } - 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); + var len = arguments.length, mixins = this.mixins, idx; + + for(idx=0; idx < len; idx++) { + mixin = arguments[idx]; + Ember.assert('Expected hash or Mixin instance, got ' + Object.prototype.toString.call(mixin), typeof mixin === 'object' && mixin !== null && Object.prototype.toString.call(mixin) !== '[object Array]'); + + if (mixin instanceof Mixin) { + mixins.push(mixin); + } else { + tmp = Mixin.create(); + tmp.properties = mixin; + mixins.push(tmp); } } - return "{" + ret.join(", ") + "}"; + + return this; }; /** - 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. - - ```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} + @method apply + @param obj + @return applied object */ -Ember.isEqual = function(a, b) { - if (a && 'function'===typeof a.isEqual) return a.isEqual(b); - return a === b; +MixinPrototype.apply = function(obj) { + return applyMixin(obj, [this], false); }; -// Used by Ember.compare -Ember.ORDER_DEFINITION = Ember.ENV.ORDER_DEFINITION || [ - 'undefined', - 'null', - 'boolean', - 'number', - 'string', - 'array', - 'object', - 'instance', - 'function', - 'class', - 'date' -]; +MixinPrototype.applyPartial = function(obj) { + return applyMixin(obj, [this], true); +}; -/** - 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. +function _detect(curMixin, targetMixin, seen) { + var guid = guidFor(curMixin); - @method keys - @for Ember - @param {Object} obj - @return {Array} Array containing keys of obj -*/ -Ember.keys = Object.keys; + if (seen[guid]) { return false; } + seen[guid] = true; -if (!Ember.keys) { - Ember.keys = function(obj) { - var ret = []; - for(var key in obj) { - if (obj.hasOwnProperty(key)) { ret.push(key); } - } - return ret; - }; + if (curMixin === targetMixin) { return true; } + var mixins = curMixin.mixins, loc = mixins ? mixins.length : 0; + while (--loc >= 0) { + if (_detect(mixins[loc], targetMixin, seen)) { return true; } + } + return false; } -// .......................................................... -// ERROR -// - -var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; - /** - A subclass of the JavaScript Error object for use in Ember. - - @class Error - @namespace Ember - @extends Error - @constructor + @method detect + @param obj + @return {Boolean} */ -Ember.Error = function() { - var tmp = Error.prototype.constructor.apply(this, arguments); +MixinPrototype.detect = function(obj) { + if (!obj) { return false; } + if (obj instanceof Mixin) { return _detect(obj, this, {}); } + var mixins = Ember.meta(obj, false).mixins; + if (mixins) { + return !!mixins[guidFor(this)]; + } + return false; +}; - // 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]]; +MixinPrototype.without = function() { + var ret = new Mixin(this); + ret._without = a_slice.call(arguments); + return ret; +}; + +function _keys(ret, mixin, seen) { + if (seen[guidFor(mixin)]) { return; } + seen[guidFor(mixin)] = true; + + if (mixin.properties) { + var props = mixin.properties; + for (var key in props) { + if (props.hasOwnProperty(key)) { ret[key] = true; } + } + } else if (mixin.mixins) { + a_forEach.call(mixin.mixins, function(x) { _keys(ret, x, seen); }); + } +} + +MixinPrototype.keys = function() { + var keys = {}, seen = {}, ret = []; + _keys(keys, this, seen); + for(var key in keys) { + if (keys.hasOwnProperty(key)) { ret.push(key); } } + return ret; }; -Ember.Error.prototype = Ember.create(Error.prototype); +// returns the mixins currently applied to the specified object +// TODO: Make Ember.mixin +Mixin.mixins = function(obj) { + var mixins = Ember.meta(obj, false).mixins, ret = []; -})(); + if (!mixins) { return ret; } + + for (var key in mixins) { + var mixin = mixins[key]; + // skip primitive mixins since these are always anonymous + if (!mixin.properties) { ret.push(mixin); } + } + return ret; +}; + +REQUIRED = new Ember.Descriptor(); +REQUIRED.toString = function() { return '(Required Property)'; }; -(function() { /** -@module ember -@submodule ember-runtime + Denotes a required property for a mixin + + @method required + @for Ember */ +Ember.required = function() { + return REQUIRED; +}; -var STRING_DASHERIZE_REGEXP = (/[ _]/g); -var STRING_DASHERIZE_CACHE = {}; -var STRING_DECAMELIZE_REGEXP = (/([a-z])([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); +Alias = function(methodName) { + this.methodName = methodName; +}; +Alias.prototype = new Ember.Descriptor(); /** - 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. + Makes a property or method available via an additional name. - @property STRINGS + ```javascript + App.PaintSample = Ember.Object.extend({ + color: 'red', + colour: Ember.alias('color'), + name: function() { + return "Zed"; + }, + moniker: Ember.alias("name") + }); + + var paintSample = App.PaintSample.create() + paintSample.get('colour'); // 'red' + paintSample.moniker(); // 'Zed' + ``` + + @method alias @for Ember - @type Hash + @param {String} methodName name of the method or property to alias + @return {Ember.Descriptor} + @deprecated Use `Ember.aliasMethod` or `Ember.computed.alias` instead */ -Ember.STRINGS = {}; +Ember.alias = function(methodName) { + return new Alias(methodName); +}; + +Ember.alias = Ember.deprecateFunc("Ember.alias is deprecated. Please use Ember.aliasMethod or Ember.computed.alias instead.", Ember.alias); /** - 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. + Makes a method available via an additional name. - @class String - @namespace Ember - @static + ```javascript + App.Person = Ember.Object.extend({ + name: function() { + return 'Tomhuda Katzdale'; + }, + moniker: Ember.aliasMethod('name') + }); + + var goodGuy = App.Person.create() + ``` + + @method aliasMethod + @for Ember + @param {String} methodName name of the method to alias + @return {Ember.Descriptor} */ -Ember.String = { +Ember.aliasMethod = function(methodName) { + return new Alias(methodName); +}; - /** - 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. +// .......................................................... +// OBSERVER HELPER +// - Ordered insertions are most useful when building loc strings where values - you need to insert may appear in different orders. +/** + Specify a method that observes property changes. - ```javascript - "Hello %@ %@".fmt('John', 'Doe'); // "Hello John Doe" - "Hello %@2, %@1".fmt('John', 'Doe'); // "Hello Doe, John" - ``` + ```javascript + Ember.Object.extend({ + valueObserver: Ember.observer(function() { + // Executes whenever the "value" property changes + }, 'value') + }); + ``` - @method fmt - @param {Object...} [args] - @return {String} formatted string - */ - 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,0) - 1 : idx++ ; - s = formats[argIndex]; - return ((s === null) ? '(null)' : (s === undefined) ? '' : s).toString(); - }) ; - }, + In the future this method may become asynchronous. If you want to ensure + synchronous behavior, use `immediateObserver`. - /** - 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. + Also available as `Function.prototype.observes` if prototype extensions are + enabled. - 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. + @method observer + @for Ember + @param {Function} func + @param {String} propertyNames* + @return func +*/ +Ember.observer = function(func) { + var paths = a_slice.call(arguments, 1); + func.__ember_observes__ = paths; + return func; +}; - ```javascript - Ember.STRINGS = { - '_Hello World': 'Bonjour le monde', - '_Hello %@ %@': 'Bonjour %@ %@' - }; +/** + Specify a method that observes property changes. - Ember.String.loc("_Hello World"); // 'Bonjour le monde'; - Ember.String.loc("_Hello %@ %@", ["John", "Smith"]); // "Bonjour John Smith"; - ``` + ```javascript + Ember.Object.extend({ + valueObserver: Ember.immediateObserver(function() { + // Executes whenever the "value" property changes + }, 'value') + }); + ``` - @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) ; - }, + In the future, `Ember.observer` may become asynchronous. In this event, + `Ember.immediateObserver` will maintain the synchronous behavior. - /** - 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`. + Also available as `Function.prototype.observesImmediately` if prototype extensions are + enabled. - ```javascript - Ember.String.w("alpha beta gamma").forEach(function(key) { - console.log(key); - }); + @method immediateObserver + @for Ember + @param {Function} func + @param {String} propertyNames* + @return func +*/ +Ember.immediateObserver = function() { + for (var i=0, l=arguments.length; i alpha - // > beta - // > gamma - ``` + return Ember.observer.apply(this, arguments); +}; - @method w - @param {String} str The string to split - @return {String} split string - */ - w: function(str) { return str.split(/\s+/); }, +/** + When observers fire, they are called with the arguments `obj`, `keyName`. - /** - Converts a camelized string into all lower case separated by underscores. + Note, `@each.property` observer is called per each add or replace of an element + and it's not called with a specific enumeration item. - ```javascript - 'innerHTML'.decamelize(); // 'inner_html' - 'action_name'.decamelize(); // 'action_name' - 'css-class-name'.decamelize(); // 'css-class-name' - 'my favorite items'.decamelize(); // 'my favorite items' - ``` + A `beforeObserver` fires before a property changes. - @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(); - }, + A `beforeObserver` is an alternative form of `.observesBefore()`. - /** - Replaces underscores or spaces with dashes. + ```javascript + App.PersonView = Ember.View.extend({ - ```javascript - 'innerHTML'.dasherize(); // 'inner-html' - 'action_name'.dasherize(); // 'action-name' - 'css-class-name'.dasherize(); // 'css-class-name' - 'my favorite items'.dasherize(); // 'my-favorite-items' - ``` + friends: [{ name: 'Tom' }, { name: 'Stefan' }, { name: 'Kris' }], - @method dasherize - @param {String} str The string to dasherize. - @return {String} the dasherized string. - */ - dasherize: function(str) { - var cache = STRING_DASHERIZE_CACHE, - ret = cache[str]; + valueWillChange: Ember.beforeObserver(function(obj, keyName) { + this.changingFrom = obj.get(keyName); + }, 'content.value'), - if (ret) { - return ret; - } else { - ret = Ember.String.decamelize(str).replace(STRING_DASHERIZE_REGEXP,'-'); - cache[str] = ret; - } + valueDidChange: Ember.observer(function(obj, keyName) { + // only run if updating a value already in the DOM + if (this.get('state') === 'inDOM') { + var color = obj.get(keyName) > this.changingFrom ? 'green' : 'red'; + // logic + } + }, 'content.value'), - return ret; - }, + friendsDidChange: Ember.observer(function(obj, keyName) { + // some logic + // obj.get(keyName) returns friends array + }, 'friends.@each.name') + }); + ``` - /** - Returns the lowerCaseCamel form of a string. + Also available as `Function.prototype.observesBefore` if prototype extensions are + enabled. - ```javascript - 'innerHTML'.camelize(); // 'innerHTML' - 'action_name'.camelize(); // 'actionName' - 'css-class-name'.camelize(); // 'cssClassName' - 'my favorite items'.camelize(); // 'myFavoriteItems' - ``` + @method beforeObserver + @for Ember + @param {Function} func + @param {String} propertyNames* + @return func +*/ +Ember.beforeObserver = function(func) { + var paths = a_slice.call(arguments, 1); + func.__ember_observesBefore__ = paths; + return func; +}; - @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() : ''; - }); - }, +})(); - /** - 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 = []; +(function() { +/** +Ember Metal - for (var i=0, l=parts.length; i 'InnerHTML' - 'action_name'.capitalize() => 'Action_name' - 'css-class-name'.capitalize() => 'Css-class-name' - 'my favorite items'.capitalize() => 'My favorite items' + if (remaining === 0) { + resolve([]); + } - @method capitalize - @param {String} str - @return {String} - */ - capitalize: function(str) { - return str.charAt(0).toUpperCase() + str.substr(1); - } + function resolver(index) { + return function(value) { + resolveAll(index, value); + }; + } -}; + function resolveAll(index, value) { + results[index] = value; + if (--remaining === 0) { + resolve(results); + } + } -})(); + for (var i = 0; i < promises.length; i++) { + promise = promises[i]; + if (promise && typeof promise.then === 'function') { + promise.then(resolver(i), reject); + } else { + resolveAll(i, promise); + } + } + }); + } -(function() { -/** -@module ember -@submodule ember-runtime -*/ + __exports__.all = all; + }); +define("rsvp/async", + ["exports"], + function(__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); + }); + }; + } + // node >= 0.10.x + function useSetImmediate() { + return function(callback, arg) { + /* global setImmediate */ + setImmediate(function(){ + callback(arg); + }); + }; + } -var fmt = Ember.String.fmt, - w = Ember.String.w, - loc = Ember.String.loc, - camelize = Ember.String.camelize, - decamelize = Ember.String.decamelize, - dasherize = Ember.String.dasherize, - underscore = Ember.String.underscore, - capitalize = Ember.String.capitalize, - classify = Ember.String.classify; + function useMutationObserver() { + var queue = []; -if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { + var observer = new BrowserMutationObserver(function() { + var toProcess = queue.slice(); + queue = []; - /** - See {{#crossLink "Ember.String/fmt"}}{{/crossLink}} + toProcess.forEach(function(tuple) { + var callback = tuple[0], arg= tuple[1]; + callback(arg); + }); + }); - @method fmt - @for String - */ - String.prototype.fmt = function() { - return fmt(this, arguments); - }; + var element = document.createElement('div'); + observer.observe(element, { attributes: true }); - /** - See {{#crossLink "Ember.String/w"}}{{/crossLink}} + // Chrome Memory Leak: https://bugs.webkit.org/show_bug.cgi?id=93661 + window.addEventListener('unload', function(){ + observer.disconnect(); + observer = null; + }, false); - @method w - @for String - */ - String.prototype.w = function() { - return w(this); - }; + return function(callback, arg) { + queue.push([callback, arg]); + element.setAttribute('drainQueue', 'drainQueue'); + }; + } - /** - See {{#crossLink "Ember.String/loc"}}{{/crossLink}} + function useSetTimeout() { + return function(callback, arg) { + local.setTimeout(function() { + callback(arg); + }, 1); + }; + } - @method loc - @for String - */ - String.prototype.loc = function() { - return loc(this, arguments); - }; + 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(); + } - /** - See {{#crossLink "Ember.String/camelize"}}{{/crossLink}} - @method camelize - @for String - */ - String.prototype.camelize = function() { - return camelize(this); - }; + __exports__.async = async; + }); +define("rsvp/config", + ["rsvp/async","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var async = __dependency1__.async; - /** - See {{#crossLink "Ember.String/decamelize"}}{{/crossLink}} + var config = {}; + config.async = async; - @method decamelize - @for String - */ - String.prototype.decamelize = function() { - return decamelize(this); - }; - /** - See {{#crossLink "Ember.String/dasherize"}}{{/crossLink}} + __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 + }; - @method dasherize - @for String - */ - String.prototype.dasherize = function() { - return dasherize(this); - }; + deferred.promise = new Promise(function(resolve, reject) { + deferred.resolve = resolve; + deferred.reject = reject; + }); - /** - See {{#crossLink "Ember.String/underscore"}}{{/crossLink}} + return deferred; + } - @method underscore - @for String - */ - String.prototype.underscore = function() { - return underscore(this); - }; - /** - See {{#crossLink "Ember.String/classify"}}{{/crossLink}} + __exports__.defer = defer; + }); +define("rsvp/events", + ["exports"], + function(__exports__) { + "use strict"; + var Event = function(type, options) { + this.type = type; - @method classify - @for String - */ - String.prototype.classify = function() { - return classify(this); - }; + for (var option in options) { + if (!options.hasOwnProperty(option)) { continue; } - /** - See {{#crossLink "Ember.String/capitalize"}}{{/crossLink}} + this[option] = options[option]; + } + }; - @method capitalize - @for String - */ - String.prototype.capitalize = function() { - return capitalize(this); - }; + var indexOf = function(callbacks, callback) { + for (var i=0, l=callbacks.length; i 2) { + resolve(Array.prototype.slice.call(arguments, 1)); + } else { + resolve(value); + } + }; + } + function denodeify(nodeFunc) { + return function() { + var nodeArgs = Array.prototype.slice.call(arguments), resolve, reject; + var thisArg = this; -})(); + var promise = new Promise(function(nodeResolve, nodeReject) { + resolve = nodeResolve; + reject = nodeReject; + }); + all(nodeArgs).then(function(nodeArgs) { + nodeArgs.push(makeNodeCallbackFor(resolve, reject)); + try { + nodeFunc.apply(thisArg, nodeArgs); + } catch(e) { + reject(e); + } + }); -(function() { + return promise; + }; + } -})(); + __exports__.denodeify = denodeify; + }); +define("rsvp/promise", + ["rsvp/config","rsvp/events","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() { -/** -@module ember -@submodule ember-runtime -*/ + function isFunction(x){ + return typeof x === "function"; + } -// .......................................................... -// HELPERS -// + var Promise = function(resolver) { + var promise = this, + resolved = false; -var get = Ember.get, set = Ember.set; -var a_slice = Array.prototype.slice; -var a_indexOf = Ember.EnumerableUtils.indexOf; + if (typeof resolver !== 'function') { + throw new TypeError('You must pass a resolver function as the sole argument to the promise constructor'); + } -var contexts = []; + if (!(promise instanceof Promise)) { + return new Promise(resolver); + } -function popCtx() { - return contexts.length===0 ? {} : contexts.pop(); -} + var resolvePromise = function(value) { + if (resolved) { return; } + resolved = true; + resolve(promise, value); + }; -function pushCtx(ctx) { - contexts.push(ctx); - return null; -} + var rejectPromise = function(value) { + if (resolved) { return; } + resolved = true; + reject(promise, value); + }; -function iter(key, value) { - var valueProvided = arguments.length === 2; + this.on('promise:resolved', function(event) { + this.trigger('success', { detail: event.detail }); + }, this); - function i(item) { - var cur = get(item, key); - return valueProvided ? value===cur : !!cur; - } - return i ; -} + this.on('promise:failed', function(event) { + this.trigger('error', { detail: event.detail }); + }, this); -/** - 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). + this.on('error', onerror); - 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. + try { + resolver(resolvePromise, rejectPromise); + } catch(e) { + rejectPromise(e); + } + }; - ## Writing Your Own Enumerable + function onerror(event) { + if (config.onerror) { + config.onerror(event.detail); + } + } - To make your own custom class enumerable, you need two items: + var invokeCallback = function(type, promise, callback, event) { + var hasCallback = isFunction(callback), + value, error, succeeded, failed; - 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().` + if (hasCallback) { + try { + value = callback(event.detail); + succeeded = true; + } catch(e) { + failed = true; + error = e; + } + } else { + value = event.detail; + succeeded = true; + } - 2. If you must implement `nextObject().` See documentation. + 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); + } + }; - 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. + Promise.prototype = { + constructor: Promise, - ## Using Ember Enumeration with Other Libraries + isRejected: undefined, + isFulfilled: undefined, + rejectedReason: undefined, + fulfillmentValue: undefined, - 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. + then: function(done, fail) { + this.off('error', onerror); - @class Enumerable - @namespace Ember - @extends Ember.Mixin - @since Ember 0.9 -*/ -Ember.Enumerable = Ember.Mixin.create( - /** @scope Ember.Enumerable.prototype */ { + var thenPromise = new this.constructor(function() {}); - // compatibility - isEnumerable: true, - - /** - Implement this method to make your class enumerable. - - 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 (this.isFulfilled) { + config.async(function(promise) { + invokeCallback('resolve', thenPromise, done, { detail: promise.fulfillmentValue }); + }, this); + } - 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. + if (this.isRejected) { + config.async(function(promise) { + invokeCallback('reject', thenPromise, fail, { detail: promise.rejectedReason }); + }, this); + } - 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. + this.on('promise:resolved', function(event) { + invokeCallback('resolve', thenPromise, done, event); + }); - 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. + this.on('promise:failed', function(event) { + invokeCallback('reject', thenPromise, fail, event); + }); - The default implementation of this method simply looks up the index. - This works great on any Array-like objects. + return thenPromise; + }, - @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), + fail: function(fail) { + return this.then(null, fail); + } + }; - /** - 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. + EventTarget.mixin(Promise.prototype); - 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`. + function resolve(promise, value) { + if (promise === value) { + fulfill(promise, value); + } else if (!handleThenable(promise, value)) { + fulfill(promise, value); + } + } - ```javascript - var arr = ["a", "b", "c"]; - arr.firstObject(); // "a" + function handleThenable(promise, value) { + var then = null, + resolved; - var arr = []; - arr.firstObject(); // undefined - ``` + try { + if (promise === value) { + throw new TypeError("A promises callback cannot return that same promise."); + } - @property firstObject - @return {Object} the object or undefined - */ - firstObject: Ember.computed(function() { - if (get(this, 'length')===0) return undefined ; + if (objectOrFunction(value)) { + then = value.then; - // handle generic enumerables - var context = popCtx(), ret; - ret = this.nextObject(0, null, context); - pushCtx(context); - return ret ; - }).property('[]'), + if (isFunction(then)) { + then.call(value, function(val) { + if (resolved) { return true; } + resolved = true; - /** - 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`. + if (value !== val) { + resolve(promise, val); + } else { + fulfill(promise, val); + } + }, function(val) { + if (resolved) { return true; } + resolved = true; - ```javascript - var arr = ["a", "b", "c"]; - arr.lastObject(); // "c" + reject(promise, val); + }); - var arr = []; - arr.lastObject(); // undefined - ``` + return true; + } + } + } catch (error) { + reject(promise, error); + return true; + } - @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('[]'), + return false; + } - /** - 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. + function fulfill(promise, value) { + config.async(function() { + promise.trigger('promise:resolved', { detail: value }); + promise.isFulfilled = true; + promise.fulfillmentValue = value; + }); + } - ```javascript - var arr = ["a", "b", "c"]; - arr.contains("a"); // true - arr.contains("z"); // false - ``` + function reject(promise, value) { + config.async(function() { + promise.trigger('promise:failed', { detail: value }); + promise.isRejected = true; + promise.rejectedReason = value; + }); + } - @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; - }, - /** - Iterates through the enumerable, calling the passed function on each - item. This method corresponds to the `forEach()` method defined in - JavaScript 1.6. + __exports__.Promise = Promise; + }); +define("rsvp/reject", + ["rsvp/promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__.Promise; - The callback method you provide should have the following signature (all - parameters are optional): + function reject(reason) { + return new Promise(function (resolve, reject) { + reject(reason); + }); + } - ```javascript - function(item, index, enumerable); - ``` - - `item` is the current item in the iteration. - - `index` is the current index in the iteration. - - `enumerable` is the enumerable object itself. + __exports__.reject = reject; + }); +define("rsvp/resolve", + ["rsvp/promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__.Promise; - 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. + function resolve(thenable) { + return new Promise(function(resolve, reject) { + resolve(thenable); + }); + } - @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(); - if (target === undefined) target = null; + __exports__.resolve = resolve; + }); +define("rsvp/rethrow", + ["exports"], + function(__exports__) { + "use strict"; + var local = (typeof global === "undefined") ? this : global; - for(var idx=0;idx true - ```javascript - function(item, index, enumerable); - ``` + container.unregister('model:user') + container.lookup('model:user') === undefined //=> true + ``` - - `item` is the current item in the iteration. - - `index` is the current index in the iteration. - - `enumerable` is the enumerable object itself. + @method unregister + @param {String} fullName + */ + unregister: function(fullName) { + var normalizedName = this.normalize(fullName); - It should return the `true` to include the item in the results, `false` - otherwise. + this.registry.remove(normalizedName); + this.cache.remove(normalizedName); + this.factoryCache.remove(normalizedName); + this._options.remove(normalizedName); + }, - 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. + /** + Given a fullName return the corresponding factory. - Usage Example: + By default `resolve` will retrieve the factory from + its container's registry. - ```javascript - if (people.some(isManager)) { Paychecks.addBiggerBonus(); } - ``` + ```javascript + var container = new Container(); + container.register('api:twitter', Twitter); - @method some - @param {Function} callback The callback to execute - @param {Object} [target] The target object to use - @return {Array} A filtered array. - */ - some: function(callback, target) { - return !!this.find(function(x, idx, i) { - return !!callback.call(target, x, idx, i); - }); - }, + container.resolve('api:twitter') // => Twitter + ``` - /** - Returns `true` if the passed property resolves to `true` for any item in - the enumerable. This method is often simpler/faster than using a callback. + 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. - @method someProperty - @param {String} key the property to test - @param {String} [value] optional value to test against. - @return {Boolean} `true` - */ - someProperty: function(key, value) { - return this.some(iter.apply(this, arguments)); - }, + ```javascript + var container = new Container(); + container.resolver = function(fullName) { + // lookup via the module system of choice + }; - /** - This will combine the values of the enumerator into a single value. It - is a useful way to collect a summary value from an enumeration. This - corresponds to the `reduce()` method defined in JavaScript 1.8. + // the twitter factory is added to the module system + container.resolve('api:twitter') // => Twitter + ``` - The callback method you provide should have the following signature (all - parameters are optional): + @method resolve + @param {String} fullName + @returns {Function} fullName's factory + */ + resolve: function(fullName) { + return this.resolver(fullName) || this.registry.get(fullName); + }, - ```javascript - function(previousValue, item, index, enumerable); - ``` + /** + A hook that can be used to describe how the resolver will + attempt to find the factory. - - `previousValue` is the value returned by the last call to the iterator. - - `item` is the current item in the iteration. - - `index` is the current index in the iteration. - - `enumerable` is the enumerable object itself. + For example, the default Ember `.describe` returns the full + class name (including namespace) where Ember's resolver expects + to find the `fullName`. - Return the new cumulative value. + @method describe + */ + describe: function(fullName) { + return fullName; + }, - In addition to the callback you can also pass an `initialValue`. An error - will be raised if you do not pass an initial value and the enumerator is - empty. + /** + A hook to enable custom fullName normalization behaviour - Note that unlike the other methods, this method does not allow you to - pass a target object to set as this for the callback. It's part of the - spec. Sorry. + @method normalize + @param {String} fullName + @return {string} normalized fullName + */ + normalize: function(fullName) { + return fullName; + }, - @method reduce - @param {Function} callback The callback to execute - @param {Object} initialValue Initial value for the reduce - @param {String} reducerProperty internal use only. - @return {Object} The reduced value. - */ - reduce: function(callback, initialValue, reducerProperty) { - if (typeof callback !== "function") { throw new TypeError(); } + /** + @method makeToString - var ret = initialValue; + @param {any} factory + @param {string} fullNae + @return {function} toString function + */ + makeToString: function(factory, fullName) { + return factory.toString(); + }, - this.forEach(function(item, i) { - ret = callback.call(null, ret, item, i, this, reducerProperty); - }, this); + /** + Given a fullName return a corresponding instance. - return ret; - }, + 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. - /** - Invokes the named method on every object in the receiver that - implements it. This method corresponds to the implementation in - Prototype 1.6. + ```javascript + var container = new Container(); + container.register('api:twitter', Twitter); - @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. - */ - invoke: function(methodName) { - var args, ret = []; - if (arguments.length>1) args = a_slice.call(arguments, 1); + var twitter = container.lookup('api:twitter'); - 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); + twitter instanceof Twitter; // => true - return ret; - }, + // by default the container will return singletons + twitter2 = container.lookup('api:twitter'); + twitter instanceof Twitter; // => true - /** - Simply converts the enumerable into a genuine array. The order is not - guaranteed. Corresponds to the method implemented by Prototype. + twitter === twitter2; //=> true + ``` - @method toArray - @return {Array} the enumerable as an array. - */ - toArray: function() { - var ret = []; - this.forEach(function(o, idx) { ret[idx] = o; }); - return ret ; - }, + If singletons are not wanted an optional flag can be provided at lookup. - /** - Returns a copy of the array with all null elements removed. + ```javascript + var container = new Container(); + container.register('api:twitter', Twitter); - ```javascript - var arr = ["a", null, "c", null]; - arr.compact(); // ["a", "c"] - ``` + var twitter = container.lookup('api:twitter', { singleton: false }); + var twitter2 = container.lookup('api:twitter', { singleton: false }); - @method compact - @return {Array} the array without null elements. - */ - compact: function() { return this.without(null); }, + twitter === twitter2; //=> false + ``` - /** - 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 lookup + @param {String} fullName + @param {Object} options + @return {any} + */ + lookup: function(fullName, options) { + fullName = this.normalize(fullName); - ```javascript - var arr = ["a", "b", "a", "c"]; - arr.without("a"); // ["b", "c"] - ``` + options = options || {}; - @method without - @param {Object} value - @return {Ember.Enumerable} - */ - without: function(value) { - if (!this.contains(value)) return this; // nothing to do - var ret = [] ; - this.forEach(function(k) { - if (k !== value) ret[ret.length] = k; - }) ; - return ret ; - }, + if (this.cache.has(fullName) && options.singleton !== false) { + return this.cache.get(fullName); + } - /** - Returns a new enumerable that contains only unique values. The default - implementation returns an array regardless of the receiver type. + var value = instantiate(this, fullName); - ```javascript - var arr = ["a", "a", "b", "b"]; - arr.uniq(); // ["a", "b"] - ``` + if (!value) { return; } - @method uniq - @return {Ember.Enumerable} - */ - uniq: function() { - var ret = []; - this.forEach(function(k){ - if (a_indexOf(ret, k)<0) ret.push(k); - }); - return ret; - }, + if (isSingleton(this, fullName) && options.singleton !== false) { + this.cache.set(fullName, value); + } - /** - This property will trigger anytime the enumerable's content changes. - You can observe this property to be notified of changes to the enumerables - content. + return value; + }, - For plain enumerables, this property is read only. `Ember.Array` overrides - this method. + /** + Given a fullName return the corresponding factory. - @property [] - @type Ember.Array - */ - '[]': Ember.computed(function(key, value) { - return this; - }), + @method lookupFactory + @param {String} fullName + @return {any} + */ + lookupFactory: function(fullName) { + return factoryFor(this, fullName); + }, - // .......................................................... - // ENUMERABLE OBSERVERS - // + /** + Given a fullName check if the container is aware of its factory + or singleton instance. - /** - Registers an enumerable observer. Must implement `Ember.EnumerableObserver` - mixin. + @method has + @param {String} fullName + @return {Boolean} + */ + has: function(fullName) { + if (this.cache.has(fullName)) { + return true; + } - @method addEnumerableObserver - @param target {Object} - @param opts {Hash} - */ - addEnumerableObserver: function(target, opts) { - var willChange = (opts && opts.willChange) || 'enumerableWillChange', - didChange = (opts && opts.didChange) || 'enumerableDidChange'; + return !!factoryFor(this, fullName); + }, - 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; - }, + /** + Allow registering options for all factories of a type. - /** - Removes a registered enumerable observer. + ```javascript + var container = new Container(); - @method removeEnumerableObserver - @param target {Object} - @param [opts] {Hash} - */ - removeEnumerableObserver: function(target, opts) { - var willChange = (opts && opts.willChange) || 'enumerableWillChange', - didChange = (opts && opts.didChange) || 'enumerableDidChange'; + // if all of type `connection` must not be singletons + container.optionsForType('connection', { singleton: false }); - 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; - }, + container.register('connection:twitter', TwitterConnection); + container.register('connection:facebook', FacebookConnection); - /** - Becomes true whenever the array currently has observers watching changes - on the array. + var twitter = container.lookup('connection:twitter'); + var twitter2 = container.lookup('connection:twitter'); - @property hasEnumerableObservers - @type Boolean - */ - hasEnumerableObservers: Ember.computed(function() { - return Ember.hasListeners(this, '@enumerable:change') || Ember.hasListeners(this, '@enumerable:before'); - }), + twitter === twitter2; // => false + var facebook = container.lookup('connection:facebook'); + var facebook2 = container.lookup('connection:facebook'); - /** - 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. + facebook === facebook2; // => false + ``` - @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) { + @method optionsForType + @param {String} type + @param {Object} options + */ + optionsForType: function(type, options) { + if (this.parent) { illegalChildOperation('optionsForType'); } - var removeCnt, addCnt, hasDelta; + this._typeOptions.set(type, options); + }, - if ('number' === typeof removing) removeCnt = removing; - else if (removing) removeCnt = get(removing, 'length'); - else removeCnt = removing = -1; + /** + @method options + @param {String} type + @param {Object} options + */ + options: function(type, options) { + this.optionsForType(type, options); + }, - if ('number' === typeof adding) addCnt = adding; - else if (adding) addCnt = get(adding,'length'); - else addCnt = adding = -1; + /* + @private - hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; + Used only via `injection`. - if (removing === -1) removing = null; - if (adding === -1) adding = null; + Provides a specialized form of injection, specifically enabling + all objects of one type to be injected with a reference to another + object. - Ember.propertyWillChange(this, '[]'); - if (hasDelta) Ember.propertyWillChange(this, 'length'); - Ember.sendEvent(this, '@enumerable:before', [this, removing, adding]); + For example, provided each object of type `controller` needed a `router`. + one would do the following: - return this; - }, + ```javascript + var container = new Container(); - /** - 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. + container.register('router:main', Router); + container.register('controller:user', UserController); + container.register('controller:post', PostController); - @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 notify = this.propertyDidChange, removeCnt, addCnt, hasDelta; + container.typeInjection('controller', 'router', 'router:main'); - if ('number' === typeof removing) removeCnt = removing; - else if (removing) removeCnt = get(removing, 'length'); - else removeCnt = removing = -1; + var user = container.lookup('controller:user'); + var post = container.lookup('controller:post'); - if ('number' === typeof adding) addCnt = adding; - else if (adding) addCnt = get(adding, 'length'); - else addCnt = adding = -1; + user.router instanceof Router; //=> true + post.router instanceof Router; //=> true - hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; + // both controllers share the same router + user.router === post.router; //=> true + ``` - if (removing === -1) removing = null; - if (adding === -1) adding = null; + @method typeInjection + @param {String} type + @param {String} property + @param {String} fullName + */ + typeInjection: function(type, property, fullName) { + if (this.parent) { illegalChildOperation('typeInjection'); } - Ember.sendEvent(this, '@enumerable:change', [this, removing, adding]); - if (hasDelta) Ember.propertyDidChange(this, 'length'); - Ember.propertyDidChange(this, '[]'); + addTypeInjection(this.typeInjections, type, property, fullName); + }, - return this ; - } + /* + Defines injection rules. -}) ; + These rules are used to inject dependencies onto objects when they + are instantiated. -})(); + Two forms of injections are possible: + * Injecting one fullName on another fullName + * Injecting one fullName on a type + Example: -(function() { -/** -@module ember -@submodule ember-runtime -*/ + ```javascript + var container = new Container(); -// .......................................................... -// HELPERS -// + container.register('source:main', Source); + container.register('model:user', User); + container.register('model:post', Post); -var get = Ember.get, set = Ember.set, meta = Ember.meta, map = Ember.EnumerableUtils.map, cacheFor = Ember.cacheFor; + // injecting one fullName on another fullName + // eg. each user model gets a post model + container.injection('model:user', 'post', 'model:post'); -function none(obj) { return obj===null || obj===undefined; } + // injecting one fullName on another type + container.injection('model', 'source', 'source:main'); -// .......................................................... -// 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. + var user = container.lookup('model:user'); + var post = container.lookup('model:post'); - 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. + user.source instanceof Source; //=> true + post.source instanceof Source; //=> true - 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. + user.post instanceof Post; //=> true - 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 if an array changes by changing the syntax of the property to - `.observes('*myProperty.[]')`. + // and both models share the same source + user.source === post.source; //=> true + ``` - To support `Ember.Array` in your own class, you must override two - primitives to use it: `replace()` and `objectAt()`. + @method injection + @param {String} factoryName + @param {String} property + @param {String} injectionName + */ + injection: function(factoryName, property, injectionName) { + if (this.parent) { illegalChildOperation('injection'); } - Note that the Ember.Array mixin also incorporates the `Ember.Enumerable` - mixin. All `Ember.Array`-like objects are also enumerable. + if (factoryName.indexOf(':') === -1) { + return this.typeInjection(factoryName, property, injectionName); + } - @class Array - @namespace Ember - @extends Ember.Mixin - @uses Ember.Enumerable - @since Ember 0.9.0 -*/ -Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.prototype */ { + addInjection(this.injections, factoryName, property, injectionName); + }, - // compatibility - isSCArray: true, - /** - Your array must support the `length` property. Your replace methods should - set this property whenever it changes. + /* + @private - @property {Number} length - */ - length: Ember.required(), + Used only via `factoryInjection`. - /** - Returns the object at the given `index`. If the given `index` is negative - or is greater or equal than the array length, returns `undefined`. + Provides a specialized form of injection, specifically enabling + all factory of one type to be injected with a reference to another + object. - 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. + For example, provided each factory of type `model` needed a `store`. + one would do the following: - ```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 - ``` + ```javascript + var container = new Container(); - @method objectAt - @param {Number} idx The index of the item to return. - */ - objectAt: function(idx) { - if ((idx < 0) || (idx>=get(this, 'length'))) return undefined ; - return get(this, idx); - }, + container.registerFactory('model:user', User); + container.register('store:main', SomeStore); - /** - This returns the objects at the specified indexes, using `objectAt`. + container.factoryTypeInjection('model', 'store', 'store:main'); - ```javascript - var arr = ['a', 'b', 'c', 'd']; - arr.objectsAt([0, 1, 2]); // ["a", "b", "c"] - arr.objectsAt([2, 3, 4]); // ["c", "d", undefined] - ``` + var store = container.lookup('store:main'); + var UserFactory = container.lookupFactory('model:user'); - @method objectsAt - @param {Array} indexes An array of indexes of items to return. - */ - objectsAt: function(indexes) { - var self = this; - return map(indexes, function(idx){ return self.objectAt(idx); }); - }, + UserFactory.store instanceof SomeStore; //=> true + ``` - // overrides Ember.Enumerable version - nextObject: function(idx) { - return this.objectAt(idx); - }, + @method factoryTypeInjection + @param {String} type + @param {String} property + @param {String} fullName + */ + factoryTypeInjection: function(type, property, fullName) { + if (this.parent) { illegalChildOperation('factoryTypeInjection'); } - /** - 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. + addTypeInjection(this.factoryTypeInjections, type, property, fullName); + }, - This property overrides the default property defined in `Ember.Enumerable`. + /* + Defines factory injection rules. - @property [] - */ - '[]': Ember.computed(function(key, value) { - if (value !== undefined) this.replace(0, get(this, 'length'), value) ; - return this ; - }), + Similar to regular injection rules, but are run against factories, via + `Container#lookupFactory`. - firstObject: Ember.computed(function() { - return this.objectAt(0); - }), + These rules are used to inject objects onto factories when they + are looked up. - lastObject: Ember.computed(function() { - return this.objectAt(get(this, 'length')-1); - }), + Two forms of injections are possible: - // optimized version from Enumerable - contains: function(obj){ - return this.indexOf(obj) >= 0; - }, + * Injecting one fullName on another fullName + * Injecting one fullName on a type - // 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. + Example: - ```javascript - var arr = ['red', 'green', 'blue']; - arr.slice(0); // ['red', 'green', 'blue'] - arr.slice(0, 2); // ['red', 'green'] - arr.slice(1, 100); // ['green', 'blue'] - ``` + ```javascript + var container = new Container(); - @method slice - @param beginIndex {Integer} (Optional) index to begin slicing from. - @param endIndex {Integer} (Optional) index to end the slice at. - @return {Array} New array with specified slice - */ - slice: function(beginIndex, endIndex) { - var ret = []; - var length = get(this, 'length') ; - if (none(beginIndex)) beginIndex = 0 ; - if (none(endIndex) || (endIndex > length)) endIndex = length ; - while(beginIndex < endIndex) { - ret[ret.length] = this.objectAt(beginIndex++) ; - } - return ret ; - }, + container.register('store:main', Store); + container.register('store:secondary', OtherStore); + container.register('model:user', User); + container.register('model:post', Post); - /** - 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. + // injecting one fullName on another type + container.factoryInjection('model', 'store', 'store:main'); - ```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 - ``` + // injecting one fullName on another fullName + container.factoryInjection('model:post', 'secondaryStore', 'store:secondary'); - @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 - */ - indexOf: function(object, startAt) { - var idx, len = get(this, 'length'); + var UserFactory = container.lookupFactory('model:user'); + var PostFactory = container.lookupFactory('model:post'); + var store = container.lookup('store:main'); - if (startAt === undefined) startAt = 0; - if (startAt < 0) startAt += len; + UserFactory.store instanceof Store; //=> true + UserFactory.secondaryStore instanceof OtherStore; //=> false - for(idx=startAt;idx true + PostFactory.secondaryStore instanceof OtherStore; //=> true - /** - Returns the index of the given object's last occurrence. - If no `startAt` argument is given, the search starts from - the last position. If it's negative, will count backward - from the end of the array. Returns -1 if no match is found. + // and both models share the same source instance + UserFactory.store === PostFactory.store; //=> true + ``` - ```javascript - var arr = ["a", "b", "c", "d", "a"]; - arr.lastIndexOf("a"); // 4 - arr.lastIndexOf("z"); // -1 - arr.lastIndexOf("a", 2); // 0 - arr.lastIndexOf("a", -1); // 4 - arr.lastIndexOf("b", 3); // 1 - arr.lastIndexOf("a", 100); // 4 - ``` + @method factoryInjection + @param {String} factoryName + @param {String} property + @param {String} injectionName + */ + factoryInjection: function(factoryName, property, injectionName) { + if (this.parent) { illegalChildOperation('injection'); } - @method lastIndexOf - @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 - */ - lastIndexOf: function(object, startAt) { - var idx, len = get(this, 'length'); + if (factoryName.indexOf(':') === -1) { + return this.factoryTypeInjection(factoryName, property, injectionName); + } - if (startAt === undefined || startAt >= len) startAt = len-1; - if (startAt < 0) startAt += len; + addInjection(this.factoryInjections, factoryName, property, injectionName); + }, - for(idx=startAt;idx>=0;idx--) { - if (this.objectAt(idx) === object) return idx ; - } - return -1; - }, + /** + A depth first traversal, destroying the container, its descendant containers and all + their managed objects. - // .......................................................... - // ARRAY OBSERVERS - // + @method destroy + */ + destroy: function() { + this.isDestroyed = true; - /** - Adds an array observer to the receiving array. The array observer object - normally must implement two methods: + for (var i=0, l=this.children.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 b` + if (!injections) { + injections = []; + rules.set(type, injections); + } - Default implementation raises an exception. + injections.push({ + property: property, + fullName: fullName + }); + } - @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) + function addInjection(rules, factoryName, property, injectionName) { + var injections = rules[factoryName] = rules[factoryName] || []; + injections.push({ property: property, fullName: injectionName }); + } + return Container; }); - })(); - - (function() { +/*globals ENV */ /** @module ember @submodule ember-runtime */ - - -var get = Ember.get, set = Ember.set; +var indexOf = Ember.EnumerableUtils.indexOf; /** - 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. - - You should generally implement the `copy()` method to return a copy of the - receiver. - - Note that `frozenCopy()` will only work if you also implement - `Ember.Freezable`. + This will compare two javascript values of possibly different types. + It will tell you which one is greater than the other by returning: - @class Copyable - @namespace Ember - @extends Ember.Mixin - @since Ember 0.9 -*/ -Ember.Copyable = Ember.Mixin.create( -/** @scope Ember.Copyable.prototype */ { + - -1 if the first is smaller than the second, + - 0 if both are equal, + - 1 if the first is greater than the second. - /** - Override to return a copy of the receiver. Default implementation raises - an exception. + 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 copy - @param deep {Boolean} if `true`, a deep copy of the object should be made - @return {Object} copy of receiver - */ - copy: Ember.required(Function), + ```javascript + Ember.compare('hello', 'hello'); // 0 + Ember.compare('abc', 'dfg'); // -1 + Ember.compare(2, 1); // 1 + ``` - /** - 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. + @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; } - Raises an exception if you try to call this method on a object that does - not support freezing. + var type1 = Ember.typeOf(v); + var type2 = Ember.typeOf(w); - 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. + var Comparable = Ember.Comparable; + if (Comparable) { + if (type1==='instance' && Comparable.detect(v.constructor)) { + return v.constructor.compare(v, w); + } - @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 Error(Ember.String.fmt("%@ does not support freezing", [this])); + if (type2 === 'instance' && Comparable.detect(w.constructor)) { + return 1-w.constructor.compare(w, v); } } -}); - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ - -var get = Ember.get, set = Ember.set; + // 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; + } -/** - 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. + // We no longer need Ember.ORDER_DEFINITION. + delete Ember.ORDER_DEFINITION; + } - ## Enforcement + var type1Index = mapping[type1]; + var type2Index = mapping[type2]; - 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 (type1Index < type2Index) { return -1; } + if (type1Index > type2Index) { return 1; } - 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. + // 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; - ## Example Usage + case 'string': + var comp = v.localeCompare(w); + if (comp < 0) { return -1; } + if (comp > 0) { return 1; } + return 0; - The example below shows a simple object that implement the `Ember.Freezable` - protocol. + 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; } - ```javascript - Contact = Ember.Object.extend(Ember.Freezable, { - firstName: null, - lastName: 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; - // 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; - } + case 'instance': + if (Ember.Comparable && Ember.Comparable.detect(v)) { + return v.compare(v, w); + } + return 0; - }); + case 'date': + var vNum = v.getTime(); + var wNum = w.getTime(); + if (vNum < wNum) { return -1; } + if (vNum > wNum) { return 1; } + return 0; - c = Context.create({ firstName: "John", lastName: "Doe" }); - c.swapNames(); // returns c - c.freeze(); - c.swapNames(); // EXCEPTION - ``` + default: + return 0; + } +}; - ## Copying +function _copy(obj, deep, seen, copies) { + var ret, loc, key; - 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. + // primitive data types are immutable, just return them. + if ('object' !== typeof obj || obj===null) return obj; - @class Freezable - @namespace Ember - @extends Ember.Mixin - @since Ember 0.9 -*/ -Ember.Freezable = Ember.Mixin.create( -/** @scope Ember.Freezable.prototype */ { + // avoid cyclical loops + if (deep && (loc=indexOf(seen, obj))>=0) return copies[loc]; - /** - Set to `true` when the object is frozen. Use this property to detect - whether your object is frozen or not. + Ember.assert('Cannot clone an Ember.Object that does not implement Ember.Copyable', !(obj instanceof Ember.Object) || (Ember.Copyable && Ember.Copyable.detect(obj))); - @property isFrozen - @type Boolean - */ - isFrozen: false, + // 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; - /** - Freezes the object. Once this method has been called the object should - no longer allow any properties to be edited. + // Prevents browsers that don't respect non-enumerability from + // copying internal Ember properties + if (key.substring(0,2) === '__') continue; - @method freeze - @return {Object} receiver - */ - freeze: function() { - if (get(this, 'isFrozen')) return this; - set(this, 'isFrozen', true); - return this; + ret[key] = deep ? _copy(obj[key], deep, seen, copies) : obj[key]; + } } -}); + if (deep) { + seen.push(obj); + copies.push(ret); + } -Ember.FROZEN_ERROR = "Frozen object cannot be modified."; + return ret; +} -})(); +/** + 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); +}; -(function() { /** -@module ember -@submodule ember-runtime -*/ - -var forEach = Ember.EnumerableUtils.forEach; - -/** - This mixin defines the API for modifying generic enumerables. These methods - can be applied to an object regardless of whether it is ordered or - unordered. - - Note that an Enumerable can change even if it does not implement this mixin. - For example, a MappedEnumerable cannot be directly modified but if its - underlying enumerable changes, it will change also. - - ## Adding Objects + Convenience method to inspect an object. This method will attempt to + convert the object into a useful string description. - To add an object to an enumerable, use the `addObject()` method. This - method will only add the object to the enumerable if the object is not - already present and the object if of a type supported by the enumerable. + It is a pretty simple implementation. If you want something more robust, + use something like JSDump: https://github.com/NV/jsDump - ```javascript - set.addObject(contact); - ``` + @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 + ''; + } - ## Removing Objects + 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(", ") + "}"; +}; - To remove an object form an enumerable, use the `removeObject()` method. This - will only remove the object if it is already in the enumerable, otherwise - this method has no effect. +/** + 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. ```javascript - set.removeObject(contact); + Ember.isEqual('hello', 'hello'); // true + Ember.isEqual(1, 2); // false + Ember.isEqual([4,2], [4,2]); // false ``` - ## Implementing In Your Own Code - - If you are implementing an object and want to support this API, just include - this mixin in your class and implement the required methods. In your unit - tests, be sure to apply the Ember.MutableEnumerableTests to your object. - - @class MutableEnumerable - @namespace Ember - @extends Ember.Mixin - @uses Ember.Enumerable + @method isEqual + @for Ember + @param {Object} a first object to compare + @param {Object} b second object to compare + @return {Boolean} */ -Ember.MutableEnumerable = Ember.Mixin.create(Ember.Enumerable, - /** @scope Ember.MutableEnumerable.prototype */ { - - /** - __Required.__ You must implement this method to apply this mixin. - - Attempts to add the passed object to the receiver if the object is not - already present in the collection. If the object is present, this method - has no effect. - - If the passed object is of a type not supported by the receiver - then this method should raise an exception. +Ember.isEqual = function(a, b) { + if (a && 'function'===typeof a.isEqual) return a.isEqual(b); + return a === b; +}; - @method addObject - @param {Object} object The object to add to the enumerable. - @return {Object} the passed object - */ - addObject: Ember.required(Function), +// Used by Ember.compare +Ember.ORDER_DEFINITION = Ember.ENV.ORDER_DEFINITION || [ + 'undefined', + 'null', + 'boolean', + 'number', + 'string', + 'array', + 'object', + 'instance', + 'function', + 'class', + 'date' +]; - /** - Adds each object in the passed enumerable to the receiver. +/** + 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. - @method addObjects - @param {Ember.Enumerable} objects the objects to add. - @return {Object} receiver - */ - addObjects: function(objects) { - Ember.beginPropertyChanges(this); - forEach(objects, function(obj) { this.addObject(obj); }, this); - Ember.endPropertyChanges(this); - return this; - }, + @method keys + @for Ember + @param {Object} obj + @return {Array} Array containing keys of obj +*/ +Ember.keys = Object.keys; - /** - __Required.__ You must implement this method to apply this mixin. +if (!Ember.keys || Ember.create.isSimulated) { + Ember.keys = function(obj) { + var ret = []; + for(var key in obj) { + // Prevents browsers that don't respect non-enumerability from + // copying internal Ember properties + if (key.substring(0,2) === '__') continue; + if (key === '_super') continue; - Attempts to remove the passed object from the receiver collection if the - object is in present in the collection. If the object is not present, - this method has no effect. + if (obj.hasOwnProperty(key)) { ret.push(key); } + } + return ret; + }; +} - If the passed object is of a type not supported by the receiver - then this method should raise an exception. +// .......................................................... +// ERROR +// - @method removeObject - @param {Object} object The object to remove from the enumerable. - @return {Object} the passed object - */ - removeObject: Ember.required(Function), +var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; +/** + A subclass of the JavaScript Error object for use in Ember. - /** - Removes each objects in the passed enumerable from the receiver. + @class Error + @namespace Ember + @extends Error + @constructor +*/ +Ember.Error = function() { + var tmp = Error.apply(this, arguments); - @method removeObjects - @param {Ember.Enumerable} objects the objects to remove - @return {Object} receiver - */ - removeObjects: function(objects) { - Ember.beginPropertyChanges(this); - forEach(objects, function(obj) { this.removeObject(obj); }, this); - Ember.endPropertyChanges(this); - return this; + // 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]]; } +}; -}); +Ember.Error.prototype = Ember.create(Error.prototype); })(); @@ -8860,878 +9475,968 @@ Ember.MutableEnumerable = Ember.Mixin.create(Ember.Enumerable, @module ember @submodule ember-runtime */ -// .......................................................... -// CONSTANTS -// - -var OUT_OF_RANGE_EXCEPTION = "Index out of range" ; -var EMPTY = []; // .......................................................... // HELPERS // -var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; +var get = Ember.get, set = Ember.set; +var a_slice = Array.prototype.slice; +var a_indexOf = Ember.EnumerableUtils.indexOf; -/** - This mixin defines the API for modifying array-like objects. These methods - can be applied only to a collection that keeps its items in an ordered set. +var contexts = []; - Note that an Array can change even if it does not implement this mixin. - For example, one might implement a SparseArray that cannot be directly - modified, but if its underlying enumerable changes, it will change also. +function popCtx() { + return contexts.length===0 ? {} : contexts.pop(); +} - @class MutableArray - @namespace Ember - @extends Ember.Mixin - @uses Ember.Array - @uses Ember.MutableEnumerable -*/ -Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, - /** @scope Ember.MutableArray.prototype */ { +function pushCtx(ctx) { + contexts.push(ctx); + return null; +} - /** - __Required.__ You must implement this method to apply this mixin. +function iter(key, value) { + var valueProvided = arguments.length === 2; - This is one of the primitives you must implement to support `Ember.Array`. - You should replace amt objects started at idx with the objects in the - passed array. You should also call `this.enumerableContentDidChange()` + function i(item) { + var cur = get(item, key); + return valueProvided ? value===cur : !!cur; + } + return i ; +} - @method replace - @param {Number} idx Starting index in the array to replace. If - 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* - */ - replace: Ember.required(), +/** + 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). - /** - Remove all elements from self. This is useful if you - want to reuse an existing array without having to recreate it. + 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 colors = ["red", "green", "blue"]; - color.length(); // 3 - colors.clear(); // [] - colors.length(); // 0 - ``` + ## Writing Your Own Enumerable - @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; - }, + To make your own custom class enumerable, you need two items: - /** - This will use the primitive `replace()` method to insert an object at the - specified index. + 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().` - ```javascript - var colors = ["red", "green", "blue"]; - colors.insertAt(2, "yellow"); // ["red", "green", "yellow", "blue"] - colors.insertAt(5, "orange"); // Error: Index out of range - ``` + 2. If you must implement `nextObject().` See documentation. - @method insertAt - @param {Number} idx index of insert the object at. - @param {Object} object object to insert - */ - insertAt: function(idx, object) { - if (idx > get(this, 'length')) throw new Error(OUT_OF_RANGE_EXCEPTION) ; - this.replace(idx, 0, [object]) ; - return this ; - }, + 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. - /** - 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. + ## Using Ember Enumeration with Other Libraries - If you pass a start and length that is beyond the - length this method will throw an `Ember.OUT_OF_RANGE_EXCEPTION` + 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. - ```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 - ``` + @class Enumerable + @namespace Ember + @since Ember 0.9 +*/ +Ember.Enumerable = Ember.Mixin.create({ - @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) { + /** + Implement this method to make your class enumerable. - if ((start < 0) || (start >= get(this, 'length'))) { - throw new Error(OUT_OF_RANGE_EXCEPTION); - } + 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. - // fast case - if (len === undefined) len = 1; - this.replace(start, len, EMPTY); - } + 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. - return this ; - }, + 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. - /** - Push the object onto the end of the array. Works just like `push()` but it - is KVO-compliant. + 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. - ```javascript - var colors = ["red", "green", "blue"]; - colors.pushObject("black"); // ["red", "green", "blue", "black"] - colors.pushObject(["yellow", "orange"]); // ["red", "green", "blue", "black", ["yellow", "orange"]] - ``` + The default implementation of this method simply looks up the index. + This works great on any Array-like objects. - @method pushObject - @param {anything} obj object to push + @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 */ - pushObject: function(obj) { - this.insertAt(get(this, 'length'), obj) ; - return obj ; - }, + nextObject: Ember.required(Function), /** - Add the objects in the passed numerable to the end of the array. Defers - notifying observers of the change until all objects are added. - - ```javascript - var colors = ["red", "green", "blue"]; - colors.pushObjects("black"); // ["red", "green", "blue", "black"] - colors.pushObjects(["yellow", "orange"]); // ["red", "green", "blue", "black", "yellow", "orange"] - ``` - - @method pushObjects - @param {Ember.Enumerable} objects the objects to add - @return {Ember.Array} receiver - */ - pushObjects: function(objects) { - this.replace(get(this, 'length'), 0, objects); - return this; - }, + 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. - /** - Pop object from array or nil if none are left. Works just like `pop()` but - it is KVO-compliant. + 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`. ```javascript - var colors = ["red", "green", "blue"]; - colors.popObject(); // "blue" - console.log(colors); // ["red", "green"] + var arr = ["a", "b", "c"]; + arr.get('firstObject'); // "a" + + var arr = []; + arr.get('firstObject'); // undefined ``` - @method popObject - @return object + @property firstObject + @return {Object} the object or undefined */ - popObject: function() { - var len = get(this, 'length') ; - if (len === 0) return null ; + firstObject: Ember.computed(function() { + if (get(this, 'length')===0) return undefined ; - var ret = this.objectAt(len-1) ; - this.removeAt(len-1, 1) ; + // handle generic enumerables + var context = popCtx(), ret; + ret = this.nextObject(0, null, context); + pushCtx(context); return ret ; - }, + }).property('[]'), /** - Shift an object from start of array or nil if none are left. Works just - like `shift()` but it is KVO-compliant. + 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 colors = ["red", "green", "blue"]; - colors.shiftObject(); // "red" - console.log(colors); // ["green", "blue"] + var arr = ["a", "b", "c"]; + arr.get('lastObject'); // "c" + + var arr = []; + arr.get('lastObject'); // undefined ``` - @method shiftObject - @return object + @property lastObject + @return {Object} the last object or undefined */ - shiftObject: function() { - if (get(this, 'length') === 0) return null ; - var ret = this.objectAt(0) ; - this.removeAt(0) ; - return ret ; - }, + 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('[]'), /** - Unshift an object to start of array. Works just like `unshift()` but it is - KVO-compliant. + 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. ```javascript - var colors = ["red", "green", "blue"]; - colors.unshiftObject("yellow"); // ["yellow", "red", "green", "blue"] - colors.unshiftObject(["black", "white"]); // [["black", "white"], "yellow", "red", "green", "blue"] + var arr = ["a", "b", "c"]; + arr.contains("a"); // true + arr.contains("z"); // false ``` - @method unshiftObject - @param {anything} obj object to unshift + @method contains + @param {Object} obj The object to search for. + @return {Boolean} `true` if object is found in enumerable. */ - unshiftObject: function(obj) { - this.insertAt(0, obj) ; - return obj ; + contains: function(obj) { + return this.find(function(item) { return item===obj; }) !== undefined; }, /** - Adds the named objects to the beginning of the array. Defers notifying - observers until all objects have been added. + Iterates through the enumerable, calling the passed function on each + item. This method corresponds to the `forEach()` method defined in + JavaScript 1.6. + + The callback method you provide should have the following signature (all + parameters are optional): ```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 + function(item, index, enumerable); ``` - @method unshiftObjects - @param {Ember.Enumerable} objects the objects to add - @return {Ember.Array} receiver + - `item` is the current item in the iteration. + - `index` is the current index in the iteration. + - `enumerable` is the enumerable object itself. + + 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 forEach + @param {Function} callback The callback to execute + @param {Object} [target] The target object to use + @return {Object} receiver */ - unshiftObjects: function(objects) { - this.replace(0, 0, objects); - return this; + forEach: function(callback, target) { + if (typeof callback !== "function") throw new TypeError() ; + var len = get(this, 'length'), last = null, context = popCtx(); + + if (target === undefined) target = null; + + for(var idx=0;idx= 0) { - var curObject = this.objectAt(loc) ; - if (curObject === obj) this.removeAt(loc) ; - } - return this ; + @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 ; }, - addObject: function(obj) { - if (!this.contains(obj)) this.pushObject(obj); - return this ; - } + /** + 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); + }); + }, + /** + Similar to map, this specialized function returns the value of the named + property on all items in the enumeration. -})(); + @method mapProperty + @param {String} key name of the property + @return {Array} The mapped array. + @deprecated Use `mapBy` instead + */ + mapProperty: Ember.aliasMethod('mapBy'), + /** + 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. -(function() { -/** -@module ember -@submodule ember-runtime -*/ + The callback method you provide should have the following signature (all + parameters are optional): -var get = Ember.get, set = Ember.set, defineProperty = Ember.defineProperty; + ```javascript + function(item, index, enumerable); + ``` -/** - ## Overview + - `item` is the current item in the iteration. + - `index` is the current index in the iteration. + - `enumerable` is the enumerable object itself. - This mixin provides properties and property observing functionality, core - features of the Ember object model. + It should return the `true` to include the item in the results, `false` + otherwise. - 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. + 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. - 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. + @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 ; + }, - 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. + /** + 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(). - ## Using `get()` and `set()` + The callback method you provide should have the following signature (all + parameters are optional): - 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(item, index, enumerable); - More documentation about `get` and `set` are below. + - *item* is the current item in the iteration. + - *index* is the current index in the iteration + - *enumerable* is the enumerable object itself. - ## Observing Property Changes + It should return the a falsey value to include the item in the results. - 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: + 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 - Ember.Object.create({ - valueObserver: function() { - // Executes whenever the "value" property changes - }.observes('value') - }); - ``` + @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)); + }); + }, - 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. + /** + 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`. - To add an observer for a property, call: + @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)); + }, - ```javascript - object.addObserver('propertyKey', targetObject, targetAction) - ``` + /** + 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`. - This will call the `targetAction` method on the `targetObject` to be called - whenever the value of the `propertyKey` changes. + @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'), - 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. + /** + 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. - @class Observable - @namespace Ember - @extends Ember.Mixin -*/ -Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { + @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); - /** - Retrieves the value of a property from the object. + return this.reject(use); + }, - This method is usually similar to using `object[keyName]` or `object.keyName`, - however it supports both computed properties and the unknownProperty - handler. + /** + 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. - 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. + @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'), - ### Computed Properties + /** + 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. - Computed properties are methods defined with the `property` modifier - declared at the end, such as: + The callback method you provide should have the following signature (all + parameters are optional): ```javascript - fullName: function() { - return this.getEach('firstName', 'lastName').compact().join(' '); - }.property('firstName', 'lastName') + function(item, index, enumerable); ``` - 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. + - `item` is the current item in the iteration. + - `index` is the current index in the iteration. + - `enumerable` is the enumerable object itself. - ### Unknown Properties + It should return the `true` to include the item in the results, `false` + otherwise. - 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. + 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 get - @param {String} key The property to retrieve - @return {Object} The property value or undefined. + @method find + @param {Function} callback The callback to execute + @param {Object} [target] The target object to use + @return {Object} Found item or `undefined`. */ - get: function(keyName) { - return get(this, keyName); + find: function(callback, target) { + var len = get(this, 'length') ; + if (target === undefined) target = null; + + var last = null, next, found = false, ret ; + var context = popCtx(); + for(var idx=0;idx1) args = a_slice.call(arguments, 1); - /** - @deprecated - @method getPath - @param {String} path The property path to retrieve - @return {Object} The property value or undefined. - */ - getPath: function(path) { - Ember.deprecate("getPath is deprecated since get now supports paths"); - return this.get(path); + 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); + + return ret; }, /** - @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} + 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. */ - setPath: function(path, value) { - Ember.deprecate("setPath is deprecated since set now supports paths"); - return this.set(path, value); + toArray: function() { + var ret = Ember.A(); + this.forEach(function(o, idx) { ret[idx] = o; }); + return ret ; }, /** - Retrieves the value of a property, or a default value in the case that the - property returns `undefined`. + Returns a copy of the array with all null and undefined elements removed. ```javascript - person.getWithDefault('lastName', 'Doe'); + var arr = ["a", null, "c", undefined]; + arr.compact(); // ["a", "c"] ``` - @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. + @method compact + @return {Array} the array without null and undefined elements. */ - getWithDefault: function(keyName, defaultValue) { - return Ember.getWithDefault(this, keyName, defaultValue); + compact: function() { + return this.filter(function(value) { return value != null; }); }, /** - Set the value of a property to the current value plus some amount. + 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 - person.incrementProperty('age'); - team.incrementProperty('score', 2); + var arr = ["a", "b", "a", "c"]; + arr.without("a"); // ["b", "c"] ``` - @method incrementProperty - @param {String} keyName The name of the property to increment - @param {Object} increment The amount to increment by. Defaults to 1 - @return {Object} The new property value + @method without + @param {Object} value + @return {Ember.Enumerable} */ - incrementProperty: function(keyName, increment) { - if (!increment) { increment = 1; } - set(this, keyName, (get(this, keyName) || 0)+increment); - return get(this, keyName); + 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 ; }, /** - Set the value of a property to the current value minus some amount. + Returns a new enumerable that contains only unique values. The default + implementation returns an array regardless of the receiver type. ```javascript - player.decrementProperty('lives'); - orc.decrementProperty('health', 5); + var arr = ["a", "a", "b", "b"]; + arr.uniq(); // ["a", "b"] ``` - @method decrementProperty - @param {String} keyName The name of the property to decrement - @param {Object} increment The amount to decrement by. Defaults to 1 - @return {Object} The new property value + @method uniq + @return {Ember.Enumerable} */ - decrementProperty: function(keyName, increment) { - if (!increment) { increment = 1; } - set(this, keyName, (get(this, keyName) || 0)-increment); - return get(this, keyName); + uniq: function() { + var ret = Ember.A(); + this.forEach(function(k) { + if (a_indexOf(ret, k)<0) ret.push(k); + }); + return ret; }, /** - Set the value of a boolean property to the opposite of it's - current value. + 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 - starship.toggleProperty('warpDriveEnaged'); - ``` + For plain enumerables, this property is read only. `Ember.Array` overrides + this method. - @method toggleProperty - @param {String} keyName The name of the property to toggle - @return {Object} The new property value + @property [] + @type Ember.Array + @return this */ - toggleProperty: function(keyName) { - set(this, keyName, !get(this, keyName)); - return get(this, keyName); - }, + '[]': Ember.computed(function(key, value) { + return this; + }), + + // .......................................................... + // ENUMERABLE OBSERVERS + // /** - 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. + Registers an enumerable observer. Must implement `Ember.EnumerableObserver` + mixin. - @method cacheFor - @param {String} keyName - @return {Object} The cached value of the computed property, if any + @method addEnumerableObserver + @param {Object} target + @param {Hash} [opts] + @return this */ - cacheFor: function(keyName) { - return Ember.cacheFor(this, keyName); - }, + addEnumerableObserver: function(target, opts) { + var willChange = (opts && opts.willChange) || 'enumerableWillChange', + didChange = (opts && opts.didChange) || 'enumerableDidChange'; - // intended for debugging purposes - observersForKey: function(keyName) { - return Ember.observersFor(this, keyName); - } -}); + 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; + }, + /** + Removes a registered enumerable observer. -})(); + @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'; + 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; + }, + /** + Becomes true whenever the array currently has observers watching changes + on the array. -(function() { -/** -@module ember -@submodule ember-runtime -*/ + @property hasEnumerableObservers + @type Boolean + */ + hasEnumerableObservers: Ember.computed(function() { + return Ember.hasListeners(this, '@enumerable:change') || Ember.hasListeners(this, '@enumerable:before'); + }), -var get = Ember.get, set = Ember.set; -/** -@class TargetActionSupport -@namespace Ember -@extends Ember.Mixin -*/ -Ember.TargetActionSupport = Ember.Mixin.create({ - target: null, - action: null, + /** + 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. - targetObject: Ember.computed(function() { - var target = get(this, 'target'); + @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) { - 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'), + var removeCnt, addCnt, hasDelta; - triggerAction: function() { - var action = get(this, 'action'), - target = get(this, 'targetObject'); + if ('number' === typeof removing) removeCnt = removing; + else if (removing) removeCnt = get(removing, 'length'); + else removeCnt = removing = -1; - if (target && action) { - var ret; + if ('number' === typeof adding) addCnt = adding; + else if (adding) addCnt = get(adding,'length'); + else addCnt = adding = -1; - if (typeof target.send === 'function') { - ret = target.send(action, this); - } else { - if (typeof action === 'string') { - action = target[action]; - } - ret = action.call(target, this); - } - if (ret !== false) ret = true; + hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; - return ret; - } else { - return false; - } + 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]); + + return this; + }, + + /** + 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. + + @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; + + 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.sendEvent(this, '@enumerable:change', [this, removing, adding]); + if (hasDelta) Ember.propertyDidChange(this, 'length'); + Ember.propertyDidChange(this, '[]'); + + return this ; } -}); + +}) ; })(); @@ -9743,1803 +10448,2257 @@ Ember.TargetActionSupport = Ember.Mixin.create({ @submodule ember-runtime */ +// .......................................................... +// HELPERS +// + +var get = Ember.get, set = Ember.set, isNone = Ember.isNone, map = Ember.EnumerableUtils.map, cacheFor = Ember.cacheFor; + +// .......................................................... +// ARRAY +// /** - This mixin allows for Ember objects to subscribe to and emit events. + 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. - ```javascript - App.Person = Ember.Object.extend(Ember.Evented, { - greet: function() { - // ... - this.trigger('greet'); - } - }); + 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. - var person = App.Person.create(); + 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. - person.on('greet', function() { - console.log('Our person has greeted'); - }); + 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.[]')`. - person.greet(); + To support `Ember.Array` in your own class, you must override two + primitives to use it: `replace()` and `objectAt()`. - // outputs: 'Our person has greeted' - ``` + Note that the Ember.Array mixin also incorporates the `Ember.Enumerable` + mixin. All `Ember.Array`-like objects are also enumerable. - @class Evented + @class Array @namespace Ember - @extends Ember.Mixin - */ -Ember.Evented = Ember.Mixin.create({ + @uses Ember.Enumerable + @since Ember 0.9.0 +*/ +Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.prototype */ { /** - Subscribes to a named event with given function. - - ```javascript - person.on('didLoad', function() { - // fired once the person has loaded - }); - ``` - - 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. + Your array must support the `length` property. Your replace methods should + set this property whenever it changes. - @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 + @property {Number} length */ - on: function(name, target, method) { - Ember.addListener(this, name, target, method); - }, + length: Ember.required(), /** - 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. + Returns the object at the given `index`. If the given `index` is negative + or is greater or equal than the array length, returns `undefined`. - 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 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. - @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 - */ - one: function(name, target, method) { - if (!method) { - method = target; - target = null; - } + ```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 + ``` - Ember.addListener(this, name, target, method, true); + @method objectAt + @param {Number} idx The index of the item to return. + @return {*} item at index or undefined + */ + objectAt: function(idx) { + if ((idx < 0) || (idx>=get(this, 'length'))) return undefined ; + return get(this, idx); }, /** - Triggers a named event for the object. Any additional arguments - will be passed as parameters to the functions that are subscribed to the - event. + This returns the objects at the specified indexes, using `objectAt`. ```javascript - person.on('didEat', function(food) { - console.log('person ate some ' + food); - }); - - person.trigger('didEat', 'broccoli'); - - // outputs: person ate some broccoli + var arr = ['a', 'b', 'c', 'd']; + arr.objectsAt([0, 1, 2]); // ["a", "b", "c"] + arr.objectsAt([2, 3, 4]); // ["c", "d", undefined] ``` - @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); - }, - fire: function(name) { - Ember.deprecate("Ember.Evented#fire() has been deprecated in favor of trigger() for compatibility with jQuery. It will be removed in 1.0. Please update your code to call trigger() instead."); - this.trigger.apply(this, 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); }); }, - /** - Cancels subscription for give name, target, and method. - - @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 - */ - off: function(name, target, method) { - Ember.removeListener(this, name, target, method); + // overrides Ember.Enumerable version + nextObject: function(idx) { + return this.objectAt(idx); }, /** - Checks to see if object has any subscriptions for named event. + 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. - @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 property overrides the default property defined in `Ember.Enumerable`. -})(); + @property [] + @return this + */ + '[]': Ember.computed(function(key, value) { + if (value !== undefined) this.replace(0, get(this, 'length'), value) ; + return this ; + }), + firstObject: Ember.computed(function() { + return this.objectAt(0); + }), + lastObject: Ember.computed(function() { + return this.objectAt(get(this, 'length')-1); + }), -(function() { -var RSVP = requireModule("rsvp"); + // optimized version from Enumerable + contains: function(obj) { + return this.indexOf(obj) >= 0; + }, -RSVP.async = function(callback, binding) { - Ember.run.schedule('actions', binding, callback); -}; + // 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. -/** -@module ember -@submodule ember-runtime -*/ + ```javascript + var arr = ['red', 'green', 'blue']; + arr.slice(0); // ['red', 'green', 'blue'] + arr.slice(0, 2); // ['red', 'green'] + arr.slice(1, 100); // ['green', 'blue'] + ``` -var get = Ember.get, - slice = Array.prototype.slice; + @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 + */ + 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 ; -/** - @class Deferred - @namespace Ember - @extends Ember.Mixin - */ -Ember.DeferredMixin = Ember.Mixin.create({ - /** - Add handlers to be called when the Deferred object is resolved or rejected. + if (beginIndex < 0) beginIndex = length + beginIndex; + if (endIndex < 0) endIndex = length + endIndex; - @method then - @param {Function} doneCallback a callback function to be called when done - @param {Function} failCallback a callback function to be called when failed - */ - then: function(doneCallback, failCallback) { - var promise = get(this, 'promise'); - return promise.then.apply(promise, arguments); + while(beginIndex < endIndex) { + ret[ret.length] = this.objectAt(beginIndex++) ; + } + return ret ; }, /** - Resolve a Deferred object and call any `doneCallbacks` with the given args. + 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. - @method resolve + ```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 + ``` + + @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 */ - resolve: function(value) { - get(this, 'promise').resolve(value); + 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; }, - promise: Ember.computed(function() { - return new RSVP.Promise(); - }) -}); + // .......................................................... + // ARRAY OBSERVERS + // + /** + Adds an array observer to the receiving array. The array observer object + normally must implement two methods: -})(); + * `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. + 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. -(function() { + @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 + */ + 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; + }, + /** + 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 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 + */ + removeArrayObserver: function(target, opts) { + var willChange = (opts && opts.willChange) || 'arrayWillChange', + didChange = (opts && opts.didChange) || 'arrayDidChange'; -(function() { -Ember.Container = requireModule('container'); -Ember.Container.set = Ember.set; + 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; + }, -})(); + /** + Becomes true whenever the array currently has observers watching changes + on the array. + @property Boolean + */ + hasArrayObservers: Ember.computed(function() { + return Ember.hasListeners(this, '@array:change') || Ember.hasListeners(this, '@array:before'); + }), + /** + 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. -(function() { -/** -@module ember -@submodule ember-runtime -*/ + @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 + */ + arrayContentWillChange: function(startIdx, removeAmt, addAmt) { + // 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; + } -// NOTE: this object should never be included directly. Instead use Ember. -// Ember.Object. We only define this separately so that Ember.Set can depend on it + // Make sure the @each proxy is set up if anyone is observing @each + if (Ember.isWatching(this, '@each')) { get(this, '@each'); } + Ember.sendEvent(this, '@array:before', [this, startIdx, removeAmt, addAmt]); -var set = Ember.set, get = Ember.get, - o_create = Ember.create, - o_defineProperty = Ember.platform.defineProperty, - a_slice = Array.prototype.slice, - GUID_KEY = Ember.GUID_KEY, - guidFor = Ember.guidFor, - generateGuid = Ember.generateGuid, - meta = Ember.meta, - rewatch = Ember.rewatch, - finishChains = Ember.finishChains, - 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; + 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= 0) { - var baseValue = this[keyName]; + @property @each + */ + '@each': Ember.computed(function() { + if (!this.__each) this.__each = new Ember.EachProxy(this); + return this.__each; + }) - if (baseValue) { - if ('function' === typeof baseValue.concat) { - value = baseValue.concat(value); - } else { - value = Ember.makeArray(baseValue).concat(value); - } - } else { - value = Ember.makeArray(value); - } - } +}) ; - 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); - delete m.proto; - finishChains(this); - this.init.apply(this, arguments); - }; +})(); - Class.toString = Mixin.prototype.toString; - Class.willReopen = function() { - if (wasApplied) { - Class.PrototypeMixin = Mixin.create(Class.PrototypeMixin); - } - wasApplied = false; - }; - Class._initMixins = function(args) { initMixins = args; }; - Class._initProperties = function(args) { initProperties = args; }; - Class.proto = function() { - var superclass = Class.superclass; - if (superclass) { superclass.proto(); } +(function() { +var get = Ember.get, + set = Ember.set, + guidFor = Ember.guidFor, + metaFor = Ember.meta, + addBeforeObserver = Ember.addBeforeObserver, + removeBeforeObserver = Ember.removeBeforeObserver, + addObserver = Ember.addObserver, + removeObserver = Ember.removeObserver, + ComputedProperty = Ember.ComputedProperty, + a_slice = [].slice, + o_create = Ember.create, + forEach = Ember.EnumerableUtils.forEach, + // Here we explicitly don't allow `@each.foo`; it would require some special + // testing, but there's no particular reason why it should be disallowed. + eachPropertyPattern = /^(.*)\.@each\.(.*)/, + doubleEachPropertyPattern = /(.*\.@each){2,}/; - if (!wasApplied) { - wasApplied = true; - Class.PrototypeMixin.applyPartial(Class.prototype); - rewatch(Class.prototype); - } +/* + Tracks changes to dependent arrays, as well as to properties of items in + dependent arrays. - return this.prototype; - }; + @class DependentArraysObserver +*/ +function DependentArraysObserver(callbacks, cp, instanceMeta, context, propertyName, sugarMeta) { + // user specified callbacks for `addedItem` and `removedItem` + this.callbacks = callbacks; - return Class; + // the computed property: remember these are shared across instances + this.cp = cp; + // the ReduceComputedPropertyInstanceMeta this DependentArraysObserver is + // associated with + this.instanceMeta = instanceMeta; + + // A map of array guids to dependentKeys, for the given context. We track + // this because we want to set up the computed property potentially before the + // dependent array even exists, but when the array observer fires, we lack + // enough context to know what to update: we can recover that context by + // getting the dependentKey. + this.dependentKeysByGuid = {}; + + // a map of dependent array guids -> Ember.TrackedArray instances. We use + // this to lazily recompute indexes for item property observers. + this.trackedArraysByGuid = {}; + + // This is used to coalesce item changes from property observers. + this.changedItems = {}; } -var CoreObject = makeCtor(); -CoreObject.toString = function() { return "Ember.CoreObject"; }; +function ItemPropertyObserverContext (dependentArray, index, trackedArray) { + Ember.assert("Internal error: trackedArray is null or undefined", trackedArray); -CoreObject.PrototypeMixin = Mixin.create({ - reopen: function() { - applyMixin(this, arguments, true); - return this; + this.dependentArray = dependentArray; + this.index = index; + this.item = dependentArray.objectAt(index); + this.trackedArray = trackedArray; + this.beforeObserver = null; + this.observer = null; +} + +DependentArraysObserver.prototype = { + setValue: function (newValue) { + this.instanceMeta.setValue(newValue); + }, + getValue: function () { + return this.instanceMeta.getValue(); }, - isInstance: true, + setupObservers: function (dependentArray, dependentKey) { + Ember.assert("dependent array must be an `Ember.Array`", Ember.Array.detect(dependentArray)); - init: function() {}, + this.dependentKeysByGuid[guidFor(dependentArray)] = dependentKey; - /** - Defines the properties that will be concatenated from the superclass - (instead of overridden). + dependentArray.addArrayObserver(this, { + willChange: 'dependentArrayWillChange', + didChange: 'dependentArrayDidChange' + }); - 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`. + if (this.cp._itemPropertyKeys[dependentKey]) { + this.setupPropertyObservers(dependentKey, this.cp._itemPropertyKeys[dependentKey]); + } + }, - Here is some sample code showing the difference between a concatenated - property and a normal one: + teardownObservers: function (dependentArray, dependentKey) { + var itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || []; - ```javascript - App.BarView = Ember.View.extend({ - someNonConcatenatedProperty: ['bar'], - classNames: ['bar'] - }); + delete this.dependentKeysByGuid[guidFor(dependentArray)]; - App.FooBarView = App.BarView.extend({ - someNonConcatenatedProperty: ['foo'], - classNames: ['foo'], + this.teardownPropertyObservers(dependentKey, itemPropertyKeys); + + dependentArray.removeArrayObserver(this, { + willChange: 'dependentArrayWillChange', + didChange: 'dependentArrayDidChange' }); + }, - var fooBarView = App.FooBarView.create(); - fooBarView.get('someNonConcatenatedProperty'); // ['foo'] - fooBarView.get('classNames'); // ['ember-view', 'bar', 'foo'] - ``` + setupPropertyObservers: function (dependentKey, itemPropertyKeys) { + var dependentArray = get(this.instanceMeta.context, dependentKey), + length = get(dependentArray, 'length'), + observerContexts = new Array(length); - This behavior extends to object creation as well. Continuing the - above example: + this.resetTransformations(dependentKey, observerContexts); - ```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 - var view = App.FooBarView.create({ - classNames: 'baz' - }) - view.get('classNames'); // ['ember-view', 'bar', 'foo', 'baz'] - ``` - - Using the `concatenatedProperties` property, we can tell to Ember that mix - the content of the properties. + forEach(dependentArray, function (item, index) { + var observerContext = this.createPropertyObserverContext(dependentArray, index, this.trackedArraysByGuid[dependentKey]); + observerContexts[index] = observerContext; - In `Ember.View` the `classNameBindings` and `attributeBindings` properties - are also concatenated, in addition to `classNames`. + forEach(itemPropertyKeys, function (propertyKey) { + addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); + addObserver(item, propertyKey, this, observerContext.observer); + }, this); + }, this); + }, - This feature is available for you to use throughout the Ember object model, - although typical app developers are likely to use it infrequently. + teardownPropertyObservers: function (dependentKey, itemPropertyKeys) { + var dependentArrayObserver = this, + trackedArray = this.trackedArraysByGuid[dependentKey], + beforeObserver, + observer, + item; - @property concatenatedProperties - @type Array - @default null - */ - concatenatedProperties: null, + if (!trackedArray) { return; } - /** - @property isDestroyed - @default false - */ - isDestroyed: false, + trackedArray.apply(function (observerContexts, offset, operation) { + if (operation === Ember.TrackedArray.DELETE) { return; } - /** - @property isDestroying - @default false - */ - isDestroying: false, + forEach(observerContexts, function (observerContext) { + beforeObserver = observerContext.beforeObserver; + observer = observerContext.observer; + item = observerContext.item; - /** - Destroys an object by setting the `isDestroyed` flag and removing its - metadata, which effectively destroys observers and bindings. + forEach(itemPropertyKeys, function (propertyKey) { + removeBeforeObserver(item, propertyKey, dependentArrayObserver, beforeObserver); + removeObserver(item, propertyKey, dependentArrayObserver, observer); + }); + }); + }); + }, - If you try to set a property on a destroyed object, an exception will be - raised. + createPropertyObserverContext: function (dependentArray, index, trackedArray) { + var observerContext = new ItemPropertyObserverContext(dependentArray, index, trackedArray); - Note that destruction is scheduled for the end of the run loop and does not - happen immediately. + this.createPropertyObserver(observerContext); - @method destroy - @return {Ember.Object} receiver - */ - destroy: function() { - if (this._didCallDestroy) { return; } + return observerContext; + }, - this.isDestroying = true; - this._didCallDestroy = true; + createPropertyObserver: function (observerContext) { + var dependentArrayObserver = this; - if (this.willDestroy) { this.willDestroy(); } + observerContext.beforeObserver = function (obj, keyName) { + dependentArrayObserver.updateIndexes(observerContext.trackedArray, observerContext.dependentArray); + return dependentArrayObserver.itemPropertyWillChange(obj, keyName, observerContext.dependentArray, observerContext.index); + }; + observerContext.observer = function (obj, keyName) { + return dependentArrayObserver.itemPropertyDidChange(obj, keyName, observerContext.dependentArray, observerContext.index); + }; + }, - schedule('destroy', this, this._scheduledDestroy); - return this; + resetTransformations: function (dependentKey, observerContexts) { + this.trackedArraysByGuid[dependentKey] = new Ember.TrackedArray(observerContexts); }, - /** - @private + addTransformation: function (dependentKey, index, newItems) { + var trackedArray = this.trackedArraysByGuid[dependentKey]; + if (trackedArray) { + trackedArray.addItems(index, newItems); + } + }, - Invoked by the run loop to actually destroy the object. This is - scheduled for execution by the `destroy` method. + removeTransformation: function (dependentKey, index, removedCount) { + var trackedArray = this.trackedArraysByGuid[dependentKey]; - @method _scheduledDestroy - */ - _scheduledDestroy: function() { - destroy(this); - set(this, 'isDestroyed', true); + if (trackedArray) { + return trackedArray.removeItems(index, removedCount); + } - if (this.didDestroy) { this.didDestroy(); } + return []; }, - bind: function(to, from) { - if (!(from instanceof Ember.Binding)) { from = Ember.Binding.from(from); } - from.to(to).connect(this); - return from; + 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; + } + + forEach(observerContexts, function (context, index) { + context.index = index + offset; + }); + }); }, - /** - Returns a string representation which attempts to provide more information - than Javascript's `toString` typically does, in a generic way for all Ember - objects. + 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; - App.Person = Em.Object.extend() - person = App.Person.create() - person.toString() //=> "" + observerContexts = this.removeTransformation(dependentKey, index, removedCount); - If the object's class is not defined on an Ember namespace, it will - indicate it is a subclass of the registered superclass: - Student = App.Person.extend() - student = Student.create() - student.toString() //=> "<(subclass of App.Person):ember1025>" + function removeObservers(propertyKey) { + removeBeforeObserver(item, propertyKey, this, observerContexts[sliceIndex].beforeObserver); + removeObserver(item, propertyKey, this, observerContexts[sliceIndex].observer); + } - If the method `toStringExtension` is defined, its return value will be - included in the output. + for (sliceIndex = removedCount - 1; sliceIndex >= 0; --sliceIndex) { + itemIndex = index + sliceIndex; + item = dependentArray.objectAt(itemIndex); - App.Teacher = App.Person.extend({ - toStringExtension: function(){ - return this.get('fullName'); - } - }); - teacher = App.Teacher.create() - teacher.toString(); // #=> "" + forEach(itemPropertyKeys, removeObservers, this); - @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; - } -}); + changeMeta = createChangeMeta(dependentArray, item, itemIndex, this.instanceMeta.propertyName, this.cp); + this.setValue( removedItem.call( + this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); + } + }, -CoreObject.PrototypeMixin.ownerConstructor = CoreObject; + 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; -function makeToString(ret) { - return function() { return ret; }; -} + 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); + } -if (Ember.config.overridePrototypeMixin) { - Ember.config.overridePrototypeMixin(CoreObject.PrototypeMixin); -} + 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); -CoreObject.__super__ = null; + this.addTransformation(dependentKey, index, observerContexts); + }, -var ClassMixin = Mixin.create({ + itemPropertyWillChange: function (obj, keyName, array, index) { + var guid = guidFor(obj); - ClassMixin: Ember.required(), + if (!this.changedItems[guid]) { + this.changedItems[guid] = { + array: array, + index: index, + obj: obj, + previousValues: {} + }; + } - PrototypeMixin: Ember.required(), + this.changedItems[guid].previousValues[keyName] = get(obj, keyName); + }, - isClass: true, + itemPropertyDidChange: function(obj, keyName, array, index) { + Ember.run.once(this, 'flushChanges'); + }, - isMethod: false, + flushChanges: function() { + var changedItems = this.changedItems, key, c, changeMeta; + for (key in changedItems) { + c = changedItems[key]; + changeMeta = createChangeMeta(c.array, c.obj, c.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 = {}; + } +}; - extend: function() { - var Class = makeCtor(), proto; - Class.ClassMixin = Mixin.create(this.ClassMixin); - Class.PrototypeMixin = Mixin.create(this.PrototypeMixin); +function createChangeMeta(dependentArray, item, index, propertyName, property, previousValues) { + var meta = { + arrayChanged: dependentArray, + index: index, + item: item, + propertyName: propertyName, + property: property + }; - Class.ClassMixin.ownerConstructor = Class; - Class.PrototypeMixin.ownerConstructor = Class; + if (previousValues) { + // previous values only available for item property changes + meta.previousValues = previousValues; + } - reopen.apply(Class.PrototypeMixin, arguments); + return meta; +} - Class.superclass = this; - Class.__super__ = this.prototype; +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); +} - proto = Class.prototype = o_create(this.prototype); - proto.constructor = Class; - generateGuid(proto, 'ember'); - meta(proto).proto = proto; // this will disable observers on prototype +function reset(cp, propertyName) { + var callbacks = cp._callbacks(), + meta; - Class.ClassMixin.apply(Class); - return Class; - }, + if (cp._hasInstanceMeta(this, propertyName)) { + meta = cp._instanceMeta(this, propertyName); + meta.setValue(cp.resetValue(meta.getValue())); + } else { + meta = cp._instanceMeta(this, propertyName); + } - createWithMixins: function() { - var C = this; - if (arguments.length>0) { this._initMixins(arguments); } - return new C(); - }, + if (cp.options.initialize) { + cp.options.initialize.call(this, meta.getValue(), { property: cp, propertyName: propertyName }, meta.sugarMeta); + } +} - create: function() { - var C = this; - if (arguments.length>0) { this._initProperties(arguments); } - return new C(); - }, +function ReduceComputedPropertyInstanceMeta(context, propertyName, initialValue) { + this.context = context; + this.propertyName = propertyName; + this.cache = metaFor(context).cache; - reopen: function() { - this.willReopen(); - reopen.apply(this.PrototypeMixin, arguments); - return this; - }, + this.dependentArrays = {}; + this.sugarMeta = {}; - reopenClass: function() { - reopen.apply(this.ClassMixin, arguments); - applyMixin(this, arguments, false); - return this; - }, + this.initialValue = initialValue; +} - detect: function(obj) { - if ('function' !== typeof obj) { return false; } - while(obj) { - if (obj===this) { return true; } - obj = obj.superclass; +ReduceComputedPropertyInstanceMeta.prototype = { + getValue: function () { + if (this.propertyName in this.cache) { + return this.cache[this.propertyName]; + } else { + return this.initialValue; } - return false; }, - detectInstance: function(obj) { - return obj instanceof this; - }, + setValue: function(newValue) { + // This lets sugars force a recomputation, handy for very simple + // implementations of eg max. + if (newValue !== undefined) { + this.cache[this.propertyName] = newValue; + } else { + delete this.cache[this.propertyName]; + } + } +}; - /** - 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. +/** + A computed property whose dependent keys are arrays and which is updated with + "one at a time" semantics. - You can pass a hash of these values to a computed property like this: + @class ReduceComputedProperty + @namespace Ember + @extends Ember.ComputedProperty + @constructor +*/ +function ReduceComputedProperty(options) { + var cp = this; - ```javascript - person: function() { - var personId = this.get('personId'); - return App.Person.create({ id: personId }); - }.property().meta({ type: App.Person }) - ``` + this.options = options; + this._instanceMetas = {}; + + 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(); + + reset.call(this, cp, propertyName); + + forEach(cp._dependentKeys, function (dependentKey) { + 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; - Once you've done this, you can retrieve the values saved to the computed - property from your class like this: + if (previousDependentArray) { + meta.dependentArraysObserver.teardownObservers(previousDependentArray, dependentKey); + } - ```javascript - MyClass.metaForProperty('person'); - ``` + if (dependentArray) { + meta.dependentArraysObserver.setupObservers(dependentArray, dependentKey); + } + } + }, this); - This will return the original hash that was passed to `meta()`. + forEach(cp._dependentKeys, function(dependentKey) { + var dependentArray = get(this, dependentKey); + if (dependentArray) { + addItems.call(this, dependentArray, callbacks, cp, propertyName, meta); + } + }, this); + }; - @method metaForProperty - @param key {String} property name - */ - metaForProperty: function(key) { - var desc = meta(this.proto(), false).descs[key]; + this.func = function (propertyName) { + Ember.assert("Computed reduce values require at least one dependent key", cp._dependentKeys); - Ember.assert("metaForProperty() could not find a computed property with key '"+key+"'.", !!desc && desc instanceof Ember.ComputedProperty); - return desc._meta || {}; - }, + recompute.call(this, propertyName); - /** - Iterate over each computed property for the class, passing its name - and any associated metadata (see `metaForProperty`) to the callback. + return cp._instanceMeta(this, propertyName).getValue(); + }; +} - @method eachComputedProperty - @param {Function} callback - @param {Object} binding - */ - eachComputedProperty: function(callback, binding) { - var proto = this.proto(), - descs = meta(proto).descs, - empty = {}, - property; +Ember.ReduceComputedProperty = ReduceComputedProperty; +ReduceComputedProperty.prototype = o_create(ComputedProperty.prototype); - for (var name in descs) { - property = descs[name]; +function defaultCallback(computedValue) { + return computedValue; +} - if (property instanceof Ember.ComputedProperty) { - callback.call(binding || this, name, property._meta || empty); - } - } +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) { + var guid = guidFor(context), + key = guid + ':' + propertyName; -ClassMixin.ownerConstructor = CoreObject; + return !!this._instanceMetas[key]; +}; -if (Ember.config.overrideClassMixin) { - Ember.config.overrideClassMixin(ClassMixin); -} +ReduceComputedProperty.prototype._instanceMeta = function (context, propertyName) { + var guid = guidFor(context), + key = guid + ':' + propertyName, + meta = this._instanceMetas[key]; -CoreObject.ClassMixin = ClassMixin; -ClassMixin.apply(CoreObject); + if (!meta) { + meta = this._instanceMetas[key] = new ReduceComputedPropertyInstanceMeta(context, propertyName, this.initialValue()); + meta.dependentArraysObserver = new DependentArraysObserver(this._callbacks(), this, meta, context, propertyName, meta.sugarMeta); + } -/** - @class CoreObject - @namespace Ember -*/ -Ember.CoreObject = CoreObject; + return meta; +}; -})(); +ReduceComputedProperty.prototype.initialValue = function () { + switch (typeof this.options.initialValue) { + case 'undefined': + throw new Error("reduce computed properties require an initial value: did you forget to pass one to Ember.reduceComputed?"); + case 'function': + return this.options.initialValue(); + default: + 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] = []; + } +}; -(function() { -/** -@module ember -@submodule ember-runtime -*/ +ReduceComputedProperty.prototype.property = function () { + var cp = this, + args = a_slice.call(arguments), + propertyArgs = [], + match, + dependentArrayKey, + itemPropertyKey; + + forEach(a_slice.call(arguments), function (dependentKey) { + if (doubleEachPropertyPattern.test(dependentKey)) { + throw new Error("Nested @each properties not supported: " + dependentKey); + } else if (match = eachPropertyPattern.exec(dependentKey)) { + dependentArrayKey = match[1]; + itemPropertyKey = match[2]; + cp.itemPropertyKey(dependentArrayKey, itemPropertyKey); + propertyArgs.push(dependentArrayKey); + } else { + propertyArgs.push(dependentKey); + } + }); -var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, none = Ember.isNone; + return ComputedProperty.prototype.property.apply(this, propertyArgs); +}; /** - An unordered collection of objects. + 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. - 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. + 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. - All Sets are observable via the Enumerable Observer API - which works - on any enumerable object including both Sets and Arrays. + `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. - ## Creating a Set + `initialize` - An optional initialize function. Typically this will be used + to set up state on the instanceMeta object. - 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. + `removedItem` - A function that is called each time an element is removed + from the array. - 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()`. + `addedItem` - A function that is called each time an element is added to + the array. + + + The `initialize` function has the following signature: ```javascript - // creates a new empty set - var foundNames = new Ember.Set(); + function (initialValue, changeMeta, instanceMeta) + ``` - // creates a set with four names in it. - var names = new Ember.Set(["Charles", "Tom", "Juan", "Alex"]); // :P + `initialValue` - The value of the `initialValue` property from the + options object. - // creates a copy of the names set. - var namesCopy = new Ember.Set(names); + `changeMeta` - An object which contains meta information about the + computed. It contains the following properties: - // same as above. - var anotherNamesCopy = names.copy(); + - `property` the computed property + - `propertyName` the name of the property on the object + + `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. + + + The `removedItem` and `addedItem` functions both have the following signature: + + ```javascript + function (accumulatedValue, item, changeMeta, instanceMeta) ``` - ## Adding/Removing Objects + `accumulatedValue` - The value returned from the last time + `removedItem` or `addedItem` was called or `initialValue`. - 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. + `item` - the element added or removed from the array - 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. + `changeMeta` - An object which contains meta information about the + change. It contains the following properties: - NOTE: You cannot add/remove `null` or `undefined` to a set. Any attempt to do - so will be ignored. + - `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. - 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. + For property changes triggered on an item property change (when + depKey is something like `someArray.@each.someProperty`), + `changeMeta` will also contain the following property: - ## Testing for an Object + - `previousValues` an object whose keys are the properties that changed on + the item, and whose values are the item's previous values. - To test for an object's presence in a set you simply call - `Ember.Set#contains()`. + `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). - ## Observing changes + `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. - 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` for more information on - enumerables. + 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. - 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. + Example - ## Other Methods + ```javascript + Ember.computed.max = function (dependentKey) { + return Ember.reduceComputed.call(null, dependentKey, { + initialValue: -Infinity, - `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. + addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + return Math.max(accumulatedValue, item); + }, - 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. + removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + if (item < accumulatedValue) { + return accumulatedValue; + } + } + }); + }; + ``` - @class Set - @namespace Ember - @extends Ember.CoreObject - @uses Ember.MutableEnumerable - @uses Ember.Copyable - @uses Ember.Freezable - @since Ember 0.9 + @method reduceComputed + @for Ember + @param {String} [dependentKeys*] + @param {Object} options + @returns {Ember.ComputedProperty} */ -Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Ember.Freezable, - /** @scope Ember.Set.prototype */ { +Ember.reduceComputed = function (options) { + var args; - // .......................................................... - // IMPLEMENT ENUMERABLE APIS - // + if (arguments.length > 1) { + args = a_slice.call(arguments, 0, -1); + options = a_slice.call(arguments, -1)[0]; + } - /** - This property will change as the number of objects in the set changes. + if (typeof options !== "object") { + throw new Error("Reduce Computed Property declared without an options hash"); + } - @property length - @type number - @default 0 - */ - length: 0, + if (!options.initialValue) { + throw new Error("Reduce Computed Property declared without an initial value"); + } - /** - Clears the set. This is useful if you want to reuse an existing set - without having to recreate it. + var cp = new ReduceComputedProperty(options); - ```javascript - var colors = new Ember.Set(["red", "green", "blue"]); - colors.length; // 3 - colors.clear(); - colors.length; // 0 - ``` + if (args) { + cp.property.apply(cp, args); + } - @method clear - @return {Ember.Set} An empty Set - */ - clear: function() { - if (this.isFrozen) { throw new Error(Ember.FROZEN_ERROR); } + return cp; +}; - var len = get(this, 'length'); - if (len === 0) { return this; } +})(); - var guid; - this.enumerableContentWillChange(len, 0); - Ember.propertyWillChange(this, 'firstObject'); - Ember.propertyWillChange(this, 'lastObject'); - for (var i=0; i < len; i++){ - guid = guidFor(this[i]); - delete this[guid]; - delete this[i]; - } +(function() { +var ReduceComputedProperty = Ember.ReduceComputedProperty, + a_slice = [].slice, + o_create = Ember.create, + forEach = Ember.EnumerableUtils.forEach; + +function ArrayComputedProperty() { + var cp = this; + + ReduceComputedProperty.apply(this, arguments); + + 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); + } - set(this, 'length', 0); + return reduceFunc.apply(this, arguments); + }; + })(this.func); - Ember.propertyDidChange(this, 'firstObject'); - Ember.propertyDidChange(this, 'lastObject'); - this.enumerableContentDidChange(len, 0); + 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; +}; - return this; - }, +/** + 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`. - /** - Returns true if the passed object is also an enumerable that contains the - same objects as the receiver. + 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. - ```javascript - var colors = ["red", "green", "blue"], - same_colors = new Ember.Set(colors); + `initialize` - An optional initialize function. Typically this will be used + to set up state on the instanceMeta object. - same_colors.isEqual(colors); // true - same_colors.isEqual(["purple", "brown"]); // false - ``` + `removedItem` - A function that is called each time an element is + removed from the array. - @method isEqual - @param {Ember.Set} obj the other object. - @return {Boolean} - */ - isEqual: function(obj) { - // fail fast - if (!Ember.Enumerable.detect(obj)) return false; + `addedItem` - A function that is called each time an element is + added to the array. - var loc = get(this, 'length'); - if (get(obj, 'length') !== loc) return false; - while(--loc >= 0) { - if (!obj.contains(this[loc])) return false; - } + The `initialize` function has the following signature: - return true; - }, + ```javascript + function (array, changeMeta, instanceMeta) + ``` - /** - 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. + `array` - The initial value of the arrayComputed, an empty array. - This is an alias for `Ember.MutableEnumerable.addObject()`. + `changeMeta` - An object which contains meta information about the + computed. It contains the following properties: - ```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"] - ``` + - `property` the computed property + - `propertyName` the name of the property on the object - @method add - @param {Object} obj The object to add. - @return {Ember.Set} The set itself. - */ - add: Ember.aliasMethod('addObject'), + `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. - /** - 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()`. - ```javascript - var colors = new Ember.Set(["red", "green", "blue"]); - colors.remove("red"); // ["blue", "green"] - colors.remove("purple"); // ["blue", "green"] - colors.remove(null); // ["blue", "green"] - ``` + The `removedItem` and `addedItem` functions both have the following signature: - @method remove - @param {Object} obj The object to remove - @return {Ember.Set} The set itself. - */ - remove: Ember.aliasMethod('removeObject'), + ```javascript + function (accumulatedValue, item, changeMeta, instanceMeta) + ``` - /** - Removes the last element from the set and returns it, or `null` if it's empty. + `accumulatedValue` - The value returned from the last time + `removedItem` or `addedItem` was called or an empty array. - ```javascript - var colors = new Ember.Set(["green", "blue"]); - colors.pop(); // "blue" - colors.pop(); // "green" - colors.pop(); // null - ``` + `item` - the element added or removed from the array - @method pop - @return {Object} The removed object from the set or null. - */ - pop: function() { - if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); - var obj = this.length > 0 ? this[this.length-1] : null; - this.remove(obj); - return obj; - }, + `changeMeta` - An object which contains meta information about the + change. It contains the following properties: - /** - Inserts the given object on to the end of the set. It returns - the set itself. + - `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. - This is an alias for `Ember.MutableEnumerable.addObject()`. + For property changes triggered on an item property change (when + depKey is something like `someArray.@each.someProperty`), + `changeMeta` will also contain the following property: - ```javascript - var colors = new Ember.Set(); - colors.push("red"); // ["red"] - colors.push("green"); // ["red", "green"] - colors.push("blue"); // ["red", "green", "blue"] - ``` - - @method push - @return {Ember.Set} The set itself. - */ - push: Ember.aliasMethod('addObject'), - - /** - Removes the last element from the set and returns it, or `null` if it's empty. - - This is an alias for `Ember.Set.pop()`. - - ```javascript - var colors = new Ember.Set(["green", "blue"]); - colors.shift(); // "blue" - colors.shift(); // "green" - colors.shift(); // null - ``` - - @method shift - @return {Object} The removed object from the set or null. - */ - shift: Ember.aliasMethod('pop'), + - `previousValues` an object whose keys are the properties that changed on + the item, and whose values are the item's previous values. - /** - Inserts the given object on to the end of the set. It returns - the set itself. + `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 is an alias of `Ember.Set.push()` + `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. - ```javascript - var colors = new Ember.Set(); - colors.unshift("red"); // ["red"] - colors.unshift("green"); // ["red", "green"] - colors.unshift("blue"); // ["red", "green", "blue"] - ``` + 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. - @method unshift - @return {Ember.Set} The set itself. - */ - unshift: Ember.aliasMethod('push'), + Example - /** - Adds each object in the passed enumerable to the set. + ```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; + } + }; - This is an alias of `Ember.MutableEnumerable.addObjects()` + return Ember.arrayComputed(dependentKey, options); + }; + ``` - ```javascript - var colors = new Ember.Set(); - colors.addEach(["red", "green", "blue"]); // ["red", "green", "blue"] - ``` + @method arrayComputed + @for Ember + @param {String} [dependentKeys*] + @param {Object} options + @returns {Ember.ComputedProperty} +*/ +Ember.arrayComputed = function (options) { + var args; - @method addEach - @param {Ember.Enumerable} objects the objects to add. - @return {Ember.Set} The set itself. - */ - addEach: Ember.aliasMethod('addObjects'), + if (arguments.length > 1) { + args = a_slice.call(arguments, 0, -1); + options = a_slice.call(arguments, -1)[0]; + } - /** - Removes each object in the passed enumerable to the set. + if (typeof options !== "object") { + throw new Error("Array Computed Property declared without an options hash"); + } - This is an alias of `Ember.MutableEnumerable.removeObjects()` + var cp = new ArrayComputedProperty(options); - ```javascript - var colors = new Ember.Set(["red", "green", "blue"]); - colors.removeEach(["red", "blue"]); // ["green"] - ``` + if (args) { + cp.property.apply(cp, args); + } - @method removeEach - @param {Ember.Enumerable} objects the objects to remove. - @return {Ember.Set} The set itself. - */ - removeEach: Ember.aliasMethod('removeObjects'), + return cp; +}; - // .......................................................... - // PRIVATE ENUMERABLE SUPPORT - // +})(); - init: function(items) { - this._super(); - if (items) this.addObjects(items); - }, - // implement Ember.Enumerable - nextObject: function(idx) { - return this[idx]; - }, - // more optimized version - firstObject: Ember.computed(function() { - return this.length > 0 ? this[0] : undefined; - }), +(function() { +/** +@module ember +@submodule ember-runtime +*/ - // more optimized version - lastObject: Ember.computed(function() { - return this.length > 0 ? this[this.length-1] : undefined; - }), +var get = Ember.get, + set = Ember.set, + guidFor = Ember.guidFor, + merge = Ember.merge, + a_slice = [].slice, + forEach = Ember.EnumerableUtils.forEach, + map = Ember.EnumerableUtils.map; - // implements Ember.MutableEnumerable - addObject: function(obj) { - if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); - if (none(obj)) return this; // nothing to do +/** + A computed property that calculates the maximum value in the + dependent array. This will return `-Infinity` when the dependent + array is empty. - var guid = guidFor(obj), - idx = this[guid], - len = get(this, 'length'), - added ; + Example - if (idx>=0 && idx=0 && idx accumulatedValue) { + return accumulatedValue; } - - 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); } + }); +}; - return this; - }, - - // optimized version - contains: function(obj) { - return this[guidFor(obj)]>=0; - }, +/** + Returns an array mapped via the callback - 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; - }, + The callback method you provide should have the following signature: - toString: function() { - var len = this.length, idx, array = []; - for(idx = 0; idx < len; idx++) { - array[idx] = this[idx]; - } - return "Ember.Set<%@>".fmt(array.join(',')); - } + ```javascript + function(item); + ``` -}); + - `item` is the current item in the iteration. -})(); + Example + ```javascript + App.Hampster = Ember.Object.extend({ + excitingChores: Ember.computed.map('chores', function(chore) { + return chore.toUpperCase() + '!'; + }) + }); + var hampster = App.Hampster.create({chores: ['cook', 'clean', 'write more unit tests']}); + hampster.get('excitingChores'); // ['COOK!', 'CLEAN!', 'WRITE MORE UNIT TESTS!'] + ``` -(function() { -/** -@module ember -@submodule ember-runtime + @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(item); + array.insertAt(changeMeta.index, mapped); + return array; + }, + removedItem: function(array, item, changeMeta, instanceMeta) { + array.removeAt(changeMeta.index, 1); + return array; + } + }; + + return Ember.arrayComputed(dependentKey, options); +}; /** - `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. + Returns an array mapped to the specified key. - @class Object - @namespace Ember - @extends Ember.CoreObject - @uses Ember.Observable -*/ -Ember.Object = Ember.CoreObject.extend(Ember.Observable); -Ember.Object.toString = function() { return "Ember.Object"; }; + Example -})(); + ```javascript + App.Person = Ember.Object.extend({ + childAges: Ember.computed.mapBy('children', 'age'), + minChildAge: Ember.computed.min('childAges') + }); + var lordByron = App.Person.create({children: []}); + lordByron.get('childAge'); // [] + lordByron.get('children').pushObject({name: 'Augusta Ada Byron', age: 7}); + lordByron.get('childAge'); // [7] + lordByron.get('children').pushObjects([{name: 'Allegra Byron', age: 5}, {name: 'Elizabeth Medora Leigh', age: 8}]); + lordByron.get('childAge'); // [7, 5, 8] + ``` + @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); +}; -(function() { /** -@module ember -@submodule ember-runtime + @method computed.mapProperty + @for Ember + @deprecated Use `Ember.computed.mapBy` instead + @param dependentKey + @param propertyKey */ - -var get = Ember.get, indexOf = Ember.ArrayPolyfills.indexOf; +Ember.computed.mapProperty = Ember.computed.mapBy; /** - 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. + Filters the array by the callback. - # Example Usage + The callback method you provide should have the following signature: ```javascript - MyFramework = Ember.Namespace.create({ - VERSION: '1.0.0' - }); + function(item); ``` - @class Namespace - @namespace Ember - @extends Ember.Object -*/ -var Namespace = Ember.Namespace = Ember.Object.extend({ - isNamespace: true, + - `item` is the current item in the iteration. - init: function() { - Ember.Namespace.NAMESPACES.push(this); - Ember.Namespace.PROCESSED = false; - }, + Example - toString: function() { - var name = get(this, 'name'); - if (name) { return name; } + ```javascript + App.Hampster = Ember.Object.extend({ + remainingChores: Ember.computed.filter('chores', function(chore) { + return !chore.done; + }) + }); - findNamespaces(); - return this[Ember.GUID_KEY+'_name']; - }, + 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}] + ``` - nameClasses: function() { - processNamespace([this.toString()], this, {}); - }, + @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(); + }, - destroy: function() { - var namespaces = Ember.Namespace.NAMESPACES; - Ember.lookup[this.toString()] = undefined; - namespaces.splice(indexOf.call(namespaces, this), 1); - this._super(); - } -}); + addedItem: function(array, item, changeMeta, instanceMeta) { + var match = !!callback(item), + filterIndex = instanceMeta.filteredArrayIndexes.addItem(changeMeta.index, match); -Namespace.reopenClass({ - NAMESPACES: [Ember], - NAMESPACES_BY_ID: {}, - PROCESSED: false, - processAll: processAllNamespaces, - byName: function(name) { - if (!Ember.BOOTED) { - processAllNamespaces(); - } + if (match) { + array.insertAt(filterIndex, item); + } - return NAMESPACES_BY_ID[name]; - } -}); + return array; + }, -var NAMESPACES_BY_ID = Namespace.NAMESPACES_BY_ID; + removedItem: function(array, item, changeMeta, instanceMeta) { + var filterIndex = instanceMeta.filteredArrayIndexes.removeItem(changeMeta.index); -var hasOwnProp = ({}).hasOwnProperty, - guidFor = Ember.guidFor; + if (filterIndex > -1) { + array.removeAt(filterIndex); + } -function processNamespace(paths, root, seen) { - var idx = paths.length; + return array; + } + }; - NAMESPACES_BY_ID[paths.join('.')] = root; + return Ember.arrayComputed(dependentKey, options); +}; - // 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]; +/** + Filters the array by the property and value - // 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; + Example - // 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 + App.Hampster = Ember.Object.extend({ + remainingChores: Ember.computed.filterBy('chores', 'done', false) + }); - // Support nested namespaces - } else if (obj && obj.isNamespace) { - // Skip aliased namespaces - if (seen[guidFor(obj)]) { continue; } - seen[guidFor(obj)] = true; + 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}] + ``` - // Process the child namespace - processNamespace(paths, obj, seen); - } + @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; + + if (arguments.length === 2) { + callback = function(item) { + return get(item, propertyKey); + }; + } else { + callback = function(item) { + return get(item, propertyKey) === value; + }; } - paths.length = idx; // cut out last item -} + return Ember.computed.filter(dependentKey + '.@each.' + propertyKey, callback); +}; -function findNamespaces() { - var Namespace = Ember.Namespace, lookup = Ember.lookup, obj, isNamespace; +/** + @method computed.filterProperty + @for Ember + @param dependentKey + @param propertyKey + @param value + @deprecated Use `Ember.computed.filterBy` instead +*/ +Ember.computed.filterProperty = Ember.computed.filterBy; - if (Namespace.PROCESSED) { return; } +/** + A computed property which returns a new array with all the unique + elements from one or more dependent arrays. - for (var prop in lookup) { - // These don't raise exceptions but can cause warnings - if (prop === "parent" || prop === "top" || prop === "frameElement") { continue; } + Example - // 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; } + ```javascript + App.Hampster = Ember.Object.extend({ + uniqueFruits: Ember.computed.uniq('fruits') + }); - // 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; - } + var hampster = App.Hampster.create({fruits: [ + 'banana', + 'grape', + 'kale', + 'banana' + ]}); + hampster.get('uniqueFruits'); // ['banana', 'grape', 'kale'] + ``` - if (isNamespace) { - Ember.deprecate("Namespaces should not begin with lowercase.", /^[A-Z]/.test(prop)); - obj[NAME_KEY] = prop; - } - } -} + @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 = {}; + }, -var NAME_KEY = Ember.NAME_KEY = Ember.GUID_KEY + '_name'; + addedItem: function(array, item, changeMeta, instanceMeta) { + var guid = guidFor(item); -function superClassString(mixin) { - var superclass = mixin.superclass; - if (superclass) { - if (superclass[NAME_KEY]) { return superclass[NAME_KEY]; } - else { return superClassString(superclass); } - } else { - return; - } -} + 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; -function classToString() { - if (!Ember.BOOTED && !this[NAME_KEY]) { - processAllNamespaces(); - } + if (--itemCounts[guid] === 0) { + array.removeObject(item); + } + return array; + } + }); + return Ember.arrayComputed.apply(null, args); +}; - var ret; +/** + Alias for [Ember.computed.uniq](/api/#method_computed_uniq). - if (this[NAME_KEY]) { - ret = this[NAME_KEY]; - } else { - var str = superClassString(this); - if (str) { - ret = "(subclass of " + str + ")"; - } else { - ret = "(unknown mixin)"; - } - this.toString = makeToString(ret); - } + @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; - return ret; -} +/** + A computed property which returns a new array with all the duplicated + elements from two or more dependeny arrays. -function processAllNamespaces() { - var unprocessedNamespaces = !Namespace.PROCESSED, - unprocessedMixins = Ember.anyUnprocessedMixins; + Example - if (unprocessedNamespaces) { - findNamespaces(); - Namespace.PROCESSED = true; - } + ```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') + }); - if (unprocessedNamespaces || unprocessedMixins) { - var namespaces = Namespace.NAMESPACES, namespace; - for (var i=0, l=namespaces.length; i 0) { + return this.binarySearch(array, item, low, mid); + } + return mid; -var get = Ember.get, set = Ember.set; + function _guidFor(item) { + if (Ember.ObjectProxy.detectInstance(item)) { + return guidFor(get(item, 'content')); + } + return guidFor(item); + } +} /** - 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. + 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. - A simple example of usage: + The callback method you provide should have the following signature: ```javascript - var pets = ['dog', 'cat', 'fish']; - var ap = Ember.ArrayProxy.create({ content: Ember.A(pets) }); + function(itemA, itemB); + ``` - ap.get('firstObject'); // 'dog' - ap.set('content', ['amoeba', 'paramecium']); - ap.get('firstObject'); // 'amoeba' - ``` + - `itemA` the first item to compare. + - `itemB` the second item to compare. - 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`: + 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`. + + Example ```javascript - var pets = ['dog', 'cat', 'fish']; - var ap = Ember.ArrayProxy.create({ - content: Ember.A(pets), - objectAtContent: function(idx) { - return this.get('content').objectAt(idx).toUpperCase(); + 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; + }), }); - - ap.get('firstObject'); // . 'DOG' + 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('priroityTodos'); // [{name:'Release', priority:1}, {name:'Unit Test', priority:2}, {name:'Documentation', priority:3}] ``` - @class ArrayProxy - @namespace Ember - @extends Ember.Object - @uses Ember.MutableArray + @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.ArrayProxy = Ember.Object.extend(Ember.MutableArray, -/** @scope Ember.ArrayProxy.prototype */ { +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); - /** - The content array. Must be an object that implements `Ember.Array` and/or - `Ember.MutableArray.` + var initFn, sortPropertiesKey; - @property content - @type Ember.Array - */ - content: null, + 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; + } - /** - 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. + sortProperties.push(sortProperty); + sortPropertyAscending[sortProperty] = asc; + changeMeta.property.itemPropertyKey(itemsKey, sortProperty); + }); - @property arrangedContent - */ - arrangedContent: Ember.computed.alias('content'), + sortPropertyDefinitions.addObserver('@each', this, updateSortPropertiesOnce); + } - /** - 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. + function updateSortPropertiesOnce() { + Ember.run.once(this, updateSortProperties, changeMeta.propertyName); + } - This method will only be called if content is non-`null`. + function updateSortProperties(propertyName) { + setupSortProperties.call(this); + changeMeta.property.recomputeOnce.call(this, propertyName); + } - @method objectAtContent - @param {Number} idx The index to retrieve. - @return {Object} the value or undefined if none found - */ - objectAtContent: function(idx) { - return get(this, 'arrangedContent').objectAt(idx); - }, + Ember.addObserver(this, sortPropertiesKey, updateSortPropertiesOnce); - /** - 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. + setupSortProperties.call(this); - This method will only be called if content is non-`null`. - @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); - }, + 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)); - /** - @private + if (result !== 0) { + asc = this.sortPropertyAscending[sortProperty]; + return asc ? result : (-1 * result); + } + } - Invoked when the content property is about to change. Notifies observers that the - entire array content will change. + return 0; + }; - @method _contentWillChange - */ - _contentWillChange: Ember.beforeObserver(function() { - this._teardownContent(); - }, 'content'), + instanceMeta.binarySearch = binarySearch; + }; + } - _teardownContent: function() { - var content = get(this, 'content'); + return Ember.arrayComputed.call(null, itemsKey, { + initialize: initFn, - if (content) { - content.removeArrayObserver(this, { - willChange: 'contentArrayWillChange', - didChange: 'contentArrayDidChange' - }); - } - }, + addedItem: function (array, item, changeMeta, instanceMeta) { + var index = instanceMeta.binarySearch(array, item); + array.insertAt(index, item); + return array; + }, - contentArrayWillChange: Ember.K, - contentArrayDidChange: Ember.K, + removedItem: function (array, item, changeMeta, instanceMeta) { + var proxyProperties, index, searchItem; - /** - @private + if (changeMeta.previousValues) { + proxyProperties = merge({ content: item }, changeMeta.previousValues); - Invoked when the content property changes. Notifies observers that the - entire array content has changed. + searchItem = Ember.ObjectProxy.create(proxyProperties); + } else { + searchItem = item; + } - @method _contentDidChange - */ - _contentDidChange: Ember.observer(function() { - var content = get(this, 'content'); + index = instanceMeta.binarySearch(array, searchItem); + array.removeAt(index); + return array; + } + }); +}; - Ember.assert("Can't set ArrayProxy's content to itself", content !== this); +})(); - this._setupContent(); - }, 'content'), - _setupContent: function() { - var content = get(this, 'content'); - if (content) { - content.addArrayObserver(this, { - willChange: 'contentArrayWillChange', - didChange: 'contentArrayDidChange' - }); - } - }, +(function() { +/** + Expose RSVP implementation + + Documentation can be found here: https://github.com/tildeio/rsvp.js/blob/master/README.md - _arrangedContentWillChange: Ember.beforeObserver(function() { - var arrangedContent = get(this, 'arrangedContent'), - len = arrangedContent ? get(arrangedContent, 'length') : 0; + @class RSVP + @namespace Ember + @constructor +*/ +Ember.RSVP = requireModule('rsvp'); - this.arrangedContentArrayWillChange(this, 0, len, undefined); - this.arrangedContentWillChange(this); +})(); - this._teardownArrangedContent(arrangedContent); - }, 'arrangedContent'), - _arrangedContentDidChange: Ember.observer(function() { - var arrangedContent = get(this, 'arrangedContent'), - len = arrangedContent ? get(arrangedContent, 'length') : 0; - Ember.assert("Can't set ArrayProxy's content to itself", arrangedContent !== this); +(function() { +/** +@module ember +@submodule ember-runtime +*/ - this._setupArrangedContent(); +var STRING_DASHERIZE_REGEXP = (/[ _]/g); +var STRING_DASHERIZE_CACHE = {}; +var STRING_DECAMELIZE_REGEXP = (/([a-z])([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); - this.arrangedContentDidChange(this); - this.arrangedContentArrayDidChange(this, 0, undefined, len); - }, 'arrangedContent'), +/** + 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. - _setupArrangedContent: function() { - var arrangedContent = get(this, 'arrangedContent'); + @property STRINGS + @for Ember + @type Hash +*/ +Ember.STRINGS = {}; - if (arrangedContent) { - arrangedContent.addArrayObserver(this, { - willChange: 'arrangedContentArrayWillChange', - didChange: 'arrangedContentArrayDidChange' - }); - } - }, +/** + 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. - _teardownArrangedContent: function() { - var arrangedContent = get(this, 'arrangedContent'); + @class String + @namespace Ember + @static +*/ +Ember.String = { - if (arrangedContent) { - arrangedContent.removeArrayObserver(this, { - willChange: 'arrangedContentArrayWillChange', - didChange: 'arrangedContentArrayDidChange' - }); - } - }, + /** + 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. - arrangedContentWillChange: Ember.K, - arrangedContentDidChange: Ember.K, + Ordered insertions are most useful when building loc strings where values + you need to insert may appear in different orders. - objectAt: function(idx) { - return get(this, 'content') && this.objectAtContent(idx); + ```javascript + "Hello %@ %@".fmt('John', 'Doe'); // "Hello John Doe" + "Hello %@2, %@1".fmt('John', 'Doe'); // "Hello Doe, John" + ``` + + @method fmt + @param {String} str The string to format + @param {Array} formats An array of parameters to interpolate into string. + @return {String} formatted string + */ + 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); + }) ; }, - length: Ember.computed(function() { - var arrangedContent = get(this, 'arrangedContent'); - return arrangedContent ? get(arrangedContent, 'length') : 0; - // No dependencies since Enumerable notifies length of change - }), + /** + 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. - replace: function(idx, amt, objects) { - Ember.assert('The content property of '+ this.constructor + ' should be set before modifying it', this.get('content')); - if (get(this, 'content')) this.replaceContent(idx, amt, objects); - return this; - }, + 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. - arrangedContentArrayWillChange: function(item, idx, removedCnt, addedCnt) { - this.arrayContentWillChange(idx, removedCnt, addedCnt); - }, + ```javascript + Ember.STRINGS = { + '_Hello World': 'Bonjour le monde', + '_Hello %@ %@': 'Bonjour %@ %@' + }; - arrangedContentArrayDidChange: function(item, idx, removedCnt, addedCnt) { - this.arrayContentDidChange(idx, removedCnt, addedCnt); + Ember.String.loc("_Hello World"); // 'Bonjour le monde'; + Ember.String.loc("_Hello %@ %@", ["John", "Smith"]); // "Bonjour John Smith"; + ``` + + @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) ; }, - init: function() { - this._super(); - this._setupContent(); - this._setupArrangedContent(); - }, - - willDestroy: function() { - this._teardownArrangedContent(); - this._teardownContent(); - } -}); - - -})(); + /** + 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`. + ```javascript + Ember.String.w("alpha beta gamma").forEach(function(key) { + console.log(key); + }); + // > alpha + // > beta + // > gamma + ``` -(function() { -/** -@module ember -@submodule ember-runtime -*/ + @method w + @param {String} str The string to split + @return {String} split string + */ + w: function(str) { return str.split(/\s+/); }, -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; + /** + Converts a camelized string into all lower case separated by underscores. -function contentPropertyWillChange(content, contentKey) { - var key = contentKey.slice(8); // remove "content." - if (key in this) { return; } // if shadowed in proxy - propertyWillChange(this, key); -} + ```javascript + 'innerHTML'.decamelize(); // 'inner_html' + 'action_name'.decamelize(); // 'action_name' + 'css-class-name'.decamelize(); // 'css-class-name' + 'my favorite items'.decamelize(); // 'my favorite items' + ``` -function contentPropertyDidChange(content, contentKey) { - var key = contentKey.slice(8); // remove "content." - if (key in this) { return; } // if shadowed in proxy - propertyDidChange(this, key); -} + @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(); + }, -/** - `Ember.ObjectProxy` forwards all properties not defined by the proxy itself - to a proxied `content` object. + /** + Replaces underscores, spaces, or camelCase with dashes. - ```javascript - object = Ember.Object.create({ - name: 'Foo' - }); + ```javascript + 'innerHTML'.dasherize(); // 'inner-html' + 'action_name'.dasherize(); // 'action-name' + 'css-class-name'.dasherize(); // 'css-class-name' + 'my favorite items'.dasherize(); // 'my-favorite-items' + ``` - proxy = Ember.ObjectProxy.create({ - content: object - }); + @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; - // Access and change existing properties - proxy.get('name') // 'Foo' - proxy.set('name', 'Bar'); - object.get('name') // 'Bar' + if (hit) { + return cache[str]; + } else { + ret = Ember.String.decamelize(str).replace(STRING_DASHERIZE_REGEXP,'-'); + cache[str] = ret; + } - // Create new 'description' property on `object` - proxy.set('description', 'Foo is a whizboo baz'); - object.get('description') // 'Foo is a whizboo baz' - ``` + return ret; + }, - While `content` is unset, setting a property to be delegated will throw an - Error. + /** + Returns the lowerCamelCase form of a string. - ```javascript - proxy = Ember.ObjectProxy.create({ - content: null, - flag: null - }); - proxy.set('flag', true); - proxy.get('flag'); // true - proxy.get('foo'); // undefined - proxy.set('foo', 'data'); // throws Error - ``` + ```javascript + 'innerHTML'.camelize(); // 'innerHTML' + 'action_name'.camelize(); // 'actionName' + 'css-class-name'.camelize(); // 'cssClassName' + 'my favorite items'.camelize(); // 'myFavoriteItems' + 'My Favorite Items'.camelize(); // 'myFavoriteItems' + ``` - Delegated properties can be bound to and will change when content is updated. + @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(); + }); + }, - Computed properties on the proxy itself can depend on delegated properties. + /** + Returns the UpperCamelCase form of a string. - ```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') - }); + ```javascript + 'innerHTML'.classify(); // 'InnerHTML' + 'action_name'.classify(); // 'ActionName' + 'css-class-name'.classify(); // 'CssClassName' + 'my favorite items'.classify(); // 'MyFavoriteItems' + ``` - proxy = ProxyWithComputedProperty.create(); + @method classify + @param {String} str the string to classify + @return {String} the classified string + */ + classify: function(str) { + var parts = str.split("."), + out = []; - proxy.get('fullName'); // undefined - proxy.set('content', { - firstName: 'Tom', lastName: 'Dale' - }); // triggers property change for fullName on proxy + for (var i=0, l=parts.length; i=idx) { - var item = content.objectAt(loc); - if (item) { - Ember.addBeforeObserver(item, keyName, proxy, 'contentKeyWillChange'); - Ember.addObserver(item, keyName, proxy, 'contentKeyDidChange'); + @method loc + @for String + */ + String.prototype.loc = function() { + return loc(this, arguments); + }; - // keep track of the indicies 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); - } - } -} + /** + See [Ember.String.camelize](/api/classes/Ember.String.html#method_camelize). -function removeObserverForContentKey(content, keyName, proxy, idx, loc) { - var objects = proxy._objects; - if (!objects) objects = proxy._objects = {}; - var indicies, guid; + @method camelize + @for String + */ + String.prototype.camelize = function() { + return camelize(this); + }; - while(--loc>=idx) { - var item = content.objectAt(loc); - if (item) { - Ember.removeBeforeObserver(item, keyName, proxy, 'contentKeyWillChange'); - Ember.removeObserver(item, keyName, proxy, 'contentKeyDidChange'); + /** + See [Ember.String.decamelize](/api/classes/Ember.String.html#method_decamelize). - guid = guidFor(item); - indicies = objects[guid]; - indicies[indicies.indexOf(loc)] = null; - } - } -} + @method decamelize + @for String + */ + String.prototype.decamelize = function() { + return decamelize(this); + }; -/** - 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. + /** + See [Ember.String.dasherize](/api/classes/Ember.String.html#method_dasherize). - @private - @class EachProxy - @namespace Ember - @extends Ember.Object -*/ -Ember.EachProxy = Ember.Object.extend({ + @method dasherize + @for String + */ + String.prototype.dasherize = function() { + return dasherize(this); + }; - init: function(content) { - this._super(); - this._content = content; - content.addArrayObserver(this); + /** + See [Ember.String.underscore](/api/classes/Ember.String.html#method_underscore). - // in case someone is already observing some keys make sure they are - // added - forEach(Ember.watchedEvents(this), function(eventName) { - this.didAddListener(eventName); - }, this); - }, + @method underscore + @for String + */ + String.prototype.underscore = function() { + return underscore(this); + }; /** - You can directly access mapped properties by simply requesting them. - The `unknownProperty` handler will generate an EachArray of each item. + See [Ember.String.classify](/api/classes/Ember.String.html#method_classify). - @method unknownProperty - @param keyName {String} - @param value {anything} + @method classify + @for String */ - unknownProperty: function(keyName, value) { - var ret; - ret = new EachArray(this._content, keyName, this); - Ember.defineProperty(this, keyName, null, ret); - this.beginObservingContentKey(keyName); - return ret; - }, + String.prototype.classify = function() { + return classify(this); + }; - // .......................................................... - // ARRAY CHANGES - // Invokes whenever the content array itself changes. + /** + See [Ember.String.capitalize](/api/classes/Ember.String.html#method_capitalize). - arrayWillChange: function(content, idx, removedCnt, addedCnt) { - var keys = this._keys, key, array, lim; + @method capitalize + @for String + */ + String.prototype.capitalize = function() { + return capitalize(this); + }; - 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); - } - Ember.propertyWillChange(this._content, '@each'); - Ember.endPropertyChanges(this); - }, - arrayDidChange: function(content, idx, removedCnt, addedCnt) { - var keys = this._keys, key, array, lim; +(function() { +/** +@module ember +@submodule ember-runtime +*/ - lim = addedCnt>0 ? idx+addedCnt : -1; - Ember.beginPropertyChanges(this); +var a_slice = Array.prototype.slice; - for(key in keys) { - if (!keys.hasOwnProperty(key)) { continue; } +if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Function) { - if (lim>0) addObserverForContentKey(content, key, this, idx, lim); + /** + 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. - Ember.propertyDidChange(this, key); - } + Computed properties allow you to treat a function like a property: - Ember.propertyDidChange(this._content, '@each'); - Ember.endPropertyChanges(this); - }, + ```javascript + MyApp.President = Ember.Object.extend({ + firstName: '', + lastName: '', - // .......................................................... - // LISTEN FOR NEW OBSERVERS AND OTHER EVENT LISTENERS - // Start monitoring keys based on who is listening... + fullName: function() { + return this.get('firstName') + ' ' + this.get('lastName'); - didAddListener: function(eventName) { - if (IS_OBSERVER.test(eventName)) { - this.beginObservingContentKey(eventName.slice(0, -7)); - } - }, + // Call this flag to mark the function as a property + }.property() + }); - didRemoveListener: function(eventName) { - if (IS_OBSERVER.test(eventName)) { - this.stopObservingContentKey(eventName.slice(0, -7)); - } - }, + var president = MyApp.President.create({ + firstName: "Barack", + lastName: "Obama" + }); - // .......................................................... - // CONTENT KEY OBSERVING - // Actual watch keys on the source content. + president.get('fullName'); // "Barack Obama" + ``` - 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]++; - } - }, + Treating a function like a property is useful because they can work with + bindings, just like any other property. - 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); - } - }, + 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: - contentKeyWillChange: function(obj, keyName) { - Ember.propertyWillChange(this, keyName); - }, + ```javascript + MyApp.President = Ember.Object.extend({ + firstName: '', + lastName: '', - contentKeyDidChange: function(obj, keyName) { - Ember.propertyDidChange(this, keyName); - } + fullName: function() { + return this.get('firstName') + ' ' + this.get('lastName'); -}); + // Tell Ember.js that this computed property depends on firstName + // and lastName + }.property('firstName', 'lastName') + }); + ``` + 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. + See [Ember.ComputedProperty](/api/classes/Ember.ComputedProperty.html), [Ember.computed](/api/#method_computed). -})(); + @method property + @for Function + */ + Function.prototype.property = function() { + var ret = Ember.computed(this); + return ret.property.apply(ret, 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. + 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: -(function() { -/** -@module ember -@submodule ember-runtime -*/ - - -var get = Ember.get, set = Ember.set; - -// 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) { - - 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 (!objects || objects.length === 0) { - this.splice(idx, amt) ; - } else { - var args = [idx, amt].concat(objects) ; - this.splice.apply(this,args) ; - } + ```javascript + Ember.Object.extend({ + valueObserver: function() { + // Executes whenever the "value" property changes + }.observes('value') + }); + ``` - this.arrayContentDidChange(idx, amt, len); - return this ; - }, + In the future this method may become asynchronous. If you want to ensure + synchronous behavior, use `observesImmediately`. - // 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 ; - }, + See `Ember.observer`. - // If browser did not implement indexOf natively, then override with - // specialized version - indexOf: function(object, startAt) { - var idx, len = this.length; + @method observes + @for Function + */ + Function.prototype.observes = function() { + this.__ember_observes__ = a_slice.call(arguments); + return this; + }; - if (startAt === undefined) startAt = 0; - else startAt = (startAt < 0) ? Math.ceil(startAt) : Math.floor(startAt); - if (startAt < 0) startAt += len; + /** + The `observesImmediately` extension of Javascript's Function prototype is + available when `Ember.EXTEND_PROTOTYPES` or + `Ember.EXTEND_PROTOTYPES.Function` is true, which is the default. - for(idx=startAt;idx=0;idx--) { - if (this[idx] === object) return idx ; - } - return -1; - }, + See `Ember.immediateObserver`. - copy: function(deep) { - if (deep) { - return this.map(function(item){ return Ember.copy(item, true); }); + @method observesImmediately + @for Function + */ + Function.prototype.observesImmediately = function() { + for (var i=0, l=arguments.length; i0) { - NativeArray = NativeArray.without.apply(NativeArray, ignore); -} + You can get notified when a property change is about to happen by + by adding the `observesBefore` call to the end of your method + declarations in classes that you write. For example: -/** - 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`. + ```javascript + Ember.Object.extend({ + valueObserver: function() { + // Executes whenever the "value" property is about to change + }.observesBefore('value') + }); + ``` - @class NativeArray - @namespace Ember - @extends Ember.Mixin - @uses Ember.MutableArray - @uses Ember.MutableEnumerable - @uses Ember.Copyable - @uses Ember.Freezable -*/ -Ember.NativeArray = NativeArray; + See `Ember.beforeObserver`. -/** - Creates an `Ember.NativeArray` from an Array like object. - Does not modify the original object. + @method observesBefore + @for Function + */ + Function.prototype.observesBefore = function() { + this.__ember_observesBefore__ = a_slice.call(arguments); + return this; + }; - @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); -}; + /** + The `on` extension of Javascript's Function prototype is available + when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is + true, which is the default. -/** - Activates the mixin on the Array.prototype if not already applied. Calling - this method more than once is safe. + You can listen for events simply by adding the `on` call to the end of + your method declarations in classes or mixins that you write. For example: - @method activate - @for Ember.NativeArray - @static - @return {void} -*/ -Ember.NativeArray.activate = function() { - NativeArray.apply(Array.prototype); + ```javascript + Ember.Mixin.create({ + doSomethingWithElement: function() { + // Executes whenever the "didInsertElement" event fires + }.on('didInsertElement') + }); + ``` - Ember.A = function(arr) { return arr || []; }; -}; + See `Ember.on`. -if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) { - Ember.NativeArray.activate(); + @method on + @for Function + */ + Function.prototype.on = function() { + var events = a_slice.call(arguments); + this.__ember_listens__ = events; + return this; + }; } @@ -11913,21 +13021,6 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) { (function() { -var DeferredMixin = Ember.DeferredMixin, // mixins/deferred - EmberObject = Ember.Object, // system/object - get = Ember.get; - -var Deferred = Ember.Object.extend(DeferredMixin); - -Deferred.reopenClass({ - promise: function(callback, binding) { - var deferred = Deferred.create(); - callback.call(binding, deferred); - return get(deferred, 'promise'); - } -}); - -Ember.Deferred = Deferred; })(); @@ -11939,137 +13032,103 @@ Ember.Deferred = Deferred; @submodule ember-runtime */ -var loadHooks = Ember.ENV.EMBER_LOAD_HOOKS || {}; -var loaded = {}; /** -@method onLoad -@for Ember -@param name {String} name of hook -@param callback {Function} callback to be called -*/ -Ember.onLoad = function(name, callback) { - var object; - - loadHooks[name] = loadHooks[name] || Ember.A(); - loadHooks[name].pushObject(callback); + Implements some standard methods for comparing objects. Add this mixin to + any class you create that can compare its instances. - if (object = loaded[name]) { - callback(object); - } -}; + You should implement the `compare()` method. -/** -@method runLoadHooks -@for Ember -@param name {String} name of hook -@param object {Object} object to pass to callbacks + @class Comparable + @namespace Ember + @since Ember 0.9 */ -Ember.runLoadHooks = function(name, object) { - var hooks; +Ember.Comparable = Ember.Mixin.create( /** @scope Ember.Comparable.prototype */{ - loaded[name] = object; + /** + Override to return the result of the comparison of the two parameters. The + compare method should return: - if (hooks = loadHooks[name]) { - loadHooks[name].forEach(function(callback) { - callback(object); - }); - } -}; + - `-1` if `a < b` + - `0` if `a == b` + - `1` if `a > b` -})(); + Default implementation raises an exception. + @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) +}); -(function() { })(); (function() { -var get = Ember.get; - /** @module ember @submodule ember-runtime */ -/** - `Ember.ControllerMixin` provides a standard interface for all classes that - compose Ember's controller layer: `Ember.Controller`, - `Ember.ArrayController`, and `Ember.ObjectController`. - - Within an `Ember.Router`-managed application single shared instaces of every - Controller object in your application's namespace will be added to the - application's `Ember.Router` instance. See `Ember.Application#initialize` - for additional information. - ## Views - By default a controller instance will be the rendering context - for its associated `Ember.View.` This connection is made during calls to - `Ember.ControllerMixin#connectOutlet`. +var get = Ember.get, set = Ember.set; - Within the view's template, the `Ember.View` instance can be accessed - through the controller with `{{view}}`. +/** + 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. - ## Target Forwarding + You should generally implement the `copy()` method to return a copy of the + receiver. - By default a controller will target your application's `Ember.Router` - instance. Calls to `{{action}}` within the template of a controller's view - are forwarded to the router. See `Ember.Handlebars.helpers.action` for - additional information. + Note that `frozenCopy()` will only work if you also implement + `Ember.Freezable`. - @class ControllerMixin + @class Copyable @namespace Ember - @extends Ember.Mixin + @since Ember 0.9 */ -Ember.ControllerMixin = Ember.Mixin.create({ - /* ducktype as a controller */ - isController: true, +Ember.Copyable = Ember.Mixin.create(/** @scope Ember.Copyable.prototype */ { /** - The object to which events from the view should be sent. - - For example, when a Handlebars template uses the `{{action}}` helper, - it will attempt to send the event to the view's controller's `target`. - - By default, a controller's `target` is set to the router after it is - instantiated by `Ember.Application#initialize`. + Override to return a copy of the receiver. Default implementation raises + an exception. - @property target - @default null + @method copy + @param {Boolean} deep if `true`, a deep copy of the object should be made + @return {Object} copy of receiver */ - target: null, - - container: null, + copy: Ember.required(Function), - store: null, + /** + 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. - model: Ember.computed.alias('content'), + Raises an exception if you try to call this method on a object that does + not support freezing. - send: function(actionName) { - var args = [].slice.call(arguments, 1), target; + 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. - if (this[actionName]) { - Ember.assert("The controller " + this + " does not have the action " + actionName, typeof this[actionName] === 'function'); - this[actionName].apply(this, args); - } else if(target = get(this, 'target')) { - Ember.assert("The target for controller " + this + " (" + target + ") did not define a `send` method", typeof target.send === 'function'); - target.send.apply(target, arguments); + @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 Error(Ember.String.fmt("%@ does not support freezing", [this])); } } }); -/** - @class Controller - @namespace Ember - @extends Ember.Object - @uses Ember.ControllerMixin -*/ -Ember.Controller = Ember.Object.extend(Ember.ControllerMixin); - })(); @@ -12080,222 +13139,207 @@ Ember.Controller = Ember.Object.extend(Ember.ControllerMixin); @submodule ember-runtime */ -var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; + +var get = Ember.get, set = Ember.set; /** - `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: + 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. + + ## Enforcement + + 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. + + 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. + + ## Example Usage + + The example below shows a simple object that implement the `Ember.Freezable` + protocol. ```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'}, - ]; + Contact = Ember.Object.extend(Ember.Freezable, { + firstName: null, + lastName: null, - songsController = Ember.ArrayController.create({ - content: songs, - sortProperties: ['trackNumber'], - sortAscending: true - }); + // 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; + } - songsController.get('firstObject'); // {trackNumber: 2, title: 'Back in the U.S.S.R.'} + }); - songsController.addObject({trackNumber: 1, title: 'Dear Prudence'}); - songsController.get('firstObject'); // {trackNumber: 1, title: 'Dear Prudence'} + c = Context.create({ firstName: "John", lastName: "Doe" }); + c.swapNames(); // returns c + c.freeze(); + c.swapNames(); // EXCEPTION ``` - @class SortableMixin + ## Copying + + 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. + + @class Freezable @namespace Ember - @extends Ember.Mixin - @uses Ember.MutableEnumerable + @since Ember 0.9 */ -Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { +Ember.Freezable = Ember.Mixin.create(/** @scope Ember.Freezable.prototype */ { /** - Specifies which properties dictate the arrangedContent's sort order. + Set to `true` when the object is frozen. Use this property to detect + whether your object is frozen or not. - @property {Array} sortProperties + @property isFrozen + @type Boolean */ - sortProperties: null, + isFrozen: false, /** - Specifies the arrangedContent's sort direction + Freezes the object. Once this method has been called the object should + no longer allow any properties to be edited. - @property {Boolean} sortAscending + @method freeze + @return {Object} receiver */ - sortAscending: true, + freeze: function() { + if (get(this, 'isFrozen')) return this; + set(this, 'isFrozen', true); + return this; + } - orderBy: function(item1, item2) { - var result = 0, - sortProperties = get(this, 'sortProperties'), - sortAscending = get(this, 'sortAscending'); +}); - Ember.assert("you need to define `sortProperties`", !!sortProperties); +Ember.FROZEN_ERROR = "Frozen object cannot be modified."; - forEach(sortProperties, function(propertyName) { - if (result === 0) { - result = Ember.compare(get(item1, propertyName), get(item2, propertyName)); - if ((result !== 0) && !sortAscending) { - result = (-1) * result; - } - } - }); +})(); - return result; - }, - destroy: 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); - } +(function() { +/** +@module ember +@submodule ember-runtime +*/ - return this._super(); - }, +var forEach = Ember.EnumerableUtils.forEach; - isSorted: Ember.computed.bool('sortProperties'), +/** + This mixin defines the API for modifying generic enumerables. These methods + can be applied to an object regardless of whether it is ordered or + unordered. - arrangedContent: Ember.computed('content', 'sortProperties.@each', function(key, value) { - var content = get(this, 'content'), - isSorted = get(this, 'isSorted'), - sortProperties = get(this, 'sortProperties'), - self = this; + Note that an Enumerable can change even if it does not implement this mixin. + For example, a MappedEnumerable cannot be directly modified but if its + underlying enumerable changes, it will change also. - 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); - } + ## Adding Objects - return content; - }), + To add an object to an enumerable, use the `addObject()` method. This + method will only add the object to the enumerable if the object is not + already present and is of a type supported by the enumerable. - _contentWillChange: Ember.beforeObserver(function() { - var content = get(this, 'content'), - sortProperties = get(this, 'sortProperties'); + ```javascript + set.addObject(contact); + ``` - if (content && sortProperties) { - forEach(content, function(item) { - forEach(sortProperties, function(sortProperty) { - Ember.removeObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); - }, this); - }, this); - } + ## Removing Objects - this._super(); - }, 'content'), + To remove an object from an enumerable, use the `removeObject()` method. This + will only remove the object if it is present in the enumerable, otherwise + this method has no effect. - sortAscendingWillChange: Ember.beforeObserver(function() { - this._lastSortAscending = get(this, 'sortAscending'); - }, 'sortAscending'), + ```javascript + set.removeObject(contact); + ``` - sortAscendingDidChange: Ember.observer(function() { - if (get(this, 'sortAscending') !== this._lastSortAscending) { - var arrangedContent = get(this, 'arrangedContent'); - arrangedContent.reverseObjects(); - } - }, 'sortAscending'), + ## Implementing In Your Own Code - contentArrayWillChange: function(array, idx, removedCount, addedCount) { - var isSorted = get(this, 'isSorted'); + If you are implementing an object and want to support this API, just include + this mixin in your class and implement the required methods. In your unit + tests, be sure to apply the Ember.MutableEnumerableTests to your object. - if (isSorted) { - var arrangedContent = get(this, 'arrangedContent'); - var removedObjects = array.slice(idx, idx+removedCount); - var sortProperties = get(this, 'sortProperties'); + @class MutableEnumerable + @namespace Ember + @uses Ember.Enumerable +*/ +Ember.MutableEnumerable = Ember.Mixin.create(Ember.Enumerable, { - forEach(removedObjects, function(item) { - arrangedContent.removeObject(item); + /** + __Required.__ You must implement this method to apply this mixin. - forEach(sortProperties, function(sortProperty) { - Ember.removeObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); - }, this); - }, this); - } + Attempts to add the passed object to the receiver if the object is not + already present in the collection. If the object is present, this method + has no effect. - return this._super(array, idx, removedCount, addedCount); - }, + If the passed object is of a type not supported by the receiver, + then this method should raise an exception. - contentArrayDidChange: function(array, idx, removedCount, addedCount) { - var isSorted = get(this, 'isSorted'), - sortProperties = get(this, 'sortProperties'); - - if (isSorted) { - var addedObjects = array.slice(idx, idx+addedCount); - var arrangedContent = get(this, 'arrangedContent'); - - forEach(addedObjects, function(item) { - this.insertItemSorted(item); - - forEach(sortProperties, function(sortProperty) { - Ember.addObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); - }, this); - }, this); - } - - return this._super(array, idx, removedCount, addedCount); - }, - - insertItemSorted: function(item) { - var arrangedContent = get(this, 'arrangedContent'); - var length = get(arrangedContent, 'length'); - - var idx = this._binarySearch(item, 0, length); - arrangedContent.insertAt(idx, item); - }, + @method addObject + @param {Object} object The object to add to the enumerable. + @return {Object} the passed object + */ + addObject: Ember.required(Function), - 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); + /** + Adds each object in the passed enumerable to the receiver. - if (leftResult < 0 || rightResult > 0) { - arrangedContent.removeObject(item); - this.insertItemSorted(item); - } + @method addObjects + @param {Ember.Enumerable} objects the objects to add. + @return {Object} receiver + */ + addObjects: function(objects) { + Ember.beginPropertyChanges(this); + forEach(objects, function(obj) { this.addObject(obj); }, this); + Ember.endPropertyChanges(this); + return this; }, - _binarySearch: function(item, low, high) { - var mid, midItem, res, arrangedContent; + /** + __Required.__ You must implement this method to apply this mixin. - if (low === high) { - return low; - } + Attempts to remove the passed object from the receiver collection if the + object is present in the collection. If the object is not present, + this method has no effect. - arrangedContent = get(this, 'arrangedContent'); + If the passed object is of a type not supported by the receiver, + then this method should raise an exception. - mid = low + Math.floor((high - low) / 2); - midItem = arrangedContent.objectAt(mid); + @method removeObject + @param {Object} object The object to remove from the enumerable. + @return {Object} the passed object + */ + removeObject: Ember.required(Function), - 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); - } + /** + Removes each object in the passed enumerable from the receiver. - return mid; + @method removeObjects + @param {Ember.Enumerable} objects the objects to remove + @return {Object} receiver + */ + removeObjects: function(objects) { + Ember.beginPropertyChanges(this); + forEach(objects, function(obj) { this.removeObject(obj); }, this); + Ember.endPropertyChanges(this); + return this; } + }); })(); @@ -12307,215 +13351,304 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { @module ember @submodule ember-runtime */ +// .......................................................... +// CONSTANTS +// + +var OUT_OF_RANGE_EXCEPTION = "Index out of range" ; +var EMPTY = []; + +// .......................................................... +// HELPERS +// -var get = Ember.get, set = Ember.set, isGlobalPath = Ember.isGlobalPath, - forEach = Ember.EnumerableUtils.forEach, replace = Ember.EnumerableUtils.replace; +var get = Ember.get, set = Ember.set; /** - `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. + This mixin defines the API for modifying array-like objects. These methods + can be applied only to a collection that keeps its items in an ordered set. - 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. + Note that an Array can change even if it does not implement this mixin. + For example, one might implement a SparseArray that cannot be directly + modified, but if its underlying enumerable changes, it will change also. - 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: + @class MutableArray + @namespace Ember + @uses Ember.Array + @uses Ember.MutableEnumerable +*/ +Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable,/** @scope Ember.MutableArray.prototype */ { - ```javascript - MyApp.listController = Ember.ArrayController.create(); + /** + __Required.__ You must implement this method to apply this mixin. - $.get('people.json', function(data) { - MyApp.listController.set('content', data); - }); - ``` + This is one of the primitives you must implement to support `Ember.Array`. + You should replace amt objects started at idx with the objects in the + passed array. You should also call `this.enumerableContentDidChange()` - Then, create a view that binds to your new controller: + @method replace + @param {Number} idx Starting index in the array to replace. If + 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* + */ + replace: Ember.required(), - ```handlebars - {{#each MyApp.listController}} - {{firstName}} {{lastName}} - {{/each}} - ``` + /** + Remove all elements from self. This is useful if you + want to reuse an existing array without having to recreate it. - 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. + ```javascript + var colors = ["red", "green", "blue"]; + color.length(); // 3 + colors.clear(); // [] + colors.length(); // 0 + ``` - 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. + @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; + }, - For example: + /** + This will use the primitive `replace()` method to insert an object at the + specified index. - ```handlebars - {{#each post in controller}} -
  • {{title}} ({{titleLength}} characters)
  • - {{/each}} - ``` + ```javascript + var colors = ["red", "green", "blue"]; + colors.insertAt(2, "yellow"); // ["red", "green", "yellow", "blue"] + colors.insertAt(5, "orange"); // Error: Index out of range + ``` - ```javascript - App.PostsController = Ember.ArrayController.extend({ - itemController: 'post' - }); + @method insertAt + @param {Number} idx index of insert the object at. + @param {Object} object object to insert + @return this + */ + insertAt: function(idx, object) { + if (idx > get(this, 'length')) throw new Error(OUT_OF_RANGE_EXCEPTION) ; + this.replace(idx, 0, [object]) ; + return this ; + }, - App.PostController = Ember.ObjectController.extend({ - // the `title` property will be proxied to the underlying post. + /** + 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. - titleLength: function() { - return this.get('title').length; - }.property('title') - }); - ``` + If you pass a start and length that is beyond the + length this method will throw an `OUT_OF_RANGE_EXCEPTION` - In some cases it is helpful to return a different `itemController` depending - on the particular item. Subclasses can do this by overriding - `lookupItemController`. + ```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 + ``` - For example: + @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) { - ```javascript - App.MyArrayController = Ember.ArrayController.extend({ - lookupItemController: function( object ) { - if (object.get('isSpecial')) { - return "special"; // use App.SpecialController - } else { - return "regular"; // use App.RegularController + if ((start < 0) || (start >= get(this, 'length'))) { + throw new Error(OUT_OF_RANGE_EXCEPTION); } - } - }); - ``` - @class ArrayController - @namespace Ember - @extends Ember.ArrayProxy - @uses Ember.SortableMixin - @uses Ember.ControllerMixin -*/ + // fast case + if (len === undefined) len = 1; + this.replace(start, len, EMPTY); + } -Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, - Ember.SortableMixin, { + return this ; + }, /** - The controller used to wrap items, if any. - - @property itemController - @type String - @default null - */ - itemController: null, + Push the object onto the end of the array. Works just like `push()` but it + is KVO-compliant. - /** - 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. + ```javascript + var colors = ["red", "green", "blue"]; + colors.pushObject("black"); // ["red", "green", "blue", "black"] + colors.pushObject(["yellow", "orange"]); // ["red", "green", "blue", "black", ["yellow", "orange"]] + ``` - For example: + @method pushObject + @param {*} obj object to push + @return {*} the same obj passed as param + */ + pushObject: function(obj) { + this.insertAt(get(this, 'length'), obj) ; + return obj ; + }, + + /** + Add the objects in the passed numerable to the end of the array. Defers + notifying observers of the change until all objects are added. ```javascript - App.MyArrayController = Ember.ArrayController.extend({ - lookupItemController: function( object ) { - if (object.get('isSpecial')) { - return "special"; // use App.SpecialController - } else { - return "regular"; // use App.RegularController - } - } - }); + var colors = ["red", "green", "blue"]; + colors.pushObjects(["black"]); // ["red", "green", "blue", "black"] + colors.pushObjects(["yellow", "orange"]); // ["red", "green", "blue", "black", "yellow", "orange"] ``` - @method - @type String - @default null + @method pushObjects + @param {Ember.Enumerable} objects the objects to add + @return {Ember.Array} receiver */ - lookupItemController: function(object) { - return get(this, 'itemController'); + 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); + return this; }, - objectAtContent: function(idx) { - var length = get(this, 'length'), - object = get(this,'arrangedContent').objectAt(idx); + /** + Pop object from array or nil if none are left. Works just like `pop()` but + it is KVO-compliant. - if (idx >= 0 && idx < length) { - var controllerClass = this.lookupItemController(object); - if (controllerClass) { - return this.controllerAt(idx, object, controllerClass); - } - } + ```javascript + var colors = ["red", "green", "blue"]; + colors.popObject(); // "blue" + console.log(colors); // ["red", "green"] + ``` - // When `controllerClass` is falsy, we have not opted in to using item - // controllers, so return the object directly. + @method popObject + @return object + */ + popObject: function() { + var len = get(this, 'length') ; + if (len === 0) return null ; - // 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; + var ret = this.objectAt(len-1) ; + this.removeAt(len-1, 1) ; + return ret ; }, - arrangedContentDidChange: function() { - this._super(); - this._resetSubContainers(); - }, + /** + Shift an object from start of array or nil if none are left. Works just + like `shift()` but it is KVO-compliant. - arrayContentDidChange: function(idx, removedCnt, addedCnt) { - var subContainers = get(this, 'subContainers'), - subContainersToRemove = subContainers.slice(idx, idx+removedCnt); + ```javascript + var colors = ["red", "green", "blue"]; + colors.shiftObject(); // "red" + console.log(colors); // ["green", "blue"] + ``` - forEach(subContainersToRemove, function(subContainer) { - if (subContainer) { subContainer.destroy(); } - }); + @method shiftObject + @return object + */ + shiftObject: function() { + if (get(this, 'length') === 0) return null ; + var ret = this.objectAt(0) ; + this.removeAt(0) ; + return ret ; + }, - replace(subContainers, idx, removedCnt, new Array(addedCnt)); + /** + Unshift an object to start of array. Works just like `unshift()` but it is + KVO-compliant. - // The shadow array of subcontainers must be updated before we trigger - // observers, otherwise observers will get the wrong subcontainer when - // calling `objectAt` - this._super(idx, removedCnt, addedCnt); - }, + ```javascript + var colors = ["red", "green", "blue"]; + colors.unshiftObject("yellow"); // ["yellow", "red", "green", "blue"] + colors.unshiftObject(["black", "white"]); // [["black", "white"], "yellow", "red", "green", "blue"] + ``` - init: function() { - this._super(); - if (!this.get('content')) { this.set('content', Ember.A()); } - this._resetSubContainers(); + @method unshiftObject + @param {*} obj object to unshift + @return {*} the same obj passed as param + */ + unshiftObject: function(obj) { + this.insertAt(0, obj) ; + return obj ; }, - controllerAt: function(idx, object, controllerClass) { - var container = get(this, 'container'), - subContainers = get(this, 'subContainers'), - subContainer = subContainers[idx], - controller; + /** + Adds the named objects to the beginning of the array. Defers notifying + observers until all objects have been added. - if (!subContainer) { - subContainer = subContainers[idx] = container.child(); - } + ```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 + ``` - controller = subContainer.lookup("controller:" + controllerClass); - if (!controller) { - throw new Error('Could not resolve itemController: "' + controllerClass + '"'); - } + @method unshiftObjects + @param {Ember.Enumerable} objects the objects to add + @return {Ember.Array} receiver + */ + unshiftObjects: function(objects) { + this.replace(0, 0, objects); + return this; + }, - controller.set('target', this); - controller.set('content', object); + /** + Reverse objects in the array. Works just like `reverse()` but it is + KVO-compliant. - return controller; + @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; }, - subContainers: null, + /** + Replace all the the receiver's content with content of the argument. + If argument is an empty array receiver will be cleared. - _resetSubContainers: function() { - var subContainers = get(this, 'subContainers'); + ```javascript + var colors = ["red", "green", "blue"]; + colors.setObjects(["black", "white"]); // ["black", "white"] + colors.setObjects([]); // [] + ``` - if (subContainers) { - forEach(subContainers, function(subContainer) { - if (subContainer) { subContainer.destroy(); } - }); + @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(); + + var len = get(this, 'length'); + this.replace(0, len, objects); + return this; + }, + + // .......................................................... + // IMPLEMENT Ember.MutableEnumerable + // + + 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 ; + }, - this.set('subContainers', Ember.A()); + addObject: function(obj) { + if (!this.contains(obj)) this.pushObject(obj); + return this ; } + }); })(); @@ -12528,715 +13661,871 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, @submodule ember-runtime */ -/** - `Ember.ObjectController` is part of Ember's Controller layer. A single shared - instance of each `Ember.ObjectController` subclass in your application's - namespace will be created at application initialization and be stored on your - application's `Ember.Router` instance. +var get = Ember.get, + set = Ember.set, + slice = Array.prototype.slice, + getProperties = Ember.getProperties; - `Ember.ObjectController` derives its functionality from its superclass - `Ember.ObjectProxy` and the `Ember.ControllerMixin` mixin. +/** + ## Overview - @class ObjectController - @namespace Ember - @extends Ember.ObjectProxy - @uses Ember.ControllerMixin -**/ -Ember.ObjectController = Ember.ObjectProxy.extend(Ember.ControllerMixin); + 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. -(function() { + ## 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. + ## Observing Property Changes -(function() { -/** -Ember Runtime + 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: -@module ember -@submodule ember-runtime -@requires ember-metal -*/ + ```javascript + Ember.Object.extend({ + valueObserver: function() { + // Executes whenever the "value" property changes + }.observes('value') + }); + ``` -})(); + 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. -(function() { -/** -@module ember -@submodule ember-views -*/ + To add an observer for a property, call: -var jQuery = Ember.imports.jQuery; -Ember.assert("Ember Views require jQuery 1.8 or 1.9", jQuery && (jQuery().jquery.match(/^1\.(8|9)(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_JQUERY)); + ```javascript + object.addObserver('propertyKey', targetObject, targetAction) + ``` -/** - Alias for jQuery + This will call the `targetAction` method on the `targetObject` whenever + the value of the `propertyKey` changes. - @method $ - @for Ember -*/ -Ember.$ = jQuery; + 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. -})(); + @class Observable + @namespace Ember +*/ +Ember.Observable = Ember.Mixin.create({ + /** + 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. -(function() { -/** -@module ember -@submodule ember-views -*/ + 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. -// 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'); + ### Computed Properties -// 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'] }; -}); + Computed properties are methods defined with the `property` modifier + declared at the end, such as: -})(); + ```javascript + fullName: function() { + return this.getEach('firstName', 'lastName').compact().join(' '); + }.property('firstName', 'lastName') + ``` + 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 -(function() { -/** -@module ember -@submodule ember-views -*/ + 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. -/*** BEGIN METAMORPH HELPERS ***/ + @method get + @param {String} keyName The property to retrieve + @return {Object} The property value or undefined. + */ + get: function(keyName) { + return get(this, keyName); + }, -// 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 = (function(){ - var testEl = document.createElement('div'); - testEl.innerHTML = "
    "; - testEl.firstChild.innerHTML = ""; - return testEl.firstChild.innerHTML === ''; -})(); + /** + To get multiple properties at once, call `getProperties` + with a list of strings or an array: -// 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 = (function() { - var testEl = document.createElement('div'); - testEl.innerHTML = "Test: Value"; - return testEl.childNodes[0].nodeValue === 'Test:' && - testEl.childNodes[2].nodeValue === ' Value'; -})(); + ```javascript + record.getProperties('firstName', 'lastName', 'zipCode'); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } + ``` -// Use this to find children by ID instead of using jQuery -var findChildById = function(element, id) { - if (element.getAttribute('id') === id) { return element; } + is equivalent to: - 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; - } + ### Chaining - innerHTMLTags[tagName] = canSet; + In addition to property changes, `set()` returns the value of the object + itself so you can do chaining like this: - return canSet; -}; + ```javascript + record.set('firstName', 'Charles').set('lastName', 'Jolley'); + ``` -var setInnerHTML = function(element, html) { - var tagName = element.tagName; + @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; + }, - if (canSetInnerHTML(tagName)) { - setInnerHTMLWithoutFix(element, html); - } else { - Ember.assert("Can't set innerHTML on "+element.tagName+" in this browser", element.outerHTML); + /** + To set multiple properties at once, call `setProperties` + with a Hash: - var startTag = element.outerHTML.match(new RegExp("<"+tagName+"([^>]*)>", 'i'))[0], - endTag = ''; + ```javascript + record.setProperties({ firstName: 'Charles', lastName: 'Jolley' }); + ``` - var wrapper = document.createElement('div'); - setInnerHTMLWithoutFix(wrapper, startTag + html + endTag); - element = wrapper.firstChild; - while (element.tagName !== tagName) { - element = element.nextSibling; - } - } + @method setProperties + @param {Hash} hash the hash of keys and values to set + @return {Ember.Observable} + */ + setProperties: function(hash) { + return Ember.setProperties(this, hash); + }, - return element; -}; + /** + Begins a grouping of property changes. -function isSimpleClick(event) { - var modifier = event.shiftKey || event.metaKey || event.altKey || event.ctrlKey, - secondaryClick = event.which > 1; // IE9 may return undefined + 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. - return !modifier && !secondaryClick; -} + @method beginPropertyChanges + @return {Ember.Observable} + */ + beginPropertyChanges: function() { + Ember.beginPropertyChanges(); + return this; + }, -Ember.ViewUtils = { - setInnerHTML: setInnerHTML, - isSimpleClick: isSimpleClick -}; + /** + 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. + @method endPropertyChanges + @return {Ember.Observable} + */ + endPropertyChanges: function() { + Ember.endPropertyChanges(); + return this; + }, + /** + Notify the observer system that a property is about to change. -(function() { -/** -@module ember -@submodule ember-views -*/ + 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. -var get = Ember.get, set = Ember.set; -var indexOf = Ember.ArrayPolyfills.indexOf; + 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. + @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; + }, + /** + Notify the observer system that a property has just changed. + 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. + 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. -var ClassSet = function() { - this.seen = {}; - this.list = []; -}; + @method propertyDidChange + @param {String} keyName The property key that has just changed. + @return {Ember.Observable} + */ + propertyDidChange: function(keyName) { + Ember.propertyDidChange(this, keyName); + return this; + }, -ClassSet.prototype = { - add: function(string) { - if (string in this.seen) { return; } - this.seen[string] = true; + /** + Convenience method to call `propertyWillChange` and `propertyDidChange` in + succession. - this.list.push(string); + @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; }, - toDOM: function() { - return this.list.join(" "); - } -}; + addBeforeObserver: function(key, target, method) { + Ember.addBeforeObserver(this, key, target, method); + }, -/** - `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. + /** + Adds an observer on a property. - @class RenderBuffer - @namespace Ember - @constructor -*/ -Ember.RenderBuffer = function(tagName) { - return new Ember._RenderBuffer(tagName); -}; + This is the core method used to register an observer for a property. -Ember._RenderBuffer = function(tagName) { - this.tagNames = [tagName || null]; - this.buffer = []; -}; + 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. -Ember._RenderBuffer.prototype = -/** @scope Ember.RenderBuffer.prototype */ { + 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. - // The root view's element - _element: null, + ### Observer Methods - /** - @private + Observer methods you pass should generally have the following signature if + you do not pass a `context` parameter: - An internal set used to de-dupe class names when `addClass()` is - used. After each call to `addClass()`, the `classes` property - will be updated. + ```javascript + fooDidChange: function(sender, key, value, rev) { }; + ``` - @property elementClasses - @type Array - @default [] - */ - elementClasses: null, + 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. - /** - Array of class names which will be applied in the class attribute. + If you pass a `context` parameter, the context will be passed before the + revision like so: - You can use `setClasses()` to set this property directly. If you - use `addClass()`, it will be maintained for you. + ```javascript + fooDidChange: function(sender, key, value, context, rev) { }; + ``` - @property classes - @type Array - @default [] + 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. + + @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 */ - classes: null, + addObserver: function(key, target, method) { + Ember.addObserver(this, key, target, method); + }, /** - The id in of the element, to be applied in the id attribute. - - You should not set this property yourself, rather, you should use - the `id()` method of `Ember.RenderBuffer`. + 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. - @property elementId - @type String - @default null + @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 */ - elementId: null, + removeObserver: function(key, target, method) { + Ember.removeObserver(this, key, target, method); + }, /** - 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'}`. - - You should not maintain this hash yourself, rather, you should use - the `attr()` method of `Ember.RenderBuffer`. + 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. - @property elementAttributes - @type Hash - @default {} + @method hasObserverFor + @param {String} key Key to check + @return {Boolean} */ - elementAttributes: null, + hasObserverFor: function(key) { + return Ember.hasListeners(this, key+':change'); + }, /** - 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}`. - - You should not maintain this hash yourself, rather, you should use - the `prop()` method of `Ember.RenderBuffer`. - - @property elementProperties - @type Hash - @default {} + @deprecated + @method getPath + @param {String} path The property path to retrieve + @return {Object} The property value or undefined. */ - elementProperties: null, + getPath: function(path) { + Ember.deprecate("getPath is deprecated since get now supports paths"); + return this.get(path); + }, /** - The tagname of the element an instance of `Ember.RenderBuffer` represents. + @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); + }, - 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 + /** + Retrieves the value of a property, or a default value in the case that the + property returns `undefined`. ```javascript - Ember.RenderBuffer('p') + person.getWithDefault('lastName', 'Doe'); ``` - @property elementTag - @type String - @default null + @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. */ - elementTag: null, + getWithDefault: function(keyName, defaultValue) { + return Ember.getWithDefault(this, keyName, defaultValue); + }, /** - 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'}`. + Set the value of a property to the current value plus some amount. - You should not maintain this hash yourself, rather, you should use - the `style()` method of `Ember.RenderBuffer`. + ```javascript + person.incrementProperty('age'); + team.incrementProperty('score', 2); + ``` - @property elementStyle - @type Hash - @default {} + @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 */ - elementStyle: null, + 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); + }, /** - Nested `RenderBuffers` will set this to their parent `RenderBuffer` - instance. - - @property parentBuffer - @type Ember._RenderBuffer - */ - parentBuffer: null, + Set the value of a property to the current value minus some amount. - /** - Adds a string of HTML to the `RenderBuffer`. + ```javascript + player.decrementProperty('lives'); + orc.decrementProperty('health', 5); + ``` - @method push - @param {String} string HTML to push into the buffer - @chainable + @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 */ - push: function(string) { - this.buffer.push(string); - return this; + 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); }, /** - Adds a class to the buffer, which will be rendered to the class attribute. - - @method addClass - @param {String} className Class name to add to the buffer - @chainable - */ - addClass: function(className) { - // lazily create elementClasses - var elementClasses = this.elementClasses = (this.elementClasses || new ClassSet()); - this.elementClasses.add(className); - this.classes = this.elementClasses.list; + Set the value of a boolean property to the opposite of it's + current value. - return this; - }, + ```javascript + starship.toggleProperty('warpDriveEngaged'); + ``` - setClasses: function(classNames) { - this.classes = classNames; + @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); }, /** - Sets the elementID to be used for the element. + 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. - @method id - @param {String} id - @chainable + @method cacheFor + @param {String} keyName + @return {Object} The cached value of the computed property, if any */ - id: function(id) { - this.elementId = id; - return this; + cacheFor: function(keyName) { + return Ember.cacheFor(this, keyName); }, - // duck type attribute functionality like jQuery so a render buffer - // can be used like a jQuery object in attribute binding scenarios. + // intended for debugging purposes + observersForKey: function(keyName) { + return Ember.observersFor(this, keyName); + } +}); - /** - Adds an attribute which will be rendered to the element. +})(); - @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 || {}); - if (arguments.length === 1) { - return attributes[name]; - } else { - attributes[name] = value; - } - return this; - }, +(function() { +/** +@module ember +@submodule ember-runtime +*/ - /** - Remove an attribute from the list of attributes to render. +var get = Ember.get, set = Ember.set; - @method removeAttr - @param {String} name The name of the attribute - @chainable - */ - removeAttr: function(name) { - var attributes = this.elementAttributes; - if (attributes) { delete attributes[name]; } +/** +`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 this; - }, +See also `Ember.ViewTargetActionSupport`, which has +view-aware defaults for target and actionContext. - /** - Adds an property which will be rendered to the element. +@class TargetActionSupport +@namespace Ember +@extends Ember.Mixin +*/ +Ember.TargetActionSupport = Ember.Mixin.create({ + target: null, + action: null, + actionContext: null, - @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 || {}); + targetObject: Ember.computed(function() { + var target = get(this, 'target'); - if (arguments.length === 1) { - return properties[name]; + if (Ember.typeOf(target) === "string") { + var value = get(this, target); + if (value === undefined) { value = get(Ember.lookup, target); } + return value; } else { - properties[name] = value; + return target; } + }).property('target'), - return this; - }, - - /** - Remove an property from the list of properties to render. - - @method removeProp - @param {String} name The name of the property - @chainable - */ - removeProp: function(name) { - var properties = this.elementProperties; - if (properties) { delete properties[name]; } + actionContextObject: Ember.computed(function() { + var actionContext = get(this, 'actionContext'); - return this; - }, + 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'), /** - Adds a style to the style attribute which will be rendered to the element. + Send an "action" with an "actionContext" to a "target". The action, actionContext + and target will be retrieved from properties of the object. For example: - @method style - @param {String} name Name of the style - @param {String} value - @chainable - */ - style: function(name, value) { - var style = this.elementStyle = (this.elementStyle || {}); + ```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 + } + }); + ``` - this.elementStyle[name] = value; - return this; - }, + The `target`, `action`, and `actionContext` can be provided as properties of + an optional object argument to `triggerAction` as well. - begin: function(tagName) { - this.tagNames.push(tagName || null); - return this; - }, + ```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 + } + }); + ``` - pushOpeningTag: function() { - var tagName = this.currentTagName(); - if (!tagName) { 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: - if (!this._element && this.buffer.length === 0) { - this._element = this.generateElement(); - return; + ```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 } + }); + ``` - var buffer = this.buffer, - id = this.elementId, - classes = this.classes, - attrs = this.elementAttributes, - props = this.elementProperties, - style = this.elementStyle, - attr, prop; + @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'] || get(this, 'actionContextObject') || this; - buffer.push('<' + tagName); + if (target && action) { + var ret; - if (id) { - buffer.push(' id="' + this._escapeAttribute(id) + '"'); - this.elementId = null; - } - if (classes) { - buffer.push(' class="' + this._escapeAttribute(classes.join(' ')) + '"'); - this.classes = null; + if (target.send) { + ret = target.send.apply(target, [action, actionContext]); + } else { + Ember.assert("The action '" + action + "' did not exist on " + target, typeof target[action] === 'function'); + ret = target[action].apply(target, [actionContext]); + } + + if (ret !== false) ret = true; + + return ret; + } else { + return false; } + } +}); - if (style) { - buffer.push(' style="'); +})(); - for (prop in style) { - if (style.hasOwnProperty(prop)) { - buffer.push(prop + ':' + this._escapeAttribute(style[prop]) + ';'); - } - } - buffer.push('"'); - this.elementStyle = null; - } +(function() { +/** +@module ember +@submodule ember-runtime +*/ - if (attrs) { - for (attr in attrs) { - if (attrs.hasOwnProperty(attr)) { - buffer.push(' ' + attr + '="' + this._escapeAttribute(attrs[attr]) + '"'); - } - } +/** + This mixin allows for Ember objects to subscribe to and emit events. - this.elementAttributes = null; + ```javascript + App.Person = Ember.Object.extend(Ember.Evented, { + greet: function() { + // ... + this.trigger('greet'); } + }); - if (props) { - for (prop in props) { - if (props.hasOwnProperty(prop)) { - var value = props[prop]; - if (value || typeof(value) === 'number') { - if (value === true) { - buffer.push(' ' + prop + '="' + prop + '"'); - } else { - buffer.push(' ' + prop + '="' + this._escapeAttribute(props[prop]) + '"'); - } - } - } - } + var person = App.Person.create(); - this.elementProperties = null; - } + person.on('greet', function() { + console.log('Our person has greeted'); + }); - buffer.push('>'); - }, + person.greet(); - pushClosingTag: function() { - var tagName = this.tagNames.pop(); - if (tagName) { this.buffer.push(''); } - }, + // outputs: 'Our person has greeted' + ``` - currentTagName: function() { - return this.tagNames[this.tagNames.length-1]; - }, + You can also chain multiple event subscriptions: - generateElement: function() { - var tagName = this.tagNames.pop(), // pop since we don't need to close - element = document.createElement(tagName), - $element = Ember.$(element), - id = this.elementId, - classes = this.classes, - attrs = this.elementAttributes, - props = this.elementProperties, - style = this.elementStyle, - styleBuffer = '', attr, prop; + ```javascript + person.on('greet', function() { + console.log('Our person has greeted'); + }).one('greet', function() { + console.log('Offer one-time special'); + }).off('event', this, forgetThis); + ``` - if (id) { - $element.attr('id', id); - this.elementId = null; - } - if (classes) { - $element.attr('class', classes.join(' ')); - this.classes = null; - } + @class Evented + @namespace Ember + */ +Ember.Evented = Ember.Mixin.create({ - if (style) { - for (prop in style) { - if (style.hasOwnProperty(prop)) { - styleBuffer += (prop + ':' + style[prop] + ';'); - } - } + /** + Subscribes to a named event with given function. - $element.attr('style', styleBuffer); + ```javascript + person.on('didLoad', function() { + // fired once the person has loaded + }); + ``` - this.elementStyle = null; - } + 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. - if (attrs) { - for (attr in attrs) { - if (attrs.hasOwnProperty(attr)) { - $element.attr(attr, attrs[attr]); - } - } + @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; + }, - this.elementAttributes = 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. - if (props) { - for (prop in props) { - if (props.hasOwnProperty(prop)) { - $element.prop(prop, props[prop]); - } - } + 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.elementProperties = null; + @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; } - return element; + Ember.addListener(this, name, target, method, true); + return this; }, /** - @method element - @return {DOMElement} The element corresponding to the generated HTML - of this buffer - */ - element: function() { - var html = this.innerString(); + Triggers a named event for the object. Any additional arguments + will be passed as parameters to the functions that are subscribed to the + event. - if (html) { - this._element = Ember.ViewUtils.setInnerHTML(this._element, html); + ```javascript + person.on('didEat', function(food) { + console.log('person ate some ' + food); + }); + + 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); + }, - return this._element; + /** + Cancels subscription for given name, target, and method. + + @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; }, /** - Generates the HTML content for this buffer. + Checks to see if object has any subscriptions for named event. - @method string - @return {String} The generated HTML + @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); + } +}); + +})(); + + + +(function() { +var RSVP = requireModule("rsvp"); + +RSVP.configure('async', function(callback, promise) { + Ember.run.schedule('actions', promise, callback, promise); +}); + +/** +@module ember +@submodule ember-runtime +*/ + +var get = Ember.get; + +/** + @class Deferred + @namespace Ember + */ +Ember.DeferredMixin = Ember.Mixin.create({ + /** + Add handlers to be called when the Deferred object is resolved or rejected. + + @method then + @param {Function} resolve a callback function to be called when done + @param {Function} reject a callback function to be called when failed */ - string: function() { - if (this._element) { - return this.element().outerHTML; - } else { - return this.innerString(); + then: function(resolve, reject) { + 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); + } } - }, - innerString: function() { - return this.buffer.join(''); + return promise.then(resolve && fulfillmentHandler, reject); }, - _escapeAttribute: function(value) { - // Stolen shamelessly from Handlebars + /** + Resolve a Deferred object and call any `doneCallbacks` with the given args. - var escape = { - "<": "<", - ">": ">", - '"': """, - "'": "'", - "`": "`" - }; + @method resolve + */ + resolve: function(value) { + var deferred, promise; - var badChars = /&(?!\w+;)|[<>"'`]/g; - var possible = /[&<>"'`]/; + deferred = get(this, '_deferred'); + promise = deferred.promise; - var escapeChar = function(chr) { - return escape[chr] || "&"; - }; + if (value === this) { + deferred.resolve(promise); + } else { + deferred.resolve(value); + } + }, - var string = value.toString(); + /** + Reject a Deferred object and call any `failCallbacks` with the given args. - if(!possible.test(string)) { return string; } - return string.replace(badChars, escapeChar); - } + @method reject + */ + reject: function(value) { + get(this, '_deferred').reject(value); + }, + + _deferred: Ember.computed(function() { + return RSVP.defer(); + }) +}); -}; })(); @@ -13245,2973 +14534,2955 @@ Ember._RenderBuffer.prototype = (function() { /** @module ember -@submodule ember-views +@submodule ember-runtime */ -var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; +var get = Ember.get; /** - `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. + 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. - @class EventDispatcher + `Ember.ActionHandler` is used internally by Ember in `Ember.View`, + `Ember.Controller`, and `Ember.Route`. + + @class ActionHandler @namespace Ember - @private - @extends Ember.Object */ -Ember.EventDispatcher = Ember.Object.extend( -/** @scope Ember.EventDispatcher.prototype */{ +Ember.ActionHandler = Ember.Mixin.create({ + mergedProperties: ['_actions'], /** @private - The root DOM element to which event listeners should be attached. Event - listeners will be attached to the document unless this is overridden. - - 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. + 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 rootElement - @type DOMElement - @default 'body' + @method willMergeMixin */ - rootElement: 'body', + willMergeMixin: function(props) { + if (props.actions && !props._actions) { + props._actions = Ember.merge(props._actions || {}, props.actions); + delete props.actions; + } + }, - /** - @private + send: function(actionName) { + var args = [].slice.call(arguments, 1), target; - Sets up event listeners for standard browser events. + 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; + } + } - 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. + 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); + } + } - @method setup - @param addedEvents {Hash} - */ - setup: function(addedEvents) { - var event, 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' - }; +}); - Ember.$.extend(events, addedEvents || {}); +})(); - var rootElement = Ember.$(get(this, 'rootElement')); - 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); - rootElement.addClass('ember-application'); +(function() { +var set = Ember.set, get = Ember.get, + resolve = Ember.RSVP.resolve, + rethrow = Ember.RSVP.rethrow, + not = Ember.computed.not, + or = Ember.computed.or; - 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')); +/** + @module ember + @submodule ember-runtime + */ - for (event in events) { - if (events.hasOwnProperty(event)) { - this.setupHandler(rootElement, event, events[event]); - } - } - }, - - /** - @private +function installPromise(proxy, promise) { + promise.then(function(value) { + set(proxy, 'isFulfilled', true); + set(proxy, 'content', value); - Registers an event listener on the document. If the given event is - triggered, the provided event handler will be triggered on the target view. + return value; + }, function(reason) { + set(proxy, 'isRejected', true); + set(proxy, 'reason', reason); + }).fail(rethrow); +} - 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. +/** + A low level mixin making ObjectProxy, ObjectController or ArrayController's promise aware. - For example, to have the `mouseDown` method called on the target view when - a `mousedown` event is received from the browser, do the following: + ```javascript + var ObjectPromiseController = Ember.ObjectController.extend(Ember.PromiseProxyMixin); - ```javascript - setupHandler('mousedown', 'mouseDown'); - ``` + var controller = ObjectPromiseController.create({ + promise: $.getJSON('/some/remote/data.json') + }); - @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; + controller.then(function(json){ + // the json + }, function(reason) { + // the reason why you have no json + }); + ``` - rootElement.delegate('.ember-view', event + '.ember', function(evt, triggeringManager) { - return Ember.handleErrors(function() { - var view = Ember.View.views[this.id], - result = true, manager = null; + the controller has bindable attributes which + track the promises life cycle - manager = self._findNearestEventManager(view,eventName); + ```javascript + controller.get('isPending') //=> true + controller.get('isSettled') //=> false + controller.get('isRejected') //=> false + controller.get('isFulfilled') //=> false + ``` - if (manager && manager !== triggeringManager) { - result = self._dispatchEvent(manager, evt, eventName, view); - } else if (view) { - result = self._bubbleEvent(view,evt,eventName); - } else { - evt.stopPropagation(); - } + When the the $.getJSON completes, and the promise is fulfilled + with json, the life cycle attributes will update accordingly. - return result; - }, this); - }); + ```javascript + controller.get('isPending') //=> false + controller.get('isSettled') //=> true + controller.get('isRejected') //=> false + controller.get('isFulfilled') //=> true + ``` - rootElement.delegate('[data-ember-action]', event + '.ember', function(evt) { - return Ember.handleErrors(function() { - var actionId = Ember.$(evt.currentTarget).attr('data-ember-action'), - action = Ember.Handlebars.ActionHelper.registeredActions[actionId]; + As the controller is an ObjectController, and the json now its content, + all the json properties will be available directly from the controller. - // 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); - }); - }, + ```javascript + // Assuming the following json: + { + firstName: 'Stefan', + lastName: 'Penner' + } - _findNearestEventManager: function(view, eventName) { - var manager = null; + // both properties will accessible on the controller + controller.get('firstName') //=> 'Stefan' + controller.get('lastName') //=> 'Penner' + ``` - while (view) { - manager = get(view, 'eventManager'); - if (manager && manager[eventName]) { break; } + If the controller is backing a template, the attributes are + bindable from within that template + ```handlebars + {{#if isPending}} + loading... + {{else}} + firstName: {{firstName}} + lastName: {{lastName}} + {{/if}} + ``` + @class Ember.PromiseProxyMixin +*/ +Ember.PromiseProxyMixin = Ember.Mixin.create({ + reason: null, + isPending: not('isSettled').readOnly(), + isSettled: or('isRejected', 'isFulfilled').readOnly(), + isRejected: false, + isFulfilled: false, - view = get(view, 'parentView'); + promise: Ember.computed(function(key, promise) { + if (arguments.length === 2) { + promise = resolve(promise); + installPromise(this, promise); + return promise; + } else { + throw new Error("PromiseProxy's promise must be set"); } + }), - return manager; - }, + then: function(fulfill, reject) { + return get(this, 'promise').then(fulfill, reject); + } +}); - _dispatchEvent: function(object, evt, eventName, view) { - var result = true; - var handler = object[eventName]; - if (Ember.typeOf(handler) === 'function') { - result = handler.call(object, evt, view); - // Do not preventDefault in eventManagers. - evt.stopPropagation(); - } - else { - result = this._bubbleEvent(view, evt, eventName); - } +})(); - return result; - }, - _bubbleEvent: function(view, evt, eventName) { - return Ember.run(function() { - return view.handleEvent(eventName, evt); - }); - }, - destroy: function() { - var rootElement = get(this, 'rootElement'); - Ember.$(rootElement).undelegate('.ember').removeClass('ember-application'); - return this._super(); - } -}); +(function() { })(); (function() { +var get = Ember.get, + forEach = Ember.EnumerableUtils.forEach, + RETAIN = 'r', + INSERT = 'i', + DELETE = 'd'; + /** -@module ember -@submodule ember-views -*/ + 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. -// 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; -queues.splice(Ember.$.inArray('actions', queues)+1, 0, 'render', 'afterRender'); + @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 = []; } -})(); + var length = get(items, 'length'); + if (length) { + this._content = [new ArrayOperation(RETAIN, length, items)]; + } else { + this._content = []; + } +}; +Ember.TrackedArray.RETAIN = RETAIN; +Ember.TrackedArray.INSERT = INSERT; +Ember.TrackedArray.DELETE = DELETE; -(function() { -/** -@module ember -@submodule ember-views -*/ +Ember.TrackedArray.prototype = { -var get = Ember.get, set = Ember.set; + /** + Track that `newItems` were added to the tracked array at `index`. -// Original class declaration and documentation in runtime/lib/controllers/controller.js -// NOTE: It may be possible with YUIDoc to combine docs in two locations + @method addItems + @param index + @param newItems + */ + addItems: function (index, newItems) { + var count = get(newItems, 'length'), + match = this._findArrayOperation(index), + arrayOperation = match.operation, + arrayOperationIndex = match.index, + arrayOperationRangeStart = match.rangeStart, + composeIndex, + splitIndex, + splitItems, + splitArrayOperation, + newArrayOperation; -/** -Additional methods for the ControllerMixin + newArrayOperation = new ArrayOperation(INSERT, count, newItems); -@class ControllerMixin -@namespace Ember -*/ -Ember.ControllerMixin.reopen({ - target: null, - namespace: null, - view: null, - container: null, - _childContainers: null, + if (arrayOperation) { + if (!match.split) { + // insert left of arrayOperation + this._content.splice(arrayOperationIndex, 0, newArrayOperation); + composeIndex = arrayOperationIndex; + } else { + this._split(arrayOperationIndex, index - arrayOperationRangeStart, newArrayOperation); + composeIndex = arrayOperationIndex + 1; + } + } else { + // insert at end + this._content.push(newArrayOperation); + composeIndex = arrayOperationIndex; + } - init: function() { - this._super(); - set(this, '_childContainers', {}); + this._composeInsert(composeIndex); }, - _modelDidChange: Ember.observer(function() { - var containers = get(this, '_childContainers'), - container; + /** + Track that `count` items were removed at `index`. - for (var prop in containers) { - if (!containers.hasOwnProperty(prop)) { continue; } - containers[prop].destroy(); + @method removeItems + @param index + @param count + */ + removeItems: function (index, count) { + var match = this._findArrayOperation(index), + arrayOperation = match.operation, + arrayOperationIndex = match.index, + arrayOperationRangeStart = match.rangeStart, + newArrayOperation, + composeIndex; + + newArrayOperation = new ArrayOperation(DELETE, count); + if (!match.split) { + // insert left of arrayOperation + this._content.splice(arrayOperationIndex, 0, newArrayOperation); + composeIndex = arrayOperationIndex; + } else { + this._split(arrayOperationIndex, index - arrayOperationRangeStart, newArrayOperation); + composeIndex = arrayOperationIndex + 1; } - set(this, '_childContainers', {}); - }, 'model') -}); + return this._composeDelete(composeIndex); + }, -})(); + /** + Apply all operations, reducing them to retain:n, for `n`, the number of + items in the array. + `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}` + @method apply -(function() { + @param {function} callback + */ + apply: function (callback) { + var items = [], + offset = 0; -})(); + forEach(this._content, function (arrayOperation) { + callback(arrayOperation.items, offset, arrayOperation.operation); + if (arrayOperation.operation !== DELETE) { + offset += arrayOperation.count; + items = items.concat(arrayOperation.items); + } + }); + this._content = [new ArrayOperation(RETAIN, items.length, items)]; + }, -(function() { -var states = {}; + /** + Return an ArrayOperationMatch for the operation that contains the item at `index`. -/** -@module ember -@submodule ember-views -*/ + @method _findArrayOperation -var get = Ember.get, set = Ember.set, addObserver = Ember.addObserver, removeObserver = Ember.removeObserver; -var meta = Ember.meta, guidFor = Ember.guidFor, fmt = Ember.String.fmt; -var a_slice = [].slice; -var a_forEach = Ember.EnumerableUtils.forEach; -var a_addObject = Ember.EnumerableUtils.addObject; + @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; -var childViewsProperty = Ember.computed(function() { - var childViews = this._childViews, ret = Ember.A(), view = this; + // 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._content.length; arrayOperationIndex < len; ++arrayOperationIndex) { + arrayOperation = this._content[arrayOperationIndex]; - a_forEach(childViews, function(view) { - if (view.isVirtual) { - ret.pushObjects(get(view, 'childViews')); - } else { - ret.push(view); - } - }); + if (arrayOperation.operation === DELETE) { continue; } - ret.replace = function (idx, removedCount, addedViews) { - if (view instanceof Ember.ContainerView) { - Ember.deprecate("Manipulating a Ember.ContainerView through its childViews property is deprecated. Please use the ContainerView instance itself as an Ember.MutableArray."); - return view.replace(idx, removedCount, addedViews); + arrayOperationRangeEnd = arrayOperationRangeStart + arrayOperation.count - 1; + + if (index === arrayOperationRangeStart) { + break; + } else if (index > arrayOperationRangeStart && index <= arrayOperationRangeEnd) { + split = true; + break; + } else { + arrayOperationRangeStart = arrayOperationRangeEnd + 1; + } } - throw new Error("childViews is immutable"); - }; - return ret; -}); + return new ArrayOperationMatch(arrayOperation, arrayOperationIndex, split, arrayOperationRangeStart); + }, -Ember.warn("The VIEW_PRESERVES_CONTEXT flag has been removed and the functionality can no longer be disabled.", Ember.ENV.VIEW_PRESERVES_CONTEXT !== false); + _split: function (arrayOperationIndex, splitIndex, newArrayOperation) { + var arrayOperation = this._content[arrayOperationIndex], + splitItems = arrayOperation.items.slice(splitIndex), + splitArrayOperation = new ArrayOperation(arrayOperation.operation, splitItems.length, splitItems); -/** - 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. + // truncate LHS + arrayOperation.count = splitIndex; + arrayOperation.items = arrayOperation.items.slice(0, splitIndex); - @property TEMPLATES - @for Ember - @type Hash -*/ -Ember.TEMPLATES = {}; + this._content.splice(arrayOperationIndex + 1, 0, newArrayOperation, splitArrayOperation); + }, -Ember.CoreView = Ember.Object.extend(Ember.Evented, { - isView: true, + // TODO: unify _composeInsert, _composeDelete + // see SubArray for a better implementation. + _composeInsert: function (index) { + var newArrayOperation = this._content[index], + leftArrayOperation = this._content[index-1], // may be undefined + rightArrayOperation = this._content[index+1], // may be undefined + leftOp = leftArrayOperation && leftArrayOperation.operation, + rightOp = rightArrayOperation && rightArrayOperation.operation; - states: states, + if (leftOp === INSERT) { + // merge left + leftArrayOperation.count += newArrayOperation.count; + leftArrayOperation.items = leftArrayOperation.items.concat(newArrayOperation.items); - init: function() { - this._super(); + if (rightOp === INSERT) { + // also merge right + leftArrayOperation.count += rightArrayOperation.count; + leftArrayOperation.items = leftArrayOperation.items.concat(rightArrayOperation.items); + this._content.splice(index, 2); + } else { + // only merge left + this._content.splice(index, 1); + } + } else if (rightOp === INSERT) { + // merge right + newArrayOperation.count += rightArrayOperation.count; + newArrayOperation.items = newArrayOperation.items.concat(rightArrayOperation.items); + this._content.splice(index + 1, 1); + } + }, - // Register the view for event handling. This hash is used by - // Ember.EventDispatcher to dispatch incoming events. - if (!this.isVirtual) { - Ember.assert("Attempted to register a view with an id already in use: "+this.elementId, !Ember.View.views[this.elementId]); - Ember.View.views[this.elementId] = this; + _composeDelete: function (index) { + var arrayOperation = this._content[index], + deletesToGo = arrayOperation.count, + leftArrayOperation = this._content[index-1], // may be undefined + leftOp = leftArrayOperation && leftArrayOperation.operation, + nextArrayOperation, + nextOp, + nextCount, + removedItems = []; + + if (leftOp === DELETE) { + arrayOperation = leftArrayOperation; + index -= 1; } - this.addBeforeObserver('elementId', function() { - throw new Error("Changing a view's elementId after creation is not allowed"); - }); + for (var i = index + 1; deletesToGo > 0; ++i) { + nextArrayOperation = this._content[i]; + nextOp = nextArrayOperation.operation; + nextCount = nextArrayOperation.count; - this.transitionTo('preRender'); - }, + if (nextOp === DELETE) { + arrayOperation.count += nextCount; + continue; + } - /** - If the view is currently inserted into the DOM of a parent view, this - property will point to the parent of the view. + if (nextCount > deletesToGo) { + removedItems = removedItems.concat(nextArrayOperation.items.splice(0, deletesToGo)); + nextArrayOperation.count -= deletesToGo; - @property parentView - @type Ember.View - @default null - */ - parentView: Ember.computed(function() { - var parent = this._parentView; + // 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; - if (parent && parent.isVirtual) { - return get(parent, 'parentView'); + deletesToGo = 0; + } else { + removedItems = removedItems.concat(nextArrayOperation.items); + deletesToGo -= nextCount; + } + + if (nextOp === INSERT) { + arrayOperation.count -= nextCount; + } + } + + if (arrayOperation.count > 0) { + this._content.splice(index+1, i-1-index); } else { - return parent; + // The delete operation can go away; it has merely reduced some other + // operation, as in D:3 I:4 + this._content.splice(index, 1); } - }).property('_parentView'), - state: null, + return removedItems; + } +}; - _parentView: null, +function ArrayOperation (operation, count, items) { + this.operation = operation; // RETAIN | INSERT | DELETE + this.count = count; + this.items = items; +} - // return the current view, not including virtual views - concreteView: Ember.computed(function() { - if (!this.isVirtual) { return this; } - else { return get(this, 'parentView'); } - }).property('parentView').volatile(), +/** + Internal data structure used to include information when looking up operations + by item index. - instrumentName: 'core_view', + @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`. +*/ +function ArrayOperationMatch(operation, index, split, rangeStart) { + this.operation = operation; + this.index = index; + this.split = split; + this.rangeStart = rangeStart; +} - instrumentDetails: function(hash) { - hash.object = this.toString(); - }, +})(); + + + +(function() { +var get = Ember.get, + forEach = Ember.EnumerableUtils.forEach, + RETAIN = 'r', + FILTER = 'f'; + +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. + + @class SubArray + @namespace Ember +*/ +Ember.SubArray = function (length) { + if (arguments.length < 1) { length = 0; } + + if (length > 0) { + this._operations = [new Operation(RETAIN, length)]; + } else { + this._operations = []; + } +}; +Ember.SubArray.prototype = { /** - @private + Track that an item was added to the tracked array. - 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. + @method addItem - You should not need to override this method; instead, implement the - `template` property, or if you need more control, override the `render` - method. + @param {number} index The index of the item in the tracked array. + @param {boolean} match `true` iff the item is included in the subarray. - @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. + @returns {number} The index of the item in the subarray. */ - renderToBuffer: function(parentBuffer, bufferOperation) { - var name = 'render.' + this.instrumentName, - details = {}; + addItem: function(index, match) { + var returnValue = -1, + itemType = match ? RETAIN : FILTER, + self = this; - this.instrumentDetails(details); + this._findOperation(index, function(operation, operationIndex, rangeStart, rangeEnd, seenInSubArray) { + var newOperation, splitOperation; - return Ember.instrument(name, details, function() { - return this._renderToBuffer(parentBuffer, bufferOperation); - }, this); - }, + 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; - _renderToBuffer: function(parentBuffer, bufferOperation) { - Ember.run.sync(); + self._operations.splice(operationIndex + 1, 0, newOperation, splitOperation); + } - // 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; + if (match) { + if (operation.type === RETAIN) { + returnValue = seenInSubArray + (index - rangeStart); + } else { + returnValue = seenInSubArray; + } + } - if (tagName === null || tagName === undefined) { - tagName = 'div'; - } + self._composeAt(operationIndex); + }, function(seenInSubArray) { + self._operations.push(new Operation(itemType, 1)); - var buffer = this.buffer = parentBuffer && parentBuffer.begin(tagName) || Ember.RenderBuffer(tagName); - this.transitionTo('inBuffer', false); + if (match) { + returnValue = seenInSubArray; + } - this.beforeRender(buffer); - this.render(buffer); - this.afterRender(buffer); + self._composeAt(self._operations.length-1); + }); - return buffer; + return returnValue; }, /** - @private + Track that an item was removed from the tracked array. - Override the default event firing from `Ember.Evented` to - also call methods with the given name. + @method removeItem - @method trigger - @param name {String} + @param {number} index The index of the item in the tracked array. + + @returns {number} The index of the item in the subarray, or `-1` if the item + was not in the subarray. */ - 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]); + removeItem: function(index) { + var returnValue = -1, + self = this; + + this._findOperation(index, function (operation, operationIndex, rangeStart, rangeEnd, seenInSubArray) { + if (operation.type === RETAIN) { + returnValue = seenInSubArray + (index - rangeStart); } - return method.apply(this, args); - } - }, - has: function(name) { - return Ember.typeOf(this[name]) === 'function' || this._super(name); + if (operation.count > 1) { + --operation.count; + } else { + self._operations.splice(operationIndex, 1); + self._composeAt(operationIndex); + } + }); + + return returnValue; }, - willDestroy: function() { - var parent = this._parentView; - // destroy the element -- this will avoid each child view destroying - // the element over and over again... - if (!this.removedFromDOM) { this.destroyElement(); } + _findOperation: function (index, foundCallback, notFoundCallback) { + var operationIndex, + len, + operation, + rangeStart, + rangeEnd, + seenInSubArray = 0; - // 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); } + // 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; - this.transitionTo('destroyed'); + if (index >= rangeStart && index <= rangeEnd) { + foundCallback(operation, operationIndex, rangeStart, rangeEnd, seenInSubArray); + return; + } else if (operation.type === RETAIN) { + seenInSubArray += operation.count; + } + } - // next remove view from global hash - if (!this.isVirtual) delete Ember.View.views[this.elementId]; + notFoundCallback(seenInSubArray); }, - clearRenderedChildren: Ember.K, - triggerRecursively: Ember.K, - invokeRecursively: Ember.K, - transitionTo: Ember.K, - destroyElement: Ember.K -}); - -/** - `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. + _composeAt: function(index) { + var op = this._operations[index], + otherOp; - ## HTML Tag + if (!op) { + // Composing out of bounds is a no-op, as when removing the last operation + // in the list. + return; + } - 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: + if (index > 0) { + otherOp = this._operations[index-1]; + if (otherOp.type === op.type) { + op.count += otherOp.count; + this._operations.splice(index-1, 1); + } + } - ```javascript - ParagraphView = Ember.View.extend({ - tagName: 'em' - }); - ``` + 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); + } + } + } +}; - Would result in instances with the following HTML: +})(); - ```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: +(function() { +Ember.Container = requireModule('container'); +Ember.Container.set = Ember.set; - ```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: +(function() { +/** +@module ember +@submodule ember-runtime +*/ - ```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: +// 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. - ```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`. +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 - MyView = Ember.View.extend({ - classNameBindings: ['hovered'], - hovered: true - }); - ``` +var undefinedDescriptor = { + configurable: true, + writable: true, + enumerable: false, + value: undefined +}; - Will result in view instances with an HTML representation of: +function makeCtor() { - ```html -
    - ``` + // 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. - 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: + var wasApplied = false, initMixins, initProperties; - ```javascript - MyView = Ember.View.extend({ - classNameBindings: ['awesome:so-very-cool'], - awesome: true - }); - ``` + 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 (initProperties) { + // capture locally so we can clear the closed over variable + var props = initProperties; + initProperties = null; - Will result in view instances with an HTML representation of: + var concatenatedProperties = this.concatenatedProperties; - ```html -
    - ``` + for (var i = 0, l = props.length; i < l; i++) { + var properties = props[i]; - Boolean value class name bindings whose property names are in a - camelCase-style format will be converted to a dasherized format: + Ember.assert("Ember.Object.create no longer supports mixing in other definitions, use createWithMixins instead.", !(properties instanceof Ember.Mixin)); - ```javascript - MyView = Ember.View.extend({ - classNameBindings: ['isUrgent'], - isUrgent: true - }); - ``` + for (var keyName in properties) { + if (!properties.hasOwnProperty(keyName)) { continue; } - Will result in view instances with an HTML representation of: + var value = properties[keyName], + IS_BINDING = Ember.IS_BINDING; - ```html -
    - ``` + 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; + } - Class name bindings can also refer to object values that are found by - traversing a path relative to the view itself: + var desc = m.descs[keyName]; - ```javascript - MyView = Ember.View.extend({ - classNameBindings: ['messages.empty'] - messages: Ember.Object.create({ - empty: true - }) - }); - ``` + 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))); - Will result in view instances with an HTML representation of: + if (concatenatedProperties && indexOf(concatenatedProperties, keyName) >= 0) { + var baseValue = this[keyName]; - ```html -
    - ``` + if (baseValue) { + if ('function' === typeof baseValue.concat) { + value = baseValue.concat(value); + } else { + value = Ember.makeArray(baseValue).concat(value); + } + } else { + value = Ember.makeArray(value); + } + } - 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: + 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"); + }; - ```javascript - // Applies 'enabled' class when isEnabled is true and 'disabled' when isEnabled is false - Ember.View.create({ - classNameBindings: ['isEnabled:enabled:disabled'] - isEnabled: true - }); - ``` + Class.toString = Mixin.prototype.toString; + Class.willReopen = function() { + if (wasApplied) { + Class.PrototypeMixin = Mixin.create(Class.PrototypeMixin); + } - Will result in view instances with an HTML representation of: + wasApplied = false; + }; + Class._initMixins = function(args) { initMixins = args; }; + Class._initProperties = function(args) { initProperties = args; }; - ```html -
    - ``` + Class.proto = function() { + var superclass = Class.superclass; + if (superclass) { superclass.proto(); } - When isEnabled is `false`, the resulting HTML reprensentation looks like - this: + if (!wasApplied) { + wasApplied = true; + Class.PrototypeMixin.applyPartial(Class.prototype); + rewatch(Class.prototype); + } - ```html -
    - ``` + return this.prototype; + }; - This syntax offers the convenience to add a class if a property is `false`: + return Class; - ```javascript - // Applies no class when isEnabled is true and class 'disabled' when isEnabled is false - Ember.View.create({ - classNameBindings: ['isEnabled::disabled'] - isEnabled: true - }); - ``` +} - Will result in view instances with an HTML representation of: +/** + @class CoreObject + @namespace Ember +*/ +var CoreObject = makeCtor(); +CoreObject.toString = function() { return "Ember.CoreObject"; }; - ```html -
    - ``` +CoreObject.PrototypeMixin = Mixin.create({ + reopen: function() { + applyMixin(this, arguments, true); + return this; + }, - When the `isEnabled` property on the view is set to `false`, it will result - in view instances with an HTML representation of: + /** + An overridable method called when objects are instantiated. By default, + does nothing unless it is overridden during class definition. - ```html -
    - ``` + Example: - 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. + ```javascript + App.Person = Ember.Object.extend({ + init: function() { + this._super(); + alert('Name is ' + this.get('name')); + } + }); - Both `classNames` and `classNameBindings` are concatenated properties. See - `Ember.Object` documentation for more information about concatenated - properties. + var steve = App.Person.create({ + name: "Steve" + }); - ## HTML Attributes + // alerts 'Name is Steve'. + ``` - 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: + 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. - ```javascript - AnchorView = Ember.View.extend({ - tagName: 'a', - attributeBindings: ['href'], - href: 'http://google.com' - }); - ``` + @method init + */ + init: function() {}, - Will result in view instances with an HTML representation of: + /** + Defines the properties that will be concatenated from the superclass + (instead of overridden). - ```html - - ``` + 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`. - 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: + Here is some sample code showing the difference between a concatenated + property and a normal one: - ```javascript - MyTextInput = Ember.View.extend({ - tagName: 'input', - attributeBindings: ['disabled'], - disabled: true - }); - ``` + ```javascript + App.BarView = Ember.View.extend({ + someNonConcatenatedProperty: ['bar'], + classNames: ['bar'] + }); - Will result in view instances with an HTML representation of: + App.FooBarView = App.BarView.extend({ + someNonConcatenatedProperty: ['foo'], + classNames: ['foo'], + }); - ```html - - ``` + var fooBarView = App.FooBarView.create(); + fooBarView.get('someNonConcatenatedProperty'); // ['foo'] + fooBarView.get('classNames'); // ['ember-view', 'bar', 'foo'] + ``` - `attributeBindings` can refer to computed properties: + This behavior extends to object creation as well. Continuing the + above example: - ```javascript - MyTextInput = Ember.View.extend({ - tagName: 'input', - attributeBindings: ['disabled'], - disabled: function(){ - if (someLogic) { - return true; - } else { - return false; - } - }.property() - }); - ``` + ```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: - 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. + ```javascript + var view = App.FooBarView.create({ + classNames: 'baz' + }) + view.get('classNames'); // ['ember-view', 'bar', 'foo', 'baz'] + ``` - `attributeBindings` is a concatenated property. See `Ember.Object` - documentation for more information about concatenated properties. + Using the `concatenatedProperties` property, we can tell to Ember that mix + the content of the properties. - ## Templates + In `Ember.View` the `classNameBindings` and `attributeBindings` properties + are also concatenated, in addition to `classNames`. - 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. + This feature is available for you to use throughout the Ember object model, + although typical app developers are likely to use it infrequently. - ```javascript - AView = Ember.View.extend({ - template: Ember.Handlebars.compile('I am the template') - }); - ``` + @property concatenatedProperties + @type Array + @default null + */ + concatenatedProperties: null, - Will result in view instances with an HTML representation of: + /** + Destroyed object property flag. - ```html -
    I am the template
    - ``` + if this property is `true` the observers and bindings were already + removed by the effect of calling the `destroy()` method. - Within an Ember application is more common to define a Handlebars templates as - part of a page: + @property isDestroyed + @default false + */ + isDestroyed: false, - ```html - - ``` + /** + Destruction scheduled flag. The `destroy()` method has been called. - And associate it by name using a view's `templateName` property: + The object stays intact until the end of the run loop at which point + the `isDestroyed` flag is set. - ```javascript - AView = Ember.View.extend({ - templateName: 'some-template' - }); - ``` + @property isDestroying + @default false + */ + isDestroying: false, - Using a value for `templateName` that does not have a Handlebars template - with a matching `data-template-name` attribute will throw an error. + /** + Destroys an object by setting the `isDestroyed` flag and removing its + metadata, which effectively destroys observers and bindings. - Assigning a value to both `template` and `templateName` properties will throw - an error. + If you try to set a property on a destroyed object, an exception will be + raised. - 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: + Note that destruction is scheduled for the end of the run loop and does not + happen immediately. It will set an isDestroying flag immediately. - ```javascript - AView = Ember.View.extend({ - defaultTemplate: Ember.Handlebars.compile('I was the default'), - template: null, - templateName: null - }); - ``` + @method destroy + @return {Ember.Object} receiver + */ + destroy: function() { + if (this.isDestroying) { return; } + this.isDestroying = true; - Will result in instances with an HTML representation of: + schedule('actions', this, this.willDestroy); + schedule('destroy', this, this._scheduledDestroy); + return this; + }, - ```html -
    I was the default
    - ``` + /** + Override to implement teardown. - If a `template` or `templateName` is provided it will take precedence over - `defaultTemplate`: + @method willDestroy + */ + willDestroy: Ember.K, - ```javascript - AView = Ember.View.extend({ - defaultTemplate: Ember.Handlebars.compile('I was the default') - }); + /** + @private - aView = AView.create({ - template: Ember.Handlebars.compile('I was the template, not default') - }); - ``` + Invoked by the run loop to actually destroy the object. This is + scheduled for execution by the `destroy` method. - Will result in the following HTML representation when rendered: + @method _scheduledDestroy + */ + _scheduledDestroy: function() { + if (this.isDestroyed) { return; } + destroy(this); + this.isDestroyed = true; + }, - ```html -
    I was the template, not default
    - ``` + bind: function(to, from) { + if (!(from instanceof Ember.Binding)) { from = Ember.Binding.from(from); } + from.to(to).connect(this); + return from; + }, - ## View Context + /** + Returns a string representation which attempts to provide more information + than Javascript's `toString` typically does, in a generic way for all Ember + objects. - The default context of the compiled template is the view's controller: + App.Person = Em.Object.extend() + person = App.Person.create() + person.toString() //=> "" - ```javascript - AView = Ember.View.extend({ - template: Ember.Handlebars.compile('Hello {{excitedGreeting}}') - }); + If the object's class is not defined on an Ember namespace, it will + indicate it is a subclass of the registered superclass: - aController = Ember.Object.create({ - firstName: 'Barry', - excitedGreeting: function(){ - return this.get("content.firstName") + "!!!" - }.property() - }); + Student = App.Person.extend() + student = Student.create() + student.toString() //=> "<(subclass of App.Person):ember1025>" - aView = AView.create({ - controller: aController, - }); - ``` + If the method `toStringExtension` is defined, its return value will be + included in the output. - Will result in an HTML representation of: + App.Teacher = App.Person.extend({ + toStringExtension: function() { + return this.get('fullName'); + } + }); + teacher = App.Teacher.create() + teacher.toString(); //=> "" - ```html -
    Hello Barry!!!
    - ``` + @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; + } +}); - 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. +CoreObject.PrototypeMixin.ownerConstructor = CoreObject; - ## Layouts +function makeToString(ret) { + return function() { return ret; }; +} - 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. +if (Ember.config.overridePrototypeMixin) { + Ember.config.overridePrototypeMixin(CoreObject.PrototypeMixin); +} - Most typically in Ember a layout will be a compiled `Ember.Handlebars` - template. +CoreObject.__super__ = null; - A view's layout can be set directly with the `layout` property or reference - an existing Handlebars template by name with the `layoutName` property. +var ClassMixin = Mixin.create({ - 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: + ClassMixin: Ember.required(), - ```javascript - AViewWithLayout = Ember.View.extend({ - layout: Ember.Handlebars.compile("
    {{yield}}
    ") - template: Ember.Handlebars.compile("I got wrapped"), - }); - ``` + PrototypeMixin: Ember.required(), - Will result in view instances with an HTML representation of: + isClass: true, - ```html -
    -
    - I got wrapped -
    -
    - ``` + isMethod: false, - See `Handlebars.helpers.yield` for more information. + extend: function() { + var Class = makeCtor(), proto; + Class.ClassMixin = Mixin.create(this.ClassMixin); + Class.PrototypeMixin = Mixin.create(this.PrototypeMixin); - ## Responding to Browser Events + Class.ClassMixin.ownerConstructor = Class; + Class.PrototypeMixin.ownerConstructor = Class; - 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. + reopen.apply(Class.PrototypeMixin, arguments); - ### Method Implementation + Class.superclass = this; + Class.__super__ = this.prototype; - 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. + proto = Class.prototype = o_create(this.prototype); + proto.constructor = Class; + generateGuid(proto, 'ember'); + meta(proto).proto = proto; // this will disable observers on prototype - ```javascript - AView = Ember.View.extend({ - click: function(event){ - // will be called when when an instance's - // rendered element is clicked - } - }); - ``` + Class.ClassMixin.apply(Class); + return Class; + }, - ### Event Managers + /** + Equivalent to doing `extend(arguments).create()`. + If possible use the normal `create` method instead. - 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. + @method createWithMixins + @static + @param [arguments]* + */ + createWithMixins: function() { + var C = this; + if (arguments.length>0) { this._initMixins(arguments); } + return new C(); + }, - ```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 + /** + Creates an instance of a class. Accepts either no arguments, or an object + containing values to initialize the newly instantiated object with. + + ```javascript + App.Person = Ember.Object.extend({ + helloWorld: function() { + alert("Hi, my name is " + this.get('name')); } - }) - }); - ``` + }); - An event defined for an event manager takes precedence over events of the - same name handled through methods on the view. + var tom = App.Person.create({ + name: 'Tom Dale' + }); - ```javascript - AView = Ember.View.extend({ - mouseEnter: function(event){ - // will never trigger. - }, - eventManager: Ember.Object.create({ - mouseEnter: function(event, view){ - // takes presedence over AView#mouseEnter - } - }) - }); - ``` + tom.helloWorld(); // alerts "Hi, my name is Tom Dale". + ``` - 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. + `create` will call the `init` function if defined during + `Ember.AnyObject.extend` - ```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 - // OutsideView or InnerView depending on - // where on the page the user interaction occured - } - }) - }); + If no arguments are passed to `create`, it will not set values to the new + instance during initialization: - 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. - } - }); - ``` + ```javascript + var noName = App.Person.create(); + noName.helloWorld(); // alerts undefined + ``` - ### Handlebars `{{action}}` Helper + 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. - See `Handlebars.helpers.action`. + @method create + @static + @param [arguments]* + */ + create: function() { + var C = this; + if (arguments.length>0) { this._initProperties(arguments); } + return new C(); + }, - ### Event Names + /** + + Augments a constructor's prototype with additional + properties and functions: + + ```javascript + MyObject = Ember.Object.extend({ + name: 'an object' + }); - Possible events names for any of the responding approaches described above - are: + o = MyObject.create(); + o.get('name'); // 'an object' - Touch events: + MyObject.reopen({ + say: function(msg){ + console.log(msg); + } + }) - * `touchStart` - * `touchMove` - * `touchEnd` - * `touchCancel` + o2 = MyObject.create(); + o2.say("hello"); // logs "hello" - Keyboard events + o.say("goodbye"); // logs "goodbye" + ``` + + To add functions and properties to the constructor itself, + see `reopenClass` - * `keyDown` - * `keyUp` - * `keyPress` + @method reopen + */ + reopen: function() { + this.willReopen(); + reopen.apply(this.PrototypeMixin, arguments); + return this; + }, - Mouse events + /** + Augments a constructor's own properties and functions: + + ```javascript + MyObject = Ember.Object.extend({ + name: 'an object' + }); - * `mouseDown` - * `mouseUp` - * `contextMenu` - * `click` - * `doubleClick` - * `mouseMove` - * `focusIn` - * `focusOut` - * `mouseEnter` - * `mouseLeave` - Form events: + MyObject.reopenClass({ + canBuild: false + }); + + MyObject.canBuild; // false + o = MyObject.create(); + ``` + + To add functions and properties to instances of + a constructor by extending the constructor's prototype + see `reopen` + + @method reopenClass + */ + reopenClass: function() { + reopen.apply(this.ClassMixin, arguments); + applyMixin(this, arguments, false); + return this; + }, - * `submit` - * `change` - * `focusIn` - * `focusOut` - * `input` + detect: function(obj) { + if ('function' !== typeof obj) { return false; } + while(obj) { + if (obj===this) { return true; } + obj = obj.superclass; + } + return false; + }, - HTML5 drag and drop events: + detectInstance: function(obj) { + return obj instanceof this; + }, - * `dragStart` - * `drag` - * `dragEnter` - * `dragLeave` - * `drop` - * `dragEnd` + /** + 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. - ## Handlebars `{{view}}` Helper + You can pass a hash of these values to a computed property like this: - Other `Ember.View` instances can be included as part of a view's template by - using the `{{view}}` Handlebars helper. See `Handlebars.helpers.view` for - additional information. + ```javascript + person: function() { + var personId = this.get('personId'); + return App.Person.create({ id: personId }); + }.property().meta({ type: App.Person }) + ``` - @class View - @namespace Ember - @extends Ember.Object - @uses Ember.Evented -*/ -Ember.View = Ember.CoreView.extend( -/** @scope Ember.View.prototype */ { + Once you've done this, you can retrieve the values saved to the computed + property from your class like this: - concatenatedProperties: ['classNames', 'classNameBindings', 'attributeBindings'], + ```javascript + MyClass.metaForProperty('person'); + ``` - /** - @property isView - @type Boolean - @default true - @final + This will return the original hash that was passed to `meta()`. + + @method metaForProperty + @param key {String} property name */ - isView: true, + metaForProperty: function(key) { + var desc = meta(this.proto(), false).descs[key]; - // .......................................................... - // TEMPLATE SUPPORT - // + Ember.assert("metaForProperty() could not find a computed property with key '"+key+"'.", !!desc && desc instanceof Ember.ComputedProperty); + return desc._meta || {}; + }, /** - 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`. + Iterate over each computed property for the class, passing its name + and any associated metadata (see `metaForProperty`) to the callback. - @property templateName - @type String - @default null + @method eachComputedProperty + @param {Function} callback + @param {Object} binding */ - templateName: null, - - /** - The name of the layout to lookup if no layout is provided. + eachComputedProperty: function(callback, binding) { + var proto = this.proto(), + descs = meta(proto).descs, + empty = {}, + property; - `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`. + for (var name in descs) { + property = descs[name]; - @property layoutName - @type String - @default null - */ - layoutName: null, + if (property instanceof Ember.ComputedProperty) { + callback.call(binding || this, name, property._meta || empty); + } + } + } - /** - The hash in which to look for `templateName`. +}); - @property templates - @type Ember.Object - @default Ember.TEMPLATES - */ - templates: Ember.TEMPLATES, +ClassMixin.ownerConstructor = CoreObject; - /** - 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. +if (Ember.config.overrideClassMixin) { + Ember.config.overrideClassMixin(ClassMixin); +} - In general, you should set the `templateName` property instead of setting - the template yourself. +CoreObject.ClassMixin = ClassMixin; +ClassMixin.apply(CoreObject); - @property template - @type Function - */ - template: Ember.computed(function(key, value) { - if (value !== undefined) { return value; } +Ember.CoreObject = CoreObject; - 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'), - container: Ember.computed(function() { - var parentView = get(this, '_parentView'); +(function() { +/** +@module ember +@submodule ember-runtime +*/ - if (parentView) { return get(parentView, 'container'); } +/** + `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. - return Ember.Container && Ember.Container.defaultContainer; - }), + @class Object + @namespace Ember + @extends Ember.CoreObject + @uses Ember.Observable +*/ +Ember.Object = Ember.CoreObject.extend(Ember.Observable); +Ember.Object.toString = function() { return "Ember.Object"; }; - /** - 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(function(key) { - var parentView = get(this, '_parentView'); - return parentView ? get(parentView, 'controller') : null; - }).property('_parentView'), - /** - 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'); +(function() { +/** +@module ember +@submodule ember-runtime +*/ - Ember.assert("You specified the layoutName " + layoutName + " for " + this + ", but it did not exist.", !layoutName || layout); +var get = Ember.get, indexOf = Ember.ArrayPolyfills.indexOf; - return layout || get(this, 'defaultLayout'); - }).property('layoutName'), +/** + 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. - templateForName: function(name, type) { - if (!name) { return; } + # Example Usage - Ember.assert("templateNames are not allowed to contain periods: "+name, name.indexOf('.') === -1); + ```javascript + MyFramework = Ember.Namespace.create({ + VERSION: '1.0.0' + }); + ``` - var container = get(this, 'container'); + @class Namespace + @namespace Ember + @extends Ember.Object +*/ +var Namespace = Ember.Namespace = Ember.Object.extend({ + isNamespace: true, - if (container) { - return container.lookup('template:' + name); - } + init: function() { + Ember.Namespace.NAMESPACES.push(this); + Ember.Namespace.PROCESSED = false; }, - /** - The object from which templates should access properties. + toString: function() { + var name = get(this, 'name'); + if (name) { return name; } - 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. + findNamespaces(); + return this[Ember.GUID_KEY+'_name']; + }, - By default, this will be the view's controller. + nameClasses: function() { + processNamespace([this.toString()], this, {}); + }, - @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'); + destroy: function() { + var namespaces = Ember.Namespace.NAMESPACES; + Ember.lookup[this.toString()] = undefined; + namespaces.splice(indexOf.call(namespaces, this), 1); + this._super(); + } +}); + +Namespace.reopenClass({ + NAMESPACES: [Ember], + NAMESPACES_BY_ID: {}, + PROCESSED: false, + processAll: processAllNamespaces, + byName: function(name) { + if (!Ember.BOOTED) { + processAllNamespaces(); } - }).volatile(), - /** - @private + return NAMESPACES_BY_ID[name]; + } +}); - 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 NAMESPACES_BY_ID = Namespace.NAMESPACES_BY_ID; - The context of a view is looked up as follows: +var hasOwnProp = ({}).hasOwnProperty, + guidFor = Ember.guidFor; - 1. Supplied context (usually by Handlebars) - 2. Specified controller - 3. `parentView`'s context (for a child of a ContainerView) +function processNamespace(paths, root, seen) { + var idx = paths.length; - 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. + NAMESPACES_BY_ID[paths.join('.')] = root; - @property _context - */ - _context: Ember.computed(function(key) { - var parentView, controller; + // 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 (controller = get(this, 'controller')) { - return controller; - } + // 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; - parentView = this._parentView; - if (parentView) { - return get(parentView, '_context'); + // 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; + + // Process the child namespace + processNamespace(paths, obj, seen); } + } - return null; - }), + paths.length = idx; // cut out last item +} - /** - @private +function findNamespaces() { + var Namespace = Ember.Namespace, lookup = Ember.lookup, obj, isNamespace; - If a value that affects template rendering changes, the view should be - re-rendered to reflect the new value. + if (Namespace.PROCESSED) { return; } - @method _displayPropertyDidChange - */ - _contextDidChange: Ember.observer(function() { - this.rerender(); - }, 'context'), + for (var prop in lookup) { + // These don't raise exceptions but can cause warnings + if (prop === "parent" || prop === "top" || prop === "frameElement" || prop === "webkitStorageInfo") { continue; } - /** - If `false`, the view will appear hidden in DOM. + // 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; } - @property isVisible - @type Boolean - @default null - */ - isVisible: true, + // 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; + } - /** - @private + if (isNamespace) { + Ember.deprecate("Namespaces should not begin with lowercase.", /^[A-Z]/.test(prop)); + obj[NAME_KEY] = prop; + } + } +} - Array of child views. You should never edit this array directly. - Instead, use `appendChild` and `removeFromParent`. +var NAME_KEY = Ember.NAME_KEY = Ember.GUID_KEY + '_name'; - @property childViews - @type Array - @default [] - */ - childViews: childViewsProperty, +function superClassString(mixin) { + var superclass = mixin.superclass; + if (superclass) { + if (superclass[NAME_KEY]) { return superclass[NAME_KEY]; } + else { return superClassString(superclass); } + } else { + return; + } +} - _childViews: [], +function classToString() { + if (!Ember.BOOTED && !this[NAME_KEY]) { + processAllNamespaces(); + } - // 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'); } - } - }, 'childViews'), + var ret; - // 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 (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)"; } - }, 'childViews'), + this.toString = makeToString(ret); + } - /** - Return the nearest ancestor that is an instance of the provided - class. + return ret; +} - @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'); +function processAllNamespaces() { + var unprocessedNamespaces = !Namespace.PROCESSED, + unprocessedMixins = Ember.anyUnprocessedMixins; - while (view) { - if(view instanceof klass) { return view; } - view = get(view, 'parentView'); - } - }, + if (unprocessedNamespaces) { + findNamespaces(); + Namespace.PROCESSED = true; + } - /** - Return the nearest ancestor that is an instance of the provided - class or mixin. + if (unprocessedNamespaces || unprocessedMixins) { + var namespaces = Namespace.NAMESPACES, namespace; + for (var i=0, l=namespaces.length; i=lengthBefore; i--) { - if (childViews[i]) { childViews[i].destroy(); } + @method _contentWillChange + */ + _contentWillChange: Ember.beforeObserver(function() { + this._teardownContent(); + }, 'content'), + + _teardownContent: function() { + var content = get(this, 'content'); + + if (content) { + content.removeArrayObserver(this, { + willChange: 'contentArrayWillChange', + didChange: 'contentArrayDidChange' + }); } }, + contentArrayWillChange: Ember.K, + contentArrayDidChange: Ember.K, + /** @private - 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. + Invoked when the content property changes. Notifies observers that the + entire array content has changed. - @method _applyClassNameBindings + @method _contentDidChange */ - _applyClassNameBindings: function(classBindings) { - var classNames = this.classNames, - elem, newClass, dasherizedClass; + _contentDidChange: Ember.observer(function() { + var content = get(this, 'content'); - // 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) { - - // 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); + Ember.assert("Can't set ArrayProxy's content to itself", content !== this); - if (dasherizedClass) { - // Ensure that it gets into the classNames array - // so it is displayed when we render. - a_addObject(classNames, dasherizedClass); + this._setupContent(); + }, 'content'), - // 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; - } + _setupContent: function() { + var content = get(this, 'content'); - 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; - } + if (content) { + content.addArrayObserver(this, { + willChange: 'contentArrayWillChange', + didChange: 'contentArrayDidChange' }); - - }, this); + } }, - /** - @private + _arrangedContentWillChange: Ember.beforeObserver(function() { + var arrangedContent = get(this, 'arrangedContent'), + len = arrangedContent ? get(arrangedContent, 'length') : 0; - 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. + this.arrangedContentArrayWillChange(this, 0, len, undefined); + this.arrangedContentWillChange(this); - @method _applyAttributeBindings - @param {Ember.RenderBuffer} buffer - */ - _applyAttributeBindings: function(buffer, attributeBindings) { - var attributeValue, elem, type; + this._teardownArrangedContent(arrangedContent); + }, 'arrangedContent'), - a_forEach(attributeBindings, function(binding) { - var split = binding.split(':'), - property = split[0], - attributeName = split[1] || property; + _arrangedContentDidChange: Ember.observer(function() { + var arrangedContent = get(this, 'arrangedContent'), + len = arrangedContent ? get(arrangedContent, 'length') : 0; - // Create an observer to add/remove/change the attribute if the - // JavaScript property changes. - var observer = function() { - elem = this.$(); - if (!elem) { return; } + Ember.assert("Can't set ArrayProxy's content to itself", arrangedContent !== this); - attributeValue = get(this, property); + this._setupArrangedContent(); - Ember.View.applyAttributeBindings(elem, attributeName, attributeValue); - }; + this.arrangedContentDidChange(this); + this.arrangedContentArrayDidChange(this, 0, undefined, len); + }, 'arrangedContent'), - this.registerObserver(this, property, observer); + _setupArrangedContent: function() { + var arrangedContent = get(this, 'arrangedContent'); - // Determine the current value and add it to the render buffer - // if necessary. - attributeValue = get(this, property); - Ember.View.applyAttributeBindings(buffer, attributeName, attributeValue); - }, this); + if (arrangedContent) { + arrangedContent.addArrayObserver(this, { + willChange: 'arrangedContentArrayWillChange', + didChange: 'arrangedContentArrayDidChange' + }); + } }, - /** - @private + _teardownArrangedContent: function() { + var arrangedContent = get(this, 'arrangedContent'); - Given a property name, returns a dasherized version of that - property name if the property evaluates to a non-falsy value. + if (arrangedContent) { + arrangedContent.removeArrayObserver(this, { + willChange: 'arrangedContentArrayWillChange', + didChange: 'arrangedContentArrayDidChange' + }); + } + }, - For example, if the view has property `isUrgent` that evaluates to true, - passing `isUrgent` to this method will return `"is-urgent"`. + arrangedContentWillChange: Ember.K, + arrangedContentDidChange: Ember.K, - @method _classStringForProperty - @param property - */ - _classStringForProperty: function(property) { - var parsedPath = Ember.View._parsePropertyPath(property); - var path = parsedPath.path; + objectAt: function(idx) { + return get(this, 'content') && this.objectAtContent(idx); + }, - var val = get(this, path); - if (val === undefined && Ember.isGlobalPath(path)) { - val = get(Ember.lookup, path); - } + length: Ember.computed(function() { + var arrangedContent = get(this, 'arrangedContent'); + return arrangedContent ? get(arrangedContent, 'length') : 0; + // No dependencies since Enumerable notifies length of change + }), - return Ember.View._classStringForValue(path, val, parsedPath.className, parsedPath.falsyClassName); + _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; }, - // .......................................................... - // ELEMENT SUPPORT - // + 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."); + } + }, - /** - Returns the current DOM element for the view. + _insertAt: function(idx, object) { + if (idx > get(this, 'content.length')) throw new Error(OUT_OF_RANGE_EXCEPTION); + this._replace(idx, 0, [object]); + return this; + }, - @property element - @type DOMElement - */ - element: Ember.computed(function(key, value) { - if (value !== undefined) { - return this.currentState.setElement(this, value); + insertAt: function(idx, object) { + if (get(this, 'arrangedContent') === get(this, 'content')) { + return this._insertAt(idx, object); } else { - return this.currentState.getElement(this); + throw new Ember.Error("Using insertAt on an arranged ArrayProxy is not allowed."); } - }).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. + removeAt: function(start, len) { + if ('number' === typeof start) { + var content = get(this, 'content'), + arrangedContent = get(this, 'arrangedContent'), + indices = [], i; - For example, calling `view.$('li')` will return a jQuery object containing - all of the `li` elements inside the DOM element of this view. + if ((start < 0) || (start >= get(this, 'length'))) { + throw new Error(OUT_OF_RANGE_EXCEPTION); + } - @property $ - @param {String} [selector] a jQuery-compatible selector string - @return {jQuery} the CoreQuery object for the DOM node - */ - $: function(sel) { - return this.currentState.$(this, sel); - }, + if (len === undefined) len = 1; - mutateChildViews: function(callback) { - var childViews = this._childViews, - idx = childViews.length, - view; + // Get a list of indices in original content to remove + for (i=start; i= 0) { - view = childViews[idx]; - callback.call(this, view, idx); + // Replace in reverse order since indices will change + indices.sort(function(a,b) { return b - a; }); + + Ember.beginPropertyChanges(); + for (i=0; i=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'); - @method _elementDidChange - */ - _elementDidChange: Ember.observer(function() { - this.forEachChildView(function(view) { - Ember.propertyDidChange(view, 'element'); - }); - }, 'element'), + // 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); + } + } +} - /** - Called when the parentView property has changed. +function removeObserverForContentKey(content, keyName, proxy, idx, loc) { + var objects = proxy._objects; + if (!objects) objects = proxy._objects = {}; + var indicies, guid; - @event parentViewDidChange - */ - parentViewDidChange: Ember.K, + while(--loc>=idx) { + var item = content.objectAt(loc); + if (item) { + Ember.removeBeforeObserver(item, keyName, proxy, 'contentKeyWillChange'); + Ember.removeObserver(item, keyName, proxy, 'contentKeyDidChange'); - instrumentName: 'view', + guid = guidFor(item); + indicies = objects[guid]; + indicies[indexOf.call(indicies, loc)] = null; + } + } +} - instrumentDetails: function(hash) { - hash.template = get(this, 'templateName'); - this._super(hash); - }, +/** + 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. - _renderToBuffer: function(parentBuffer, bufferOperation) { - this.lengthBeforeRender = this._childViews.length; - var buffer = this._super(parentBuffer, bufferOperation); - this.lengthAfterRender = this._childViews.length; + @private + @class EachProxy + @namespace Ember + @extends Ember.Object +*/ +Ember.EachProxy = Ember.Object.extend({ - return buffer; - }, + init: function(content) { + this._super(); + this._content = content; + content.addArrayObserver(this); - renderToBufferIfNeeded: function () { - return this.currentState.renderToBufferIfNeeded(this, this); + // in case someone is already observing some keys make sure they are + // added + forEach(Ember.watchedEvents(this), function(eventName) { + this.didAddListener(eventName); + }, this); }, - beforeRender: function(buffer) { - this.applyAttributesToBuffer(buffer); - buffer.pushOpeningTag(); - }, + /** + You can directly access mapped properties by simply requesting them. + The `unknownProperty` handler will generate an EachArray of each item. - afterRender: function(buffer) { - buffer.pushClosingTag(); + @method unknownProperty + @param keyName {String} + @param value {*} + */ + unknownProperty: function(keyName, value) { + var ret; + ret = new EachArray(this._content, keyName, this); + Ember.defineProperty(this, keyName, null, ret); + this.beginObservingContentKey(keyName); + return ret; }, - 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); - } + // .......................................................... + // ARRAY CHANGES + // Invokes whenever the content array itself changes. - // 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); - } + arrayWillChange: function(content, idx, removedCnt, addedCnt) { + var keys = this._keys, key, lim; - buffer.setClasses(this.classNames); - buffer.id(this.elementId); + lim = removedCnt>0 ? idx+removedCnt : -1; + Ember.beginPropertyChanges(this); - var role = get(this, 'ariaRole'); - if (role) { - buffer.attr('role', role); - } + for(key in keys) { + if (!keys.hasOwnProperty(key)) { continue; } - if (get(this, 'isVisible') === false) { - buffer.style('display', 'none'); + if (lim>0) { removeObserverForContentKey(content, key, this, idx, lim); } + + Ember.propertyWillChange(this, key); } + + Ember.propertyWillChange(this._content, '@each'); + Ember.endPropertyChanges(this); }, - // .......................................................... - // STANDARD RENDER PROPERTIES - // + arrayDidChange: function(content, idx, removedCnt, addedCnt) { + var keys = this._keys, lim; - /** - 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. + lim = addedCnt>0 ? idx+addedCnt : -1; + Ember.changeProperties(function() { + for(var key in keys) { + if (!keys.hasOwnProperty(key)) { continue; } - By default, the render buffer will use a `
    ` tag for views. + if (lim>0) { addObserverForContentKey(content, key, this, idx, lim); } - @property tagName - @type String - @default null - */ + Ember.propertyDidChange(this, key); + } - // We leave this null by default so we can tell the difference between - // the default case and a user-specified tag. - tagName: null, + Ember.propertyDidChange(this._content, '@each'); + }, this); + }, - /** - 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. + // .......................................................... + // LISTEN FOR NEW OBSERVERS AND OTHER EVENT LISTENERS + // Start monitoring keys based on who is listening... - The full list of valid WAI-ARIA roles is available at: - http://www.w3.org/TR/wai-aria/roles#roles_categorization + didAddListener: function(eventName) { + if (IS_OBSERVER.test(eventName)) { + this.beginObservingContentKey(eventName.slice(0, -7)); + } + }, - @property ariaRole - @type String - @default null - */ - ariaRole: null, + didRemoveListener: function(eventName) { + if (IS_OBSERVER.test(eventName)) { + this.stopObservingContentKey(eventName.slice(0, -7)); + } + }, - /** - 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. + // .......................................................... + // CONTENT KEY OBSERVING + // Actual watch keys on the source content. - @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.create({ - 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.create({ - 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.create({ - classNameBindings: ['isUrgent:urgent'] - isUrgent: true - }); - ``` - - This list of properties is inherited from the view's superclasses as well. - - @property classNameBindings - @type Array - @default [] - */ - classNameBindings: [], + 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]++; + } + }, - /** - 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. + 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); + } + }, - ```javascript - // Applies the type attribute to the element - // with the value "button", like
    - Ember.View.create({ - attributeBindings: ['type'], - type: 'button' - }); - ``` + contentKeyWillChange: function(obj, keyName) { + Ember.propertyWillChange(this, keyName); + }, - If the value of the property is a Boolean, the name of that property is - added as an attribute. + contentKeyDidChange: function(obj, keyName) { + Ember.propertyDidChange(this, keyName); + } - ```javascript - // Renders something like
    - Ember.View.create({ - attributeBindings: ['enabled'], - enabled: true - }); - ``` +}); - @property attributeBindings - */ - attributeBindings: [], - // ....................................................... - // CORE DISPLAY METHODS - // - /** - @private +})(); - 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 - */ - init: function() { - this.elementId = this.elementId || guidFor(this); - this._super(); +(function() { +/** +@module ember +@submodule ember-runtime +*/ - // 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()); +var get = Ember.get, set = Ember.set, replace = Ember.EnumerableUtils._replace; - Ember.assert("Only arrays are allowed for 'classNames'", Ember.typeOf(this.classNames) === 'array'); - this.classNames = Ember.A(this.classNames.slice()); +// 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, { - var viewController = get(this, 'viewController'); - if (viewController) { - viewController = get(viewController); - if (viewController) { - set(viewController, 'view', this); - } - } + // 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); }, - appendChild: function(view, options) { - return this.currentState.appendChild(this, view, options); + objectAt: function(idx) { + return this[idx]; }, - /** - 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); + // primitive for array support. + replace: function(idx, amt, objects) { - // remove view from childViews array. - var childViews = this._childViews; + if (this.isFrozen) throw Ember.FROZEN_ERROR; - Ember.EnumerableUtils.removeObject(childViews, view); + // 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); - this.propertyDidChange('childViews'); // HUH?! what happened to will change? + if (!objects || objects.length === 0) { + this.splice(idx, amt); + } else { + replace(this, idx, amt, objects); + } + this.arrayContentDidChange(idx, amt, len); return this; }, - /** - Removes all children from the `parentView`. - - @method removeAllChildren - @return {Ember.View} receiver - */ - removeAllChildren: function() { - return this.mutateChildViews(function(view) { - this.removeChild(view); - }); - }, - - destroyAllChildren: function() { - return this.mutateChildViews(function(view) { - view.destroy(); - }); + // 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 ; }, - /** - 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; + // If browser did not implement indexOf natively, then override with + // specialized version + indexOf: function(object, startAt) { + var idx, len = this.length; - // Remove DOM element from parent - this.remove(); + if (startAt === undefined) startAt = 0; + else startAt = (startAt < 0) ? Math.ceil(startAt) : Math.floor(startAt); + if (startAt < 0) startAt += len; - if (parent) { parent.removeChild(this); } - return this; + for(idx=startAt;idx=0; i--) { - childViews[i].removedFromDOM = true; + for(idx=startAt;idx>=0;idx--) { + if (this[idx] === object) return idx ; } + return -1; + }, - // remove from non-virtual parent view if viewName was specified - if (this.viewName) { - var nonVirtualParentView = get(this, 'parentView'); - if (nonVirtualParentView) { - set(nonVirtualParentView, this.viewName, null); - } + copy: function(deep) { + if (deep) { + return this.map(function(item) { return Ember.copy(item, true); }); } - // 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('destroyed'); + return this.slice(); + } +}); - childLen = childViews.length; - for (i=childLen-1; i>=0; i--) { - childViews[i].destroy(); - } +// 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); +}); - // next remove view from global hash - if (!this.isVirtual) delete Ember.View.views[get(this, 'elementId')]; - }, +if (ignore.length>0) { + NativeArray = NativeArray.without.apply(NativeArray, ignore); +} - /** - 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} viewClass - @param {Hash} [attrs] Attributes to add - @return {Ember.View} new instance - */ - createChildView: function(view, attrs) { - if (view.isView && view._parentView === this) { return view; } - - if (Ember.CoreView.detect(view)) { - attrs = attrs || {}; - attrs._parentView = this; - attrs.templateData = attrs.templateData || get(this, 'templateData'); +/** + 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`. - view = view.create(attrs); + @class NativeArray + @namespace Ember + @uses Ember.MutableArray + @uses Ember.Observable + @uses Ember.Copyable +*/ +Ember.NativeArray = NativeArray; - // 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 { - Ember.assert('You must pass instance or subclass of View', view.isView); +/** + 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 (attrs) { - view.setProperties(attrs); - } + Example - if (!get(view, 'templateData')) { - set(view, 'templateData', get(this, 'templateData')); + ```js + var Pagination = Ember.CollectionView.extend({ + tagName: 'ul', + classNames: ['pagination'], + init: function() { + this._super(); + if (!this.get('content')) { + this.set('content', Ember.A([])); } - - set(view, '_parentView', this); } + }); + ``` - return view; - }, - - becameVisible: Ember.K, - becameHidden: Ember.K, + @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); +}; - /** - @private +/** + 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`. - When the view's `isVisible` property changes, toggle the visibility - element of the actual DOM element. + Example - @method _isVisibleDidChange - */ - _isVisibleDidChange: Ember.observer(function() { - var $el = this.$(); - if (!$el) { return; } + ```js + if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) { + Ember.NativeArray.activate(); + } + ``` - var isVisible = get(this, 'isVisible'); + @method activate + @for Ember.NativeArray + @static + @return {void} +*/ +Ember.NativeArray.activate = function() { + NativeArray.apply(Array.prototype); - $el.toggle(isVisible); + Ember.A = function(arr) { return arr || []; }; +}; - if (this._isAncestorHidden()) { return; } +if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) { + Ember.NativeArray.activate(); +} - if (isVisible) { - this._notifyBecameVisible(); - } else { - this._notifyBecameHidden(); - } - }, 'isVisible'), - _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'); +(function() { +/** +@module ember +@submodule ember-runtime +*/ - if (isVisible || isVisible === null) { - view._notifyBecameHidden(); - } - }); - }, +var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, isNone = Ember.isNone, fmt = Ember.String.fmt; - _isAncestorHidden: function() { - var parent = get(this, 'parentView'); +/** + An unordered collection of objects. - while (parent) { - if (get(parent, 'isVisible') === false) { return 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. - parent = get(parent, 'parentView'); - } + All Sets are observable via the Enumerable Observer API - which works + on any enumerable object including both Sets and Arrays. - return false; - }, + ## Creating a Set - clearBuffer: function() { - this.invokeRecursively(function(view) { - view.buffer = null; - }); - }, + 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. - transitionTo: function(state, children) { - this.currentState = this.states[state]; - this.state = state; + 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 (children !== false) { - this.forEachChildView(function(view) { - view.transitionTo(state); - }); - } - }, + ```javascript + // creates a new empty set + var foundNames = new Ember.Set(); - // ....................................................... - // EVENT HANDLING - // + // creates a set with four names in it. + var names = new Ember.Set(["Charles", "Tom", "Juan", "Alex"]); // :P - /** - @private + // creates a copy of the names set. + var namesCopy = new Ember.Set(names); - Handle events from `Ember.EventDispatcher` + // same as above. + var anotherNamesCopy = names.copy(); + ``` - @method handleEvent - @param eventName {String} - @param evt {Event} - */ - handleEvent: function(eventName, evt) { - return this.currentState.handleEvent(this, eventName, evt); - }, + ## Adding/Removing Objects - registerObserver: function(root, path, target, observer) { - Ember.addObserver(root, path, target, observer); + 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. - this.one('willClearRender', function() { - Ember.removeObserver(root, path, target, observer); - }); - } + 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. -}); + NOTE: You cannot add/remove `null` or `undefined` to a set. Any attempt to do + so will be ignored. -/* - Describe how the specified actions should behave in the various - states that a view can exist in. Possible states: + 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. - * 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 - * 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. -*/ + ## Testing for an Object - // in the destroyed state, everything is illegal + To test for an object's presence in a set you simply call + `Ember.Set#contains()`. - // before rendering has begun, all legal manipulations are noops. + ## Observing changes - // inside the buffer, legal manipulations are done on the buffer + 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. - // once the view has been inserted into the DOM, legal manipulations - // are done on the DOM element. + 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. -var DOMManager = { - prepend: function(view, html) { - view.$().prepend(html); - }, + ## Other Methods - after: function(view, html) { - view.$().after(html); - }, + `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. - html: function(view, html) { - view.$().html(html); - }, + 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. - replace: function(view) { - var element = get(view, 'element'); + @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 */ { - set(view, 'element', null); - - view._insertElementLater(function() { - Ember.$(element).replaceWith(get(view, 'element')); - }); - }, - - remove: function(view) { - view.$().remove(); - }, - - empty: function(view) { - view.$().empty(); - } -}; - -Ember.View.reopen({ - domManager: DOMManager -}); - -Ember.View.reopenClass({ + // .......................................................... + // IMPLEMENT ENUMERABLE APIS + // /** - @private + This property will change as the number of objects in the set changes. - Parse a path and return an object which holds the parsed properties. + @property length + @type number + @default 0 + */ + length: 0, - For example a path like "content.isEnabled:enabled:disabled" wil return the - following object: + /** + Clears the set. This is useful if you want to reuse an existing set + without having to recreate it. ```javascript - { - path: "content.isEnabled", - className: "enabled", - falsyClassName: "disabled", - classNames: ":enabled:disabled" - } + var colors = new Ember.Set(["red", "green", "blue"]); + colors.length; // 3 + colors.clear(); + colors.length; // 0 ``` - @method _parsePropertyPath - @static + @method clear + @return {Ember.Set} An empty Set */ - _parsePropertyPath: function(path) { - var split = path.split(':'), - propertyPath = split[0], - classNames = "", - className, - falsyClassName; + clear: function() { + if (this.isFrozen) { throw new Error(Ember.FROZEN_ERROR); } - // 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]; } + var len = get(this, 'length'); + if (len === 0) { return this; } - classNames = ':' + className; - if (falsyClassName) { classNames += ":" + falsyClassName; } + var guid; + + this.enumerableContentWillChange(len, 0); + Ember.propertyWillChange(this, 'firstObject'); + Ember.propertyWillChange(this, 'lastObject'); + + for (var i=0; i < len; i++) { + guid = guidFor(this[i]); + delete this[guid]; + delete this[i]; } - return { - path: propertyPath, - classNames: classNames, - className: (className === '') ? undefined : className, - falsyClassName: falsyClassName - }; + set(this, 'length', 0); + + Ember.propertyDidChange(this, 'firstObject'); + Ember.propertyDidChange(this, 'lastObject'); + this.enumerableContentDidChange(len, 0); + + return this; }, /** - @private + Returns true if the passed object is also an enumerable that contains the + same objects as the receiver. - Get the class name for a given value, based on the path, optional - `className` and optional `falsyClassName`. + ```javascript + var colors = ["red", "green", "blue"], + same_colors = new Ember.Set(colors); - - 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 + same_colors.isEqual(colors); // true + same_colors.isEqual(["purple", "brown"]); // false + ``` - @method _classStringForValue - @param path - @param val - @param className - @param falsyClassName - @static + @method isEqual + @param {Ember.Set} obj the other object. + @return {Boolean} */ - _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]); + isEqual: function(obj) { + // fail fast + if (!Ember.Enumerable.detect(obj)) return false; - // If the value is not false, undefined, or null, return the current - // value of the property. - } else if (val !== false && val !== undefined && val !== null) { - return val; + var loc = get(this, 'length'); + if (get(obj, 'length') !== loc) return false; - // Nothing to display. Return null so that the old class is removed - // but no new class is added. - } else { - return null; + while(--loc >= 0) { + if (!obj.contains(this[loc])) return false; } - } -}); -/** - Global views hash - - @property views - @static - @type Hash -*/ -Ember.View.views = {}; + return true; + }, -// 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; + /** + 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. -Ember.View.applyAttributeBindings = function(elem, name, value) { - var type = Ember.typeOf(value); + This is an alias for `Ember.MutableEnumerable.addObject()`. - // 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 (value !== elem.prop(name)) { - // value and booleans should always be properties - elem.prop(name, value); - } - } else if (!value) { - elem.removeAttr(name); - } -}; + ```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"] + ``` -Ember.View.states = states; + @method add + @param {Object} obj The object to add. + @return {Ember.Set} The set itself. + */ + add: Ember.aliasMethod('addObject'), -})(); + /** + 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()`. + ```javascript + var colors = new Ember.Set(["red", "green", "blue"]); + colors.remove("red"); // ["blue", "green"] + colors.remove("purple"); // ["blue", "green"] + colors.remove(null); // ["blue", "green"] + ``` + @method remove + @param {Object} obj The object to remove + @return {Ember.Set} The set itself. + */ + remove: Ember.aliasMethod('removeObject'), -(function() { -/** -@module ember -@submodule ember-views -*/ + /** + Removes the last element from the set and returns it, or `null` if it's empty. -var get = Ember.get, set = Ember.set; + ```javascript + var colors = new Ember.Set(["green", "blue"]); + colors.pop(); // "blue" + colors.pop(); // "green" + colors.pop(); // null + ``` -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"; + @method pop + @return {Object} The removed object from the set or null. + */ + pop: function() { + if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); + var obj = this.length > 0 ? this[this.length-1] : null; + this.remove(obj); + return obj; }, - $: function() { - return undefined; - }, + /** + Inserts the given object on to the end of the set. It returns + the set itself. - getElement: function() { - return null; - }, + This is an alias for `Ember.MutableEnumerable.addObject()`. - // Handle events from `Ember.EventDispatcher` - handleEvent: function() { - return true; // continue event propagation - }, + ```javascript + var colors = new Ember.Set(); + colors.push("red"); // ["red"] + colors.push("green"); // ["red", "green"] + colors.push("blue"); // ["red", "green", "blue"] + ``` - destroyElement: function(view) { - set(view, 'element', null); - if (view._scheduledInsert) { - Ember.run.cancel(view._scheduledInsert); - view._scheduledInsert = null; - } - return view; - }, + @method push + @return {Ember.Set} The set itself. + */ + push: Ember.aliasMethod('addObject'), - renderToBufferIfNeeded: function () { - return false; - }, + /** + Removes the last element from the set and returns it, or `null` if it's empty. - rerender: Ember.K -}; + This is an alias for `Ember.Set.pop()`. -})(); + ```javascript + var colors = new Ember.Set(["green", "blue"]); + colors.shift(); // "blue" + colors.shift(); // "green" + colors.shift(); // null + ``` + @method shift + @return {Object} The removed object from the set or null. + */ + shift: Ember.aliasMethod('pop'), + /** + Inserts the given object on to the end of the set. It returns + the set itself. -(function() { -/** -@module ember -@submodule ember-views -*/ + This is an alias of `Ember.Set.push()` -var preRender = Ember.View.states.preRender = Ember.create(Ember.View.states._default); + ```javascript + var colors = new Ember.Set(); + colors.unshift("red"); // ["red"] + colors.unshift("green"); // ["red", "green"] + colors.unshift("blue"); // ["red", "green", "blue"] + ``` -Ember.merge(preRender, { - // a view leaves the preRender state once its element has been - // created (createElement). - insertElement: function(view, fn) { - view.createElement(); - view.triggerRecursively('willInsertElement'); - // after createElement, the view will be in the hasElement state. - fn.call(view); - view.transitionTo('inDOM'); - view.triggerRecursively('didInsertElement'); - }, + @method unshift + @return {Ember.Set} The set itself. + */ + unshift: Ember.aliasMethod('push'), - renderToBufferIfNeeded: function(view) { - return view.renderToBuffer(); - }, + /** + Adds each object in the passed enumerable to the set. - empty: Ember.K, + This is an alias of `Ember.MutableEnumerable.addObjects()` - setElement: function(view, value) { - if (value !== null) { - view.transitionTo('hasElement'); - } - return value; - } -}); + ```javascript + var colors = new Ember.Set(); + colors.addEach(["red", "green", "blue"]); // ["red", "green", "blue"] + ``` -})(); + @method addEach + @param {Ember.Enumerable} objects the objects to add. + @return {Ember.Set} The set itself. + */ + addEach: Ember.aliasMethod('addObjects'), + /** + Removes each object in the passed enumerable to the set. + This is an alias of `Ember.MutableEnumerable.removeObjects()` -(function() { -/** -@module ember -@submodule ember-views -*/ + ```javascript + var colors = new Ember.Set(["red", "green", "blue"]); + colors.removeEach(["red", "blue"]); // ["green"] + ``` -var get = Ember.get, set = Ember.set, meta = Ember.meta; + @method removeEach + @param {Ember.Enumerable} objects the objects to remove. + @return {Ember.Set} The set itself. + */ + removeEach: Ember.aliasMethod('removeObjects'), -var inBuffer = Ember.View.states.inBuffer = Ember.create(Ember.View.states._default); + // .......................................................... + // PRIVATE ENUMERABLE SUPPORT + // -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.$(); + init: function(items) { + this._super(); + if (items) this.addObjects(items); }, - // 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."); + // implement Ember.Enumerable + nextObject: function(idx) { + return this[idx]; }, - // 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; + // more optimized version + firstObject: Ember.computed(function() { + return this.length > 0 ? this[0] : undefined; + }), - childView = view.createChildView(childView, options); - view._childViews.push(childView); + // more optimized version + lastObject: Ember.computed(function() { + return this.length > 0 ? this[this.length-1] : undefined; + }), - childView.renderToBuffer(buffer); + // implements Ember.MutableEnumerable + addObject: function(obj) { + if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); + if (isNone(obj)) return this; // nothing to do - view.propertyDidChange('childViews'); + var guid = guidFor(obj), + idx = this[guid], + len = get(this, 'length'), + added ; - return childView; - }, + if (idx>=0 && idx=0 && idx=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."; + 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 value; + return ret; }, - // 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(); + toString: function() { + var len = this.length, idx, array = []; + for(idx = 0; idx < len; idx++) { + array[idx] = this[idx]; + } + return fmt("Ember.Set<%@>", [array.join(',')]); + } - 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 - } - } -}); +(function() { +var DeferredMixin = Ember.DeferredMixin, // mixins/deferred + get = Ember.get; -var inDOM = Ember.View.states.inDOM = Ember.create(hasElement); +var Deferred = Ember.Object.extend(DeferredMixin); -Ember.merge(inDOM, { - insertElement: function(view, fn) { - throw "You can't insert an element into the DOM that has already been inserted"; +Deferred.reopenClass({ + promise: function(callback, binding) { + var deferred = Deferred.create(); + callback.call(binding, deferred); + return deferred; } }); +Ember.Deferred = Deferred; + })(); (function() { +var forEach = Ember.ArrayPolyfills.forEach; + /** @module ember -@submodule ember-views +@submodule ember-runtime */ -var destroyedError = "You can't call %@ on a destroyed view", fmt = Ember.String.fmt; - -var destroyed = Ember.View.states.destroyed = Ember.create(Ember.View.states._default); +var loadHooks = Ember.ENV.EMBER_LOAD_HOOKS || {}; +var loaded = {}; -Ember.merge(destroyed, { - appendChild: function() { - throw fmt(destroyedError, ['appendChild']); - }, - rerender: function() { - throw fmt(destroyedError, ['rerender']); - }, - destroyElement: function() { - throw fmt(destroyedError, ['destroyElement']); - }, - empty: function() { - throw fmt(destroyedError, ['empty']); - }, +/** - setElement: function() { - throw fmt(destroyedError, ["set('element', ...)"]); - }, +Detects when a specific package of Ember (e.g. 'Ember.Handlebars') +has fully loaded and is available for extension. - renderToBufferIfNeeded: function() { - throw fmt(destroyedError, ["renderToBufferIfNeeded"]); - }, +The provided `callback` will be called with the `name` passed +resolved from a string into the object: - // Since element insertion is scheduled, don't do anything if - // the view has been destroyed between scheduling and execution - insertElement: Ember.K +```javascript +Ember.onLoad('Ember.Handlebars' function(hbars){ + hbars.registerHelper(...); }); +``` -})(); +@method onLoad +@for Ember +@param name {String} name of hook +@param callback {Function} callback to be called +*/ +Ember.onLoad = function(name, callback) { + var object; + loadHooks[name] = loadHooks[name] || Ember.A(); + loadHooks[name].pushObject(callback); + if (object = loaded[name]) { + callback(object); + } +}; -(function() { -Ember.View.cloneStates = function(from) { - var into = {}; +/** - into._default = {}; - into.preRender = Ember.create(into._default); - into.destroyed = Ember.create(into._default); - into.inBuffer = Ember.create(into._default); - into.hasElement = Ember.create(into._default); - into.inDOM = Ember.create(into.hasElement); +Called when an Ember.js package (e.g Ember.Handlebars) has finished +loading. Triggers any callbacks registered for this event. - var viewState; +@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; - for (var stateName in from) { - if (!from.hasOwnProperty(stateName)) { continue; } - Ember.merge(into[stateName], from[stateName]); + if (loadHooks[name]) { + forEach.call(loadHooks[name], function(callback) { + callback(object); + }); } - - return into; }; })(); @@ -16219,394 +17490,353 @@ Ember.View.cloneStates = function(from) { (function() { -var states = Ember.View.cloneStates(Ember.View.states); + +})(); + + + +(function() { +var get = Ember.get; /** @module ember -@submodule ember-views +@submodule ember-runtime */ -var get = Ember.get, set = Ember.set, meta = Ember.meta; -var forEach = Ember.EnumerableUtils.forEach; - /** - A `ContainerView` is an `Ember.View` subclass that implements `Ember.MutableArray` - allowing programatic management of its child views. + `Ember.ControllerMixin` provides a standard interface for all classes that + compose Ember's controller layer: `Ember.Controller`, + `Ember.ArrayController`, and `Ember.ObjectController`. - ## Setting Initial Child Views + @class ControllerMixin + @namespace Ember +*/ +Ember.ControllerMixin = Ember.Mixin.create(Ember.ActionHandler, { + /* ducktype as a controller */ + isController: true, - 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`: + /** + The object to which actions from the view should be sent. - ```javascript - aContainer = Ember.ContainerView.create({ - childViews: [Ember.View.create(), Ember.View.create()] - }); - ``` + For example, when a Handlebars template uses the `{{action}}` helper, + it will attempt to send the action to the view's controller's `target`. - You can also provide a list of property names whose values are instances of - `Ember.View`: + By default, a controller's `target` is set to the router after it is + instantiated by `Ember.Application#initialize`. - ```javascript - aContainer = Ember.ContainerView.create({ - childViews: ['aView', 'bView', 'cView'], - aView: Ember.View.create(), - bView: Ember.View.create(), - cView: Ember.View.create() - }); - ``` + @property target + @default null + */ + target: null, - The two strategies can be combined: + container: null, - ```javascript - aContainer = Ember.ContainerView.create({ - childViews: ['aView', Ember.View.create()], - aView: Ember.View.create() - }); - ``` + parentController: null, - 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. + store: null, - ## Adding and Removing Child Views + model: Ember.computed.alias('content'), - The container view implements `Ember.MutableArray` allowing programatic management of its child views. + deprecatedSendHandles: function(actionName) { + return !!this[actionName]; + }, - To remove a view, pass that view into a `removeObject` call on the container view. + 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; + } +}); - Given an empty `` the following code +/** + @class Controller + @namespace Ember + @extends Ember.Object + @uses Ember.ControllerMixin +*/ +Ember.Controller = Ember.Object.extend(Ember.ControllerMixin); - ```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
    -
    - ``` +(function() { +/** +@module ember +@submodule ember-runtime +*/ - Removing a view +var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; + +/** + `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: ```javascript - aContainer.toArray(); // [aContainer.aView, aContainer.bView] - aContainer.removeObject(aContainer.get('bView')); - aContainer.toArray(); // [aContainer.aView] - ``` + 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'}, + ]; - Will result in the following HTML + songsController = Ember.ArrayController.create({ + content: songs, + sortProperties: ['trackNumber'], + sortAscending: true + }); - ```html -
    -
    A
    -
    + songsController.get('firstObject'); // {trackNumber: 2, title: 'Back in the U.S.S.R.'} + + songsController.addObject({trackNumber: 1, title: 'Dear Prudence'}); + songsController.get('firstObject'); // {trackNumber: 1, title: 'Dear Prudence'} ``` - Similarly, adding a child view is accomplished by adding `Ember.View` instances to the - container view. - - Given an empty `` the following code + If you add or remove the properties to sort by or change the sort direction the content + sort order will be automatically updated. ```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") - }) - }); + songsController.set('sortProperties', ['title']); + songsController.get('firstObject'); // {trackNumber: 2, title: 'Back in the U.S.S.R.'} - aContainer.appendTo('body'); + songsController.toggleProperty('sortAscending'); + songsController.get('firstObject'); // {trackNumber: 4, title: 'Ob-La-Di, Ob-La-Da'} ``` - Results in the HTML - - ```html -
    -
    A
    -
    B
    -
    - ``` + 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: - Adding a view + ```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. - ```javascript - AnotherViewClass = Ember.View.extend({ - template: Ember.Handlebars.compile("Another view") - }); + @class SortableMixin + @namespace Ember + @uses Ember.MutableEnumerable +*/ +Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { - aContainer.toArray(); // [aContainer.aView, aContainer.bView] - aContainer.pushObject(AnotherViewClass.create()); - aContainer.toArray(); // [aContainer.aView, aContainer.bView, ] - ``` + /** + Specifies which properties dictate the arrangedContent's sort order. - Will result in the following HTML + When specifying multiple properties the sorting will use properties + from the `sortProperties` array prioritized from first to last. - ```html -
    -
    A
    -
    B
    -
    Another view
    -
    - ``` + @property {Array} sortProperties + */ + sortProperties: null, - ## Templates and Layout + /** + Specifies the arrangedContent's sort direction - 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. + @property {Boolean} sortAscending + */ + sortAscending: true, - ## Binding a View to Display + /** + 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: - If you would like to display a single view in your ContainerView, you can set - its `currentView` property. When the `currentView` property is set to a view - instance, it will be added to the ContainerView. If the `currentView` property - is later changed to a different view, the new view will replace the old view. - If `currentView` is set to `null`, the last `currentView` will be removed. + ```javascript + function(x,y) { // These are assumed to be integers + if (x === y) + return 0; + return x < y ? -1 : 1; + } + ``` - This functionality is useful for cases where you want to bind the display of - a ContainerView to a controller or state manager. For example, you can bind - the `currentView` of a container to a controller like this: + @property sortFunction + @type {Function} + @default Ember.compare + */ + sortFunction: Ember.compare, - ```javascript - App.appController = Ember.Object.create({ - view: Ember.View.create({ - templateName: 'person_template' - }) - }); - ``` + orderBy: function(item1, item2) { + var result = 0, + sortProperties = get(this, 'sortProperties'), + sortAscending = get(this, 'sortAscending'), + sortFunction = get(this, 'sortFunction'); - ```handlebars - {{view Ember.ContainerView currentViewBinding="App.appController.view"}} - ``` + Ember.assert("you need to define `sortProperties`", !!sortProperties); - @class ContainerView - @namespace Ember - @extends Ember.View -*/ -Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { - states: states, + forEach(sortProperties, function(propertyName) { + if (result === 0) { + result = sortFunction(get(item1, propertyName), get(item2, propertyName)); + if ((result !== 0) && !sortAscending) { + result = (-1) * result; + } + } + }); - init: function() { - this._super(); + return result; + }, - var childViews = get(this, 'childViews'); + destroy: function() { + var content = get(this, 'content'), + sortProperties = get(this, 'sortProperties'); - // redefine view's childViews property that was obliterated - Ember.defineProperty(this, 'childViews', Ember.View.childViewsProperty); + if (content && sortProperties) { + forEach(content, function(item) { + forEach(sortProperties, function(sortProperty) { + Ember.removeObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); + }, this); + }, this); + } - var _childViews = this._childViews; + return this._super(); + }, - forEach(childViews, function(viewName, idx) { - var view; + isSorted: Ember.computed.bool('sortProperties'), - if ('string' === typeof viewName) { - view = get(this, viewName); - view = this.createChildView(view); - set(this, viewName, view); - } else { - view = this.createChildView(viewName); - } + /** + 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 + */ - _childViews[idx] = view; - }, this); + arrangedContent: Ember.computed('content', 'sortProperties.@each', function(key, value) { + var content = get(this, 'content'), + isSorted = get(this, 'isSorted'), + sortProperties = get(this, 'sortProperties'), + self = this; - var currentView = get(this, 'currentView'); - if (currentView) { - _childViews.push(this.createChildView(currentView)); + 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); } - }, - replace: function(idx, removedCount, addedViews) { - var addedCount = addedViews ? get(addedViews, 'length') : 0; + return content; + }), - this.arrayContentWillChange(idx, removedCount, addedCount); - this.childViewsWillChange(this._childViews, idx, removedCount); + _contentWillChange: Ember.beforeObserver(function() { + var content = get(this, 'content'), + sortProperties = get(this, 'sortProperties'); - if (addedCount === 0) { - this._childViews.splice(idx, removedCount) ; - } else { - var args = [idx, removedCount].concat(addedViews); - this._childViews.splice.apply(this._childViews, args); + if (content && sortProperties) { + forEach(content, function(item) { + forEach(sortProperties, function(sortProperty) { + Ember.removeObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); + }, this); + }, this); } - this.arrayContentDidChange(idx, removedCount, addedCount); - this.childViewsDidChange(this._childViews, idx, removedCount, addedCount); - - return this; - }, + this._super(); + }, 'content'), - objectAt: function(idx) { - return this._childViews[idx]; - }, + sortAscendingWillChange: Ember.beforeObserver(function() { + this._lastSortAscending = get(this, 'sortAscending'); + }, 'sortAscending'), - length: Ember.computed(function () { - return this._childViews.length; - }), + sortAscendingDidChange: Ember.observer(function() { + if (get(this, 'sortAscending') !== this._lastSortAscending) { + var arrangedContent = get(this, 'arrangedContent'); + arrangedContent.reverseObjects(); + } + }, 'sortAscending'), - /** - @private + contentArrayWillChange: function(array, idx, removedCount, addedCount) { + var isSorted = get(this, 'isSorted'); - Instructs each child view to render to the passed render buffer. + if (isSorted) { + var arrangedContent = get(this, 'arrangedContent'); + var removedObjects = array.slice(idx, idx+removedCount); + var sortProperties = get(this, 'sortProperties'); - @method render - @param {Ember.RenderBuffer} buffer the buffer to render to - */ - render: function(buffer) { - this.forEachChildView(function(view) { - view.renderToBuffer(buffer); - }); - }, + forEach(removedObjects, function(item) { + arrangedContent.removeObject(item); - instrumentName: 'render.container', + forEach(sortProperties, function(sortProperty) { + Ember.removeObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); + }, this); + }, this); + } - /** - @private + return this._super(array, idx, removedCount, addedCount); + }, - When a child view is removed, destroy its element so that - it is removed from the DOM. + contentArrayDidChange: function(array, idx, removedCount, addedCount) { + var isSorted = get(this, 'isSorted'), + sortProperties = get(this, 'sortProperties'); - The array observer that triggers this action is set up in the - `renderToBuffer` method. + if (isSorted) { + var addedObjects = array.slice(idx, idx+addedCount); - @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'); + forEach(addedObjects, function(item) { + this.insertItemSorted(item); - 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); + forEach(sortProperties, function(sortProperty) { + Ember.addObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); + }, this); + }, this); } + + return this._super(array, idx, removedCount, addedCount); }, - removeChild: function(child) { - this.removeObject(child); - return this; - }, - - /** - @private - - 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. + insertItemSorted: function(item) { + var arrangedContent = get(this, 'arrangedContent'); + var length = get(arrangedContent, 'length'); - @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'); + var idx = this._binarySearch(item, 0, length); + arrangedContent.insertAt(idx, item); }, - initializeViews: function(views, parentView, templateData) { - forEach(views, function(view) { - set(view, '_parentView', parentView); + 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); - if (!get(view, 'templateData')) { - set(view, 'templateData', templateData); - } - }); + if (leftResult < 0 || rightResult > 0) { + arrangedContent.removeObject(item); + this.insertItemSorted(item); + } }, - currentView: null, - - _currentViewWillChange: Ember.beforeObserver(function() { - var currentView = get(this, 'currentView'); - if (currentView) { - currentView.destroy(); - } - }, 'currentView'), + _binarySearch: function(item, low, high) { + var mid, midItem, res, arrangedContent; - _currentViewDidChange: Ember.observer(function() { - var currentView = get(this, 'currentView'); - if (currentView) { - this.pushObject(currentView); + if (low === high) { + return low; } - }, 'currentView'), - _ensureChildrenAreInDOM: function () { - this.currentState.ensureChildrenAreInDOM(this); - } -}); + arrangedContent = get(this, 'arrangedContent'); -Ember.merge(states._default, { - childViewsWillChange: Ember.K, - childViewsDidChange: Ember.K, - ensureChildrenAreInDOM: Ember.K -}); + mid = low + Math.floor((high - low) / 2); + midItem = arrangedContent.objectAt(mid); -Ember.merge(states.inBuffer, { - childViewsDidChange: function(parentView, views, start, added) { - throw new Error('You cannot modify child views while in the inBuffer state'); - } -}); + res = this.orderBy(midItem, item); -Ember.merge(states.hasElement, { - childViewsWillChange: function(view, views, start, removed) { - for (var i=start; i 0) { + return this._binarySearch(item, low, mid); } - }, - - childViewsDidChange: function(view, views, start, added) { - Ember.run.scheduleOnce('render', view, '_ensureChildrenAreInDOM'); - }, - ensureChildrenAreInDOM: function(view) { - var childViews = view._childViews, i, len, childView, previous, buffer; - for (i = 0, len = childViews.length; i < len; i++) { - childView = childViews[i]; - buffer = childView.renderToBufferIfNeeded(); - if (buffer) { - childView.triggerRecursively('willInsertElement'); - if (previous) { - previous.domManager.after(previous, buffer.string()); - } else { - view.domManager.prepend(view, buffer.string()); - } - childView.transitionTo('inDOM'); - childView.propertyDidChange('element'); - childView.triggerRecursively('didInsertElement'); - } - previous = childView; - } + return mid; } }); @@ -16617,1456 +17847,8182 @@ Ember.merge(states.hasElement, { (function() { /** @module ember -@submodule ember-views +@submodule ember-runtime */ -var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; +var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach, + replace = Ember.EnumerableUtils.replace; /** - `Ember.CollectionView` is an `Ember.View` descendent responsible for managing - a collection (an array or array-like object) by maintaing 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. + `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. - ## Setting content + 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. - The managed collection of objects is referenced as the `Ember.CollectionView` - instance's `content` property. + 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: ```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. - - ## Specifying itemViewClass - - 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. - - Given an empty `` and the following code: + MyApp.listController = Ember.ArrayController.create(); - ```javascript - someItemsView = Ember.CollectionView.create({ - classNames: ['a-collection'], - content: ['A','B','C'], - itemViewClass: Ember.View.extend({ - template: Ember.Handlebars.compile("the letter: {{view.content}}") - }) + $.get('people.json', function(data) { + MyApp.listController.set('content', data); }); - - someItemsView.appendTo('body'); ``` - Will result in the following HTML structure + Then, create a view that binds to your new controller: - ```html -
    -
    the letter: A
    -
    the letter: B
    -
    the letter: C
    -
    + ```handlebars + {{#each MyApp.listController}} + {{firstName}} {{lastName}} + {{/each}} ``` - ## 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. + 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. - Given an empty `` and the following code: + 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. - ```javascript - anUndorderedListView = Ember.CollectionView.create({ - tagName: 'ul', - content: ['A','B','C'], - itemViewClass: Ember.View.extend({ - template: Ember.Handlebars.compile("the letter: {{view.content}}") - }) - }); + For example: - anUndorderedListView.appendTo('body'); + ```handlebars + {{#each post in controller}} +
  • {{title}} ({{titleLength}} characters)
  • + {{/each}} ``` - Will result in the following HTML structure - - ```html -
      -
    • the letter: A
    • -
    • the letter: B
    • -
    • the letter: C
    • -
    - ``` + ```javascript + App.PostsController = Ember.ArrayController.extend({ + itemController: 'post' + }); - Additional `tagName` pairs can be provided by adding to - `Ember.CollectionView.CONTAINER_MAP ` + App.PostController = Ember.ObjectController.extend({ + // the `title` property will be proxied to the underlying post. - ```javascript - Ember.CollectionView.CONTAINER_MAP['article'] = 'section' + titleLength: function() { + return this.get('title').length; + }.property('title') + }); ``` - ## Programatic creation of child views + In some cases it is helpful to return a different `itemController` depending + on the particular item. Subclasses can do this by overriding + `lookupItemController`. - For cases where additional customization beyond the use of a single - `itemViewClass` or `tagName` matching is required CollectionView's - `createChildView` method can be overidden: + For example: ```javascript - CustomCollectionView = Ember.CollectionView.extend({ - createChildView: function(viewClass, attrs) { - if (attrs.content.kind == 'album') { - viewClass = App.AlbumView; + App.MyArrayController = Ember.ArrayController.extend({ + lookupItemController: function( object ) { + if (object.get('isSpecial')) { + return "special"; // use App.SpecialController } else { - viewClass = App.SongView; + return "regular"; // use App.RegularController } - this._super(viewClass, attrs); } }); ``` - ## Empty View + The itemController instances will have a `parentController` property set to + either the the `parentController` property of the `ArrayController` + or to the `ArrayController` instance itself. - 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. + @class ArrayController + @namespace Ember + @extends Ember.ArrayProxy + @uses Ember.SortableMixin + @uses Ember.ControllerMixin +*/ - ```javascript - aListWithNothing = Ember.CollectionView.create({ - classNames: ['nothing'] - content: null, - emptyView: Ember.View.extend({ - template: Ember.Handlebars.compile("The collection is empty") - }) - }); +Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, + Ember.SortableMixin, { - aListWithNothing.appendTo('body'); - ``` + /** + The controller used to wrap items, if any. - 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. - - ## Use in templates via the `{{collection}}` `Ember.Handlebars` helper - - `Ember.Handlebars` provides a helper specifically for adding - `CollectionView`s to templates. See `Ember.Handlebars.collection` for more - details - - @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 + @property itemController + @type String @default null */ - content: null, + itemController: null, /** - @private - - This provides metadata about what kind of empty view class this - collection would like if it is being instantiated from another - system (like Handlebars) - - @property emptyViewClass - */ - emptyViewClass: Ember.View, + 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. - /** - An optional view to display if content is set to an empty array. + For example: - @property emptyView - @type Ember.View - @default null - */ - emptyView: null, + ```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 itemViewClass - @type Ember.View - @default Ember.View + @method lookupItemController + @param {Object} object + @return {String} */ - itemViewClass: Ember.View, - - init: function() { - var ret = this._super(); - this._contentDidChange(); - return ret; + lookupItemController: function(object) { + return get(this, 'itemController'); }, - _contentWillChange: Ember.beforeObserver(function() { - var content = this.get('content'); + objectAtContent: function(idx) { + var length = get(this, 'length'), + arrangedContent = get(this,'arrangedContent'), + object = arrangedContent && arrangedContent.objectAt(idx); - if (content) { content.removeArrayObserver(this); } - var len = content ? get(content, 'length') : 0; - this.arrayWillChange(content, 0, len); - }, 'content'), + if (idx >= 0 && idx < length) { + var controllerClass = this.lookupItemController(object); + if (controllerClass) { + return this.controllerAt(idx, object, controllerClass); + } + } - /** - @private + // When `controllerClass` is falsy, we have not opted in to using item + // controllers, so return the object directly. - 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. + // 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; + }, - @method _contentDidChange - */ - _contentDidChange: Ember.observer(function() { - var content = get(this, 'content'); + arrangedContentDidChange: function() { + this._super(); + this._resetSubControllers(); + }, - if (content) { - Ember.assert(fmt("an Ember.CollectionView's content must implement Ember.Array. You passed %@", [content]), Ember.Array.detect(content)); - content.addArrayObserver(this); - } + arrayContentDidChange: function(idx, removedCnt, addedCnt) { + var subControllers = get(this, '_subControllers'), + subControllersToRemove = subControllers.slice(idx, idx+removedCnt); - var len = content ? get(content, 'length') : 0; - this.arrayDidChange(content, 0, null, len); - }, 'content'), + forEach(subControllersToRemove, function(subController) { + if (subController) { subController.destroy(); } + }); - willDestroy: function() { - var content = get(this, 'content'); - if (content) { content.removeArrayObserver(this); } + replace(subControllers, idx, removedCnt, new Array(addedCnt)); + + // 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); + }, + init: function() { this._super(); - if (this._createdEmptyView) { - this._createdEmptyView.destroy(); - } + this.set('_subControllers', Ember.A()); }, - 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(); - } + content: Ember.computed(function () { + return Ember.A(); + }), - // 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; + controllerAt: function(idx, object, controllerClass) { + var container = get(this, 'container'), + subControllers = get(this, '_subControllers'), + subController = subControllers[idx], + factory, fullName; - len = this._childViews.length; + if (subController) { return subController; } - var removingAll = removedCount === len; + fullName = "controller:" + controllerClass; - if (removingAll) { - this.currentState.empty(this); + if (!container.has(fullName)) { + throw new Error('Could not resolve itemController: "' + controllerClass + '"'); } - for (idx = start + removedCount - 1; idx >= start; idx--) { - childView = childViews[idx]; - if (removingAll) { childView.removedFromDOM = true; } - childView.destroy(); - } + subController = container.lookupFactory(fullName).create({ + target: this, + parentController: get(this, 'parentController') || this, + content: object + }); + + subControllers[idx] = subController; + + return subController; }, - /** - Called when a mutation to the underlying content array occurs. + _subControllers: null, - This method will replay that mutation against the views that compose the - `Ember.CollectionView`, ensuring that the view reflects the model. + _resetSubControllers: function() { + var subControllers = get(this, '_subControllers'); + if (subControllers) { + forEach(subControllers, function(subController) { + if (subController) { subController.destroy(); } + }); + } - This array observer is added in `contentDidChange`. + this.set('_subControllers', Ember.A()); + } +}); - @method arrayDidChange - @param {Array} addedObjects the objects that were added to the content - @param {Array} removedObjects the objects that were removed from the content - @param {Number} changeIndex the index at which the changes occurred - */ - arrayDidChange: function(content, start, removed, added) { - var itemViewClass = get(this, 'itemViewClass'), - addedViews = [], view, item, idx, len, itemTagName; +})(); - if ('string' === typeof itemViewClass) { - itemViewClass = get(itemViewClass); - } - Ember.assert(fmt("itemViewClass must be a subclass of Ember.View, not %@", [itemViewClass]), Ember.View.detect(itemViewClass)); - len = content ? get(content, 'length') : 0; - if (len) { - for (idx = start; idx < start+added; idx++) { - item = content.objectAt(idx); +(function() { +/** +@module ember +@submodule ember-runtime +*/ - view = this.createChildView(itemViewClass, { - content: item, - contentIndex: idx - }); +/** + `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`. - addedViews.push(view); - } - } else { - var emptyView = get(this, 'emptyView'); - if (!emptyView) { return; } + `Ember.ObjectController` derives this functionality from its superclass + `Ember.ObjectProxy` and the `Ember.ControllerMixin` mixin. + + @class ObjectController + @namespace Ember + @extends Ember.ObjectProxy + @uses Ember.ControllerMixin +**/ +Ember.ObjectController = Ember.ObjectProxy.extend(Ember.ControllerMixin); - var isClass = Ember.CoreView.detect(emptyView); +})(); - emptyView = this.createChildView(emptyView); - addedViews.push(emptyView); - set(this, 'emptyView', emptyView); - if (isClass) { this._createdEmptyView = emptyView; } - } - this.replace(start, 0, addedViews); - }, - createChildView: function(view, attrs) { - view = this._super(view, attrs); +(function() { - var itemTagName = get(view, 'tagName'); - var tagName = (itemTagName === null || itemTagName === undefined) ? Ember.CollectionView.CONTAINER_MAP[get(this, 'tagName')] : itemTagName; +})(); - set(view, 'tagName', tagName); - return view; - } -}); +(function() { /** - 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. +Ember Runtime - @property CONTAINER_MAP - @type Hash - @static - @final +@module ember +@submodule ember-runtime +@requires ember-metal */ -Ember.CollectionView.CONTAINER_MAP = { - ul: 'li', - ol: 'li', - table: 'tr', - thead: 'tr', - tbody: 'tr', - tfoot: 'tr', - tr: 'td', - select: 'option' -}; })(); +(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)); +/** + Alias for jQuery -(function() { + @method $ + @for Ember +*/ +Ember.$ = jQuery; })(); (function() { -/*globals jQuery*/ /** -Ember Views - @module ember @submodule ember-views -@requires ember-runtime -@main 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'); + + // 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'] }; + }); +} })(); -(function() { -define("metamorph", - [], - function() { - "use strict"; - // ========================================================================== - // Project: metamorph - // Copyright: ©2011 My Company Inc. All rights reserved. - // ========================================================================== - - var K = function(){}, - guid = 0, - document = window.document, - // Feature-detect the W3C range API, the extended check is for IE9 which only partially supports ranges - supportsRange = ('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 = (function(){ - var testEl = document.createElement('div'); - testEl.innerHTML = "
    "; - testEl.firstChild.innerHTML = ""; - return testEl.firstChild.innerHTML === ''; - })(), +(function() { +/** +@module ember +@submodule ember-views +*/ +/* BEGIN METAMORPH HELPERS */ - // 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 = (function() { - var testEl = document.createElement('div'); - testEl.innerHTML = "Test: Value"; - return testEl.childNodes[0].nodeValue === 'Test:' && - testEl.childNodes[2].nodeValue === ' Value'; - })(); +// 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 ­ - // Constructor that supports either Metamorph('foo') or new - // Metamorph('foo'); - // - // Takes a string of HTML as the argument. +var needsShy = this.document && (function() { + var testEl = document.createElement('div'); + testEl.innerHTML = "
    "; + testEl.firstChild.innerHTML = ""; + return testEl.firstChild.innerHTML === ''; +})(); - var Metamorph = function(html) { - var self; +// 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'; +})(); - if (this instanceof Metamorph) { - self = this; - } else { - self = new K(); - } +// Use this to find children by ID instead of using jQuery +var findChildById = function(element, id) { + if (element.getAttribute('id') === id) { return element; } - self.innerHTML = html; - var myGuid = 'metamorph-'+(guid++); - self.start = myGuid + '-start'; - self.end = myGuid + '-end'; + var len = element.childNodes.length, idx, node, found; + for (idx=0; idx 0) { + var len = matches.length, idx; + for (idx=0; idx\x3C/script>"; - }; + if (needsShy) { + var shyElement = element.firstChild; + while (shyElement.nodeType === 1 && !shyElement.nodeName) { + shyElement = shyElement.firstChild; + } + if (shyElement.nodeType === 3 && shyElement.nodeValue.charAt(0) === "\u00AD") { + shyElement.nodeValue = shyElement.nodeValue.slice(1); + } + } +}; - endTagFunc = 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(!POSSIBLE_CHARS_REGEXP.test(string)) { return string; } + return string.replace(BAD_CHARS_REGEXP, escapeChar); +} - return start; - }; +/** + `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. - /** - * 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); - } - }; + ```javascript + var buffer = Ember.RenderBuffer('div'); + ``` - 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; + @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); +}; - // make sure that the start and end nodes share the same - // parent. If not, fix it. - fixParentage(start, end); +Ember._RenderBuffer = function(tagName) { + this.tagNames = [tagName || null]; + this.buffer = ""; +}; - // 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; +Ember._RenderBuffer.prototype = +/** @scope Ember.RenderBuffer.prototype */ { - // 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; } - } + // The root view's element + _element: null, - node.parentNode.removeChild(node); + _hasElement: true, - // 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; } + /** + @private - node = nextSibling; - } + An internal set used to de-dupe class names when `addClass()` is + used. After each call to `addClass()`, the `classes` property + will be updated. - // 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); + @property elementClasses + @type Array + @default [] + */ + elementClasses: null, - // copy the nodes for the HTML between the starting and ending - // placeholder. - while (node) { - nextSibling = node.nextSibling; - parentNode.insertBefore(node, end); - node = nextSibling; - } - }; + /** + Array of class names which will be applied in the class attribute. - // 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); + You can use `setClasses()` to set this property directly. If you + use `addClass()`, it will be maintained for you. - this.html(''); - start.parentNode.removeChild(start); - end.parentNode.removeChild(end); - }; + @property classes + @type Array + @default [] + */ + classes: null, - appendToFunc = function(parentNode) { - var node = firstNodeFor(parentNode, this.outerHTML()); - var nextSibling; + /** + The id in of the element, to be applied in the id attribute. - while (node) { - nextSibling = node.nextSibling; - parentNode.appendChild(node); - node = nextSibling; - } - }; + You should not set this property yourself, rather, you should use + the `id()` method of `Ember.RenderBuffer`. - 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; + @property elementId + @type String + @default null + */ + elementId: null, - // 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); + /** + 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'}`. - // 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; + You should not maintain this hash yourself, rather, you should use + the `attr()` method of `Ember.RenderBuffer`. - while (node) { - nextSibling = node.nextSibling; - parentNode.insertBefore(node, insertBefore); - node = nextSibling; - } - }; - } + @property elementAttributes + @type Hash + @default {} + */ + elementAttributes: null, - Metamorph.prototype.html = function(html) { - this.checkRemoved(); - if (html === undefined) { return this.innerHTML; } + /** + 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}`. - htmlFunc.call(this, html); + You should not maintain this hash yourself, rather, you should use + the `prop()` method of `Ember.RenderBuffer`. - this.innerHTML = html; - }; + @property elementProperties + @type Hash + @default {} + */ + elementProperties: null, - Metamorph.prototype.replaceWith = function(html) { - this.checkRemoved(); - htmlFunc.call(this, html, true); - }; + /** + The tagname of the element an instance of `Ember.RenderBuffer` represents. - 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; + 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 - Metamorph.prototype.isRemoved = function() { - var before = document.getElementById(this.start); - var after = document.getElementById(this.end); + ```javascript + Ember.RenderBuffer('p') + ``` - return !before || !after; - }; + @property elementTag + @type String + @default null + */ + elementTag: null, - Metamorph.prototype.checkRemoved = function() { - if (this.isRemoved()) { - throw new Error("Cannot perform operations on a Metamorph that is not in the DOM."); - } - }; + /** + 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'}`. - return Metamorph; - }); + You should not maintain this hash yourself, rather, you should use + the `style()` method of `Ember.RenderBuffer`. -})(); + @property elementStyle + @type Hash + @default {} + */ + elementStyle: null, -(function() { -/** -@module ember -@submodule ember-handlebars -*/ + /** + Nested `RenderBuffers` will set this to their parent `RenderBuffer` + instance. -// Eliminate dependency on any Ember to simplify precompilation workflow -var objectCreate = Object.create || function(parent) { - function F() {} - F.prototype = parent; - return new F(); -}; + @property parentBuffer + @type Ember._RenderBuffer + */ + parentBuffer: null, -var Handlebars = this.Handlebars || Ember.imports.Handlebars; -Ember.assert("Ember Handlebars requires Handlebars 1.0.0-rc.3 or greater", Handlebars && Handlebars.VERSION.match(/^1\.0\.[0-9](\.rc\.[23456789]+)?/)); + /** + Adds a string of HTML to the `RenderBuffer`. -/** - Prepares the Handlebars templating library for use inside Ember's view - system. + @method push + @param {String} string HTML to push into the buffer + @chainable + */ + push: function(string) { + this.buffer += string; + return this; + }, - 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. + /** + Adds a class to the buffer, which will be rendered to the class attribute. - To create an `Ember.Handlebars` template, call `Ember.Handlebars.compile()`. - This will return a function that can be used by `Ember.View` for rendering. + @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; - @class Handlebars - @namespace Ember -*/ -Ember.Handlebars = objectCreate(Handlebars); + return this; + }, -/** -@class helpers -@namespace Ember.Handlebars -*/ -Ember.Handlebars.helpers = objectCreate(Handlebars.helpers); + setClasses: function(classNames) { + this.classes = classNames; + }, -/** - Override the the opcode compiler and JavaScript compiler for Handlebars. + /** + Sets the elementID to be used for the element. - @class Compiler - @namespace Ember.Handlebars - @private - @constructor -*/ -Ember.Handlebars.Compiler = function() {}; + @method id + @param {String} id + @chainable + */ + id: function(id) { + this.elementId = id; + return this; + }, -// Handlebars.Compiler doesn't exist in runtime-only -if (Handlebars.Compiler) { - Ember.Handlebars.Compiler.prototype = objectCreate(Handlebars.Compiler.prototype); -} + // duck type attribute functionality like jQuery so a render buffer + // can be used like a jQuery object in attribute binding scenarios. -Ember.Handlebars.Compiler.prototype.compiler = Ember.Handlebars.Compiler; + /** + Adds an attribute which will be rendered to the element. -/** - @class JavaScriptCompiler - @namespace Ember.Handlebars - @private - @constructor -*/ -Ember.Handlebars.JavaScriptCompiler = function() {}; + @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 || {}); -// 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; -} + if (arguments.length === 1) { + return attributes[name]; + } else { + attributes[name] = value; + } + return this; + }, -Ember.Handlebars.JavaScriptCompiler.prototype.namespace = "Ember.Handlebars"; + /** + Remove an attribute from the list of attributes to render. + @method removeAttr + @param {String} name The name of the attribute + @chainable + */ + removeAttr: function(name) { + var attributes = this.elementAttributes; + if (attributes) { delete attributes[name]; } -Ember.Handlebars.JavaScriptCompiler.prototype.initializeBuffer = function() { - return "''"; -}; + return this; + }, -/** - @private + /** + Adds an property which will be rendered to the element. - 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. + @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 || {}); - @method appendToBuffer - @param string {String} -*/ -Ember.Handlebars.JavaScriptCompiler.prototype.appendToBuffer = function(string) { - return "data.buffer.push("+string+");"; -}; + if (arguments.length === 1) { + return properties[name]; + } else { + properties[name] = value; + } -var prefix = "ember" + (+new Date()), incr = 1; + return this; + }, -/** - @private + /** + Remove an property from the list of properties to render. - Rewrite simple mustaches from `{{foo}}` to `{{bind "foo"}}`. This means that + @method removeProp + @param {String} name The name of the property + @chainable + */ + removeProp: function(name) { + var properties = this.elementProperties; + if (properties) { delete properties[name]; } + + return this; + }, + + /** + Adds a style to the style attribute which will be rendered to the element. + + @method style + @param {String} name Name of the style + @param {String} value + @chainable + */ + style: function(name, value) { + this.elementStyle = (this.elementStyle || {}); + + this.elementStyle[name] = value; + return this; + }, + + begin: function(tagName) { + this.tagNames.push(tagName || null); + return this; + }, + + pushOpeningTag: function() { + var tagName = this.currentTagName(); + if (!tagName) { return; } + + if (this._hasElement && !this._element && this.buffer.length === 0) { + this._element = this.generateElement(); + return; + } + + var buffer = this.buffer, + id = this.elementId, + classes = this.classes, + attrs = this.elementAttributes, + props = this.elementProperties, + style = this.elementStyle, + attr, prop; + + buffer += '<' + stripTagName(tagName); + + if (id) { + buffer += ' id="' + escapeAttribute(id) + '"'; + this.elementId = null; + } + if (classes) { + buffer += ' class="' + escapeAttribute(classes.join(' ')) + '"'; + this.classes = null; + } + + if (style) { + buffer += ' style="'; + + for (prop in style) { + if (style.hasOwnProperty(prop)) { + buffer += prop + ':' + escapeAttribute(style[prop]) + ';'; + } + } + + buffer += '"'; + + this.elementStyle = null; + } + + if (attrs) { + for (attr in attrs) { + if (attrs.hasOwnProperty(attr)) { + buffer += ' ' + attr + '="' + escapeAttribute(attrs[attr]) + '"'; + } + } + + this.elementAttributes = null; + } + + 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]) + '"'; + } + } + } + } + + this.elementProperties = null; + } + + 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 + element = document.createElement(tagName), + $element = Ember.$(element), + id = this.elementId, + classes = this.classes, + attrs = this.elementAttributes, + props = this.elementProperties, + style = this.elementStyle, + styleBuffer = '', attr, prop; + + if (id) { + $element.attr('id', id); + this.elementId = null; + } + if (classes) { + $element.attr('class', classes.join(' ')); + this.classes = null; + } + + if (style) { + for (prop in style) { + if (style.hasOwnProperty(prop)) { + styleBuffer += (prop + ':' + style[prop] + ';'); + } + } + + $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; + } + + if (props) { + for (prop in props) { + if (props.hasOwnProperty(prop)) { + $element.prop(prop, props[prop]); + } + } + + this.elementProperties = null; + } + + return element; + }, + + /** + @method element + @return {DOMElement} The element corresponding to the generated HTML + of this buffer + */ + element: function() { + var html = this.innerString(); + + if (html) { + this._element = Ember.ViewUtils.setInnerHTML(this._element, html); + } + + return this._element; + }, + + /** + Generates the HTML content for this buffer. + + @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(); + } + }, + + innerString: function() { + return this.buffer; + } +}; + +})(); + + + +(function() { +/** +@module ember +@submodule ember-views +*/ + +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. + + @class EventDispatcher + @namespace Ember + @private + @extends Ember.Object +*/ +Ember.EventDispatcher = Ember.Object.extend(/** @scope Ember.EventDispatcher.prototype */{ + + /** + 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. + + This set will be modified by `setup` to also include any events added at that time. + + @property events + @type Object + */ + 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' + }, + + /** + @private + + The root DOM element to which event listeners should be attached. Event + listeners will be attached to the document unless this is overridden. + + 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 + + Sets up event listeners for standard browser events. + + 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 setup + @param addedEvents {Hash} + */ + setup: function(addedEvents, rootElement) { + var event, events = get(this, 'events'); + + Ember.$.extend(events, addedEvents || {}); + + + if (!Ember.isNone(rootElement)) { + set(this, 'rootElement', rootElement); + } + + rootElement = Ember.$(get(this, 'rootElement')); + + 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); + + rootElement.addClass('ember-application'); + + 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')); + + for (event in events) { + if (events.hasOwnProperty(event)) { + this.setupHandler(rootElement, event, events[event]); + } + } + }, + + /** + @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. + + 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. + + For example, to have the `mouseDown` method called on the target view when + a `mousedown` event is received from the browser, do the following: + + ```javascript + setupHandler('mousedown', 'mouseDown'); + ``` + + @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; + + rootElement.on(event + '.ember', '.ember-view', function(evt, triggeringManager) { + return Ember.handleErrors(function() { + var view = Ember.View.views[this.id], + result = true, manager = null; + + manager = self._findNearestEventManager(view,eventName); + + if (manager && manager !== triggeringManager) { + result = self._dispatchEvent(manager, evt, eventName, view); + } else if (view) { + result = self._bubbleEvent(view,evt,eventName); + } else { + evt.stopPropagation(); + } + + 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); + }); + }, + + _findNearestEventManager: function(view, eventName) { + var manager = null; + + while (view) { + manager = get(view, 'eventManager'); + if (manager && manager[eventName]) { break; } + + view = get(view, 'parentView'); + } + + return manager; + }, + + _dispatchEvent: function(object, evt, eventName, view) { + var result = true; + + var handler = object[eventName]; + if (Ember.typeOf(handler) === 'function') { + result = Ember.run(function() { + return handler.call(object, evt, view); + }); + // Do not preventDefault in eventManagers. + evt.stopPropagation(); + } + else { + result = this._bubbleEvent(view, evt, eventName); + } + + return result; + }, + + _bubbleEvent: function(view, evt, eventName) { + return Ember.run(function() { + return view.handleEvent(eventName, evt); + }); + }, + + destroy: function() { + var rootElement = get(this, 'rootElement'); + Ember.$(rootElement).off('.ember', '**').removeClass('ember-application'); + return this._super(); + } +}); + +})(); + + + +(function() { +/** +@module ember +@submodule ember-views +*/ + +// 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'); + +})(); + + + +(function() { +/** +@module ember +@submodule ember-views +*/ + +var get = Ember.get, set = Ember.set; + +// Original class declaration and documentation in runtime/lib/controllers/controller.js +// NOTE: It may be possible with YUIDoc to combine docs in two locations + +/** +Additional methods for the ControllerMixin + +@class ControllerMixin +@namespace Ember +*/ +Ember.ControllerMixin.reopen({ + target: null, + namespace: null, + view: null, + container: null, + _childContainers: null, + + init: function() { + this._super(); + set(this, '_childContainers', {}); + }, + + _modelDidChange: Ember.observer(function() { + var containers = get(this, '_childContainers'); + + for (var prop in containers) { + if (!containers.hasOwnProperty(prop)) { continue; } + containers[prop].destroy(); + } + + set(this, '_childContainers', {}); + }, 'model') +}); + +})(); + + + +(function() { + +})(); + + + +(function() { +var states = {}; + +/** +@module ember +@submodule ember-views +*/ + +var get = Ember.get, set = Ember.set; +var guidFor = Ember.guidFor; +var a_forEach = Ember.EnumerableUtils.forEach; +var a_addObject = Ember.EnumerableUtils.addObject; + +var childViewsProperty = Ember.computed(function() { + var childViews = this._childViews, ret = Ember.A(), view = this; + + a_forEach(childViews, function(view) { + var currentChildViews; + if (view.isVirtual) { + if (currentChildViews = get(view, 'childViews')) { + ret.pushObjects(currentChildViews); + } + } else { + ret.push(view); + } + }); + + 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 Error("childViews is immutable"); + }; + + return ret; +}); + +Ember.warn("The VIEW_PRESERVES_CONTEXT flag has been removed and the functionality can no longer be disabled.", Ember.ENV.VIEW_PRESERVES_CONTEXT !== false); + +/** + 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. + + @property TEMPLATES + @for Ember + @type Hash +*/ +Ember.TEMPLATES = {}; + +/** + `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`. + + Unless you have specific needs for `CoreView`, you will use `Ember.View` + in your applications. + + @class CoreView + @namespace Ember + @extends Ember.Object + @uses Ember.Evented +*/ + +Ember.CoreView = Ember.Object.extend(Ember.Evented, Ember.ActionHandler, { + isView: true, + + states: states, + + init: function() { + this._super(); + this.transitionTo('preRender'); + }, + + /** + If the view is currently inserted into the DOM of a parent view, this + property will point to the parent of the view. + + @property parentView + @type Ember.View + @default null + */ + parentView: Ember.computed(function() { + var parent = this._parentView; + + if (parent && parent.isVirtual) { + return get(parent, 'parentView'); + } else { + return parent; + } + }).property('_parentView'), + + state: null, + + _parentView: null, + + // return the current view, not including virtual views + concreteView: Ember.computed(function() { + if (!this.isVirtual) { return this; } + else { return get(this, 'parentView'); } + }).property('parentView'), + + instrumentName: 'core_view', + + instrumentDetails: function(hash) { + hash.object = this.toString(); + }, + + /** + @private + + 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. + + You should not need to override this method; instead, implement the + `template` property, or if you need more control, override the `render` + method. + + @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. + */ + 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); + }, + + _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; + + if (tagName === null || tagName === undefined) { + tagName = 'div'; + } + + var buffer = this.buffer = parentBuffer && parentBuffer.begin(tagName) || Ember.RenderBuffer(tagName); + this.transitionTo('inBuffer', false); + + this.beforeRender(buffer); + this.render(buffer); + this.afterRender(buffer); + + return buffer; + }, + + /** + @private + + Override the default event firing from `Ember.Evented` to + also call methods with the given name. + + @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); + } + }, + + deprecatedSendHandles: function(actionName) { + return !!this[actionName]; + }, + + 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; + }, + + has: function(name) { + return Ember.typeOf(this[name]) === 'function' || this._super(name); + }, + + 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); + + return this; + }, + + clearRenderedChildren: Ember.K, + triggerRecursively: Ember.K, + invokeRecursively: Ember.K, + transitionTo: Ember.K, + destroyElement: Ember.K +}); + +var ViewCollection = Ember._ViewCollection = function(initialViews) { + var views = this.views = initialViews || []; + this.length = views.length; +}; + +ViewCollection.prototype = { + length: 0, + + 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); } + } + }, + + triggerRecursively: function(eventName) { + var views = this.views; + for (var i = 0, l = views.length; i < l; i++) { + views[i].triggerRecursively(eventName); + } + }, + + invokeRecursively: function(fn) { + var views = this.views, view; + + for (var i = 0, l = views.length; i < l; i++) { + view = views[i]; + fn(view); + } + }, + + transitionTo: function(state, children) { + var views = this.views; + for (var i = 0, l = views.length; i < l; i++) { + views[i].transitionTo(state, children); + } + }, + + push: function() { + this.length += arguments.length; + var views = this.views; + return views.push.apply(views, arguments); + }, + + objectAt: function(idx) { + return this.views[idx]; + }, + + forEach: function(callback) { + var views = this.views; + return a_forEach(views, callback); + }, + + clear: function() { + this.length = 0; + this.views.length = 0; + } +}; + +var EMPTY_ARRAY = []; + +/** + `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 + + 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: + + ```javascript + ParagraphView = Ember.View.extend({ + tagName: 'em' + }); + ``` + + Would result in instances with the following HTML: + + ```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'] + }); + ``` + + 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' + }); + ``` + + 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( +/** @scope Ember.View.prototype */ { + + concatenatedProperties: ['classNames', 'classNameBindings', 'attributeBindings'], + + /** + @property isView + @type Boolean + @default true + @final + */ + isView: true, + + // .......................................................... + // TEMPLATE SUPPORT + // + + /** + 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`. + + @property templateName + @type String + @default null + */ + templateName: null, + + /** + The name of the layout to lookup if no layout 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`. + + @property layoutName + @type String + @default null + */ + layoutName: null, + + /** + The hash in which to look for `templateName`. + + @property templates + @type Ember.Object + @default Ember.TEMPLATES + */ + templates: Ember.TEMPLATES, + + /** + 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(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'), + + /** + 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(function(key) { + var parentView = get(this, '_parentView'); + return parentView ? get(parentView, 'controller') : null; + }).property('_parentView'), + + /** + 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 + + 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 + */ + _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; + }), + + /** + @private + + If a value that affects template rendering changes, the view should be + re-rendered to reflect the new value. + + @method _contextDidChange + */ + _contextDidChange: Ember.observer(function() { + this.rerender(); + }, 'context'), + + /** + If `false`, the view will appear hidden in DOM. + + @property isVisible + @type Boolean + @default null + */ + isVisible: true, + + /** + @private + + Array of child views. You should never edit this array directly. + Instead, use `appendChild` and `removeFromParent`. + + @property childViews + @type Array + @default [] + */ + 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(function() { + if (this.isVirtual) { + var parentView = get(this, 'parentView'); + if (parentView) { Ember.propertyWillChange(parentView, 'childViews'); } + } + }, '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'); } + } + }, 'childViews'), + + /** + Return the nearest ancestor that is an instance of the provided + class. + + @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'); + + 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. + + @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); }; + + while (view) { + if (isOfType(view)) { return view; } + view = get(view, 'parentView'); + } + }, + + /** + 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'); + + while (view) { + if (property in view) { return view; } + view = get(view, 'parentView'); + } + }, + + /** + 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 + */ + nearestChildOf: function(klass) { + var view = get(this, 'parentView'); + + while (view) { + if (get(view, 'parentView') instanceof klass) { return view; } + view = get(view, 'parentView'); + } + }, + + /** + @private + + When the parent view changes, recursively invalidate `controller` + + @method _parentViewDidChange + */ + _parentViewDidChange: Ember.observer(function() { + if (this.isDestroying) { return; } + + this.trigger('parentViewDidChange'); + + 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'), + + 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(); } + } + }, + + /** + @private + + 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 + */ + _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) { + + // 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); + }, + + /** + @private + + 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 + */ + _applyAttributeBindings: function(buffer, attributeBindings) { + var attributeValue, elem; + + a_forEach(attributeBindings, function(binding) { + var split = binding.split(':'), + property = split[0], + attributeName = split[1] || property; + + // 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); + + // Determine the current value and add it to the render buffer + // if necessary. + attributeValue = get(this, property); + Ember.View.applyAttributeBindings(buffer, attributeName, attributeValue); + }, this); + }, + + /** + @private + + 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 + */ + _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(function(key, value) { + if (value !== undefined) { + return this.currentState.setElement(this, value); + } else { + return this.currentState.getElement(this); + } + }).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. + + @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} 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; + }, + + /** + @private + + 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 + */ + _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, + + /** + @private + + Run this callback on the current view (unless includeSelf is false) and recursively on child views. + + @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; + + 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 + // + + /** + @private + + 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 + */ + 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, + + /** + @private + + When the view's `isVisible` property changes, toggle the visibility + element of the actual DOM element. + + @method _isVisibleDidChange + */ + _isVisibleDidChange: Ember.observer(function() { + var $el = this.$(); + if (!$el) { return; } + + var isVisible = get(this, 'isVisible'); + + $el.toggle(isVisible); + + if (this._isAncestorHidden()) { return; } + + if (isVisible) { + this._notifyBecameVisible(); + } else { + this._notifyBecameHidden(); + } + }, 'isVisible'), + + _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(function(view) { + view.buffer = null; + }); + }, + + 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 (children !== false) { + this.forEachChildView(function(view) { + view.transitionTo(state); + }); + } + }, + + // ....................................................... + // EVENT HANDLING + // + + /** + @private + + Handle events from `Ember.EventDispatcher` + + @method handleEvent + @param eventName {String} + @param evt {Event} + */ + 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; + } + + 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 + * 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({ + + /** + @private + + 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 + */ + _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 + }; + }, + + /** + @private + + 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 + */ + _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') { + // We can't set properties to undefined or null + if (Ember.isNone(value)) { value = ''; } + + if (value !== elem.prop(name)) { + // value and booleans 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'); + // after createElement, the view will be in the hasElement state. + fn.call(view); + 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); + } +}); + +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 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 programatic 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 programatic 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. + + ## Binding a View to Display + + If you would like to display a single view in your ContainerView, you can set + its `currentView` property. When the `currentView` property is set to a view + instance, it will be added to the ContainerView. If the `currentView` property + is later changed to a different view, the new view will replace the old view. + If `currentView` is set to `null`, the last `currentView` will be removed. + + This functionality is useful for cases where you want to bind the display of + a ContainerView to a controller or state manager. For example, you can bind + the `currentView` of a container to a controller like this: + + ```javascript + App.appController = Ember.Object.create({ + view: Ember.View.create({ + templateName: 'person_template' + }) + }); + ``` + + ```handlebars + {{view Ember.ContainerView currentViewBinding="App.appController.view"}} + ``` + + @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(), + + /** + @private + + Instructs each child view to render to the passed render buffer. + + @method render + @param {Ember.RenderBuffer} buffer the buffer to render to + */ + render: function(buffer) { + this.forEachChildView(function(view) { + view.renderToBuffer(buffer); + }); + }, + + instrumentName: 'container', + + /** + @private + + 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. + + @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; + }, + + /** + @private + + 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. + + @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(function() { + var currentView = get(this, 'currentView'); + if (currentView) { + currentView.destroy(); + } + }, 'currentView'), + + _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'), + + _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 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 + 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'); + ``` + + 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' + ``` + + ## 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: + + ```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(/** @scope Ember.CollectionView.prototype */ { + + /** + A list of items to be displayed by the `Ember.CollectionView`. + + @property content + @type Ember.Array + @default null + */ + content: null, + + /** + @private + + This provides metadata about what kind of empty view class this + collection would like if it is being instantiated from another + system (like Handlebars) + + @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; + }, + + /** + @private + + Invoked when the content property is about to change. Notifies observers that the + entire array content will change. + + @method _contentWillChange + */ + _contentWillChange: Ember.beforeObserver(function() { + var content = this.get('content'); + + if (content) { content.removeArrayObserver(this); } + var len = content ? get(content, 'length') : 0; + this.arrayWillChange(content, 0, len); + }, 'content'), + + /** + @private + + 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 _contentDidChange + */ + _contentDidChange: Ember.observer(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); + }, 'content'), + + /** + @private + + Ensure that the content implements Ember.Array + + @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, set = Ember.set, isNone = Ember.isNone; + +/** +@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 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}} + ``` + + ```html + +

    {{person.title}}

    + {{yield}} + ``` + + 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({ + hello: function(name) { + console.log("Hello", name); + } + }); + ``` + + And then use it in the component's template: + + ```html + + +

    {{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, { + init: function() { + this._super(); + set(this, 'context', this); + set(this, 'controller', this); + }, + + // 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: get(this, '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'), + + /** + Sends an action to component's controller. A component inherits its + controller from the context in which it is used. + + By default, calling `sendAction()` will send an action with the name + of the component's `action` property. + + 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. + + If you provide the `action` argument to `sendAction()`, that key will + be used to look up the action name. + + 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. + + 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. + + 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. + + 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: + + ```javascript + App.MyTreeComponent = Ember.Component.extend({ + click: function() { + this.sendAction('didClickTreeNode', this.get('node')); + } + }); + + App.CategoriesController = Ember.Controller.extend({ + didClickCategory: function(category) { + //Do something with the node/category that was clicked + } + }); + ``` + + ```handlebars + {{! categories.hbs}} + {{my-tree didClickTreeNode='didClickCategory'}} + ``` + + @method sendAction + @param [action] {String} the action to trigger + @param [context] {*} a context to send with the action + */ + sendAction: function(action, context) { + var actionName; + + // 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: context + }); + } +}); + +})(); + + + +(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: ©2011 My Company Inc. All rights reserved. + // ========================================================================== + + var K = function() {}, + guid = 0, + document = this.document, + disableRange = ('undefined' === typeof ENV ? {} : ENV).DISABLE_RANGE_API, + + // 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, + + // 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 === ''; + })(), + + + // 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); + + // 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; + } + }; + } + + Metamorph.prototype.html = function(html) { + this.checkRemoved(); + if (html === undefined) { return this.innerHTML; } + + htmlFunc.call(this, html); + + this.innerHTML = html; + }; + + Metamorph.prototype.replaceWith = function(html) { + this.checkRemoved(); + htmlFunc.call(this, html, true); + }; + + 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; + + Metamorph.prototype.isRemoved = function() { + var before = document.getElementById(this.start); + var after = document.getElementById(this.end); + + return !before || !after; + }; + + 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() { +/** +@module ember +@submodule ember-handlebars-compiler +*/ + +// 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); + +/** + 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. + + To create an `Ember.Handlebars` template, call `Ember.Handlebars.compile()`. + This will return a function that can be used by `Ember.View` for rendering. + + @class Handlebars + @namespace Ember +*/ +Ember.Handlebars = objectCreate(Handlebars); + +function makeBindings(options) { + var hash = options.hash, + hashType = options.hashTypes; + + for (var prop in hash) { + if (hashType[prop] === 'ID') { + hash[prop + 'Binding'] = hash[prop]; + hashType[prop + 'Binding'] = 'STRING'; + delete hash[prop]; + delete hashType[prop]; + } + } +} + +/** + Register a bound helper or custom view helper. + + ## Simple bound helper example + + ```javascript + Ember.Handlebars.helper('capitalize', function(value) { + return value.toUpperCase(); + }); + ``` + + The above bound helper can be used inside of templates as follows: + + ```handlebars + {{capitalize name}} + ``` + + 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`. + + ## Custom view helper example + + Assuming a view subclass named `App.CalendarView` were defined, a helper + for rendering instances of this view could be registered as follows: + + ```javascript + Ember.Handlebars.helper('calendar', App.CalendarView): + ``` + + The above bound helper can be used inside of templates as follows: + + ```handlebars + {{calendar}} + ``` + + 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); + makeBindings(options); + return Ember.Handlebars.helpers.view.call(this, value, options); + }); + } else { + Ember.Handlebars.registerBoundHelper.apply(null, arguments); + } +}; + +/** +@class helpers +@namespace 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() {}; + +// Handlebars.Compiler doesn't exist in runtime-only +if (Handlebars.Compiler) { + Ember.Handlebars.Compiler.prototype = objectCreate(Handlebars.Compiler.prototype); +} + +Ember.Handlebars.Compiler.prototype.compiler = Ember.Handlebars.Compiler; + +/** + @class JavaScriptCompiler + @namespace Ember.Handlebars + @private + @constructor +*/ +Ember.Handlebars.JavaScriptCompiler = function() {}; + +// 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; +} + + +Ember.Handlebars.JavaScriptCompiler.prototype.namespace = "Ember.Handlebars"; + + +Ember.Handlebars.JavaScriptCompiler.prototype.initializeBuffer = function() { + return "''"; +}; + +/** + @private + + 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. + + @method appendToBuffer + @param string {String} +*/ +Ember.Handlebars.JavaScriptCompiler.prototype.appendToBuffer = function(string) { + return "data.buffer.push("+string+");"; +}; + +var prefix = "ember" + (+new Date()), incr = 1; + +/** + @private + + 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. - @method mustache - @for Ember.Handlebars.Compiler - @param mustache + @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' }]); + + // 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); + } + + return Handlebars.Compiler.prototype.mustache.call(this, mustache); +}; + +/** + Used for precompilation of Ember Handlebars templates. This will not be used + during normal app execution. + + @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, + bindAttr: 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) { + /** + The entry point for Ember Handlebars. This replaces the default + `Handlebars.compile` and turns on template-local data and String + parameters. + + @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); + + var template = Ember.Handlebars.template(templateSpec); + template.isMethod = false; //Make sure we don't wrap templates with ._super + + return template; + }; +} + + +})(); + +(function() { +var slice = Array.prototype.slice; + +/** + @private + + If a path starts with a reserved keyword, returns the root + that should be used. + + @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; + + // Get the first segment of the path. For example, if the + // path is "foo.bar.baz", returns "foo". + keyword = path.split('.', 1)[0]; + + // 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; + + // 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); + } + } + + return { root: root, path: path, isKeyword: isKeyword }; +}; + + +/** + 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. + + @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; + + // 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; + + value = Ember.get(root, path); + + // 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); + +Ember.Handlebars.resolveParams = function(context, params, options) { + var resolvedParams = [], types = options.types, param, type; + + for (var i=0, l=params.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 htmlSafe = Ember.String.htmlSafe; + +if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { + + /** + Mark a string as being safe for unescaped output with Handlebars. + + ```javascript + '
    someString
    '.htmlSafe() + ``` + + See [Ember.String.htmlSafe](/api/classes/Ember.String.html#method_htmlSafe). + + @method htmlSafe + @for String + @return {Handlebars.SafeString} a string that will not be html escaped by Handlebars + */ + String.prototype.htmlSafe = function() { + return htmlSafe(this); + }; +} + +})(); + + + +(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 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"`. + + If the rendering context of this template is the following object: + + ```javascript + { + imageUrl: 'http://lolcats.info/haz-a-funny', + imageTitle: 'A humorous image of a cat' + } + ``` + + The resulting HTML output will be: + + ```html + A humorous image of a cat + ``` + + `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: + + ```handlebars + imageTitle + ``` + + ### `bind-attr` and the `class` attribute + + `bind-attr` supports a special syntax for handling a number of cases unique + to the `class` DOM element attribute. The `class` attribute combines + multiple discreet values into a single attribute as a space-delimited + list of strings. Each string can be: + + * a string return value of an object's property. + * a boolean return value of an object's property + * a hard-coded value + + 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 + AView = Ember.View.extend({ + someProperty: function() { + return "aValue"; + }.property() + }) + ``` + + ```handlebars + + ``` + + 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`. + + A class name is provided via the syntax + `somePropertyName:class-name-if-true`. + + ```javascript + AView = Ember.View.extend({ + someBool: true + }) + ``` + + ```handlebars + + ``` + + Result in the following rendered output: + + ```html + + ``` + + 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: + + ```handlebars + + ``` + + A hard-coded value can be used by prepending `:` to the desired + class name: `:class-name-to-always-apply`. + + ```handlebars + + ``` + + Results in the following rendered output: + + ```html + + ``` + + All three strategies - string return value, boolean return value, and + hard-coded value – can be combined in a single declaration: + + ```handlebars + + ``` + + @method bind-attr + @for Ember.Handlebars.helpers + @param {Hash} options + @return {String} HTML string */ -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(['_triageMustache']); +EmberHandlebars.registerHelper('bind-attr', function(options) { + + var attrs = options.hash; + + Ember.assert("You must specify at least one hash argument to bind-attr", !!Ember.keys(attrs).length); + + var view = options.data.view; + var ret = []; + var ctx = this; + + // 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; + + // 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); + + ret.push('class="' + Handlebars.Utils.escapeExpression(classResults.join(' ')) + '"'); + delete attrs['class']; + } + + var attrKeys = Ember.keys(attrs); + + // 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; + + Ember.assert(fmt("You must provide an expression as the value of bound attribute. You specified: %@=%@", [attr, path]), typeof path === 'string'); + + normalized = normalizePath(ctx, path, options.data); + + var value = (path === 'this') ? normalized.root : handlebarsGet(ctx, path, options), + type = Ember.typeOf(value); + + Ember.assert(fmt("Attributes must be numbers, strings or booleans, not %@", [value]), value === null || value === undefined || type === 'number' || type === 'string' || type === 'boolean'); + + var observer, invoker; + + observer = function observer() { + var result = handlebarsGet(ctx, path, options); + + 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'); + + var elem = view.$("[data-bindattr-" + dataId + "='" + dataId + "']"); + + // 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; + } + + Ember.View.applyAttributeBindings(elem, attr, result); + }; + + // 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); + } + + // 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); + + // 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(' ')); +}); + +/** + See `bind-attr` + + @method bindAttr + @for Ember.Handlebars.helpers + @deprecated + @param {Function} context + @param {Hash} options + @return {String} HTML string +*/ +EmberHandlebars.registerHelper('bindAttr', EmberHandlebars.helpers['bind-attr']); + +/** + @private + + 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. + + 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. + + @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; + + // 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; + + if (path === 'this') { + val = root; + } else if (path === '') { + val = true; + } else { + val = handlebarsGet(root, path, options); + } + + return Ember.View._classStringForValue(path, val, parsedPath.className, parsedPath.falsyClassName); + }; + + // For each property passed, loop through and setup + // an observer. + forEach.call(classBindings.split(' '), function(binding) { + + // 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; + + var observer, invoker; + + var parsedPath = Ember.View._parsePropertyPath(binding), + path = parsedPath.path, + pathRoot = context, + normalized; + + if (path !== '' && path !== 'this') { + normalized = normalizePath(context, path, options.data); + + pathRoot = normalized.root; + path = normalized.path; + } + + // 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.$(); + + // 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); + } + + // 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 (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 +*/ + +var get = Ember.get, set = Ember.set; +var EmberHandlebars = Ember.Handlebars; +var LOWERCASE_A_Z = /^[a-z]/; +var VIEW_PREFIX = /^view\./; + +EmberHandlebars.ViewHelper = Ember.Object.create({ + + propertiesFromHTMLOptions: function(options, thisContext) { + var hash = options.hash, data = options.data; + var extensions = {}, + classes = hash['class'], + dup = false; - // 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")]); + if (hash.id) { + extensions.elementId = hash.id; + dup = true; } - mustache = new Handlebars.AST.MustacheNode([id].concat([mustache.id]), mustache.hash, !mustache.escaped); - } - return Handlebars.Compiler.prototype.mustache.call(this, mustache); -}; - -/** - Used for precompilation of Ember Handlebars templates. This will not be used - during normal app execution. + if (hash.tag) { + extensions.tagName = hash.tag; + dup = true; + } - @method precompile - @for Ember.Handlebars - @static - @param {String} string The template to precompile -*/ -Ember.Handlebars.precompile = function(string) { - var ast = Handlebars.parse(string); + if (classes) { + classes = classes.split(' '); + extensions.classNames = classes; + dup = true; + } - var options = { - knownHelpers: { - action: true, - unbound: true, - bindAttr: true, - template: true, - view: true, - _triageMustache: true - }, - data: true, - stringParams: true - }; + if (hash.classBinding) { + extensions.classNameBindings = hash.classBinding.split(' '); + dup = true; + } - var environment = new Ember.Handlebars.Compiler().compile(ast, options); - return new Ember.Handlebars.JavaScriptCompiler().compile(environment, options, undefined, true); -}; + if (hash.classNameBindings) { + if (extensions.classNameBindings === undefined) extensions.classNameBindings = []; + extensions.classNameBindings = extensions.classNameBindings.concat(hash.classNameBindings.split(' ')); + dup = true; + } -// 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. + 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; + } - @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); + if (dup) { + hash = Ember.$.extend({}, hash); + delete hash.id; + delete hash.tag; + delete hash['class']; + delete hash.classBinding; + } - return Ember.Handlebars.template(templateSpec); - }; -} + // 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; + // Evaluate the context of regular attribute bindings: + for (var prop in hash) { + if (!hash.hasOwnProperty(prop)) { continue; } -})(); + // 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; } + } + } -(function() { -var slice = Array.prototype.slice; + // 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; } + } + } + } -/** - @private + return Ember.$.extend(hash, extensions); + }, - If a path starts with a reserved keyword, returns the root - that should be used. + // 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; + } + }, - @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; + helper: function(thisContext, path, options) { + var data = options.data, + fn = options.fn, + newView; - // Get the first segment of the path. For example, if the - // path is "foo.bar.baz", returns "foo". - keyword = path.split('.', 1)[0]; + if ('string' === typeof path) { - // 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; + // 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); + } - // Handle cases where the entire path is the reserved - // word. In that case, return the object itself. - if (path === keyword) { - path = ''; + Ember.assert("Unable to find view at path '" + path + "'", !!newView); } else { - // Strip the keyword from the path and look up - // the remainder from the newly found root. - path = path.substr(keyword.length+1); + newView = path; } - } - return { root: root, path: path, isKeyword: isKeyword }; -}; + 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; -/** - 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 (fn) { + Ember.assert("You cannot provide a template block if you also specified a templateName", !get(viewOptions, 'templateName') && !get(newViewProto, 'templateName')); + viewOptions.template = fn; + } - @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; + // 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; + } - // 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; + currentView.appendChild(newView, viewOptions); + } +}); - value = Ember.get(root, path); +/** + `{{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. - // 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); + An empty `` and the following template: -Ember.Handlebars.resolveParams = function(context, params, options) { - var resolvedParams = [], types = options.types, param, type; + ```handlebars + A span: + {{#view tagName="span"}} + hello. + {{/view}} + ``` - for (var i=0, l=params.length; i + + +
    + A span: + + Hello. + +
    + + ``` - if (type === 'ID') { - resolvedParams.push(handlebarsGet(context, param, options)); - } else { - resolvedParams.push(param); - } - } + ### `parentView` setting - return resolvedParams; -}; + 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. -Ember.Handlebars.resolveHash = function(context, hash, options) { - var resolvedHash = {}, types = options.hashTypes, type; + ```javascript + aView = Ember.View.create({ + template: Ember.Handlebars.compile("{{#view}} my parent: {{parentView.elementId}} {{/view}}") + }); - for (var key in hash) { - if (!hash.hasOwnProperty(key)) { continue; } + aView.appendTo('body'); + ``` - type = types[key]; + Will result in HTML structure: - if (type === 'ID') { - resolvedHash[key] = handlebarsGet(context, hash[key], options); - } else { - resolvedHash[key] = hash[key]; - } - } + ```html +
    +
    + my parent: ember1 +
    +
    + ``` - return resolvedHash; -}; + ### Setting CSS id and class attributes -/** - @private + 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`. - Registers a helper in Handlebars that will be called if no property with the - given name can be found on the current context object, and no helper with - that name is registered. + ```handlebars + {{#view tagName="span" id="a-custom-id"}} + hello. + {{/view}} + ``` - This throws an exception with a more helpful error message so the user can - track down where the problem is happening. + Results in the following HTML structure: - @method helperMissing - @for Ember.Handlebars.helpers - @param {String} path - @param {Hash} options -*/ -Ember.Handlebars.registerHelper('helperMissing', function(path, options) { - var error, view = ""; + ```html +
    + + hello. + +
    + ``` - 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 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: -/** - 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 tagName="span" class="a-custom-class"}} + hello. + {{/view}} + ``` - ## Simple example + Results in the following HTML structure: - ```javascript - Ember.Handlebars.registerBoundHelper('capitalize', function(value) { - return value.toUpperCase(); - }); + ```html +
    + + hello. + +
    ``` - The above bound helper can be used inside of templates as follows: + ### 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. ```handlebars - {{capitalize name}} + {{#view "MyApp.CustomView"}} + hello. + {{/view}} ``` - 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. - - ## Example with options - - Like normal handlebars helpers, bound helpers have access to the options - passed into the helper call. + The first argument can also be a relative path accessible from the current + context. ```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(''); + 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}}') }); + + MyApp.OuterView.create().appendTo('body'); ``` - This helper could be used in a template as follows: + Will result in the following HTML: - ```handlebars - {{repeat text count=3}} + ```html +
    +
    + hi +
    +
    ``` - ## Example with bound options + ### Blockless use - Bound hash options are also supported. Example: + 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 - {{repeat text countBinding="numRepeats"}} + {{view "MyApp.ViewWithATemplateDefined"}} ``` - 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 + ### `viewName` property - 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. + 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 - Ember.Handlebars.registerBoundHelper('capitalizeName', function(value) { - return value.get('name').toUpperCase(); - }, 'name'); + aView = Ember.View.create({ + template: Ember.Handlebars.compile('{{#view viewName="aChildByName"}} hi {{/view}}') + }); + + aView.appendTo('body'); + aView.get('aChildByName') // the instance of Ember.View created by {{view}} helper ``` - ## Example with multiple bound properties + @method view + @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.registerBoundHelper` supports binding to - multiple properties, e.g.: + // If no path is provided, treat path param as options. + if (path && path.data && path.data.isRenderData) { + options = path; + path = "Ember.View"; + } - ```javascript - Ember.Handlebars.registerBoundHelper('concatenate', function() { - var values = arguments[arguments.length - 1]; - return values.join('||'); - }); - ``` + return EmberHandlebars.ViewHelper.helper(this, path, options); +}); - 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 - The {{unbound}} helper can be used with bound helper invocations - to render them in their unbound form, e.g. +})(); - ```handlebars - {{unbound capitalize name}} - ``` - In this example, if the name property changes, the helper - will not re-render. +(function() { +/*globals Handlebars */ - @method registerBoundHelper - @for Ember.Handlebars - @param {String} name - @param {Function} function - @param {String} dependentKeys* +// TODO: Don't require all of this module +/** +@module ember +@submodule ember-handlebars */ -Ember.Handlebars.registerBoundHelper = function(name, fn) { - var dependentKeys = slice.call(arguments, 2); - function helper() { - var properties = slice.call(arguments, 0, -1), - numProperties = properties.length, - options = arguments[arguments.length - 1], - normalizedProperties = [], - data = options.data, - hash = options.hash, - view = data.view, - currentContext = (options.contexts && options.contexts[0]) || this, - normalized, - pathRoot, path, - loc, hashOption; +var get = Ember.get, handlebarsGet = Ember.Handlebars.get, fmt = Ember.String.fmt; - // Detect bound options (e.g. countBinding="otherCount") - hash.boundOptions = {}; - for (hashOption in hash) { - if (!hash.hasOwnProperty(hashOption)) { continue; } +/** + `{{collection}}` is a `Ember.Handlebars` helper for adding instances of + `Ember.CollectionView` to a template. See [Ember.CollectionView](/api/classes/Ember.CollectionView.html) + for additional information on how a `CollectionView` functions. - if (Ember.IS_BINDING.test(hashOption) && typeof hash[hashOption] === 'string') { - // Lop off 'Binding' suffix. - hash.boundOptions[hashOption.slice(0, -7)] = hash[hashOption]; - } - } + `{{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. + + The provided block will be applied as the template for each item's view. + + Given an empty `` the following template: + + ```handlebars + {{#collection contentBinding="App.items"}} + Hi {{view.content.name}} + {{/collection}} + ``` + + And the following application code + + ```javascript + App = Ember.Application.create() + App.items = [ + Ember.Object.create({name: 'Dave'}), + Ember.Object.create({name: 'Mary'}), + Ember.Object.create({name: 'Sara'}) + ] + ``` - // Expose property names on data.properties object. - data.properties = []; - for (loc = 0; loc < numProperties; ++loc) { - data.properties.push(properties[loc]); - normalizedProperties.push(normalizePath(currentContext, properties[loc], data)); - } + Will result in the HTML structure below - if (data.isUnbound) { - return evaluateUnboundHelper(this, fn, normalizedProperties, options); - } + ```html +
    +
    Hi Dave
    +
    Hi Mary
    +
    Hi Sara
    +
    + ``` - if (dependentKeys.length === 0) { - return evaluateMultiPropertyBoundHelper(currentContext, fn, normalizedProperties, options); - } + ### Blockless Use - Ember.assert("Dependent keys can only be used with single-property helpers.", properties.length === 1); + If you provide an `itemViewClass` option that has its own `template` you can + omit the block. - normalized = normalizedProperties[0]; + The following template: - pathRoot = normalized.root; - path = normalized.path; + ```handlebars + {{collection contentBinding="App.items" itemViewClass="App.AnItemView"}} + ``` - var bindView = new Ember._SimpleHandlebarsView( - path, pathRoot, !options.hash.unescaped, options.data - ); + And application code - bindView.normalizedValue = function() { - var value = Ember._SimpleHandlebarsView.prototype.normalizedValue.call(bindView); - return fn.call(view, value, options); - }; + ```javascript + App = Ember.Application.create(); + App.items = [ + Ember.Object.create({name: 'Dave'}), + Ember.Object.create({name: 'Mary'}), + Ember.Object.create({name: 'Sara'}) + ]; - view.appendChild(bindView); + App.AnItemView = Ember.View.extend({ + template: Ember.Handlebars.compile("Greetings {{view.content.name}}") + }); + ``` - view.registerObserver(pathRoot, path, bindView, rerenderBoundHelperView); + Will result in the HTML structure below - for (var i=0, l=dependentKeys.length; i +
    Greetings Dave
    +
    Greetings Mary
    +
    Greetings Sara
    +
    + ``` - helper._rawFunction = fn; - Ember.Handlebars.registerHelper(name, helper); -}; + ### Specifying a CollectionView subclass -/** - @private + 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: - Renders the unbound form of an otherwise bound helper function. + ```handlebars + {{#collection App.MyCustomCollectionClass contentBinding="App.items"}} + Hi {{view.content.name}} + {{/collection}} + ``` - @param {Function} fn - @param {Object} context - @param {Array} normalizedProperties - @param {String} options -*/ -function evaluateMultiPropertyBoundHelper(context, fn, normalizedProperties, options) { - var numProperties = normalizedProperties.length, - self = this, - data = options.data, - view = data.view, - hash = options.hash, - boundOptions = hash.boundOptions, - watchedProperties, - boundOption, bindView, loc, property, len; + ### Forwarded `item.*`-named Options - bindView = new Ember._SimpleHandlebarsView(null, null, !hash.unescaped, data); - bindView.normalizedValue = function() { - var args = [], value, boundOption; + 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): - // Copy over bound options. - for (boundOption in boundOptions) { - if (!boundOptions.hasOwnProperty(boundOption)) { continue; } - property = normalizePath(context, boundOptions[boundOption], data); - bindView.path = property.path; - bindView.pathRoot = property.root; - hash[boundOption] = Ember._SimpleHandlebarsView.prototype.normalizedValue.call(bindView); - } + ```handlebars + {{#collection contentBinding="App.items" + itemTagName="p" + itemClassNames="greeting"}} + Howdy {{view.content.name}} + {{/collection}} + ``` - for (loc = 0; loc < numProperties; ++loc) { - property = normalizedProperties[loc]; - bindView.path = property.path; - bindView.pathRoot = property.root; - args.push(Ember._SimpleHandlebarsView.prototype.normalizedValue.call(bindView)); - } - args.push(options); - return fn.apply(context, args); - }; + Will result in the following HTML structure: - view.appendChild(bindView); + ```html +
    +

    Howdy Dave

    +

    Howdy Mary

    +

    Howdy Sara

    +
    + ``` - // Assemble liast of watched properties that'll re-render this helper. - watchedProperties = []; - for (boundOption in boundOptions) { - if (boundOptions.hasOwnProperty(boundOption)) { - watchedProperties.push(normalizePath(context, boundOptions[boundOption], data)); - } - } - watchedProperties = watchedProperties.concat(normalizedProperties); + @method collection + @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'); - // Observe each property. - for (loc = 0, len = watchedProperties.length; loc < len; ++loc) { - property = watchedProperties[loc]; - view.registerObserver(property.root, property.path, bindView, rerenderBoundHelperView); + // 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); } -} + var fn = options.fn; + var data = options.data; + var inverse = options.inverse; + var view = options.data.view; -/** - @private + // 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); - An observer function used with bound helpers which - will schedule a re-render of the _SimpleHandlebarsView - connected with the helper. -*/ -function rerenderBoundHelperView() { - Ember.run.scheduleOnce('render', this, 'rerender'); -} + var hash = options.hash, itemHash = {}, match; -/** - @private + // 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; + } - Renders the unbound form of an otherwise bound helper function. + Ember.assert(fmt("%@ #collection: Could not find itemViewClass %@", [data.view, itemViewClass]), !!itemViewClass); - @param {Function} fn - @param {Object} context - @param {Array} normalizedProperties - @param {String} options -*/ -function evaluateUnboundHelper(context, fn, normalizedProperties, options) { - var args = [], hash = options.hash, boundOptions = hash.boundOptions, loc, len, property, boundOption; + delete hash.itemViewClass; + delete hash.itemView; - for (boundOption in boundOptions) { - if (!boundOptions.hasOwnProperty(boundOption)) { continue; } - hash[boundOption] = Ember.Handlebars.get(context, boundOptions[boundOption], options); + // 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(.)(.*)$/); + + 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]; + } + } } - for(loc = 0, len = normalizedProperties.length; loc < len; ++loc) { - property = normalizedProperties[loc]; - args.push(Ember.Handlebars.get(context, property.path, options)); + if (fn) { + itemHash.template = fn; + delete options.fn; } - args.push(options); - return fn.apply(context, args); -} -/** - @private + 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; } - Overrides Handlebars.template so that we can distinguish - user-created, top-level templates from inner contexts. + if (!hash.keyword) { + itemHash._context = Ember.computed.alias('content'); + } - @method template - @for Ember.Handlebars - @param {String} template spec -*/ -Ember.Handlebars.template = function(spec){ - var t = Handlebars.template(spec); - t.isTop = true; - return t; -}; + 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); +}); })(); @@ -18074,487 +26030,503 @@ Ember.Handlebars.template = function(spec){ (function() { +/*globals Handlebars */ /** - @method htmlSafe - @for Ember.String - @static +@module ember +@submodule ember-handlebars */ -Ember.String.htmlSafe = function(str) { - return new Handlebars.SafeString(str); -}; - -var htmlSafe = Ember.String.htmlSafe; - -if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { - /** - See {{#crossLink "Ember.String/htmlSafe"}}{{/crossLink}} +var handlebarsGet = Ember.Handlebars.get; - @method htmlSafe - @for String - */ - String.prototype.htmlSafe = function() { - return htmlSafe(this); - }; -} +/** + `unbound` allows you to output a property without binding. *Important:* The + output will not be updated if the property changes. Use with caution. -})(); + ```handlebars +
    {{unbound somePropertyThatDoesntChange}}
    + ``` + `unbound` can also be used in conjunction with a bound helper to + render it in its unbound form: + ```handlebars +
    {{unbound helperName somePropertyThatDoesntChange}}
    + ``` -(function() { -Ember.Handlebars.resolvePaths = function(options) { - var ret = [], - contexts = options.contexts, - roots = options.roots, - data = options.data; + @method unbound + @for Ember.Handlebars.helpers + @param {String} property + @return {String} HTML string +*/ +Ember.Handlebars.registerHelper('unbound', function(property, fn) { + var options = arguments[arguments.length - 1], helper, context, out; - for (var i=0, l=contexts.length; i 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; } - return ret; -}; + context = (fn.contexts && fn.contexts[0]) || this; + return handlebarsGet(context, property, fn); +}); })(); (function() { -/*jshint newcap:false*/ +/*jshint debug:true*/ /** @module ember @submodule ember-handlebars */ -var set = Ember.set, get = Ember.get; -var Metamorph = requireModule('metamorph'); - -// DOMManager should just abstract dom manipulation between jquery and metamorph -var DOMManager = { - remove: function(view) { - view.morph.remove(); - }, - - prepend: function(view, html) { - view.morph.prepend(html); - }, - - after: function(view, html) { - view.morph.after(html); - }, +var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.normalizePath; - html: function(view, html) { - view.morph.html(html); - }, +/** + `log` allows you to output the value of a variable in the current rendering + context. - // This is messed up. - replace: function(view) { - var morph = view.morph; + ```handlebars + {{log myVariable}} + ``` - view.transitionTo('preRender'); + @method log + @for Ember.Handlebars.helpers + @param {String} property +*/ +Ember.Handlebars.registerHelper('log', function(property, options) { + var context = (options.contexts && 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); +}); - Ember.run.schedule('render', this, function() { - if (view.isDestroying) { return; } +/** + Execute the `debugger` statement in the current context. - view.clearRenderedChildren(); - var buffer = view.renderToBuffer(); + ```handlebars + {{debugger}} + ``` - view.invokeRecursively(function(view) { - view.propertyDidChange('element'); - }); + @method debugger + @for Ember.Handlebars.helpers + @param {String} property +*/ +Ember.Handlebars.registerHelper('debugger', function(options) { + debugger; +}); - view.triggerRecursively('willInsertElement'); - morph.replaceWith(buffer.string()); - view.transitionTo('inDOM'); - view.triggerRecursively('didInsertElement'); - }); - }, +})(); - empty: function(view) { - view.morph.html(""); - } -}; -// The `morph` and `outerHTML` properties are internal only -// and not observable. +(function() { /** - @class _Metamorph - @namespace Ember - @extends Ember.Mixin - @private +@module ember +@submodule ember-handlebars */ -Ember._Metamorph = Ember.Mixin.create({ - isVirtual: true, - tagName: '', - instrumentName: 'render.metamorph', +var get = Ember.get, set = Ember.set; +Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { init: function() { - this._super(); - this.morph = Metamorph(); - }, + var itemController = get(this, 'itemController'); + var binding; - beforeRender: function(buffer) { - buffer.push(this.morph.startTag()); - buffer.pushOpeningTag(); - }, + if (itemController) { + var controller = get(this, 'controller.container').lookupFactory('controller:array').create({ + parentController: get(this, 'controller'), + itemController: itemController, + target: get(this, 'controller'), + _eachView: this + }); - afterRender: function(buffer) { - buffer.pushClosingTag(); - buffer.push(this.morph.endTag()); + this.disableContentObservers(function() { + set(this, 'content', controller); + binding = new Ember.Binding('content', '_eachView.dataSource').oneWay(); + binding.connect(controller); + }); + + set(this, '_arrayController', controller); + } else { + this.disableContentObservers(function() { + binding = new Ember.Binding('content', 'dataSource').oneWay(); + binding.connect(this); + }); + } + + return this._super(); }, - createElement: function() { - var buffer = this.renderToBuffer(); - this.outerHTML = buffer.string(); - this.clearBuffer(); + _assertArrayLike: function(content) { + Ember.assert("The value that #each loops over must be an Array. You passed " + content.constructor + ", but it should have been an ArrayController", !Ember.ControllerMixin.detect(content) || (content && content.isGenerated) || content instanceof Ember.ArrayController); + Ember.assert("The value that #each loops over must be an Array. You passed " + ((Ember.ControllerMixin.detect(content) && content.get('model') !== undefined) ? ("" + content.get('model') + " (wrapped in " + content + ")") : ("" + content)), Ember.Array.detect(content)); }, - domManager: DOMManager -}); + disableContentObservers: function(callback) { + Ember.removeBeforeObserver(this, 'content', null, '_contentWillChange'); + Ember.removeObserver(this, 'content', null, '_contentDidChange'); -/** - @class _MetamorphView - @namespace Ember - @extends Ember.View - @uses Ember._Metamorph - @private -*/ -Ember._MetamorphView = Ember.View.extend(Ember._Metamorph); + callback.call(this); -/** - @class _SimpleMetamorphView - @namespace Ember - @extends Ember.View - @uses Ember._Metamorph - @private -*/ -Ember._SimpleMetamorphView = Ember.CoreView.extend(Ember._Metamorph); + Ember.addBeforeObserver(this, 'content', null, '_contentWillChange'); + Ember.addObserver(this, 'content', null, '_contentDidChange'); + }, + itemViewClass: Ember._MetamorphView, + emptyViewClass: Ember._MetamorphView, -})(); + createChildView: function(view, attrs) { + view = this._super(view, attrs); + // 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'); + if (keyword) { + var data = get(view, 'templateData'); -(function() { -/*globals Handlebars */ -/*jshint newcap:false*/ -/** -@module ember -@submodule ember-handlebars -*/ + data = Ember.copy(data); + data.keywords = view.cloneKeywords(); + set(view, 'templateData', data); -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; + // In this case, we do not bind, because the `content` of + // a #each item cannot change. + data.keywords[keyword] = content; + } - this.morph = Metamorph(); - this.state = 'preRender'; - this.updateId = null; -} + // If {{#each}} is looping over an array of controllers, + // point each child view at their respective controller. + if (content && get(content, 'isController')) { + set(view, 'controller', content); + } -Ember._SimpleHandlebarsView = SimpleHandlebarsView; + return view; + }, -SimpleHandlebarsView.prototype = { - isVirtual: true, - isView: true, + destroy: function() { + if (!this._super()) { return; } - destroy: function () { - if (this.updateId) { - Ember.run.cancel(this.updateId); - this.updateId = null; + var arrayController = get(this, '_arrayController'); + + if (arrayController) { + arrayController.destroy(); } - this.morph = null; - }, - propertyDidChange: Ember.K, + return this; + } +}); - normalizedValue: function() { - var path = this.path, - pathRoot = this.pathRoot, - result, templateData; +var GroupedEach = Ember.Handlebars.GroupedEach = function(context, path, options) { + var self = this, + normalized = Ember.Handlebars.normalizePath(context, path, options.data); - // 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 }); - } + 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(); + + this.containingView.on('willClearRender', function() { + self.destroy(); + }); +}; + +GroupedEach.prototype = { + contentWillChange: function() { + this.removeArrayObservers(); + }, + + contentDidChange: function() { + this.content = this.lookupContent(); + this.addArrayObservers(); + this.rerenderContainingView(); + }, + + contentArrayWillChange: Ember.K, - return result; + contentArrayDidChange: function() { + this.rerenderContainingView(); }, - renderToBuffer: function(buffer) { - var string = ''; + lookupContent: function() { + return Ember.Handlebars.get(this.normalizedRoot, this.normalizedPath, this.options); + }, - string += this.morph.startTag(); - string += this.render(); - string += this.morph.endTag(); + addArrayObservers: function() { + if (!this.content) { return; } - buffer.push(string); + this.content.addArrayObserver(this, { + willChange: 'contentArrayWillChange', + didChange: 'contentArrayDidChange' + }); }, - render: function() { - // If not invoked via a triple-mustache ({{{foo}}}), escape - // the content of the template. - var escape = this.isEscaped; - var result = this.normalizedValue(); - - if (result === null || result === undefined) { - result = ""; - } else if (!(result instanceof Handlebars.SafeString)) { - result = String(result); - } + removeArrayObservers: function() { + if (!this.content) { return; } - if (escape) { result = Handlebars.Utils.escapeExpression(result); } - return result; + this.content.removeArrayObserver(this, { + willChange: 'contentArrayWillChange', + didChange: 'contentArrayDidChange' + }); }, - 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; - } - - return this; + addContentObservers: function() { + Ember.addBeforeObserver(this.normalizedRoot, this.normalizedPath, this, this.contentWillChange); + Ember.addObserver(this.normalizedRoot, this.normalizedPath, this, this.contentDidChange); }, - update: function () { - this.updateId = null; - this.morph.html(this.render()); + removeContentObservers: function() { + Ember.removeBeforeObserver(this.normalizedRoot, this.normalizedPath, this.contentWillChange); + Ember.removeObserver(this.normalizedRoot, this.normalizedPath, this.contentDidChange); }, - transitionTo: function(state) { - this.state = state; - } -}; + render: function() { + if (!this.content) { return; } -var states = Ember.View.cloneStates(Ember.View.states), merge = Ember.merge; + var content = this.content, + contentLength = get(content, 'length'), + data = this.options.data, + template = this.template; -merge(states._default, { - rerenderIfNeeded: Ember.K -}); + data.insideEach = true; + for (var i = 0; i < contentLength; i++) { + template(content.objectAt(i), { data: data }); + } + }, -merge(states.inDOM, { - rerenderIfNeeded: function(view) { - if (get(view, 'normalizedValue') !== view._lastNormalizedValue) { - view.rerender(); + rerenderContainingView: function() { + var self = this; + Ember.run.scheduleOnce('render', this, function() { + // It's possible it's been destroyed after we enqueued a re-render call. + if (!self.destroyed) { + self.containingView.rerender(); + } + }); + }, + + destroy: function() { + this.removeContentObservers(); + if (this.content) { + this.removeArrayObservers(); } + this.destroyed = true; } -}); +}; /** - `Ember._HandlebarsBoundView` is a private view created by the Handlebars - `{{bind}}` helpers that is used to keep track of bound properties. + The `{{#each}}` helper loops over elements in a collection, rendering its + block once for each item. It is an extension of the base Handlebars `{{#each}}` + helper: - 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. + ```javascript + Developers = [{name: 'Yehuda'},{name: 'Tom'}, {name: 'Paul'}]; + ``` - @class _HandlebarsBoundView - @namespace Ember - @extends Ember._MetamorphView - @private -*/ -Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ - instrumentName: 'render.boundHandlebars', - states: states, + ```handlebars + {{#each Developers}} + {{name}} + {{/each}} + ``` - /** - 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. + `{{each}}` supports an alternative syntax with element naming: - @property shouldDisplayFunc - @type Function - @default null - */ - shouldDisplayFunc: null, + ```handlebars + {{#each person in Developers}} + {{person.name}} + {{/each}} + ``` - /** - 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`. + When looping over objects that do not have properties, `{{this}}` can be used + to render the object: - 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`. + ```javascript + DeveloperNames = ['Yehuda', 'Tom', 'Paul'] + ``` - @property preserveContext - @type Boolean - @default false - */ - preserveContext: false, + ```handlebars + {{#each DeveloperNames}} + {{this}} + {{/each}} + ``` + ### {{else}} condition + `{{#each}}` can have a matching `{{else}}`. The contents of this block will render + if the collection is empty. - /** - If `preserveContext` is true, this is the object that will be used - to render the template. + ``` + {{#each person in Developers}} + {{person.name}} + {{else}} +

    Sorry, 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. - @property previousContext - @type Object - */ - previousContext: null, + The following template: - /** - The template to render when `shouldDisplayFunc` evaluates to `true`. + ```handlebars + {{#view App.MyView }} + {{each view.items itemViewClass="App.AnItemView"}} + {{/view}} + ``` - @property displayTemplate - @type Function - @default null - */ - displayTemplate: null, + And application code - /** - The template to render when `shouldDisplayFunc` evaluates to `false`. + ```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'}) + ] + }) + }); - @property inverseTemplate - @type Function - @default null - */ - inverseTemplate: null, + App.AnItemView = Ember.View.extend({ + template: Ember.Handlebars.compile("Greetings {{name}}") + }); + ``` + Will result in the HTML structure below - /** - The path to look up on `pathRoot` that is passed to - `shouldDisplayFunc` to determine which template to render. + ```html +
    +
    Greetings Dave
    +
    Greetings Mary
    +
    Greetings Sara
    +
    + ``` - In addition, if `preserveContext` is `false,` the object at this path will - be passed to the template when rendering. + 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. - @property path - @type String - @default null - */ - path: null, + ### 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. - /** - 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. + This is useful in cases where properties of model objects need transformation + or synthesis for display: - @property pathRoot - @type Object - */ - pathRoot: null, + ```javascript + App.DeveloperController = Ember.ObjectController.extend({ + isAvailableForHire: function() { + return !this.get('content.isEmployed') && this.get('content.isSeekingWork'); + }.property('isEmployed', 'isSeekingWork') + }) + ``` - normalizedValue: Ember.computed(function() { - var path = get(this, 'path'), - pathRoot = get(this, 'pathRoot'), - valueNormalizer = get(this, 'valueNormalizerFunc'), - result, templateData; + ```handlebars + {{#each person in developers itemController="developer"}} + {{person.name}} {{#if person.isAvailableForHire}}Hire me!{{/if}} + {{/each}} + ``` - // 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 }); - } + Each itemController will receive a reference to the current controller as + a `parentController` property. - return valueNormalizer ? valueNormalizer(result) : result; - }).property('path', 'pathRoot', 'valueNormalizerFunc').volatile(), + ### (Experimental) Grouped Each - rerenderIfNeeded: function() { - this.currentState.rerenderIfNeeded(this); - }, + 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). - /** - Determines which template to invoke, sets up the correct state based on - that logic, then invokes the default `Ember.View` `render` implementation. + ```handlebars + {{#group}} + {{#each people}} + {{firstName}} {{lastName}} + {{/each}} + {{/group}} + ``` - 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. + This can be faster than the normal way that Handlebars re-renders items + in some cases. - 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`. + 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). - @method render - @param {Ember.RenderBuffer} buffer - */ - render: function(buffer) { - // If not invoked via a triple-mustache ({{{foo}}}), escape - // the content of the template. - var escape = get(this, 'isEscaped'); + For example, - var shouldDisplay = get(this, 'shouldDisplayFunc'), - preserveContext = get(this, 'preserveContext'), - context = get(this, 'previousContext'); + ```handlebars + {{dealershipName}} - var inverseTemplate = get(this, 'inverseTemplate'), - displayTemplate = get(this, 'displayTemplate'); + {{#group}} + {{#each dealers}} + {{firstName}} {{lastName}} + {{/each}} - var result = get(this, 'normalizedValue'); - this._lastNormalizedValue = result; + {{#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). - // First, test the conditional to see if we should - // render the template or not. - if (shouldDisplay(result)) { - set(this, 'template', displayTemplate); + Note that `group` behavior is also disabled by specifying an `itemViewClass`. - // 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); - } + @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(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"); - if (escape) { result = Handlebars.Utils.escapeExpression(result); } - buffer.push(result); - return; - } - } - } else if (inverseTemplate) { - set(this, 'template', inverseTemplate); + var keywordName = arguments[0]; - if (preserveContext) { - set(this, '_context', context); - } else { - set(this, '_context', result); - } - } else { - set(this, 'template', function() { return ''; }); - } + options = arguments[3]; + path = arguments[2]; + if (path === '') { path = "this"; } - return this._super(buffer); + options.hash.keyword = keywordName; + } + + if (arguments.length === 1) { + options = path; + path = 'this'; + } + + options.hash.dataSourceBinding = path; + // Set up emptyView as a metamorph with no tag + //options.hash.emptyViewClass = Ember._MetamorphView; + + 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); } }); @@ -18568,1286 +26540,1438 @@ 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; +/** + `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: -var EmberHandlebars = Ember.Handlebars, helpers = EmberHandlebars.helpers; + ```html + + ``` -// 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; + ```html + + ``` - normalized = normalizePath(currentContext, property, data); + ```handlebars + {{#if isUser}} + {{template "user_info"}} + {{else}} + {{template "unlogged_user_info"}} + {{/if}} + ``` - // Set up observers for observable objects - if ('object' === typeof this) { - if (data.insideGroup) { - observer = function() { - Ember.run.once(view, 'rerender'); - }; + This helper looks for templates in the global `Ember.TEMPLATES` hash. If you + add ` + ``` - var result = handlebarsGet(currentContext, property, options); - if (result === null || result === undefined) { result = ""; } - data.buffer.push(result); - } else { - var bindView = new Ember._SimpleHandlebarsView( - property, currentContext, !options.hash.unescaped, options.data - ); + The `data-template-name` attribute of a partial template + is prefixed with an underscore. - bindView._parentView = view; - view.appendChild(bindView); + ```html + + ``` + + @method partial + @for Ember.Handlebars.helpers + @param {String} partialName the name of the template to render minus the leading underscore +*/ + +Ember.Handlebars.registerHelper('partial', function(name, options) { + var nameParts = name.split("/"), + lastPart = nameParts[nameParts.length - 1]; + + nameParts[nameParts.length - 1] = "_" + lastPart; + + var view = options.data.view, + underscoredName = nameParts.join("/"), + template = view.templateForName(underscoredName), + deprecatedTemplate = !template && view.templateForName(name); - observer = function() { - Ember.run.scheduleOnce('render', bindView, 'rerender'); - }; - } + Ember.assert("Unable to find partial with name '"+name+"'.", template || deprecatedTemplate); - // 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. - data.buffer.push(handlebarsGet(currentContext, property, options)); - } -} + template = template || deprecatedTemplate; -/** - @private + template(this, { data: options.data }); +}); - '_triageMustache' is used internally select between a binding and helper 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. +})(); - This would not be typically invoked by directly. - @method _triageMustache - @for Ember.Handlebars.helpers - @param {String} property Property/helperID to triage - @param {Function} fn Context to provide for rendering - @return {String} HTML string + +(function() { +/** +@module ember +@submodule ember-handlebars */ -EmberHandlebars.registerHelper('_triageMustache', function(property, fn) { - Ember.assert("You cannot pass more than one argument to the _triageMustache helper", arguments.length <= 2); - if (helpers[property]) { - return helpers[property].call(this, fn); - } - else { - return helpers.bind.apply(this, arguments); - } -}); + +var get = Ember.get, set = Ember.set; /** - @private - `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`: + `{{yield}}` denotes an area of a template that will be rendered inside + of another template. It has two main uses: - ```handlebars - {{bind "content.title"}} - ``` + ### 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. - 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. + An empty `` and the following application code: - @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(property, options) { - Ember.assert("You cannot pass more than one argument to the bind helper", arguments.length <= 2); + ```javascript + AView = Ember.View.extend({ + classNames: ['a-view-with-layout'], + layout: Ember.Handlebars.compile('
    {{yield}}
    '), + template: Ember.Handlebars.compile('I am wrapped') + }); - var context = (options.contexts && options.contexts[0]) || this; + aView = AView.create(); + aView.appendTo('body'); + ``` - if (!options.fn) { - return simpleBind.call(context, property, options); - } + Will result in the following HTML output: + + ```html + +
    +
    + I am wrapped +
    +
    + + ``` + + 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. - return bind.call(context, property, options, false, function(result) { - return !Ember.isNone(result); + ```javascript + BView = Ember.View.extend({ + classNames: ['a-view-with-layout'], + template: Ember.Handlebars.compile('{{yield}}') }); -}); -/** - @private + bView = BView.create(); + bView.appendTo('body'); - Use the `boundIf` helper to create a conditional that re-evaluates - whenever the truthiness of the bound value changes. + // 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: ```handlebars - {{#boundIf "content.shouldDisplayTitle"}} - {{content.title}} - {{/boundIf}} + + {{#labeled-textfield value=someProperty}} + First name: + {{/my-component}} ``` - @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[0]) || this; - var func = function(result) { - var truthy = result && get(result, 'isTruthy'); - if (typeof truthy === 'boolean') { return truthy; } + ```handlebars + + + ``` - if (Ember.isArray(result)) { - return get(result, 'length') !== 0; - } else { - return !!result; - } - }; + Result: - return bind.call(context, property, fn, true, func, func, ['isTruthy', 'length']); -}); + ```html +