From 3b27a5d45b12f6bac65da2a8e17387bfda42a2f1 Mon Sep 17 00:00:00 2001 From: Stefan Wintermeyer Date: Thu, 20 Jun 2013 19:02:50 +0200 Subject: Update Ember, Ember-Data and Handlebars. --- public/js/app.js | 4 +- public/js/libs/ember-data.js | 5450 +++++++++++++----------- public/js/libs/ember.js | 7361 +++++++++++++++++++++------------ public/js/libs/handlebars.js | 2527 ++++++------ public/js/libs/new-ember-data.js | 8431 -------------------------------------- 5 files changed, 9032 insertions(+), 14741 deletions(-) delete mode 100644 public/js/libs/new-ember-data.js (limited to 'public') diff --git a/public/js/app.js b/public/js/app.js index 893b4af..51b3b71 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -38,9 +38,7 @@ App.SwitchboardController = Ember.ObjectController.extend({ }); // Models -App.Store = DS.Store.extend({ - revision: 12 -}); +App.Store = DS.Store.extend(); DS.RESTAdapter.configure("plurals", { switchboard_entry: "switchboard_entries" diff --git a/public/js/libs/ember-data.js b/public/js/libs/ember-data.js index 00a6e25..798478b 100644 --- a/public/js/libs/ember-data.js +++ b/public/js/libs/ember-data.js @@ -1,10 +1,119 @@ -// Last commit: 5fd6d65 (2013-03-28 01:13:50 +0100) +// Version: v0.13-33-g8cf224d +// Last commit: 8cf224d (2013-06-19 00:23:32 -0400) (function() { -window.DS = Ember.Namespace.create({ - // this one goes past 11 - CURRENT_API_REVISION: 12 +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 + * © 2011 Colin Snover + * Released under MIT license. + */ + +Ember.Date = Ember.Date || {}; + +var origParse = Date.parse, numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ]; +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() { +var Evented = Ember.Evented, // ember-runtime/mixins/evented + Deferred = Ember.DeferredMixin, // ember-runtime/mixins/evented run = Ember.run, // ember-metal/run-loop get = Ember.get; // ember-metal/accessors -var LoadPromise = Ember.Mixin.create(Evented, DeferredMixin, { +var LoadPromise = Ember.Mixin.create(Evented, Deferred, { init: function() { this._super.apply(this, arguments); - this.one('didLoad', function() { - run(this, 'resolve', this); + + this.one('didLoad', this, function() { + this.resolve(this); + }); + + this.one('becameError', this, function() { + this.reject(this); }); if (get(this, 'isLoaded')) { @@ -37,6 +209,9 @@ DS.LoadPromise = LoadPromise; (function() { +/** +*/ + var get = Ember.get, set = Ember.set; var LoadPromise = DS.LoadPromise; // system/mixins/load_promise @@ -47,9 +222,19 @@ var LoadPromise = DS.LoadPromise; // system/mixins/load_promise 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. + + @module data + @submodule data-record-array + @main data-record-array + + @class RecordArray + @namespace DS + @extends Ember.ArrayProxy + @uses Ember.Evented + @uses DS.LoadPromise */ -DS.RecordArray = Ember.ArrayProxy.extend(Ember.Evented, LoadPromise, { +DS.RecordArray = Ember.ArrayProxy.extend(LoadPromise, { /** The model type contained by this record array. @@ -111,8 +296,19 @@ DS.RecordArray = Ember.ArrayProxy.extend(Ember.Evented, LoadPromise, { (function() { +/** + @module data + @submodule data-record-array +*/ + var get = Ember.get; +/** + @class FilteredRecordArray + @namespace DS + @extends DS.RecordArray + @constructor +*/ DS.FilteredRecordArray = DS.RecordArray.extend({ filterFunction: null, isLoaded: true, @@ -123,8 +319,8 @@ DS.FilteredRecordArray = DS.RecordArray.extend({ }, updateFilter: Ember.observer(function() { - var store = get(this, 'store'); - store.updateRecordArrayFilter(this, get(this, 'type'), get(this, 'filterFunction')); + var manager = get(this, 'manager'); + manager.updateFilter(this, get(this, 'type'), get(this, 'filterFunction')); }, 'filterFunction') }); @@ -133,8 +329,19 @@ DS.FilteredRecordArray = DS.RecordArray.extend({ (function() { +/** + @module data + @submodule data-record-array +*/ + var get = Ember.get, set = Ember.set; +/** + @class AdapterPopulatedRecordArray + @namespace DS + @extends DS.RecordArray + @constructor +*/ DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({ query: null, @@ -144,18 +351,13 @@ DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({ }, load: function(references) { - var store = get(this, 'store'), type = get(this, 'type'); - - this.beginPropertyChanges(); - set(this, 'content', Ember.A(references)); - set(this, 'isLoaded', true); - this.endPropertyChanges(); + this.setProperties({ + content: Ember.A(references), + isLoaded: true + }); - var self = this; // TODO: does triggering didLoad event should be the last action of the runLoop? - Ember.run.once(function() { - self.trigger('didLoad'); - }); + Ember.run.once(this, 'trigger', 'didLoad'); } }); @@ -164,6 +366,11 @@ DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({ (function() { +/** + @module data + @submodule data-record-array +*/ + var get = Ember.get, set = Ember.set; /** @@ -195,6 +402,11 @@ var get = Ember.get, set = Ember.set; We call the record to which a relationship belongs the relationship's _owner_. + + @class ManyArray + @namespace DS + @extends DS.RecordArray + @constructor */ DS.ManyArray = DS.RecordArray.extend({ init: function() { @@ -211,6 +423,15 @@ DS.ManyArray = DS.RecordArray.extend({ */ owner: null, + /** + @private + + `true` if the relationship is polymorphic, `false` otherwise. + + @property {Boolean} + */ + isPolymorphic: false, + // LOADING STATE isLoaded: false, @@ -230,17 +451,16 @@ DS.ManyArray = DS.RecordArray.extend({ fetch: function() { var references = get(this, 'content'), store = get(this, 'store'), - type = get(this, 'type'), owner = get(this, 'owner'); - store.fetchUnloadedReferences(type, references, owner); + store.fetchUnloadedReferences(references, 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 = added.map(function(record) { - Ember.assert("You can only add records of " + (get(this, 'type') && get(this, 'type').toString()) + " to this relationship.", !get(this, 'type') || (get(this, 'type') === record.constructor)); + Ember.assert("You can only add records of " + (get(this, 'type') && get(this, 'type').toString()) + " to this relationship.", !get(this, 'type') || (get(this, 'type').detectInstance(record)) ); return get(record, '_reference'); }, this); @@ -328,6 +548,8 @@ DS.ManyArray = DS.RecordArray.extend({ type = get(this, 'type'), record; + Ember.assert("You can not create records of " + (get(this, 'type') && get(this, 'type').toString()) + " on this polymorphic relationship.", !get(this, 'isPolymorphic')); + transaction = transaction || get(owner, 'transaction'); record = store.createRecord.call(store, type, hash, transaction); @@ -343,14 +565,22 @@ DS.ManyArray = DS.RecordArray.extend({ (function() { +/** + @module data + @submodule data-record-array +*/ })(); (function() { -var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt, - removeObject = Ember.EnumerableUtils.removeObject, forEach = Ember.EnumerableUtils.forEach; +var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; + +/** + @module data + @submodule data-transaction +*/ /** A transaction allows you to collect multiple records into a unit of work @@ -430,8 +660,6 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt, calling commit. */ -var arrayDefault = function() { return []; }; - DS.Transaction = Ember.Object.extend({ /** @private @@ -440,15 +668,7 @@ DS.Transaction = Ember.Object.extend({ type. */ init: function() { - set(this, 'buckets', { - clean: Ember.OrderedSet.create(), - created: Ember.OrderedSet.create(), - updated: Ember.OrderedSet.create(), - deleted: Ember.OrderedSet.create(), - inflight: Ember.OrderedSet.create() - }); - - set(this, 'relationships', Ember.OrderedSet.create()); + set(this, 'records', Ember.OrderedSet.create()); }, /** @@ -475,11 +695,11 @@ DS.Transaction = Ember.Object.extend({ isDefault: Ember.computed(function() { return this === get(this, 'store.defaultTransaction'); - }), + }).volatile(), /** Adds an existing record to this transaction. Only records without - modficiations (i.e., records whose `isDirty` property is `false`) + modificiations (i.e., records whose `isDirty` property is `false`) can be added to a transaction. @param {DS.Model} record the record to add to the transaction @@ -487,29 +707,57 @@ DS.Transaction = Ember.Object.extend({ add: function(record) { Ember.assert("You must pass a record into transaction.add()", record instanceof DS.Model); - var recordTransaction = get(record, 'transaction'), - defaultTransaction = get(this, 'store.defaultTransaction'); + var store = get(this, 'store'); + var adapter = get(store, '_adapter'); + var serializer = get(adapter, 'serializer'); + serializer.eachEmbeddedRecord(record, function(embeddedRecord, embeddedType) { + if (embeddedType === 'load') { return; } - // Make `add` idempotent - if (recordTransaction === this) { return; } + this.add(embeddedRecord); + }, this); - // XXX it should be possible to move a dirty transaction from the default transaction + this.adoptRecord(record); + }, - // we could probably make this work if someone has a valid use case. Do you? - Ember.assert("Once a record has changed, you cannot move it into a different transaction", !get(record, 'isDirty')); + relationships: Ember.computed(function() { + var relationships = Ember.OrderedSet.create(), + records = get(this, 'records'), + store = get(this, 'store'); - Ember.assert("Models cannot belong to more than one transaction at a time.", recordTransaction === defaultTransaction); + records.forEach(function(record) { + var reference = get(record, '_reference'); + var changes = store.relationshipChangesFor(reference); + for(var i = 0; i < changes.length; i++) { + relationships.add(changes[i]); + } + }); - this.adoptRecord(record); - }, + return relationships; + }).volatile(), - relationshipBecameDirty: function(relationship) { - get(this, 'relationships').add(relationship); - }, + commitDetails: Ember.computed(function() { + var commitDetails = Ember.MapWithDefault.create({ + defaultValue: function() { + return { + created: Ember.OrderedSet.create(), + updated: Ember.OrderedSet.create(), + deleted: Ember.OrderedSet.create() + }; + } + }); - relationshipBecameClean: function(relationship) { - get(this, 'relationships').remove(relationship); - }, + var records = get(this, 'records'), + store = get(this, 'store'); + + records.forEach(function(record) { + if(!get(record, 'isDirty')) return; + record.send('willCommit'); + var adapter = store.adapterForType(record.constructor); + commitDetails.get(adapter)[get(record, 'dirtyType')].add(record); + }); + + return commitDetails; + }).volatile(), /** Commits the transaction, which causes all of the modified records that @@ -522,36 +770,22 @@ DS.Transaction = Ember.Object.extend({ */ commit: function() { var store = get(this, 'store'); - var adapter = get(store, '_adapter'); - var defaultTransaction = get(store, 'defaultTransaction'); - - var iterate = function(records) { - var set = records.copy(); - set.forEach(function (record) { - record.send('willCommit'); - }); - return set; - }; - var relationships = get(this, 'relationships'); - - var commitDetails = { - created: iterate(this.bucketForType('created')), - updated: iterate(this.bucketForType('updated')), - deleted: iterate(this.bucketForType('deleted')), - relationships: relationships - }; - - if (this === defaultTransaction) { + if (get(this, 'isDefault')) { set(store, 'defaultTransaction', store.transaction()); } this.removeCleanRecords(); - if (!commitDetails.created.isEmpty() || !commitDetails.updated.isEmpty() || !commitDetails.deleted.isEmpty() || !relationships.isEmpty()) { - if (adapter && adapter.commit) { adapter.commit(store, commitDetails); } - else { throw fmt("Adapter is either null or does not implement `commit` method", this); } - } + var commitDetails = get(this, 'commitDetails'), + relationships = get(this, 'relationships'); + + commitDetails.forEach(function(adapter, commitDetails) { + Ember.assert("You tried to commit records but you have no adapter", adapter); + Ember.assert("You tried to commit records but your adapter does not implement `commit`", adapter.commit); + + adapter.commit(store, commitDetails); + }); // Once we've committed the transaction, there is no need to // keep the OneToManyChanges around. Destroy them so they @@ -575,21 +809,36 @@ DS.Transaction = Ember.Object.extend({ current transaction should not be used again. */ rollback: function() { - // Loop through all of the records in each of the dirty states - // and initiate a rollback on them. As a side effect of telling - // the record to roll back, it should also move itself out of - // the dirty bucket and into the clean bucket. - ['created', 'updated', 'deleted', 'inflight'].forEach(function(bucketType) { - var records = this.bucketForType(bucketType); - forEach(records, function(record) { - record.send('rollback'); - }); - records.clear(); - }, this); + // Destroy all relationship changes and compute + // all references affected + var references = Ember.OrderedSet.create(); + var relationships = get(this, 'relationships'); + relationships.forEach(function(r) { + references.add(r.firstRecordReference); + references.add(r.secondRecordReference); + r.destroy(); + }); + + var records = get(this, 'records'); + records.forEach(function(record) { + if (!record.get('isDirty')) return; + record.send('rollback'); + }); // Now that all records in the transaction are guaranteed to be // clean, migrate them all to the store's default transaction. this.removeCleanRecords(); + + // Remaining associated references are not part of the transaction, but + // can still have hasMany's which have not been reloaded + references.forEach(function(r) { + if (r && r.record) { + var record = r.record; + record.suspendRelationshipObservers(function() { + record.reloadHasManys(); + }); + } + }, this); }, /** @@ -615,27 +864,12 @@ DS.Transaction = Ember.Object.extend({ Removes all of the records in the transaction's clean bucket. */ removeCleanRecords: function() { - var clean = this.bucketForType('clean'); - clean.forEach(function(record) { - this.remove(record); - }, this); - clean.clear(); - }, - - /** - @private - - Returns the bucket for the given bucket type. For example, you might call - `this.bucketForType('updated')` to get the `Ember.Map` that contains all - of the records that have changes pending. - - @param {String} bucketType the type of bucket - @returns Ember.Map - */ - bucketForType: function(bucketType) { - var buckets = get(this, 'buckets'); - - return get(buckets, bucketType); + var records = get(this, 'records'); + records.forEach(function(record) { + if(!record.get('isDirty')) { + this.remove(record); + } + }, this); }, /** @@ -656,81 +890,51 @@ DS.Transaction = Ember.Object.extend({ var oldTransaction = get(record, 'transaction'); if (oldTransaction) { - oldTransaction.removeFromBucket('clean', record); + oldTransaction.removeRecord(record); } - this.addToBucket('clean', record); + get(this, 'records').add(record); set(record, 'transaction', this); }, /** - @private - - Adds a record to the named bucket. - - @param {String} bucketType one of `clean`, `created`, `updated`, or `deleted` - */ - addToBucket: function(bucketType, record) { - this.bucketForType(bucketType).add(record); - }, - - /** - @private - - Removes a record from the named bucket. - - @param {String} bucketType one of `clean`, `created`, `updated`, or `deleted` - */ - removeFromBucket: function(bucketType, record) { - this.bucketForType(bucketType).remove(record); - }, - - /** - @private - - Called by a record's state manager to indicate that the record has entered - a dirty state. The record will be moved from the `clean` bucket and into - the appropriate dirty bucket. + @private - @param {String} bucketType one of `created`, `updated`, or `deleted` + Removes the record without performing the normal checks + to ensure that the record is re-added to the store's + default transaction. */ - recordBecameDirty: function(bucketType, record) { - this.removeFromBucket('clean', record); - this.addToBucket(bucketType, record); - }, - - /** - @private - - Called by a record's state manager to indicate that the record has entered - inflight state. The record will be moved from its current dirty bucket and into - the `inflight` bucket. + removeRecord: function(record) { + get(this, 'records').remove(record); + } - @param {String} bucketType one of `created`, `updated`, or `deleted` - */ - recordBecameInFlight: function(kind, record) { - this.removeFromBucket(kind, record); - this.addToBucket('inflight', record); - }, +}); - recordIsMoving: function(kind, record) { - this.removeFromBucket(kind, record); - this.addToBucket('clean', record); - }, +DS.Transaction.reopenClass({ + ensureSameTransaction: function(records){ + var transactions = Ember.A(); + forEach( records, function(record){ + if (record){ transactions.pushObject(get(record, 'transaction')); } + }); - /** - @private + var transaction = transactions.reduce(function(prev, t) { + if (!get(t, 'isDefault')) { + if (prev === null) { return t; } + Ember.assert("All records in a changed relationship must be in the same transaction. You tried to change the relationship between records when one is in " + t + " and the other is in " + prev, t === prev); + } - Called by a record's state manager to indicate that the record has entered - a clean state. The record will be moved from its current dirty or inflight bucket and into - the `clean` bucket. + return prev; + }, null); - @param {String} bucketType one of `created`, `updated`, or `deleted` - */ - recordBecameClean: function(kind, record) { - this.removeFromBucket(kind, record); - this.remove(record); - } + if (transaction) { + forEach( records, function(record){ + if (record){ transaction.add(record); } + }); + } else { + transaction = transactions.objectAt(0); + } + return transaction; + } }); })(); @@ -738,11 +942,9 @@ DS.Transaction = Ember.Object.extend({ (function() { -var classify = Ember.String.classify, get = Ember.get; +var get = Ember.get; /** -@private - The Mappable mixin is designed for classes that would like to behave as a map for configuration purposes. @@ -791,11 +993,16 @@ var classify = Ember.String.classify, get = Ember.get; }) }); - The function passed to `generateMapFunctionFor` is invoked every time a - new value is added to the map. + The function passed to `generateMapFunctionFor` is invoked every time a + new value is added to the map. + + @class _Mappable + @private + @namespace DS + @extends Ember.Mixin **/ -var resolveMapConflict = function(oldValue, newValue, mappingsKey) { +var resolveMapConflict = function(oldValue, newValue) { return oldValue; }; @@ -809,7 +1016,7 @@ var transformMapValue = function(key, value) { DS._Mappable = Ember.Mixin.create({ createInstanceMapFor: function(mapName) { - var instanceMeta = Ember.metaPath(this, ['DS.Mappable'], true); + var instanceMeta = getMappableMeta(this); instanceMeta.values = instanceMeta.values || {}; @@ -829,7 +1036,7 @@ DS._Mappable = Ember.Mixin.create({ }, _copyMap: function(mapName, klass, instanceMap) { - var classMeta = Ember.metaPath(klass, ['DS.Mappable'], true); + var classMeta = getMappableMeta(klass); var classMap = classMeta[mapName]; if (classMap) { @@ -844,7 +1051,7 @@ DS._Mappable = Ember.Mixin.create({ var newValue = transformedValue; if (oldValue) { - newValue = (this.constructor.resolveMapConflict || resolveMapConflict)(oldValue, newValue, mapName); + newValue = (this.constructor.resolveMapConflict || resolveMapConflict)(oldValue, newValue); } instanceMap.set(transformedKey, newValue); @@ -856,7 +1063,8 @@ DS._Mappable = Ember.Mixin.create({ DS._Mappable.generateMapFunctionFor = function(mapName, transform) { return function(key, value) { - var meta = Ember.metaPath(this, ['DS.Mappable'], true); + var meta = getMappableMeta(this); + var map = meta[mapName] || Ember.MapWithDefault.create({ defaultValue: function() { return {}; } }); @@ -867,15 +1075,38 @@ DS._Mappable.generateMapFunctionFor = function(mapName, transform) { }; }; -})(); +function getMappableMeta(obj) { + var meta = Ember.meta(obj, true), + keyName = 'DS.Mappable', + value = meta[keyName]; + if (!value) { meta[keyName] = {}; } + if (!meta.hasOwnProperty(keyName)) { + meta[keyName] = Ember.create(meta[keyName]); + } -(function() { + return meta[keyName]; +} + +})(); + + + +(function() { /*globals Ember*/ /*jshint eqnull:true*/ -var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt, once = Ember.run.once; +/** + @module data + @submodule data-store +*/ + +var get = Ember.get, set = Ember.set; +var once = Ember.run.once; +var isNone = Ember.isNone; var forEach = Ember.EnumerableUtils.forEach; +var map = Ember.EnumerableUtils.map; + // These values are used in the data cache when clientIds are // needed but the underlying data has not yet been loaded by // the server. @@ -890,10 +1121,13 @@ var CREATED = { created: true }; // scheme: // // * +id+ means an identifier managed by an external source, provided inside -// the data provided by that source. +// the data provided by that source. These are always coerced to be strings +// before being used internally. // * +clientId+ means a transient numerical identifier generated at runtime by // the data store. It is important primarily because newly created objects may // not yet have an externally generated id. +// * +reference+ means a record reference object, which holds metadata about a +// record, even if it has not yet been fully materialized. // * +type+ means a subclass of DS.Model. // Used by the store to normalize IDs entering the store. Despite the fact @@ -906,32 +1140,48 @@ var coerceId = function(id) { return id == null ? null : id+''; }; -var map = Ember.EnumerableUtils.map; /** The store contains all of the data for records loaded from the server. - It is also responsible for creating instances of DS.Model that wraps + It is also responsible for creating instances of DS.Model that wrap the individual data for a record, so that they can be bound to in your Handlebars templates. - Create a new store like this: + Define your application's store like this: + + MyApp.Store = DS.Store.extend(); + + Most Ember.js applications will only have a single `DS.Store` that is + automatically created by their `Ember.Application`. - MyApp.store = DS.Store.create(); + You can retrieve models from the store in several ways. To retrieve a record + for a specific id, use `DS.Model`'s `find()` method: - You can retrieve DS.Model instances from the store in several ways. To retrieve - a record for a specific id, use the `find()` method: + var person = App.Person.find(123); - var record = MyApp.store.find(MyApp.Contact, 123); + If your application has multiple `DS.Store` instances (an unusual case), you can + specify which store should be used: - By default, the store will talk to your backend using a standard REST mechanism. - You can customize how the store talks to your backend by specifying a custom adapter: + var person = store.find(App.Person, 123); + + In general, you should retrieve models using the methods on `DS.Model`; you should + rarely need to interact with the store directly. + + By default, the store will talk to your backend using a standard REST mechanism. + You can customize how the store talks to your backend by specifying a custom adapter: MyApp.store = DS.Store.create({ adapter: 'MyApp.CustomAdapter' }); - You can learn more about writing a custom adapter by reading the `DS.Adapter` - documentation. + You can learn more about writing a custom adapter by reading the `DS.Adapter` + documentation. + + @class Store + @namespace DS + @extends Ember.Object + @uses DS._Mappable + @constructor */ DS.Store = Ember.Object.extend(DS._Mappable, { @@ -939,42 +1189,23 @@ DS.Store = Ember.Object.extend(DS._Mappable, { Many methods can be invoked without specifying which store should be used. In those cases, the first store created will be used as the default. If an application has multiple stores, it should specify which store to use - when performing actions, such as finding records by id. + when performing actions, such as finding records by ID. The init method registers this store as the default if none is specified. */ init: function() { - // Enforce API revisioning. See BREAKING_CHANGES.md for more. - var revision = get(this, 'revision'); - - if (revision !== DS.CURRENT_API_REVISION && !Ember.ENV.TESTING) { - throw new Error("Error: The Ember Data library has had breaking API changes since the last time you updated the library. Please review the list of breaking changes at https://github.com/emberjs/data/blob/master/BREAKING_CHANGES.md, then update your store's `revision` property to " + DS.CURRENT_API_REVISION); - } - if (!get(DS, 'defaultStore') || get(this, 'isDefaultStore')) { set(DS, 'defaultStore', this); } // internal bookkeeping; not observable this.typeMaps = {}; - this.recordCache = []; - this.clientIdToId = {}; - this.clientIdToType = {}; - this.clientIdToData = {}; - this.clientIdToPrematerializedData = {}; - this.recordArraysByClientId = {}; + this.recordArrayManager = DS.RecordArrayManager.create({ + store: this + }); this.relationshipChanges = {}; - this.recordReferences = {}; - - // Internally, we maintain a map of all unloaded IDs requested by - // a ManyArray. As the adapter loads data into the store, the - // store notifies any interested ManyArrays. When the ManyArray's - // total number of loading records drops to zero, it becomes - // `isLoaded` and fires a `didLoad` event. - this.loadingRecordArrays = {}; - - this._recordsToSave = Ember.OrderedSet.create(); + set(this, 'currentTransaction', this.transaction()); set(this, 'defaultTransaction', this.transaction()); }, @@ -999,31 +1230,6 @@ DS.Store = Ember.Object.extend(DS._Mappable, { return DS.Transaction.create({ store: this }); }, - ensureSameTransaction: function(records){ - var transactions = Ember.A(); - forEach( records, function(record){ - if (record){ transactions.pushObject(get(record, 'transaction')); } - }); - - var transaction = transactions.reduce(function(prev, t) { - if (!get(t, 'isDefault')) { - if (prev === null) { return t; } - Ember.assert("All records in a changed relationship must be in the same transaction. You tried to change the relationship between records when one is in " + t + " and the other is in " + prev, t === prev); - } - - return prev; - }, null); - - if (transaction) { - forEach( records, function(record){ - if (record){ transaction.add(record); } - }); - } else { - transaction = transactions.objectAt(0); - } - return transaction; - - }, /** @private @@ -1042,47 +1248,22 @@ DS.Store = Ember.Object.extend(DS._Mappable, { @param {DS.Model} record */ materializeData: function(record) { - var clientId = get(record, 'clientId'), - cidToData = this.clientIdToData, - adapter = this.adapterForType(record.constructor), - data = cidToData[clientId]; - - cidToData[clientId] = MATERIALIZED; + var reference = get(record, '_reference'), + data = reference.data, + adapter = this.adapterForType(record.constructor); - var prematerialized = this.clientIdToPrematerializedData[clientId]; + reference.data = MATERIALIZED; - // Ensures the record's data structures are setup - // before being populated by the adapter. record.setupData(); if (data !== CREATED) { // Instructs the adapter to extract information from the // opaque data and materialize the record's attributes and // relationships. - adapter.materialize(record, data, prematerialized); + adapter.materialize(record, data, reference.prematerialized); } }, - /** - @private - - Returns true if there is already a record for this clientId. - - This is used to determine whether cleanup is required, so that - "changes" to unmaterialized records do not trigger mass - materialization. - - For example, if a parent record in a relationship with a large - number of children is deleted, we want to avoid materializing - those children. - - @param {Object} reference - @return {Boolean} - */ - recordIsMaterialized: function(reference) { - return !!this.recordCache[reference.clientId]; - }, - /** The adapter to use to communicate to a backend server or other persistence layer. @@ -1091,7 +1272,14 @@ DS.Store = Ember.Object.extend(DS._Mappable, { @property {DS.Adapter|String} */ - adapter: 'DS.RESTAdapter', + adapter: Ember.computed(function(){ + if (!Ember.testing) { + Ember.debug("A custom DS.Adapter was not provided as the 'Adapter' property of your application's Store. The default (DS.RESTAdapter) will be used."); + } + + return 'DS.RESTAdapter'; + }).property(), + /** @private @@ -1165,6 +1353,7 @@ DS.Store = Ember.Object.extend(DS._Mappable, { If you want to create a record inside of a given transaction, use `transaction.createRecord()` instead of `store.createRecord()`. + @method createRecord @param {subclass of DS.Model} type @param {Object} properties a hash of properties to set on the newly created record. @@ -1197,15 +1386,17 @@ DS.Store = Ember.Object.extend(DS._Mappable, { // give the adapter an opportunity to generate one. Typically, // client-side ID generators will use something like uuid.js // to avoid conflicts. - var adapter; - if (Ember.isNone(id)) { - adapter = this.adapterForType(type); + + if (isNone(id)) { + var adapter = this.adapterForType(type); + if (adapter && adapter.generateIdForRecord) { id = coerceId(adapter.generateIdForRecord(this, record)); properties.id = id; } } + // Coerce ID to a string id = coerceId(id); // Create a new `clientId` and associate it with the @@ -1214,24 +1405,20 @@ DS.Store = Ember.Object.extend(DS._Mappable, { // the sentinel value CREATED as the data for this // clientId. If we see this value later, we will skip // materialization. - var clientId = this.pushData(CREATED, id, type); + var reference = this.createReference(type, id); + reference.data = CREATED; - // Now that we have a clientId, attach it to the record we + // Now that we have a reference, attach it to the record we // just created. - set(record, 'clientId', clientId); + set(record, '_reference', reference); + reference.record = record; // Move the record out of its initial `empty` state into // the `loaded` state. record.loadedData(); - // Make sure the data is set up so the record doesn't - // try to materialize its nonexistent data. record.setupData(); - // Store the record we just created in the record cache for - // this clientId. - this.recordCache[clientId] = record; - // Set the properties specified on the record. record.setProperties(properties); @@ -1320,6 +1507,10 @@ DS.Store = Ember.Object.extend(DS._Mappable, { You can check whether a query results `RecordArray` has loaded by checking its `isLoaded` property. + + @method find + @param {DS.Model} type + @param {Object|String|Integer|null} id */ find: function(type, id) { if (id === undefined) { @@ -1347,22 +1538,42 @@ DS.Store = Ember.Object.extend(DS._Mappable, { `getByReference`. */ findById: function(type, id) { - var clientId = this.typeMapFor(type).idToCid[id]; + var reference; + + if (this.hasReferenceForId(type, id)) { + reference = this.referenceForId(type, id); + + if (reference.data !== UNLOADED) { + return this.recordForReference(reference); + } + } - if (clientId) { - return this.findByClientId(type, clientId); + if (!reference) { + reference = this.createReference(type, id); } - clientId = this.pushData(LOADING, id, type); + reference.data = LOADING; // create a new instance of the model type in the // 'isLoading' state - var record = this.materializeRecord(type, clientId, id); + var record = this.materializeRecord(reference); - // let the adapter set the data, possibly async - var adapter = this.adapterForType(type); - if (adapter && adapter.find) { adapter.find(this, type, id); } - else { throw "Adapter is either null or does not implement `find` method"; } + if (reference.data === LOADING) { + // let the adapter set the data, possibly async + var adapter = this.adapterForType(type), + store = this; + + Ember.assert("You tried to find a record but you have no adapter (for " + type + ")", adapter); + Ember.assert("You tried to find a record but your adapter does not implement `find`", adapter.find); + + var thenable = adapter.find(this, type, id); + + if (thenable && thenable.then) { + thenable.then(null /* for future use */, function(error) { + store.recordWasError(record); + }); + } + } return record; }, @@ -1370,56 +1581,45 @@ DS.Store = Ember.Object.extend(DS._Mappable, { reloadRecord: function(record) { var type = record.constructor, adapter = this.adapterForType(type), + store = this, id = get(record, 'id'); Ember.assert("You cannot update a record without an ID", id); Ember.assert("You tried to update a record but you have no adapter (for " + type + ")", adapter); Ember.assert("You tried to update a record but your adapter does not implement `find`", adapter.find); - adapter.find(this, type, id); + var thenable = adapter.find(this, type, id); + + if (thenable && thenable.then) { + thenable.then(null /* for future use */, function(error) { + store.recordWasError(record); + }); + } }, /** @private - This method returns a record for a given clientId. - - If there is no record object yet for the clientId, this method materializes - a new record object. This allows adapters to eagerly load large amounts of - data into the store, and avoid incurring the cost to create the objects - until they are requested. + This method returns a record for a given record refeence. - Several parts of Ember Data call this method: - - * findById, if a clientId already exists for a given type and - id combination - * OneToManyChange, which is backed by clientIds, when getChild, - getOldParent or getNewParent are called - * RecordArray, which is backed by clientIds, when an object at - a particular index is looked up + If no record for the reference has yet been materialized, this method will + materialize a new `DS.Model` instance. This allows adapters to eagerly load + large amounts of data into the store, and avoid incurring the cost of + creating models until they are requested. In short, it's a convenient way to get a record for a known - clientId, materializing it if necessary. + record reference, materializing it if necessary. - @param {Class} type - @param {Number|String} clientId + @param {Object} reference + @returns {DS.Model} */ - findByClientId: function(type, clientId) { - var cidToData, record, id; - - record = this.recordCache[clientId]; + recordForReference: function(reference) { + var record = reference.record; if (!record) { // create a new instance of the model type in the // 'isLoading' state - id = this.clientIdToId[clientId]; - record = this.materializeRecord(type, clientId, id); - - cidToData = this.clientIdToData; - - if (typeof cidToData[clientId] === 'object') { - record.loadedData(); - } + record = this.materializeRecord(reference); } return record; @@ -1428,27 +1628,25 @@ DS.Store = Ember.Object.extend(DS._Mappable, { /** @private - Given a type and array of `clientId`s, determines which of those + Given an array of `reference`s, determines which of those `clientId`s has not yet been loaded. In preparation for loading, this method also marks any unloaded `clientId`s as loading. */ - neededReferences: function(type, references) { - var neededReferences = [], - cidToData = this.clientIdToData, - reference; + unloadedReferences: function(references) { + var unloadedReferences = []; for (var i=0, l=references.length; i tuple for a polymorphic association. + return store.referenceForId(id.type, id.id); + } + } return store.referenceForId(type, id); }); @@ -3739,7 +3751,7 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { Ember.run.once(this, this.updateRecordArrays); }, - setupData: function(prematerialized) { + setupData: function() { this._data = { attributes: {}, belongsTo: {}, @@ -3761,12 +3773,45 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { this._data.attributes[name] = value; }, - materializeHasMany: function(name, ids) { - this._data.hasMany[name] = ids; + materializeHasMany: function(name, tuplesOrReferencesOrOpaque) { + var tuplesOrReferencesOrOpaqueType = typeof tuplesOrReferencesOrOpaque; + + if (tuplesOrReferencesOrOpaque && tuplesOrReferencesOrOpaqueType !== 'string' && tuplesOrReferencesOrOpaque.length > 1) { + Ember.assert('materializeHasMany expects tuples, references or opaque token, not ' + tuplesOrReferencesOrOpaque[0], tuplesOrReferencesOrOpaque[0].hasOwnProperty('id') && tuplesOrReferencesOrOpaque[0].type); + } + + if( tuplesOrReferencesOrOpaqueType === "string" ) { + this._data.hasMany[name] = tuplesOrReferencesOrOpaque; + } else { + var references = tuplesOrReferencesOrOpaque; + + if (tuplesOrReferencesOrOpaque && Ember.isArray(tuplesOrReferencesOrOpaque)) { + references = this._convertTuplesToReferences(tuplesOrReferencesOrOpaque); + } + + this._data.hasMany[name] = references; + } + }, + + materializeBelongsTo: function(name, tupleOrReference) { + if (tupleOrReference) { Ember.assert('materializeBelongsTo expects a tuple or a reference, not a ' + tupleOrReference, !tupleOrReference || (tupleOrReference.hasOwnProperty('id') && tupleOrReference.hasOwnProperty('type'))); } + + this._data.belongsTo[name] = tupleOrReference; + }, + + _convertTuplesToReferences: function(tuplesOrReferences) { + return map(tuplesOrReferences, function(tupleOrReference) { + return this._convertTupleToReference(tupleOrReference); + }, this); }, - materializeBelongsTo: function(name, id) { - this._data.belongsTo[name] = id; + _convertTupleToReference: function(tupleOrReference) { + var store = get(this, 'store'); + if(tupleOrReference.clientId) { + return tupleOrReference; + } else { + return store.referenceForId(tupleOrReference.type, tupleOrReference.id); + } }, rollback: function() { @@ -3815,10 +3860,54 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { becameInFlight: function() { }, - // FOR USE BY THE BASIC ADAPTER + /** + @private + + */ + resolveOn: function(successEvent) { + var model = this; + + return new Ember.RSVP.Promise(function(resolve, reject) { + function success() { + this.off('becameError', error); + this.off('becameInvalid', error); + resolve(this); + } + function error() { + this.off(successEvent, success); + reject(this); + } + + model.one(successEvent, success); + model.one('becameError', error); + model.one('becameInvalid', error); + }); + }, + + /** + Save the record. + @method save + */ save: function() { this.get('store').scheduleSave(this); + + return this.resolveOn('didCommit'); + }, + + /** + Reload the record from the adapter. + + This will only work if the record has already finished loading + and has not yet been modified (`isLoaded` but not `isDirty`, + or `isSaving`). + + @method reload + */ + reload: function() { + this.send('reloadRecord'); + + return this.resolveOn('didReload'); }, // FOR USE DURING COMMIT PROCESS @@ -3840,10 +3929,6 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { this.updateRecordArraysLater(); }, - _reference: Ember.computed(function() { - return get(this, 'store').referenceForClientId(get(this, 'clientId')); - }), - adapterDidInvalidate: function(errors) { this.send('becameInvalid', errors); }, @@ -3880,18 +3965,67 @@ var storeAlias = function(methodName) { }; DS.Model.reopenClass({ - isLoaded: storeAlias('recordIsLoaded'), - find: storeAlias('find'), - all: storeAlias('all'), - query: storeAlias('findQuery'), - filter: storeAlias('filter'), + /** @private + Alias DS.Model's `create` method to `_create`. This allows us to create DS.Model + instances from within the store, but if end users accidentally call `create()` + (instead of `createRecord()`), we can raise an error. + */ _create: DS.Model.create, + /** @private + + Override the class' `create()` method to raise an error. This prevents end users + from inadvertently calling `create()` instead of `createRecord()`. The store is + still able to create instances by calling the `_create()` method. + */ create: function() { throw new Ember.Error("You should not call `create` on a model. Instead, call `createRecord` with the attributes you would like to set."); }, + /** + See {{#crossLink "DS.Store/find:method"}}`DS.Store.find()`{{/crossLink}}. + + @method find + @param {Object|String|Array|null} query A query to find records by. + + */ + find: storeAlias('find'), + + /** + See {{#crossLink "DS.Store/all:method"}}`DS.Store.all()`{{/crossLink}}. + + @method all + @return {DS.RecordArray} + */ + all: storeAlias('all'), + + /** + See {{#crossLink "DS.Store/findQuery:method"}}`DS.Store.findQuery()`{{/crossLink}}. + + @method query + @param {Object} query an opaque query to be used by the adapter + @return {DS.AdapterPopulatedRecordArray} + */ + query: storeAlias('findQuery'), + + /** + See {{#crossLink "DS.Store/filter:method"}}`DS.Store.filter()`{{/crossLink}}. + + @method filter + @param {Function} filter + @return {DS.FilteredRecordArray} + */ + filter: storeAlias('filter'), + + /** + See {{#crossLink "DS.Store/createRecord:method"}}`DS.Store.createRecord()`{{/crossLink}}. + + @method createRecord + @param {Object} properties a hash of properties to set on the + newly created record. + @returns DS.Model + */ createRecord: storeAlias('createRecord') }); @@ -3900,7 +4034,13 @@ DS.Model.reopenClass({ (function() { +/** + @module data + @submodule data-model +*/ + var get = Ember.get; + DS.Model.reopenClass({ attributes: Ember.computed(function() { var map = Ember.Map.create(); @@ -3918,29 +4058,6 @@ DS.Model.reopenClass({ }) }); -var AttributeChange = DS.AttributeChange = function(options) { - this.reference = options.reference; - this.store = options.store; - this.name = options.name; - this.oldValue = options.oldValue; -}; - -AttributeChange.createChange = function(options) { - return new AttributeChange(options); -}; - -AttributeChange.prototype = { - sync: function() { - this.store.recordAttributeDidChange(this.reference, this.name, this.value, this.oldValue); - - // TODO: Use this object in the commit process - this.destroy(); - }, - - destroy: function() { - delete this.store.recordForReference(this.reference)._changesToSync[this.name]; - } -}; DS.Model.reopen({ eachAttribute: function(callback, binding) { @@ -3966,7 +4083,11 @@ function getAttr(record, options, key) { var value = attributes[key]; if (value === undefined) { - value = options.defaultValue; + if (typeof options.defaultValue === "function") { + value = options.defaultValue(); + } else { + value = options.defaultValue; + } } return value; @@ -4001,1184 +4122,1343 @@ DS.attr = function(type, options) { (function() { +/** + @module data + @submodule data-model +*/ })(); (function() { -var get = Ember.get, set = Ember.set, - none = Ember.isNone; - -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 || {}; +/** + An AttributeChange object is created whenever a record's + attribute changes value. It is used to track changes to a + record between transaction commits. +*/ - var meta = { type: type, isRelationship: true, options: options, kind: 'belongsTo' }; +var AttributeChange = DS.AttributeChange = function(options) { + this.reference = options.reference; + this.store = options.store; + this.name = options.name; + this.oldValue = options.oldValue; +}; - return Ember.computed(function(key, value) { - if (arguments.length === 2) { - return value === undefined ? null : value; - } +AttributeChange.createChange = function(options) { + return new AttributeChange(options); +}; - var data = get(this, 'data').belongsTo, - store = get(this, 'store'), id; +AttributeChange.prototype = { + sync: function() { + this.store.recordAttributeDidChange(this.reference, this.name, this.value, this.oldValue); - if (typeof type === 'string') { - type = get(this, type, false) || get(Ember.lookup, type); - } + // TODO: Use this object in the commit process + this.destroy(); + }, - id = data[key]; + /** + If the AttributeChange is destroyed (either by being rolled back + or being committed), remove it from the list of pending changes + on the record. + */ + destroy: function() { + var record = this.reference.record; - if(!id) { - return null; - } else if (typeof id === 'object') { - return store.recordForReference(id); - } else { - return store.find(type, id); - } - }).property('data').meta(meta); + delete record._changesToSync[this.name]; + } }; -/** - These observers observe all `belongsTo` relationships on the record. See - `relationships/ext` to see how these observers get their dependencies. +})(); -*/ -DS.Model.reopen({ - /** @private */ - belongsToWillChange: Ember.beforeObserver(function(record, key) { - if (get(record, 'isLoaded')) { - var oldParent = get(record, key); - var childReference = get(record, '_reference'), - store = get(record, 'store'); - if (oldParent){ - var change = DS.RelationshipChange.createChange(childReference, get(oldParent, '_reference'), store, { key: key, kind:"belongsTo", changeType: "remove" }); - change.sync(); - this._changesToSync[key] = change; - } - } - }), +(function() { +var get = Ember.get, set = Ember.set; +var forEach = Ember.EnumerableUtils.forEach; - /** @private */ - belongsToDidChange: Ember.immediateObserver(function(record, key) { - if (get(record, 'isLoaded')) { - var newParent = get(record, key); - if(newParent){ - var childReference = get(record, '_reference'), - store = get(record, 'store'); - var change = DS.RelationshipChange.createChange(childReference, get(newParent, '_reference'), store, { key: key, kind:"belongsTo", changeType: "add" }); - change.sync(); - if(this._changesToSync[key]){ - DS.OneToManyChange.ensureSameTransaction([change, this._changesToSync[key]], store); - } - } - } - delete this._changesToSync[key]; - }) -}); +DS.RelationshipChange = function(options) { + this.parentReference = options.parentReference; + this.childReference = options.childReference; + this.firstRecordReference = options.firstRecordReference; + this.firstRecordKind = options.firstRecordKind; + this.firstRecordName = options.firstRecordName; + this.secondRecordReference = options.secondRecordReference; + this.secondRecordKind = options.secondRecordKind; + this.secondRecordName = options.secondRecordName; + this.changeType = options.changeType; + this.store = options.store; -})(); + this.committed = {}; +}; +DS.RelationshipChangeAdd = function(options){ + DS.RelationshipChange.call(this, options); +}; +DS.RelationshipChangeRemove = function(options){ + DS.RelationshipChange.call(this, options); +}; -(function() { -var get = Ember.get, set = Ember.set; -var hasRelationship = function(type, options) { - options = options || {}; +/** @private */ +DS.RelationshipChange.create = function(options) { + return new DS.RelationshipChange(options); +}; - var meta = { type: type, isRelationship: true, options: options, kind: 'hasMany' }; +/** @private */ +DS.RelationshipChangeAdd.create = function(options) { + return new DS.RelationshipChangeAdd(options); +}; - return Ember.computed(function(key, value) { - var data = get(this, 'data').hasMany, - store = get(this, 'store'), - ids, relationship; +/** @private */ +DS.RelationshipChangeRemove.create = function(options) { + return new DS.RelationshipChangeRemove(options); +}; - if (typeof type === 'string') { - type = get(this, type, false) || get(Ember.lookup, type); +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"; } + } + +}; - ids = data[key]; - relationship = store.findMany(type, ids, this, meta); - set(relationship, 'owner', this); - set(relationship, 'name', key); +DS.RelationshipChange.createChange = function(firstRecordReference, secondRecordReference, store, options){ + // Get the type of the child based on the child's client ID + var firstRecordType = firstRecordReference.type, changeType; + changeType = DS.RelationshipChange.determineRelationshipType(firstRecordType, options); + if (changeType === "oneToMany"){ + return DS.OneToManyChange.createChange(firstRecordReference, secondRecordReference, store, options); + } + else if (changeType === "manyToOne"){ + return DS.OneToManyChange.createChange(secondRecordReference, firstRecordReference, store, options); + } + else if (changeType === "oneToNone"){ + return DS.OneToNoneChange.createChange(firstRecordReference, secondRecordReference, store, options); + } + else if (changeType === "manyToNone"){ + return DS.ManyToNoneChange.createChange(firstRecordReference, secondRecordReference, store, options); + } + else if (changeType === "oneToOne"){ + return DS.OneToOneChange.createChange(firstRecordReference, secondRecordReference, store, options); + } + else if (changeType === "manyToMany"){ + return DS.ManyToManyChange.createChange(firstRecordReference, secondRecordReference, store, options); + } +}; - return relationship; - }).property().meta(meta); +/** @private */ +DS.OneToNoneChange.createChange = function(childReference, parentReference, store, options) { + var key = options.key; + var change = DS.RelationshipChange._createChange({ + parentReference: parentReference, + childReference: childReference, + firstRecordReference: childReference, + store: store, + changeType: options.changeType, + firstRecordName: key, + firstRecordKind: "belongsTo" + }); + + store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + + return change; }; -DS.hasMany = function(type, options) { - Ember.assert("The type passed to DS.hasMany must be defined", !!type); - return hasRelationship(type, options); +/** @private */ +DS.ManyToNoneChange.createChange = function(childReference, parentReference, store, options) { + var key = options.key; + var change = DS.RelationshipChange._createChange({ + parentReference: childReference, + childReference: parentReference, + secondRecordReference: childReference, + store: store, + changeType: options.changeType, + secondRecordName: options.key, + secondRecordKind: "hasMany" + }); + + store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + return change; }; -})(); +/** @private */ +DS.ManyToManyChange.createChange = function(childReference, parentReference, 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({ + parentReference: parentReference, + childReference: childReference, + firstRecordReference: childReference, + secondRecordReference: parentReference, + firstRecordKind: "hasMany", + secondRecordKind: "hasMany", + store: store, + changeType: options.changeType, + firstRecordName: key + }); -(function() { -var get = Ember.get, set = Ember.set; + store.addRelationshipChangeFor(childReference, key, parentReference, null, change); -/** - @private - This file defines several extensions to the base `DS.Model` class that - add support for one-to-many relationships. -*/ + return change; +}; -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. - didDefineProperty: function(proto, key, value) { - // Check if the value being set is a computed property. - if (value instanceof Ember.Descriptor) { +/** @private */ +DS.OneToOneChange.createChange = function(childReference, parentReference, store, options) { + var key; - // 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 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); + } - if (meta.isRelationship && meta.kind === 'belongsTo') { - Ember.addObserver(proto, key, null, 'belongsToDidChange'); - Ember.addBeforeObserver(proto, key, null, 'belongsToWillChange'); - } + var change = DS.RelationshipChange._createChange({ + parentReference: parentReference, + childReference: childReference, + firstRecordReference: childReference, + secondRecordReference: parentReference, + firstRecordKind: "belongsTo", + secondRecordKind: "belongsTo", + store: store, + changeType: options.changeType, + firstRecordName: key + }); - if (meta.isAttribute) { - Ember.addObserver(proto, key, null, 'attributeDidChange'); - Ember.addBeforeObserver(proto, key, null, 'attributeWillChange'); - } + store.addRelationshipChangeFor(childReference, key, parentReference, null, change); - meta.parentType = proto.constructor; + + return change; +}; + +DS.OneToOneChange.maintainInvariant = function(options, store, childReference, key){ + if (options.changeType === "add" && store.recordIsMaterialized(childReference)) { + var child = store.recordForReference(childReference); + var oldParent = get(child, key); + if (oldParent){ + var correspondingChange = DS.OneToOneChange.createChange(childReference, oldParent.get('_reference'), store, { + parentType: options.parentType, + hasManyName: options.hasManyName, + changeType: "remove", + key: options.key + }); + store.addRelationshipChangeFor(childReference, key, options.parentReference , null, correspondingChange); + correspondingChange.sync(); } } -}); +}; -/** - These DS.Model extensions add class methods that provide relationship - introspection abilities about relationships. +/** @private */ +DS.OneToManyChange.createChange = function(childReference, parentReference, store, options) { + var key; - A note about the computed properties contained here: + // 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, childReference, key ); + } else if (options.key) { + key = options.key; + } else { + Ember.assert("You must pass either a parentType or belongsToName option to OneToManyChange.forChildAndParent", false); + } - **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. + var change = DS.RelationshipChange._createChange({ + parentReference: parentReference, + childReference: childReference, + firstRecordReference: childReference, + secondRecordReference: parentReference, + firstRecordKind: "belongsTo", + secondRecordKind: "hasMany", + store: store, + changeType: options.changeType, + firstRecordName: key + }); - 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. -*/ + store.addRelationshipChangeFor(childReference, key, parentReference, change.getSecondRecordName(), change); + + + return change; +}; + + +DS.OneToManyChange.maintainInvariant = function(options, store, childReference, key){ + var child = childReference.record; + + if (options.changeType === "add" && child) { + var oldParent = get(child, key); + if (oldParent){ + var correspondingChange = DS.OneToManyChange.createChange(childReference, oldParent.get('_reference'), store, { + parentType: options.parentType, + hasManyName: options.hasManyName, + changeType: "remove", + key: options.key + }); + store.addRelationshipChangeFor(childReference, key, options.parentReference, correspondingChange.getSecondRecordName(), correspondingChange); + correspondingChange.sync(); + } + } +}; + +DS.OneToManyChange.ensureSameTransaction = function(changes){ + var records = Ember.A(); + forEach(changes, function(change){ + records.addObject(change.getSecondRecord()); + records.addObject(change.getFirstRecord()); + }); + + return DS.Transaction.ensureSameTransaction(records); +}; + +DS.RelationshipChange.prototype = { + + getSecondRecordName: function() { + var name = this.secondRecordName, parent; + + if (!name) { + parent = this.secondRecordReference; + if (!parent) { return; } + + var childType = this.firstRecordReference.type; + var inverse = childType.inverseFor(this.firstRecordName); + this.secondRecordName = inverse.name; + } + + return this.secondRecordName; + }, + + /** + Get the name of the relationship on the belongsTo side. + + @return {String} + */ + getFirstRecordName: function() { + var name = this.firstRecordName; + return name; + }, + + /** @private */ + destroy: function() { + var childReference = this.childReference, + belongsToName = this.getFirstRecordName(), + hasManyName = this.getSecondRecordName(), + store = this.store; + + store.removeRelationshipChangeFor(childReference, belongsToName, this.parentReference, hasManyName, this.changeType); + }, + + /** @private */ + getByReference: function(reference) { + // return null or undefined if the original reference was null or undefined + if (!reference) { return reference; } + + if (reference.record) { + return reference.record; + } + }, + + getSecondRecord: function(){ + return this.getByReference(this.secondRecordReference); + }, + + /** @private */ + getFirstRecord: function() { + return this.getByReference(this.firstRecordReference); + }, + + /** + @private + + Make sure that all three parts of the relationship change are part of + the same transaction. If any of the three records is clean and in the + default transaction, and the rest are in a different transaction, move + them all into that transaction. + */ + ensureSameTransaction: function() { + var child = this.getFirstRecord(), + parentRecord = this.getSecondRecord(); + + var transaction = DS.Transaction.ensureSameTransaction([child, parentRecord]); + + this.transaction = transaction; + return transaction; + }, -DS.Model.reopenClass({ - /** - For a given relationship name, returns the model type of the relationship. + callChangeEvents: function(){ + var child = this.getFirstRecord(), + parentRecord = this.getSecondRecord(); - For example, if you define a model like this: + var dirtySet = new Ember.OrderedSet(); - App.Post = DS.Model.extend({ - comments: DS.hasMany(App.Comment) - }); + // TODO: This implementation causes a race condition in key-value + // stores. The fix involves buffering changes that happen while + // a record is loading. A similar fix is required for other parts + // of ember-data, and should be done as new infrastructure, not + // a one-off hack. [tomhuda] + if (parentRecord && get(parentRecord, 'isLoaded')) { + this.store.recordHasManyDidChange(dirtySet, parentRecord, this); + } - Calling `App.Post.typeForRelationship('comments')` will return `App.Comment`. + if (child) { + this.store.recordBelongsToDidChange(dirtySet, child, this); + } - @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; + dirtySet.forEach(function(record) { + record.adapterDidDirty(); + }); }, - /** - 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. + coalesce: function(){ + var relationshipPairs = this.store.relationshipChangePairsFor(this.firstRecordReference); + forEach(relationshipPairs, function(pair){ + var addedChange = pair["add"]; + var removedChange = pair["remove"]; + if(addedChange && removedChange) { + addedChange.destroy(); + removedChange.destroy(); + } + }); + } +}; - For example, given the following model definition: +DS.RelationshipChangeAdd.prototype = Ember.create(DS.RelationshipChange.create({})); +DS.RelationshipChangeRemove.prototype = Ember.create(DS.RelationshipChange.create({})); - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), - posts: DS.hasMany(App.Post) - }); +DS.RelationshipChangeAdd.prototype.changeType = "add"; +DS.RelationshipChangeAdd.prototype.sync = function() { + var secondRecordName = this.getSecondRecordName(), + firstRecordName = this.getFirstRecordName(), + firstRecord = this.getFirstRecord(), + secondRecord = this.getSecondRecord(); - This computed property would return a map describing these - relationships, like this: + //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); - var relationships = Ember.get(App.Blog, 'relationships'); - associatons.get(App.User); - //=> [ { name: 'users', kind: 'hasMany' }, - // { name: 'owner', kind: 'belongsTo' } ] - relationships.get(App.Post); - //=> [ { name: 'posts', kind: 'hasMany' } ] + this.ensureSameTransaction(); - @type Ember.Map - @readOnly - */ - relationships: Ember.computed(function() { - var map = new Ember.MapWithDefault({ - defaultValue: function() { return []; } - }); + this.callChangeEvents(); - // Loop through each computed property on the class - this.eachComputedProperty(function(name, meta) { + if (secondRecord && firstRecord) { + if(this.secondRecordKind === "belongsTo"){ + secondRecord.suspendRelationshipObservers(function(){ + set(secondRecord, secondRecordName, firstRecord); + }); - // 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); - } + } + else if(this.secondRecordKind === "hasMany"){ + secondRecord.suspendRelationshipObservers(function(){ + get(secondRecord, secondRecordName).addObject(firstRecord); + }); + } + } - var relationshipsForType = map.get(meta.type); + if (firstRecord && secondRecord && get(firstRecord, firstRecordName) !== secondRecord) { + if(this.firstRecordKind === "belongsTo"){ + firstRecord.suspendRelationshipObservers(function(){ + set(firstRecord, firstRecordName, secondRecord); + }); + } + else if(this.firstRecordKind === "hasMany"){ + firstRecord.suspendRelationshipObservers(function(){ + get(firstRecord, firstRecordName).addObject(secondRecord); + }); + } + } - relationshipsForType.push({ name: name, kind: meta.kind }); - } - }); + this.coalesce(); +}; - return map; - }), +DS.RelationshipChangeRemove.prototype.changeType = "remove"; +DS.RelationshipChangeRemove.prototype.sync = function() { + var secondRecordName = this.getSecondRecordName(), + firstRecordName = this.getFirstRecordName(), + firstRecord = this.getFirstRecord(), + secondRecord = this.getSecondRecord(); - /** - A hash containing lists of the model's relationships, grouped - by the relationship kind. For example, given a model with this - definition: + //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); - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), + this.ensureSameTransaction(firstRecord, secondRecord, secondRecordName, firstRecordName); - posts: DS.hasMany(App.Post) - }); + this.callChangeEvents(); - This property would contain the following: + if (secondRecord && firstRecord) { + if(this.secondRecordKind === "belongsTo"){ + secondRecord.suspendRelationshipObservers(function(){ + set(secondRecord, secondRecordName, null); + }); + } + else if(this.secondRecordKind === "hasMany"){ + secondRecord.suspendRelationshipObservers(function(){ + get(secondRecord, secondRecordName).removeObject(firstRecord); + }); + } + } - var relationshipNames = Ember.get(App.Blog, 'relationshipNames'); - relationshipNames.hasMany; - //=> ['users', 'posts'] - relationshipNames.belongsTo; - //=> ['owner'] + if (firstRecord && get(firstRecord, firstRecordName)) { + if(this.firstRecordKind === "belongsTo"){ + firstRecord.suspendRelationshipObservers(function(){ + set(firstRecord, firstRecordName, null); + }); + } + else if(this.firstRecordKind === "hasMany"){ + firstRecord.suspendRelationshipObservers(function(){ + get(firstRecord, firstRecordName).removeObject(secondRecord); + }); + } + } - @type Object - @readOnly - */ - relationshipNames: Ember.computed(function() { - var names = { hasMany: [], belongsTo: [] }; + this.coalesce(); +}; - 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: +(function() { +/** + @module data + @submodule data-changes +*/ - 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 ] - @type Ember.Array - @readOnly - */ - relatedTypes: Ember.computed(function() { - var type, - types = Ember.A([]); +(function() { +var get = Ember.get, set = Ember.set, + isNone = Ember.isNone; - // 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; +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))); - if (typeof type === 'string') { - type = get(this, type, false) || get(Ember.lookup, type); - } + options = options || {}; - if (!types.contains(type)) { - Ember.assert("Trying to sideload " + name + " on " + this.toString() + " but the type doesn't exist.", !!type); - types.push(type); - } - } - }); + var meta = { type: type, isRelationship: true, options: options, kind: 'belongsTo' }; - return types; - }), + return Ember.computed(function(key, value) { + if (typeof type === 'string') { + type = get(this, type, false) || get(Ember.lookup, type); + } - /** - A map whose keys are the relationships of a model and whose values are - relationship descriptors. + if (arguments.length === 2) { + Ember.assert("You can only add a record of " + type.toString() + " to this relationship", !value || type.detectInstance(value)); + return value === undefined ? null : value; + } - For example, given a model with this - definition: + var data = get(this, 'data').belongsTo, + store = get(this, 'store'), belongsTo; - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), + belongsTo = data[key]; - posts: DS.hasMany(App.Post) - }); + // TODO (tomdale) The value of the belongsTo in the data hash can be + // one of: + // 1. null/undefined + // 2. a record reference + // 3. a tuple returned by the serializer's polymorphism code + // + // We should really normalize #3 to be the same as #2 to reduce the + // complexity here. + + if (isNone(belongsTo)) { + return null; + } + + // The data has been normalized to a record reference, so + // just ask the store for the record for that reference, + // materializing it if necessary. + if (belongsTo.clientId) { + return store.recordForReference(belongsTo); + } + + // The data has been normalized into a type/id pair by the + // serializer's polymorphism code. + return store.findById(belongsTo.type, belongsTo.id); + }).property('data').meta(meta); +}; - This property would contain the following: +/** + These observers observe all `belongsTo` relationships on the record. See + `relationships/ext` to see how these observers get their dependencies. - 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 } +*/ - @type Ember.Map - @readOnly - */ - relationshipsByName: Ember.computed(function() { - var map = Ember.Map.create(), type; +DS.Model.reopen({ + /** @private */ + belongsToWillChange: Ember.beforeObserver(function(record, key) { + if (get(record, 'isLoaded')) { + var oldParent = get(record, key); - this.eachComputedProperty(function(name, meta) { - if (meta.isRelationship) { - meta.key = name; - type = meta.type; + var childReference = get(record, '_reference'), + store = get(record, 'store'); + if (oldParent){ + var change = DS.RelationshipChange.createChange(childReference, get(oldParent, '_reference'), store, { key: key, kind:"belongsTo", changeType: "remove" }); + change.sync(); + this._changesToSync[key] = change; + } + } + }), - if (typeof type === 'string') { - type = get(this, type, false) || get(Ember.lookup, type); - meta.type = type; + /** @private */ + belongsToDidChange: Ember.immediateObserver(function(record, key) { + if (get(record, 'isLoaded')) { + var newParent = get(record, key); + if(newParent){ + var childReference = get(record, '_reference'), + store = get(record, 'store'); + var change = DS.RelationshipChange.createChange(childReference, get(newParent, '_reference'), store, { key: key, kind:"belongsTo", changeType: "add" }); + change.sync(); + if(this._changesToSync[key]){ + DS.OneToManyChange.ensureSameTransaction([change, this._changesToSync[key]], store); } - - map.set(name, meta); } - }); + } + delete this._changesToSync[key]; + }) +}); - 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), +(function() { +var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; +var hasRelationship = function(type, options) { + options = options || {}; - posts: DS.hasMany(App.Post), + var meta = { type: type, isRelationship: true, options: options, kind: 'hasMany' }; - title: DS.attr('string') - }); + return Ember.computed(function(key, value) { + var data = get(this, 'data').hasMany, + store = get(this, 'store'), + ids, relationship; - var fields = Ember.get(App.Blog, 'fields'); - fields.forEach(function(field, kind) { - console.log(field, kind); - }); + if (typeof type === 'string') { + type = get(this, type, false) || get(Ember.lookup, type); + } - // prints: - // users, hasMany - // owner, belongsTo - // posts, hasMany - // title, attribute + //ids can be references or opaque token + //(e.g. `{url: '/relationship'}`) that will be passed to the adapter + ids = data[key]; - @type Ember.Map - @readOnly - */ - fields: Ember.computed(function() { - var map = Ember.Map.create(), type; + relationship = store.findMany(type, ids, this, meta); + set(relationship, 'owner', this); + set(relationship, 'name', key); + set(relationship, 'isPolymorphic', options.polymorphic); - this.eachComputedProperty(function(name, meta) { - if (meta.isRelationship) { - map.set(name, meta.kind); - } else if (meta.isAttribute) { - map.set(name, 'attribute'); - } - }); + return relationship; + }).property().meta(meta); +}; - return map; - }), +DS.hasMany = function(type, options) { + Ember.assert("The type passed to DS.hasMany must be defined", !!type); + return hasRelationship(type, options); +}; - /** - 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. +function clearUnmaterializedHasMany(record, relationship) { + var data = get(record, 'data').hasMany; - @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); - }); - }, + var references = data[relationship.key]; - /** - 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. + if (!references) { return; } - @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); + var inverse = record.constructor.inverseFor(relationship.key); + + if (inverse) { + forEach(references, function(reference) { + var childRecord; + + if (childRecord = reference.record) { + record.suspendRelationshipObservers(function() { + set(childRecord, inverse.name, null); + }); + } }); } -}); +} 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. + clearHasMany: function(relationship) { + var hasMany = this.cacheFor(relationship.name); - @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); + if (hasMany) { + hasMany.clear(); + } else { + clearUnmaterializedHasMany(this, relationship); + } } }); -/** - @private +})(); + - Helper method to look up the name of the inverse of a relationship. - In a has-many relationship, there are always two sides: the `belongsTo` side - and the `hasMany` side. When one side changes, the other side should be updated - automatically. +(function() { +var get = Ember.get, set = Ember.set; - Given a model, the model of the inverse, and the kind of the relationship, this - helper returns the name of the relationship on the inverse. +/** + @private - For example, imagine the following two associated models: + This file defines several extensions to the base `DS.Model` class that + add support for one-to-many relationships. +*/ - App.Post = DS.Model.extend({ - comments: DS.hasMany('App.Comment') - }); +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. + didDefineProperty: function(proto, key, value) { + // Check if the value being set is a computed property. + if (value instanceof Ember.Descriptor) { - App.Comment = DS.Model.extend({ - post: DS.belongsTo('App.Post') - }); + // 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 the `post` property of a `Comment` was modified, Ember Data would invoke - this helper like this: + if (meta.isRelationship && meta.kind === 'belongsTo') { + Ember.addObserver(proto, key, null, 'belongsToDidChange'); + Ember.addBeforeObserver(proto, key, null, 'belongsToWillChange'); + } - DS._inverseNameFor(App.Comment, App.Post, 'hasMany'); - //=> 'comments' + if (meta.isAttribute) { + Ember.addObserver(proto, key, null, 'attributeDidChange'); + Ember.addBeforeObserver(proto, key, null, 'attributeWillChange'); + } - Ember Data uses the name of the relationship returned to reflect the changed - relationship on the other side. -*/ -DS._inverseRelationshipFor = function(modelType, inverseModelType) { - var relationshipMap = get(modelType, 'relationships'), - possibleRelationships = relationshipMap.get(inverseModelType), - possible, actual, oldValue; - - if (!possibleRelationships) { return; } - if (possibleRelationships.length > 1) { return; } - return possibleRelationships[0]; -}; + meta.parentType = proto.constructor; + } + } +}); /** - @private + These DS.Model extensions add class methods that provide relationship + introspection abilities about relationships. - Given a model and a relationship name, returns the model type of - the named relationship. + A note about the computed properties contained here: - App.Post = DS.Model.extend({ - comments: DS.hasMany('App.Comment') - }); + **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. - DS._inverseTypeFor(App.Post, 'comments'); - //=> App.Comment - @param {DS.Model class} modelType - @param {String} relationshipName - @return {DS.Model class} + 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._inverseTypeFor = function(modelType, relationshipName) { - var relationships = get(modelType, 'relationshipsByName'), - relationship = relationships.get(relationshipName); - - if (relationship) { return relationship.type; } -}; - -})(); - +DS.Model.reopenClass({ + /** + For a given relationship name, returns the model type of the relationship. -(function() { -var get = Ember.get, set = Ember.set; -var forEach = Ember.EnumerableUtils.forEach; + For example, if you define a model like this: -DS.RelationshipChange = function(options) { - this.parentReference = options.parentReference; - this.childReference = options.childReference; - this.firstRecordReference = options.firstRecordReference; - this.firstRecordKind = options.firstRecordKind; - this.firstRecordName = options.firstRecordName; - this.secondRecordReference = options.secondRecordReference; - this.secondRecordKind = options.secondRecordKind; - this.secondRecordName = options.secondRecordName; - this.store = options.store; - this.committed = {}; - this.changeType = options.changeType; -}; + App.Post = DS.Model.extend({ + comments: DS.hasMany(App.Comment) + }); -DS.RelationshipChangeAdd = function(options){ - DS.RelationshipChange.call(this, options); -}; + Calling `App.Post.typeForRelationship('comments')` will return `App.Comment`. -DS.RelationshipChangeRemove = function(options){ - DS.RelationshipChange.call(this, options); -}; + @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; + }, -/** @private */ -DS.RelationshipChange.create = function(options) { - return new DS.RelationshipChange(options); -}; + inverseFor: function(name) { + var inverseType = this.typeForRelationship(name); -/** @private */ -DS.RelationshipChangeAdd.create = function(options) { - return new DS.RelationshipChangeAdd(options); -}; + if (!inverseType) { return null; } -/** @private */ -DS.RelationshipChangeRemove.create = function(options) { - return new DS.RelationshipChangeRemove(options); -}; + var options = this.metaForProperty(name).options; + var inverseName, inverseKind; -DS.OneToManyChange = {}; -DS.OneToNoneChange = {}; -DS.ManyToNoneChange = {}; -DS.OneToOneChange = {}; -DS.ManyToManyChange = {}; + if (options.inverse) { + inverseName = options.inverse; + inverseKind = Ember.get(inverseType, 'relationshipsByName').get(inverseName).kind; + } else { + var possibleRelationships = findPossibleInverses(this, inverseType); -DS.RelationshipChange._createChange = function(options){ - if(options.changeType === "add"){ - return DS.RelationshipChangeAdd.create(options); - } - if(options.changeType === "remove"){ - return DS.RelationshipChangeRemove.create(options); - } -}; + 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); -DS.RelationshipChange.determineRelationshipType = function(recordType, knownSide){ - var knownKey = knownSide.key, key, type, otherContainerType,assoc; - var knownContainerType = knownSide.kind; - var options = recordType.metaForProperty(knownKey).options; - var otherType = DS._inverseTypeFor(recordType, knownKey); - - if(options.inverse){ - key = options.inverse; - otherContainerType = get(otherType, 'relationshipsByName').get(key).kind; - } - else if(assoc = DS._inverseRelationshipFor(otherType, recordType)){ - key = assoc.name; - otherContainerType = assoc.kind; - } - if(!key){ - return knownContainerType === "belongsTo" ? "oneToNone" : "manyToNone"; - } - else{ - if(otherContainerType === "belongsTo"){ - return knownContainerType === "belongsTo" ? "oneToOne" : "manyToOne"; - } - else{ - return knownContainerType === "belongsTo" ? "oneToMany" : "manyToMany"; + inverseName = possibleRelationships[0].name; + inverseKind = possibleRelationships[0].kind; } - } - -}; -DS.RelationshipChange.createChange = function(firstRecordReference, secondRecordReference, store, options){ - // Get the type of the child based on the child's client ID - var firstRecordType = firstRecordReference.type, key, changeType; - changeType = DS.RelationshipChange.determineRelationshipType(firstRecordType, options); - if (changeType === "oneToMany"){ - return DS.OneToManyChange.createChange(firstRecordReference, secondRecordReference, store, options); - } - else if (changeType === "manyToOne"){ - return DS.OneToManyChange.createChange(secondRecordReference, firstRecordReference, store, options); - } - else if (changeType === "oneToNone"){ - return DS.OneToNoneChange.createChange(firstRecordReference, secondRecordReference, store, options); - } - else if (changeType === "manyToNone"){ - return DS.ManyToNoneChange.createChange(firstRecordReference, secondRecordReference, store, options); - } - else if (changeType === "oneToOne"){ - return DS.OneToOneChange.createChange(firstRecordReference, secondRecordReference, store, options); - } - else if (changeType === "manyToMany"){ - return DS.ManyToManyChange.createChange(firstRecordReference, secondRecordReference, store, options); - } -}; + function findPossibleInverses(type, inverseType, possibleRelationships) { + possibleRelationships = possibleRelationships || []; -/** @private */ -DS.OneToNoneChange.createChange = function(childReference, parentReference, store, options) { - var key = options.key; - var change = DS.RelationshipChange._createChange({ - parentReference: parentReference, - childReference: childReference, - firstRecordReference: childReference, - store: store, - changeType: options.changeType, - firstRecordName: key, - firstRecordKind: "belongsTo" - }); + var relationshipMap = get(inverseType, 'relationships'); + if (!relationshipMap) { return; } - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + var relationships = relationshipMap.get(type); + if (relationships) { + possibleRelationships.push.apply(possibleRelationships, relationshipMap.get(type)); + } - return change; -}; + if (type.superclass) { + findPossibleInverses(type.superclass, inverseType, possibleRelationships); + } -/** @private */ -DS.ManyToNoneChange.createChange = function(childReference, parentReference, store, options) { - var key = options.key; - var change = DS.RelationshipChange._createChange({ - parentReference: childReference, - childReference: parentReference, - secondRecordReference: childReference, - store: store, - changeType: options.changeType, - secondRecordName: options.key, - secondRecordKind: "hasMany" - }); + return possibleRelationships; + } - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); - return change; -}; + 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. -/** @private */ -DS.ManyToManyChange.createChange = function(childReference, parentReference, store, options) { - // Get the type of the child based on the child's client ID - var childType = childReference.type, key; + For example, given the following model definition: - // 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. - key = options.key; + App.Blog = DS.Model.extend({ + users: DS.hasMany(App.User), + owner: DS.belongsTo(App.User), + posts: DS.hasMany(App.Post) + }); - var change = DS.RelationshipChange._createChange({ - parentReference: parentReference, - childReference: childReference, - firstRecordReference: childReference, - secondRecordReference: parentReference, - firstRecordKind: "hasMany", - secondRecordKind: "hasMany", - store: store, - changeType: options.changeType, - firstRecordName: key - }); + This computed property would return a map describing these + relationships, like this: - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + 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' } ] + @type Ember.Map + @readOnly + */ + relationships: Ember.computed(function() { + var map = new Ember.MapWithDefault({ + defaultValue: function() { return []; } + }); - return change; -}; + // Loop through each computed property on the class + this.eachComputedProperty(function(name, meta) { -/** @private */ -DS.OneToOneChange.createChange = function(childReference, parentReference, store, options) { - // Get the type of the child based on the child's client ID - var childType = childReference.type, key; + // 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); + } - // 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 = inverseBelongsToName(options.parentType, childType, options.key); - //DS.OneToOneChange.maintainInvariant( options, store, childReference, 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 relationshipsForType = map.get(meta.type); - var change = DS.RelationshipChange._createChange({ - parentReference: parentReference, - childReference: childReference, - firstRecordReference: childReference, - secondRecordReference: parentReference, - firstRecordKind: "belongsTo", - secondRecordKind: "belongsTo", - store: store, - changeType: options.changeType, - firstRecordName: key - }); + relationshipsForType.push({ name: name, kind: meta.kind }); + } + }); - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + return map; + }), + /** + A hash containing lists of the model's relationships, grouped + by the relationship kind. For example, given a model with this + definition: - return change; -}; + App.Blog = DS.Model.extend({ + users: DS.hasMany(App.User), + owner: DS.belongsTo(App.User), -DS.OneToOneChange.maintainInvariant = function(options, store, childReference, key){ - if (options.changeType === "add" && store.recordIsMaterialized(childReference)) { - var child = store.recordForReference(childReference); - var oldParent = get(child, key); - if (oldParent){ - var correspondingChange = DS.OneToOneChange.createChange(childReference, oldParent.get('_reference'), store, { - parentType: options.parentType, - hasManyName: options.hasManyName, - changeType: "remove", - key: options.key + posts: DS.hasMany(App.Post) }); - store.addRelationshipChangeFor(childReference, key, options.parentReference , null, correspondingChange); - correspondingChange.sync(); - } - } -}; -/** @private */ -DS.OneToManyChange.createChange = function(childReference, parentReference, store, options) { - // Get the type of the child based on the child's client ID - var childType = childReference.type, key; + This property would contain the following: - // 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 = inverseBelongsToName(options.parentType, childType, options.key); - DS.OneToManyChange.maintainInvariant( options, store, childReference, 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 relationshipNames = Ember.get(App.Blog, 'relationshipNames'); + relationshipNames.hasMany; + //=> ['users', 'posts'] + relationshipNames.belongsTo; + //=> ['owner'] - var change = DS.RelationshipChange._createChange({ - parentReference: parentReference, - childReference: childReference, - firstRecordReference: childReference, - secondRecordReference: parentReference, - firstRecordKind: "belongsTo", - secondRecordKind: "hasMany", - store: store, - changeType: options.changeType, - firstRecordName: key - }); + @type Object + @readOnly + */ + relationshipNames: Ember.computed(function() { + var names = { hasMany: [], belongsTo: [] }; - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + this.eachComputedProperty(function(name, meta) { + if (meta.isRelationship) { + names[meta.kind].push(name); + } + }); + return names; + }), - return change; -}; + /** + 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: -DS.OneToManyChange.maintainInvariant = function(options, store, childReference, key){ - if (options.changeType === "add" && store.recordIsMaterialized(childReference)) { - var child = store.recordForReference(childReference); - var oldParent = get(child, key); - if (oldParent){ - var correspondingChange = DS.OneToManyChange.createChange(childReference, oldParent.get('_reference'), store, { - parentType: options.parentType, - hasManyName: options.hasManyName, - changeType: "remove", - key: options.key + App.Blog = DS.Model.extend({ + users: DS.hasMany(App.User), + owner: DS.belongsTo(App.User), + posts: DS.hasMany(App.Post) }); - store.addRelationshipChangeFor(childReference, key, options.parentReference , null, correspondingChange); - correspondingChange.sync(); - } - } -}; -DS.OneToManyChange.ensureSameTransaction = function(changes, store){ - var records = Ember.A(); - forEach(changes, function(change){ - records.addObject(change.getSecondRecord()); - records.addObject(change.getFirstRecord()); - }); - var transaction = store.ensureSameTransaction(records); - forEach(changes, function(change){ - change.transaction = transaction; - }); -}; + This property would contain the following: -DS.RelationshipChange.prototype = { + var relatedTypes = Ember.get(App.Blog, 'relatedTypes'); + //=> [ App.User, App.Post ] - getSecondRecordName: function() { - var name = this.secondRecordName, store = this.store, parent; + @type Ember.Array + @readOnly + */ + relatedTypes: Ember.computed(function() { + var type, + types = Ember.A([]); - if (!name) { - parent = this.secondRecordReference; - if (!parent) { return; } + // 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; - var childType = this.firstRecordReference.type; - var inverseType = DS._inverseTypeFor(childType, this.firstRecordName); - name = inverseHasManyName(inverseType, childType, this.firstRecordName); - this.secondRecordName = name; - } + if (typeof type === 'string') { + type = get(this, type, false) || get(Ember.lookup, type); + } - return name; - }, + 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; + }), /** - Get the name of the relationship on the belongsTo side. + A map whose keys are the relationships of a model and whose values are + relationship descriptors. - @returns {String} - */ - getFirstRecordName: function() { - var name = this.firstRecordName, store = this.store, parent, child; + For example, given a model with this + definition: - if (!name) { - parent = this.secondRecordReference; - child = this.firstRecordReference; - if (!(child && parent)) { return; } + App.Blog = DS.Model.extend({ + users: DS.hasMany(App.User), + owner: DS.belongsTo(App.User), - name = DS._inverseRelationshipFor(child.type, parent.type).name; + posts: DS.hasMany(App.Post) + }); - this.firstRecordName = name; - } + This property would contain the following: - return name; - }, + 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 } - /** @private */ - destroy: function() { - var childReference = this.childReference, - belongsToName = this.getFirstRecordName(), - hasManyName = this.getSecondRecordName(), - store = this.store, - child, oldParent, newParent, lastParent, transaction; + @type Ember.Map + @readOnly + */ + relationshipsByName: Ember.computed(function() { + var map = Ember.Map.create(), type; - store.removeRelationshipChangeFor(childReference, belongsToName, this.parentReference, hasManyName, this.changeType); + this.eachComputedProperty(function(name, meta) { + if (meta.isRelationship) { + meta.key = name; + type = meta.type; - if (transaction = this.transaction) { - transaction.relationshipBecameClean(this); - } - }, + if (typeof type === 'string') { + type = get(this, type, false) || get(Ember.lookup, type); + meta.type = type; + } - /** @private */ - getByReference: function(reference) { - var store = this.store; + map.set(name, meta); + } + }); - // return null or undefined if the original reference was null or undefined - if (!reference) { return reference; } + return map; + }), - if (store.recordIsMaterialized(reference)) { - return store.recordForReference(reference); - } - }, + /** + 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. - getSecondRecord: function(){ - return this.getByReference(this.secondRecordReference); - }, + For example: - /** @private */ - getFirstRecord: function() { - return this.getByReference(this.firstRecordReference); - }, + App.Blog = DS.Model.extend({ + users: DS.hasMany(App.User), + owner: DS.belongsTo(App.User), - /** - @private + posts: DS.hasMany(App.Post), - Make sure that all three parts of the relationship change are part of - the same transaction. If any of the three records is clean and in the - default transaction, and the rest are in a different transaction, move - them all into that transaction. - */ - ensureSameTransaction: function() { - var child = this.getFirstRecord(), - parentRecord = this.getSecondRecord(); + title: DS.attr('string') + }); - var transaction = this.store.ensureSameTransaction([child, parentRecord]); + var fields = Ember.get(App.Blog, 'fields'); + fields.forEach(function(field, kind) { + console.log(field, kind); + }); - this.transaction = transaction; - return transaction; - }, + // prints: + // users, hasMany + // owner, belongsTo + // posts, hasMany + // title, attribute - callChangeEvents: function(){ - var hasManyName = this.getSecondRecordName(), - belongsToName = this.getFirstRecordName(), - child = this.getFirstRecord(), - parentRecord = this.getSecondRecord(); + @type Ember.Map + @readOnly + */ + fields: Ember.computed(function() { + var map = Ember.Map.create(); - var dirtySet = new Ember.OrderedSet(); + this.eachComputedProperty(function(name, meta) { + if (meta.isRelationship) { + map.set(name, meta.kind); + } else if (meta.isAttribute) { + map.set(name, 'attribute'); + } + }); - // TODO: This implementation causes a race condition in key-value - // stores. The fix involves buffering changes that happen while - // a record is loading. A similar fix is required for other parts - // of ember-data, and should be done as new infrastructure, not - // a one-off hack. [tomhuda] - if (parentRecord && get(parentRecord, 'isLoaded')) { - this.store.recordHasManyDidChange(dirtySet, parentRecord, this); - } + return map; + }), - if (child) { - this.store.recordBelongsToDidChange(dirtySet, child, this); - } + /** + 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. - dirtySet.forEach(function(record) { - record.adapterDidDirty(); + @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); }); }, - coalesce: function(){ - var relationshipPairs = this.store.relationshipChangePairsFor(this.firstRecordReference); - forEach(relationshipPairs, function(pair){ - var addedChange = pair["add"]; - var removedChange = pair["remove"]; - if(addedChange && removedChange) { - addedChange.destroy(); - removedChange.destroy(); - } + /** + 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. + + @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.RelationshipChangeAdd.prototype = Ember.create(DS.RelationshipChange.create({})); -DS.RelationshipChangeRemove.prototype = Ember.create(DS.RelationshipChange.create({})); +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. -DS.RelationshipChangeAdd.prototype.changeType = "add"; -DS.RelationshipChangeAdd.prototype.sync = function() { - var secondRecordName = this.getSecondRecordName(), - firstRecordName = this.getFirstRecordName(), - firstRecord = this.getFirstRecord(), - secondRecord = this.getSecondRecord(); + @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); + } +}); - //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); +})(); - var transaction = this.ensureSameTransaction(); - transaction.relationshipBecameDirty(this); - this.callChangeEvents(); - if (secondRecord && firstRecord) { - if(this.secondRecordKind === "belongsTo"){ - secondRecord.suspendRelationshipObservers(function(){ - set(secondRecord, secondRecordName, firstRecord); - }); +(function() { +/** + @module data + @submodule data-relationships +*/ - } - else if(this.secondRecordKind === "hasMany"){ - secondRecord.suspendRelationshipObservers(function(){ - get(secondRecord, secondRecordName).addObject(firstRecord); - }); - } - } +})(); - if (firstRecord && secondRecord && get(firstRecord, firstRecordName) !== secondRecord) { - if(this.firstRecordKind === "belongsTo"){ - firstRecord.suspendRelationshipObservers(function(){ - set(firstRecord, firstRecordName, secondRecord); - }); - } - else if(this.firstdRecordKind === "hasMany"){ - firstRecord.suspendRelationshipObservers(function(){ - get(firstRecord, firstRecordName).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(); +(function() { +var get = Ember.get, set = Ember.set; +var once = Ember.run.once; +var forEach = Ember.EnumerableUtils.forEach; - //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); +DS.RecordArrayManager = Ember.Object.extend({ + init: function() { + this.filteredRecordArrays = Ember.MapWithDefault.create({ + defaultValue: function() { return []; } + }); - var transaction = this.ensureSameTransaction(firstRecord, secondRecord, secondRecordName, firstRecordName); - transaction.relationshipBecameDirty(this); + this.changedReferences = []; + }, - this.callChangeEvents(); + referenceDidChange: function(reference) { + this.changedReferences.push(reference); + once(this, this.updateRecordArrays); + }, - if (secondRecord && firstRecord) { - if(this.secondRecordKind === "belongsTo"){ - secondRecord.suspendRelationshipObservers(function(){ - set(secondRecord, secondRecordName, null); - }); - } - else if(this.secondRecordKind === "hasMany"){ - secondRecord.suspendRelationshipObservers(function(){ - get(secondRecord, secondRecordName).removeObject(firstRecord); - }); - } - } + recordArraysForReference: function(reference) { + reference.recordArrays = reference.recordArrays || Ember.OrderedSet.create(); + return reference.recordArrays; + }, - if (firstRecord && get(firstRecord, firstRecordName)) { - if(this.firstRecordKind === "belongsTo"){ - firstRecord.suspendRelationshipObservers(function(){ - set(firstRecord, firstRecordName, null); - }); - } - else if(this.firstdRecordKind === "hasMany"){ - firstRecord.suspendRelationshipObservers(function(){ - get(firstRecord, firstRecordName).removeObject(secondRecord); - }); - } - } + /** + @private - this.coalesce(); -}; + 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. -function inverseBelongsToName(parentType, childType, hasManyName) { - // Get the options passed to the parent's DS.hasMany() - var options = parentType.metaForProperty(hasManyName).options; - var belongsToName; + It updates all filters that a record belongs to. - if (belongsToName = options.inverse) { - return belongsToName; - } + To avoid thrashing, it only runs once per run loop per record. - return DS._inverseRelationshipFor(childType, parentType).name; -} + @param {Class} type + @param {Number|String} clientId + */ + updateRecordArrays: function() { + forEach(this.changedReferences, function(reference) { + var type = reference.type, + recordArrays = this.filteredRecordArrays.get(type), + filter; + + forEach(recordArrays, function(array) { + filter = get(array, 'filterFunction'); + this.updateRecordArray(array, filter, type, reference); + }, this); -function inverseHasManyName(parentType, childType, belongsToName) { - var options = childType.metaForProperty(belongsToName).options; - var hasManyName; + // loop through all manyArrays containing an unloaded copy of this + // clientId and notify them that the record was loaded. + var manyArrays = reference.loadingRecordArrays; - if (hasManyName = options.inverse) { - return hasManyName; - } + if (manyArrays) { + for (var i=0, l=manyArrays.length; i types) before sideloading. - // We can't do this conversion immediately here, because `configure` - // may be called before certain types have been defined. - this.sideloadMapping.normalized = false; - + sideloadMapping = this.aliases.sideloadMapping || Ember.Map.create(); + sideloadMapping.set(sideloadAs, type); + this.aliases.sideloadMapping = sideloadMapping; delete configuration.sideloadAs; } @@ -6432,18 +6958,6 @@ DS.JSONSerializer = DS.Serializer.extend({ hash[key] = value; }, - /** - @private - - Creates an empty hash that will be filled in by the hooks called from the - `serialize()` method. - - @return {Object} - */ - createSerializedForm: function() { - return {}; - }, - extractAttribute: function(type, hash, attributeName) { var key = this._keyForAttributeName(type, attributeName); return hash[key]; @@ -6463,6 +6977,10 @@ DS.JSONSerializer = DS.Serializer.extend({ } }, + extractEmbeddedData: function(hash, key) { + return hash[key]; + }, + extractHasMany: function(type, hash, key) { return hash[key]; }, @@ -6471,24 +6989,53 @@ DS.JSONSerializer = DS.Serializer.extend({ return hash[key]; }, + extractBelongsToPolymorphic: function(type, hash, key) { + var keyForId = this.keyForPolymorphicId(key), + keyForType, + id = hash[keyForId]; + + if (id) { + keyForType = this.keyForPolymorphicType(key); + return {id: id, type: hash[keyForType]}; + } + + return null; + }, + addBelongsTo: function(hash, record, key, relationship) { var type = record.constructor, name = relationship.key, value = null, - embeddedChild; + includeType = (relationship.options && relationship.options.polymorphic), + embeddedChild, + child, + id; if (this.embeddedType(type, name)) { if (embeddedChild = get(record, name)) { - value = this.serialize(embeddedChild, { includeId: true }); + value = this.serialize(embeddedChild, { includeId: true, includeType: includeType }); } hash[key] = value; } else { - var id = get(record, relationship.key+'.id'); - if (!Ember.isNone(id)) { hash[key] = id; } + child = get(record, relationship.key); + id = get(child, 'id'); + + if (relationship.options && relationship.options.polymorphic && !Ember.isNone(id)) { + this.addBelongsToPolymorphic(hash, key, id, child.constructor); + } else { + hash[key] = id === undefined ? null : this.serializeId(id); + } } }, + addBelongsToPolymorphic: function(hash, key, id, type) { + var keyForId = this.keyForPolymorphicId(key), + keyForType = this.keyForPolymorphicType(key); + hash[keyForId] = id; + hash[keyForType] = this.rootForType(type); + }, + /** Adds a has-many relationship to the JSON hash being built. @@ -6504,10 +7051,12 @@ DS.JSONSerializer = DS.Serializer.extend({ should be saved @param {Object} relationship metadata about the relationship being serialized */ + addHasMany: function(hash, record, key, relationship) { var type = record.constructor, name = relationship.key, serializedHasMany = [], + includeType = (relationship.options && relationship.options.polymorphic), manyArray, embeddedType; // If the has-many is not embedded, there is nothing to do. @@ -6519,7 +7068,7 @@ DS.JSONSerializer = DS.Serializer.extend({ // Build up the array of serialized records manyArray.forEach(function (record) { - serializedHasMany.push(this.serialize(record, { includeId: true })); + serializedHasMany.push(this.serialize(record, { includeId: true, includeType: includeType })); }, this); // Set the appropriate property of the serialized JSON to the @@ -6527,6 +7076,11 @@ DS.JSONSerializer = DS.Serializer.extend({ hash[key] = serializedHasMany; }, + addType: function(hash, type) { + var keyForType = this.keyForEmbeddedType(); + hash[keyForType] = this.rootForType(type); + }, + // EXTRACTION extract: function(loader, json, type, record) { @@ -6538,6 +7092,8 @@ DS.JSONSerializer = DS.Serializer.extend({ if (json[root]) { if (record) { loader.updateId(record, json[root]); } this.extractRecordRepresentation(loader, type, json[root]); + } else { + Ember.Logger.warn("Extract requested, but no data given for " + type + ". This may cause weird problems."); } }, @@ -6563,12 +7119,32 @@ DS.JSONSerializer = DS.Serializer.extend({ }, extractMeta: function(loader, type, json) { - var meta = json[this.configOption(type, 'meta')], since; - if (!meta) { return; } + var meta = this.configOption(type, 'meta'), + data = json, value; - if (since = meta[this.configOption(type, 'since')]) { - loader.sinceForType(type, since); + if(meta && json[meta]){ + data = json[meta]; } + + this.metadataMapping.forEach(function(property, key){ + value = data[property]; + if(!Ember.isNone(value)){ + loader.metaForType(type, key, value); + } + }); + }, + + extractEmbeddedType: function(relationship, data) { + var foundType = relationship.type; + if(relationship.options && relationship.options.polymorphic) { + var key = this.keyFor(relationship), + keyForEmbeddedType = this.keyForEmbeddedType(key); + + foundType = this.typeFromAlias(data[keyForEmbeddedType]); + delete data[keyForEmbeddedType]; + } + + return foundType; }, /** @@ -6590,45 +7166,22 @@ DS.JSONSerializer = DS.Serializer.extend({ sideload: function(loader, type, json, root) { var sideloadedType; - this.normalizeSideloadMappings(); this.configureSideloadMappingForType(type); for (var prop in json) { if (!json.hasOwnProperty(prop) || prop === root || - prop === this.configOption(type, 'meta')) { + !!this.metadataMapping.get(prop)) { continue; } - sideloadedType = this.sideloadMapping.get(prop); - Ember.assert("Your server returned a hash with the key " + prop + - " but you have no mapping for it", - !!sideloadedType); + sideloadedType = this.typeFromAlias(prop); + Ember.assert("Your server returned a hash with the key " + prop + " but you have no mapping for it", !!sideloadedType); this.loadValue(loader, sideloadedType, json[prop]); } }, - /** - @private - - Iterates over all the `sideloadAs` mappings and converts any that are - strings to their equivalent types. - - This is an optimization used to avoid performing lookups for every - call to `sideload`. - */ - normalizeSideloadMappings: function() { - if (! this.sideloadMapping.normalized) { - this.sideloadMapping.forEach(function(key, value) { - if (typeof value === 'string') { - this.sideloadMapping.set(key, get(Ember.lookup, value)); - } - }, this); - this.sideloadMapping.normalized = true; - } - }, - /** @private @@ -6645,11 +7198,9 @@ DS.JSONSerializer = DS.Serializer.extend({ type.eachRelatedType(function(relatedType) { if (!configured.contains(relatedType)) { - var root = this.sideloadMappingForType(relatedType); - if (!root) { - root = this.defaultSideloadRootForType(relatedType); - this.sideloadMapping.set(root, relatedType); - } + var root = this.defaultSideloadRootForType(relatedType); + this.aliases.set(root, relatedType); + this.configureSideloadMappingForType(relatedType, configured); } }, this); @@ -6665,33 +7216,44 @@ DS.JSONSerializer = DS.Serializer.extend({ } }, - // HELPERS + /** + A hook you can use in your serializer subclass to customize + how a polymorphic association's name is converted into a key for the id. - // define a plurals hash in your subclass to define - // special-case pluralization - pluralize: function(name) { - var plurals = this.configurations.get('plurals'); - return (plurals && plurals[name]) || name + "s"; + @param {String} name the association name to convert into a key + + @return {String} the key + */ + keyForPolymorphicId: function(key){ + return key; }, - // use the same plurals hash to determine - // special-case singularization - singularize: function(name) { - var plurals = this.configurations.get('plurals'); - if (plurals) { - for (var i in plurals) { - if (plurals[i] === name) { - return i; - } - } - } - if (name.lastIndexOf('s') === name.length - 1) { - return name.substring(0, name.length - 1); - } else { - return name; - } + /** + A hook you can use in your serializer subclass to customize + how a polymorphic association's name is converted into a key for the type. + + @param {String} name the association name to convert into a key + + @return {String} the key + */ + keyForPolymorphicType: function(key){ + return this.keyForPolymorphicId(key) + '_type'; + }, + + /** + A hook you can use in your serializer subclass to customize + the key used to store the type of a record of an embedded polymorphic association. + + By default, this method return 'type'. + + @return {String} the key + */ + keyForEmbeddedType: function() { + return 'type'; }, + // HELPERS + /** @private @@ -6702,7 +7264,7 @@ DS.JSONSerializer = DS.Serializer.extend({ `user_group`. @param {DS.Model subclass} type - @returns {String} name of the root element + @return {String} name of the root element */ rootForType: function(type) { var typeString = type.toString(); @@ -6715,29 +7277,13 @@ DS.JSONSerializer = DS.Serializer.extend({ return name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1); }, - /** - @private - - Determines the root name mapped to a particular sideloaded type. - - @param {DS.Model subclass} type - @returns {String} name of the root element, if any is registered - */ - sideloadMappingForType: function(type) { - this.sideloadMapping.forEach(function(key, value) { - if (type === value) { - return key; - } - }); - }, - /** @private The default root name for a particular sideloaded type. @param {DS.Model subclass} type - @returns {String} name of the root element + @return {String} name of the root element */ defaultSideloadRootForType: function(type) { return this.pluralize(this.rootForType(type)); @@ -6749,6 +7295,13 @@ DS.JSONSerializer = DS.Serializer.extend({ (function() { +/** + @module data + @submodule data-adapter +*/ + +var get = Ember.get, set = Ember.set, merge = Ember.merge; + function loaderFor(store) { return { load: function(type, data, prematerialized) { @@ -6766,7 +7319,7 @@ function loaderFor(store) { populateArray: Ember.K, sideload: function(type, data) { - return store.load(type, data); + return store.adapterForType(type).load(store, type, data); }, sideloadMany: function(type, array) { @@ -6774,11 +7327,11 @@ function loaderFor(store) { }, prematerialize: function(reference, prematerialized) { - store.prematerialize(reference, prematerialized); + reference.prematerialized = prematerialized; }, - sinceForType: function(type, since) { - store.sinceForType(type, since); + metaForType: function(type, property, data) { + store.metaForType(type, property, data); } }; } @@ -6802,7 +7355,6 @@ DS.loaderFor = loaderFor; To tell your store which adapter to use, set its `adapter` property: App.store = DS.Store.create({ - revision: 3, adapter: App.MyAdapter.create() }); @@ -6815,17 +7367,22 @@ DS.loaderFor = loaderFor; * `updateRecord()` * `deleteRecord()` - To improve the network performance of your application, you can optimize - your adapter by overriding these lower-level methods: + To improve the network performance of your application, you can optimize + your adapter by overriding these lower-level methods: * `findMany()` * `createRecords()` * `updateRecords()` * `deleteRecords()` * `commit()` -*/ -var get = Ember.get, set = Ember.set, merge = Ember.merge; + For an example implementation, see {{#crossLink "DS.RestAdapter"}} the + included REST adapter.{{/crossLink}}. + + @class Adapter + @namespace DS + @extends Ember.Object +*/ DS.Adapter = Ember.Object.extend(DS._Mappable, { @@ -6862,28 +7419,27 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { For example: - ```javascript - adapter.load(store, App.Person, { - id: 123, - firstName: "Yehuda", - lastName: "Katz", - occupations: [{ - id: 345, - title: "Tricycle Mechanic" - }] - }); - ``` + adapter.load(store, App.Person, { + id: 123, + firstName: "Yehuda", + lastName: "Katz", + occupations: [{ + id: 345, + title: "Tricycle Mechanic" + }] + }); This will load the payload for the `App.Person` with ID `123` and the embedded `App.Occupation` with ID `345`. + @method load @param {DS.Store} store @param {subclass of DS.Model} type @param {any} payload */ load: function(store, type, payload) { var loader = loaderFor(store); - get(this, 'serializer').extractRecordRepresentation(loader, type, payload); + return get(this, 'serializer').extractRecordRepresentation(loader, type, payload); }, /** @@ -6900,12 +7456,13 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { For example, the `RESTAdapter` saves newly created records by making an Ajax request. When the server returns, the adapter calls didCreateRecord. If the server returns a response body, - it is passed as the payload. + it is passed as the payload. + @method didCreateRecord @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} record - @param {any} payload + @param {any} payload */ didCreateRecord: function(store, type, record, payload) { store.didSaveRecord(record); @@ -6926,17 +7483,18 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { Acknowledges that the adapter has finished creating several records. Your adapter should call this method from `createRecords` when it - has saved multiple created records to its persistent storage + has saved multiple created records to its persistent storage received an acknowledgement. If the persistent storage returns a new payload in response to the creation, and you want to update the existing record with the new information, pass the payload as the fourth parameter. + @method didCreateRecords @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} record - @param {any} payload + @param {any} payload */ didCreateRecords: function(store, type, records, payload) { records.forEach(function(record) { @@ -6962,16 +7520,16 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { update or delete, and you want to update the existing record with the new information, pass the payload as the fourth parameter. + @method didSaveRecord @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} record - @param {any} payload + @param {any} payload */ didSaveRecord: function(store, type, record, payload) { store.didSaveRecord(record); - var serializer = get(this, 'serializer'), - mappings = serializer.mappingForType(type); + var serializer = get(this, 'serializer'); serializer.eachEmbeddedRecord(record, function(embeddedRecord, embeddedType) { if (embeddedType === 'load') { return; } @@ -6995,10 +7553,11 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { If the persistent storage returns a new payload in response to the update, pass the payload as the fourth parameter. + @method didUpdateRecord @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} record - @param {any} payload + @param {any} payload */ didUpdateRecord: function() { this.didSaveRecord.apply(this, arguments); @@ -7014,17 +7573,18 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { If the persistent storage returns a new payload in response to the deletion, pass the payload as the fourth parameter. + @method didDeleteRecord @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} record - @param {any} payload + @param {any} payload */ didDeleteRecord: function() { this.didSaveRecord.apply(this, arguments); }, /** - Acknowledges that the adapter has finished updating or deleting + Acknowledges that the adapter has finished updating or deleting multiple records. Your adapter should call this method from its `updateRecords` or @@ -7034,10 +7594,11 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { If the persistent storage returns a new payload in response to the creation, pass the payload as the fourth parameter. + @method didSaveRecords @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} records - @param {any} payload + @param {any} payload */ didSaveRecords: function(store, type, records, payload) { records.forEach(function(record) { @@ -7060,10 +7621,11 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { If the persistent storage returns a new payload in response to the update, pass the payload as the fourth parameter. + @method didUpdateRecords @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} records - @param {any} payload + @param {any} payload */ didUpdateRecords: function() { this.didSaveRecords.apply(this, arguments); @@ -7079,10 +7641,11 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { If the persistent storage returns a new payload in response to the deletion, pass the payload as the fourth parameter. + @method didDeleteRecords @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} records - @param {any} payload + @param {any} payload */ didDeleteRecords: function() { this.didSaveRecords.apply(this, arguments); @@ -7098,9 +7661,10 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { to your find method so that the store knows which record to associate the new data with. + @method didFindRecord @param {DS.Store} store @param {subclass of DS.Model} type - @param {any} payload + @param {any} payload @param {String} id */ didFindRecord: function(store, type, payload, id) { @@ -7122,9 +7686,10 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { You adapter should call this method from its `findAll` method with the response from the backend. + @method didFindAll @param {DS.Store} store @param {subclass of DS.Model} type - @param {any} payload + @param {any} payload */ didFindAll: function(store, type, payload) { var loader = DS.loaderFor(store), @@ -7141,9 +7706,10 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { Your adapter should call this method from its `findQuery` method with the response from the backend. + @method didFindQuery @param {DS.Store} store @param {subclass of DS.Model} type - @param {any} payload + @param {any} payload @param {DS.AdapterPopulatedRecordArray} recordArray */ didFindQuery: function(store, type, payload, recordArray) { @@ -7162,9 +7728,10 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { You adapter should call this method from its `findMany` method with the response from the backend. + @method didFindMany @param {DS.Store} store @param {subclass of DS.Model} type - @param {any} payload + @param {any} payload */ didFindMany: function(store, type, payload) { var loader = DS.loaderFor(store); @@ -7179,6 +7746,7 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { Your adapter should call this method to indicate that the backend returned an error for a request. + @method didError @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} record @@ -7217,6 +7785,7 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { Once it registers a transform for a given type, it ignores subsequent transforms for the same attribute type. + @method registerSerializerTransforms @param {Class} klass the DS.Adapter subclass to extract the transforms from @param {DS.Serializer} serializer the serializer to register @@ -7225,6 +7794,7 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { */ registerSerializerTransforms: function(klass, serializer, seen) { var transforms = klass._registeredTransforms, superclass, prop; + var enumTransforms = klass._registeredEnumTransforms; for (prop in transforms) { if (!transforms.hasOwnProperty(prop) || prop in seen) { continue; } @@ -7233,6 +7803,13 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { serializer.registerTransform(prop, transforms[prop]); } + for (prop in enumTransforms) { + if (!enumTransforms.hasOwnProperty(prop) || prop in seen) { continue; } + seen[prop] = true; + + serializer.registerEnumTransform(prop, enumTransforms[prop]); + } + if (superclass = klass.superclass) { this.registerSerializerTransforms(superclass, serializer, seen); } @@ -7245,6 +7822,7 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { registers any class-registered mappings on the adapter's serializer. + @method registerSerializerMappings @param {Class} klass the DS.Adapter subclass to extract the transforms from @param {DS.Serializer} serializer the serializer to register the @@ -7267,17 +7845,19 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { Here is an example `find` implementation: - find: function(store, type, id) { - var url = type.url; - url = url.fmt(id); + find: function(store, type, id) { + var url = type.url; + url = url.fmt(id); - jQuery.getJSON(url, function(data) { - // data is a hash of key/value pairs. If your server returns a - // root, simply do something like: - // store.load(type, id, data.person) - store.load(type, id, data); - }); - } + jQuery.getJSON(url, function(data) { + // data is a hash of key/value pairs. If your server returns a + // root, simply do something like: + // store.load(type, id, data.person) + store.load(type, id, data); + }); + } + + @method find */ find: null, @@ -7288,7 +7868,7 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { }, /** - A public method that allows you to register an enumerated + A public method that allows you to register an enumerated type on your adapter. This is useful if you want to utilize a text representation of an integer value. @@ -7296,21 +7876,23 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { in your app, but you want to persist those as 0,1,2 in your backend. You would first register the transform on your adapter instance: - adapter.registerEnumTransform('priority', ['low', 'medium', 'high']); + adapter.registerEnumTransform('priority', ['low', 'medium', 'high']); You would then refer to the 'priority' DS.attr in your model: - App.Task = DS.Model.extend({ - priority: DS.attr('priority') - }); + + App.Task = DS.Model.extend({ + priority: DS.attr('priority') + }); And lastly, you would set/get the text representation on your model instance, but the transformed result will be the index number of the type. - App: myTask.get('priority') => 'low' - Server Response / Load: { myTask: {priority: 0} } + App: myTask.get('priority') => 'low' + Server Response / Load: { myTask: {priority: 0} } + @method registerEnumTransform @param {String} type of the transform - @param {Array} array of String objects to use for the enumerated values. + @param {Array} array of String objects to use for the enumerated values. This is an ordered list and the index values will be used for the transform. */ registerEnumTransform: function(attributeType, objects) { @@ -7335,6 +7917,10 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { var uuid = App.generateUUIDWithStatisticallyLowOddsOfCollision(); return uuid; } + + @method generateIdForRecord + @param {DS.Store} store + @param {DS.Model} record */ generateIdForRecord: null, @@ -7430,6 +8016,14 @@ DS.Adapter.reopenClass({ this._registeredTransforms = registeredTransforms; }, + registerEnumTransform: function(attributeType, objects) { + var registeredEnumTransforms = this._registeredEnumTransforms || {}; + + registeredEnumTransforms[attributeType] = objects; + + this._registeredEnumTransforms = registeredEnumTransforms; + }, + map: DS._Mappable.generateMapFunctionFor('attributes', function(key, newValue, map) { var existingValue = map.get(key); @@ -7450,7 +8044,7 @@ DS.Adapter.reopenClass({ merge(existingValue, newValue); }), - resolveMapConflict: function(oldValue, newValue, mappingsKey) { + resolveMapConflict: function(oldValue, newValue) { merge(newValue, oldValue); return newValue; @@ -7462,6 +8056,18 @@ DS.Adapter.reopenClass({ (function() { +/** + @module data + @submodule data-serializers +*/ + +/** + @class FixtureSerializer + @constructor + @namespace DS + @extends DS.Serializer +*/ + var get = Ember.get, set = Ember.set; DS.FixtureSerializer = DS.Serializer.extend({ @@ -7489,21 +8095,9 @@ DS.FixtureSerializer = DS.Serializer.extend({ addHasMany: function(hash, record, key, relationship) { var ids = get(record, relationship.key).map(function(item) { return item.get('id'); - }); - - hash[relationship.key] = ids; - }, - - /** - @private - - Creates an empty hash that will be filled in by the hooks called from the - `serialize()` method. - - @return {Object} - */ - createSerializedForm: function() { - return {}; + }); + + hash[relationship.key] = ids; }, extract: function(loader, fixture, type, record) { @@ -7548,7 +8142,32 @@ DS.FixtureSerializer = DS.Serializer.extend({ }, extractBelongsTo: function(type, hash, key) { - return hash[key]; + var val = hash[key]; + if (val != null) { + val = val + ''; + } + return val; + }, + + extractBelongsToPolymorphic: function(type, hash, key) { + var keyForId = this.keyForPolymorphicId(key), + keyForType, + id = hash[keyForId]; + + if (id) { + keyForType = this.keyForPolymorphicType(key); + return {id: id, type: hash[keyForType]}; + } + + return null; + }, + + keyForPolymorphicId: function(key) { + return key; + }, + + keyForPolymorphicType: function(key) { + return key + '_type'; } }); @@ -7557,6 +8176,11 @@ DS.FixtureSerializer = DS.Serializer.extend({ (function() { +/** + @module data + @submodule data-adapters +*/ + var get = Ember.get, fmt = Ember.String.fmt, dump = Ember.get(window, 'JSON.stringify') || function(object) { return object.toString(); }; @@ -7569,6 +8193,10 @@ var get = Ember.get, fmt = Ember.String.fmt, system would do. Its possible to do develop your entire application with `DS.FixtureAdapter`. + @class FixtureAdapter + @constructor + @namespace DS + @extends DS.Adapter */ DS.FixtureAdapter = DS.Adapter.extend({ @@ -7585,8 +8213,9 @@ DS.FixtureAdapter = DS.Adapter.extend({ if (type.FIXTURES) { var fixtures = Ember.A(type.FIXTURES); return fixtures.map(function(fixture){ - if(!fixture.id){ - throw new Error(fmt('the id property must be defined for fixture %@', [dump(fixture)])); + var fixtureIdType = typeof fixture.id; + if(fixtureIdType !== "number" && fixtureIdType !== "string"){ + throw new Error(fmt('the id property must be defined as a number or string for fixture %@', [dump(fixture)])); } fixture.id = fixture.id + ''; return fixture; @@ -7721,8 +8350,6 @@ DS.FixtureAdapter = DS.Adapter.extend({ @private */ deleteLoadedFixture: function(type, record) { - var id = this.extractId(type, record); - var existingFixture = this.findExistingFixture(type, record); if(existingFixture) { @@ -7740,8 +8367,6 @@ DS.FixtureAdapter = DS.Adapter.extend({ }, findFixtureById: function(fixtures, id) { - var adapter = this; - return Ember.A(fixtures).find(function(r) { if(''+get(r, 'id') === ''+id) { return true; @@ -7767,6 +8392,20 @@ DS.FixtureAdapter = DS.Adapter.extend({ (function() { +/** + @module data + @submodule data-serializers +*/ + +/** + @class RESTSerializer + @constructor + @namespace DS + @extends DS.Serializer +*/ + +var get = Ember.get; + DS.RESTSerializer = DS.JSONSerializer.extend({ keyForAttributeName: function(type, name) { return Ember.String.decamelize(name); @@ -7790,6 +8429,27 @@ DS.RESTSerializer = DS.JSONSerializer.extend({ } return this.singularize(key) + "_ids"; + }, + + keyForPolymorphicId: function(key) { + return key; + }, + + keyForPolymorphicType: function(key) { + return key.replace(/_id$/, '_type'); + }, + + extractValidationErrors: function(type, json) { + var errors = {}; + + get(type, 'attributes').forEach(function(name) { + var key = this._keyForAttributeName(type, name); + if (json['errors'].hasOwnProperty(key)) { + errors[name] = json['errors'][key]; + } + }, this); + + return errors; } }); @@ -7798,9 +8458,18 @@ DS.RESTSerializer = DS.JSONSerializer.extend({ (function() { -/*global jQuery*/ +/** + @module data + @submodule data-adapters +*/ -var get = Ember.get, set = Ember.set, merge = Ember.merge; +var get = Ember.get, set = Ember.set; + +DS.rejectionHandler = function(reason) { + Ember.Logger.assert([reason, reason.message, reason.stack]); + + throw reason; +}; /** The REST adapter allows your store to communicate with an HTTP server by @@ -7856,8 +8525,14 @@ var get = Ember.get, set = Ember.set, merge = Ember.merge; } } ``` + + @class RESTAdapter + @constructor + @namespace DS + @extends DS.Adapter */ DS.RESTAdapter = DS.Adapter.extend({ + namespace: null, bulkCommit: false, since: 'since', @@ -7873,26 +8548,6 @@ DS.RESTAdapter = DS.Adapter.extend({ return !reference.parent; }, - createRecord: function(store, type, record) { - var root = this.rootForType(type); - - var data = {}; - data[root] = this.serialize(record, { includeId: true }); - - this.ajax(this.buildURL(root), "POST", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didCreateRecord(store, type, record, json); - }); - }, - error: function(xhr) { - this.didError(store, type, record, xhr); - } - }); - }, - dirtyRecordsForRecordChange: function(dirtySet, record) { this._dirtyTree(dirtySet, record); }, @@ -7924,7 +8579,26 @@ DS.RESTAdapter = DS.Adapter.extend({ } }, + createRecord: function(store, type, record) { + var root = this.rootForType(type); + var adapter = this; + var data = {}; + + data[root] = this.serialize(record, { includeId: true }); + + return this.ajax(this.buildURL(root), "POST", { + data: data + }).then(function(json){ + adapter.didCreateRecord(store, type, record, json); + }, function(xhr) { + adapter.didError(store, type, record, xhr); + throw xhr; + }).then(null, DS.rejectionHandler); + }, + createRecords: function(store, type, records) { + var adapter = this; + if (get(this, 'bulkCommit') === false) { return this._super(store, type, records); } @@ -7938,153 +8612,144 @@ DS.RESTAdapter = DS.Adapter.extend({ data[plural].push(this.serialize(record, { includeId: true })); }, this); - this.ajax(this.buildURL(root), "POST", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didCreateRecords(store, type, records, json); - }); - } - }); + return this.ajax(this.buildURL(root), "POST", { + data: data + }).then(function(json) { + adapter.didCreateRecords(store, type, records, json); + }).then(null, DS.rejectionHandler); }, updateRecord: function(store, type, record) { - var id = get(record, 'id'); - var root = this.rootForType(type); + var id, root, adapter, data; - var data = {}; + id = get(record, 'id'); + root = this.rootForType(type); + adapter = this; + + data = {}; data[root] = this.serialize(record); - this.ajax(this.buildURL(root, id), "PUT", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didSaveRecord(store, type, record, json); - }); - }, - error: function(xhr) { - this.didError(store, type, record, xhr); - } - }); + return this.ajax(this.buildURL(root, id), "PUT",{ + data: data + }).then(function(json){ + adapter.didUpdateRecord(store, type, record, json); + }, function(xhr) { + adapter.didError(store, type, record, xhr); + throw xhr; + }).then(null, DS.rejectionHandler); }, updateRecords: function(store, type, records) { + var root, plural, adapter, data; + if (get(this, 'bulkCommit') === false) { return this._super(store, type, records); } - var root = this.rootForType(type), - plural = this.pluralize(root); + root = this.rootForType(type); + plural = this.pluralize(root); + adapter = this; + + data = {}; - var data = {}; data[plural] = []; + records.forEach(function(record) { data[plural].push(this.serialize(record, { includeId: true })); }, this); - this.ajax(this.buildURL(root, "bulk"), "PUT", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didSaveRecords(store, type, records, json); - }); - } - }); + return this.ajax(this.buildURL(root, "bulk"), "PUT", { + data: data + }).then(function(json) { + adapter.didUpdateRecords(store, type, records, json); + }).then(null, DS.rejectionHandler); }, deleteRecord: function(store, type, record) { - var id = get(record, 'id'); - var root = this.rootForType(type); + var id, root, adapter; - this.ajax(this.buildURL(root, id), "DELETE", { - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didSaveRecord(store, type, record, json); - }); - } - }); + id = get(record, 'id'); + root = this.rootForType(type); + adapter = this; + + return this.ajax(this.buildURL(root, id), "DELETE").then(function(json){ + adapter.didDeleteRecord(store, type, record, json); + }, function(xhr){ + adapter.didError(store, type, record, xhr); + throw xhr; + }).then(null, DS.rejectionHandler); }, deleteRecords: function(store, type, records) { + var root, plural, serializer, adapter, data; + if (get(this, 'bulkCommit') === false) { return this._super(store, type, records); } - var root = this.rootForType(type), - plural = this.pluralize(root), - serializer = get(this, 'serializer'); + root = this.rootForType(type); + plural = this.pluralize(root); + serializer = get(this, 'serializer'); + adapter = this; + + data = {}; - var data = {}; data[plural] = []; records.forEach(function(record) { data[plural].push(serializer.serializeId( get(record, 'id') )); }); - this.ajax(this.buildURL(root, 'bulk'), "DELETE", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didSaveRecords(store, type, records, json); - }); - } - }); + return this.ajax(this.buildURL(root, 'bulk'), "DELETE", { + data: data + }).then(function(json){ + adapter.didDeleteRecords(store, type, records, json); + }).then(null, DS.rejectionHandler); }, find: function(store, type, id) { - var root = this.rootForType(type); + var root = this.rootForType(type), adapter = this; - this.ajax(this.buildURL(root, id), "GET", { - success: function(json) { - Ember.run(this, function(){ - this.didFindRecord(store, type, json, id); - }); - } - }); + return this.ajax(this.buildURL(root, id), "GET"). + then(function(json){ + adapter.didFindRecord(store, type, json, id); + }).then(null, DS.rejectionHandler); }, findAll: function(store, type, since) { - var root = this.rootForType(type); + var root, adapter; - this.ajax(this.buildURL(root), "GET", { - data: this.sinceQuery(since), - success: function(json) { - Ember.run(this, function(){ - this.didFindAll(store, type, json); - }); - } - }); + root = this.rootForType(type); + adapter = this; + + return this.ajax(this.buildURL(root), "GET",{ + data: this.sinceQuery(since) + }).then(function(json) { + adapter.didFindAll(store, type, json); + }).then(null, DS.rejectionHandler); }, findQuery: function(store, type, query, recordArray) { - var root = this.rootForType(type); + var root = this.rootForType(type), + adapter = this; - this.ajax(this.buildURL(root), "GET", { - data: query, - success: function(json) { - Ember.run(this, function(){ - this.didFindQuery(store, type, json, recordArray); - }); - } - }); + return this.ajax(this.buildURL(root), "GET", { + data: query + }).then(function(json){ + adapter.didFindQuery(store, type, json, recordArray); + }).then(null, DS.rejectionHandler); }, findMany: function(store, type, ids, owner) { - var root = this.rootForType(type); + var root = this.rootForType(type), + adapter = this; + ids = this.serializeIds(ids); - this.ajax(this.buildURL(root), "GET", { - data: {ids: ids}, - success: function(json) { - Ember.run(this, function(){ - this.didFindMany(store, type, json); - }); - } - }); + return this.ajax(this.buildURL(root), "GET", { + data: {ids: ids} + }).then(function(json) { + adapter.didFindMany(store, type, json); + }).then(null, DS.rejectionHandler); }, /** @@ -8092,7 +8757,7 @@ DS.RESTAdapter = DS.Adapter.extend({ This method serializes a list of IDs using `serializeId` - @returns {Array} an array of serialized IDs + @return {Array} an array of serialized IDs */ serializeIds: function(ids) { var serializer = get(this, 'serializer'); @@ -8104,25 +8769,41 @@ DS.RESTAdapter = DS.Adapter.extend({ didError: function(store, type, record, xhr) { if (xhr.status === 422) { - var data = JSON.parse(xhr.responseText); - store.recordWasInvalid(record, data['errors']); + var json = JSON.parse(xhr.responseText), + serializer = get(this, 'serializer'), + errors = serializer.extractValidationErrors(type, json); + + store.recordWasInvalid(record, errors); } else { this._super.apply(this, arguments); } }, ajax: function(url, type, hash) { - hash.url = url; - hash.type = type; - hash.dataType = 'json'; - hash.contentType = 'application/json; charset=utf-8'; - hash.context = this; + var adapter = this; - if (hash.data && type !== 'GET') { - hash.data = JSON.stringify(hash.data); - } + 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); + } + + hash.success = function(json) { + Ember.run(null, resolve, json); + }; - jQuery.ajax(hash); + hash.error = function(jqXHR, textStatus, errorThrown) { + Ember.run(null, reject, jqXHR); + }; + + Ember.$.ajax(hash); + }); }, url: "", @@ -8144,7 +8825,7 @@ DS.RESTAdapter = DS.Adapter.extend({ Ember.assert("Record URL (" + record + ") must not start with slash", !record || record.toString().charAt(0) !== "/"); Ember.assert("URL suffix (" + suffix + ") must not start with slash", !suffix || suffix.toString().charAt(0) !== "/"); - if (this.namespace !== undefined) { + if (!Ember.isNone(this.namespace)) { url.push(this.namespace); } @@ -8163,257 +8844,19 @@ DS.RESTAdapter = DS.Adapter.extend({ } }); - })(); (function() { -var camelize = Ember.String.camelize, - capitalize = Ember.String.capitalize, - get = Ember.get, - map = Ember.ArrayPolyfills.map, - registeredTransforms; - -var passthruTransform = { - serialize: function(value) { return value; }, - deserialize: function(value) { return value; } -}; - -var defaultTransforms = { - string: passthruTransform, - boolean: passthruTransform, - number: passthruTransform -}; - -function camelizeKeys(json) { - var value; - - for (var prop in json) { - value = json[prop]; - delete json[prop]; - json[camelize(prop)] = value; - } -} - -function munge(json, callback) { - callback(json); -} - -function applyTransforms(json, type, transformType) { - var transforms = registeredTransforms[transformType]; - - Ember.assert("You are trying to apply the '" + transformType + "' transforms, but you didn't register any transforms with that name", transforms); - - get(type, 'attributes').forEach(function(name, attribute) { - var attributeType = attribute.type, - value = json[name]; - - var transform = transforms[attributeType] || defaultTransforms[attributeType]; - - Ember.assert("Your model specified the '" + attributeType + "' type for the '" + name + "' attribute, but no transform for that type was registered", transform); - - json[name] = transform.deserialize(value); - }); -} - -function ObjectProcessor(json, type, store) { - this.json = json; - this.type = type; - this.store = store; -} - -ObjectProcessor.prototype = { - camelizeKeys: function() { - camelizeKeys(this.json); - return this; - }, - - munge: function(callback) { - munge(this.json, callback); - return this; - }, - - applyTransforms: function(transformType) { - applyTransforms(this.json, this.type, transformType); - return this; - } -}; - -function LoadObjectProcessor() { - ObjectProcessor.apply(this, arguments); -} - -LoadObjectProcessor.prototype = Ember.create(ObjectProcessor.prototype); - -LoadObjectProcessor.prototype.load = function() { - this.store.load(this.type, {}, this.json); -}; - -function loadObjectProcessorFactory(store, type) { - return function(json) { - return new LoadObjectProcessor(json, type, store); - }; -} - -function ArrayProcessor(json, type, array, store) { - this.json = json; - this.type = type; - this.array = array; - this.store = store; -} - -ArrayProcessor.prototype = { - load: function() { - var store = this.store, - type = this.type; - - var references = this.json.map(function(object) { - return store.load(type, {}, object); - }); - - this.array.load(references); - }, - - camelizeKeys: function() { - this.json.forEach(camelizeKeys); - return this; - }, - - munge: function(callback) { - this.json.forEach(function(object) { - munge(object, callback); - }); - return this; - }, - - applyTransforms: function(transformType) { - var type = this.type; - - this.json.forEach(function(object) { - applyTransforms(object, type, transformType); - }); - - return this; - } -}; - -function arrayProcessorFactory(store, type, array) { - return function(json) { - return new ArrayProcessor(json, type, array, store); - }; -} - -var HasManyProcessor = function(json, store, record, relationship) { - this.json = json; - this.store = store; - this.record = record; - this.type = record.constructor; - this.relationship = relationship; -}; - -HasManyProcessor.prototype = Ember.create(ArrayProcessor.prototype); - -HasManyProcessor.prototype.load = function() { - var store = this.store; - var ids = map.call(this.json, function(obj) { return obj.id; }); - - store.loadMany(this.relationship.type, this.json); - store.loadHasMany(this.record, this.relationship.key, ids); -}; - -function hasManyProcessorFactory(store, record, relationship) { - return function(json) { - return new HasManyProcessor(json, store, record, relationship); - }; -} - -function SaveProcessor(record, store, type, includeId) { - this.record = record; - ObjectProcessor.call(this, record.toJSON({ includeId: includeId }), type, store); -} - -SaveProcessor.prototype = Ember.create(ObjectProcessor.prototype); - -SaveProcessor.prototype.save = function(callback) { - callback(this.json); -}; - -function saveProcessorFactory(store, type, includeId) { - return function(record) { - return new SaveProcessor(record, store, type, includeId); - }; -} - -DS.BasicAdapter = DS.Adapter.extend({ - find: function(store, type, id) { - var sync = type.sync; - - Ember.assert("You are trying to use the BasicAdapter to find id '" + id + "' of " + type + " but " + type + ".sync was not found", sync); - Ember.assert("The sync code on " + type + " does not implement find(), but you are trying to find id '" + id + "'.", sync.find); - - sync.find(id, loadObjectProcessorFactory(store, type)); - }, - - findQuery: function(store, type, query, recordArray) { - var sync = type.sync; - - Ember.assert("You are trying to use the BasicAdapter to query " + type + " but " + type + ".sync was not found", sync); - Ember.assert("The sync code on " + type + " does not implement query(), but you are trying to query " + type + ".", sync.query); - - sync.query(query, arrayProcessorFactory(store, type, recordArray)); - }, - - findHasMany: function(store, record, relationship, data) { - var name = capitalize(relationship.key), - sync = record.constructor.sync, - processor = hasManyProcessorFactory(store, record, relationship); - - var options = { - relationship: relationship.key, - data: data - }; - - if (sync['find'+name]) { - sync['find' + name](record, options, processor); - } else if (sync.findHasMany) { - sync.findHasMany(record, options, processor); - } else { - Ember.assert("You are trying to use the BasicAdapter to find the " + relationship.key + " has-many relationship, but " + record.constructor + ".sync did not implement findHasMany or find" + name + ".", false); - } - }, - - createRecord: function(store, type, record) { - var sync = type.sync; - sync.createRecord(record, saveProcessorFactory(store, type)); - }, - - updateRecord: function(store, type, record) { - var sync = type.sync; - sync.updateRecord(record, saveProcessorFactory(store, type, true)); - }, - - deleteRecord: function(store, type, record) { - var sync = type.sync; - sync.deleteRecord(record, saveProcessorFactory(store, type, true)); - } -}); - -DS.registerTransforms = function(kind, object) { - registeredTransforms[kind] = object; -}; - -DS.clearTransforms = function() { - registeredTransforms = {}; -}; - -DS.clearTransforms(); - -})(); - +/** + Adapters included with Ember-Data. + @module data + @submodule data-adapters -(function() { + @main data-adapters +*/ })(); @@ -8440,5 +8883,12 @@ DS.clearTransforms(); //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE //SOFTWARE. +/** + Ember Data + @module data +*/ + })(); + +})(); diff --git a/public/js/libs/ember.js b/public/js/libs/ember.js index 7f44724..2bfa603 100644 --- a/public/js/libs/ember.js +++ b/public/js/libs/ember.js @@ -1,5 +1,5 @@ -// Version: v1.0.0-rc.2 -// Last commit: 656fa6e (2013-03-29 13:40:38 -0700) +// Version: v1.0.0-rc.5-1-gf84c193 +// Last commit: f84c193 (2013-06-01 13:57:19 -0400) (function() { @@ -151,8 +151,8 @@ Ember.deprecateFunc = function(message, func) { })(); -// Version: v1.0.0-rc.2 -// Last commit: 656fa6e (2013-03-29 13:40:38 -0700) +// Version: v1.0.0-rc.5-1-gf84c193 +// Last commit: f84c193 (2013-06-01 13:57:19 -0400) (function() { @@ -169,11 +169,18 @@ 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 = []; + exports; 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; +}; + +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 @@ -723,7 +834,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 @@ -872,6 +983,7 @@ Ember.setMeta = function setMeta(obj, property, value) { }; /** + @deprecated @private In order to store defaults for a class, a prototype may need to create @@ -904,6 +1016,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= 0) { + utils.forEach(array1, function(element) { + if (utils.indexOf(array2, element) >= 0) { intersection.push(element); } }); @@ -1398,781 +1590,1362 @@ 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 - -// 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 META_KEY = Ember.META_KEY, get; -// 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 MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; - if (this === void 0 || this === null) { - throw new TypeError(); - } +var IS_GLOBAL_PATH = /^([A-Z$]|([0-9][A-Z$])).*[\.\*]/; +var HAS_THIS = /^this[\.\*]/; +var FIRST_KEY = /^([^\.\*]+)/; - var t = Object(this); - var len = t.length >>> 0; - if (typeof fun !== "function") { - 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 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); - } - } +/** + 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. - return res; -}; + 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.) -// 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"; + 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. - if (this === void 0 || this === null) { - throw new TypeError(); - } + Note that if the object itself is `undefined`, this method will throw + an error. - var t = Object(this); - var len = t.length >>> 0; - if (typeof fun !== "function") { - 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 thisp = arguments[1]; - for (var i = 0; i < len; i++) { - if (i in t) { - fun.call(thisp, t[i], i, t); - } + if (!keyName && 'string'===typeof obj) { + keyName = obj; + obj = null; } -}; -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; } + Ember.assert("Cannot call get with '"+ keyName +"' on an undefined object.", obj !== undefined); + + 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 firstKey(path) { + return path.match(FIRST_KEY)[0]; +} +// assumes path is already normalized +function normalizeTuple(target, path) { + var hasThis = HAS_THIS.test(path), + isGlobal = !hasThis && IS_GLOBAL_PATH.test(path), + key; + if (!target || isGlobal) target = Ember.lookup; + if (hasThis) path = path.slice(5); -(function() { -/** -@module ember-metal -*/ + if (target === Ember.lookup) { + key = firstKey(path); + target = get(target, key); + path = path.slice(key.length+1); + } -/* - JavaScript (before ES6) does not have a Map implementation. Objects, - which are often used as dictionaries, may only have Strings as keys. + // must return some kind of path to be valid else other things will break. + if (!path || path.length===0) throw new Error('Invalid Path'); - 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. + return [ target, path ]; +} - 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. +var getPath = Ember._getPath = function(root, path) { + var hasThis, parts, tuple, idx, len; - Map is mocked out to look like an Ember object, so you can do - `Ember.Map.create()` for symmetry with other Ember classes. -*/ -var guidFor = Ember.guidFor, - indexOf = Ember.ArrayPolyfills.indexOf; + // 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); } -var copy = function(obj) { - var output = {}; + // detect complicated paths and normalize them + hasThis = HAS_THIS.test(path); - for (var prop in obj) { - if (obj.hasOwnProperty(prop)) { output[prop] = obj[prop]; } + if (!root || hasThis) { + tuple = normalizeTuple(root, path); + root = tuple[0]; + path = tuple[1]; + tuple.length = 0; } - return output; + 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; }; -var copyMap = function(original, newObject) { - var keys = original.keys.copy(), - values = copy(original.values); +/** + @private - newObject.keys = keys; - newObject.values = values; + 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. - return newObject; + @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. +*/ +Ember.normalizeTuple = function(target, path) { + return normalizeTuple(target, path); }; -/** - 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; }; -/** - @method create - @static - @return {Ember.OrderedSet} -*/ -OrderedSet.create = function() { - return new OrderedSet(); -}; +Ember.get = get; +Ember.getPath = Ember.deprecateFunc('getPath is deprecated since get now supports paths', Ember.get); -OrderedSet.prototype = { - /** - @method clear - */ - clear: function() { - this.presenceSet = {}; - this.list = []; - }, +})(); - /** - @method add - @param obj - */ - add: function(obj) { - var guid = guidFor(obj), - presenceSet = this.presenceSet, - list = this.list; - if (guid in presenceSet) { return; } - presenceSet[guid] = true; - list.push(obj); - }, +(function() { +/** +@module ember-metal +*/ - /** - @method remove - @param obj - */ - remove: function(obj) { - var guid = guidFor(obj), - presenceSet = this.presenceSet, - list = this.list; +var o_create = Ember.create, + metaFor = Ember.meta, + META_KEY = Ember.META_KEY, + /* listener flags */ + ONCE = 1, SUSPENDED = 2; - delete presenceSet[guid]; +/* + 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. - var index = indexOf.call(list, obj); - if (index > -1) { - list.splice(index, 1); - } - }, + The hashes are stored in the object's meta hash, and look like this: - /** - @method isEmpty - @return {Boolean} - */ - isEmpty: function() { - return this.list.length === 0; - }, + // Object's meta hash + { + listeners: { // variable name: `listenerSet` + "foo:changed": [ // variable name: `actions` + [target, method, flags] + ] + } + } - /** - @method has - @param obj - @return {Boolean} - */ - has: function(obj) { - var guid = guidFor(obj), - presenceSet = this.presenceSet; +*/ - return guid in presenceSet; - }, +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; } + } + return index; +} - /** - @method forEach - @param {Function} fn - @param self - */ - forEach: function(fn, self) { - // allow mutation during iteration - var list = this.list.slice(); +function actionsFor(obj, eventName) { + var meta = metaFor(obj, true), + actions; - for (var i = 0, j = list.length; i < j; i++) { - fn.call(self, list[i]); - } - }, + if (!meta.listeners) { meta.listeners = {}; } - /** - @method toArray - @return {Array} - */ - toArray: function() { - return this.list.slice(); - }, + if (!meta.hasOwnProperty('listeners')) { + // setup inherited copy of the listeners object + meta.listeners = o_create(meta.listeners); + } - /** - @method copy - @return {Ember.OrderedSet} - */ - copy: function() { - var set = new OrderedSet(); + actions = meta.listeners[eventName]; - set.presenceSet = copy(this.presenceSet); - set.list = this.list.slice(); + // 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] = []; + } - return set; + return actions; +} + +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 - 1; i >= 0; i--) { + var target = actions[i][0], + method = actions[i][1], + flags = actions[i][2], + actionIndex = indexOf(otherActions, target, method); + + if (actionIndex === -1) { + otherActions.push([target, method, flags]); + } } -}; +} -/** - A Map stores values indexed by keys. Unlike JavaScript's - default Objects, the keys of a Map can be any JavaScript - object. +function actionsDiff(obj, eventName, otherActions) { + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName], + diffActions = []; - Internally, a Map has two data structures: + if (!actions) { return; } + for (var i = actions.length - 1; i >= 0; i--) { + var target = actions[i][0], + method = actions[i][1], + flags = actions[i][2], + actionIndex = indexOf(otherActions, target, method); - 1. `keys`: an OrderedSet of all of the existing keys - 2. `values`: a JavaScript Object indexed by the `Ember.guidFor(key)` + if (actionIndex !== -1) { continue; } - 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`. + otherActions.push([target, method, flags]); + diffActions.push([target, method, flags]); + } - @class Map - @namespace Ember - @private - @constructor -*/ -var Map = Ember.Map = function() { - this.keys = Ember.OrderedSet.create(); - this.values = {}; -}; + return diffActions; +} /** - @method create - @static -*/ -Map.create = function() { - return new Map(); -}; + Add an event listener -Map.prototype = { - /** - Retrieve the value associated with a given key. + @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); - @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 (!method && 'function' === typeof target) { + method = target; + target = null; + } - return values[guid]; - }, + var actions = actionsFor(obj, eventName), + actionIndex = indexOf(actions, target, method), + flags = 0; - /** - 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. + if (once) flags |= ONCE; - @method set - @param {anything} key - @param {anything} value - */ - set: function(key, value) { - var keys = this.keys, - values = this.values, - guid = guidFor(key); + if (actionIndex !== -1) { return; } - keys.add(key); - values[guid] = value; - }, + actions.push([target, method, flags]); - /** - Removes a value from the map for an associated key. + if ('function' === typeof obj.didAddListener) { + obj.didAddListener(eventName, 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; +/** + Remove an event listener - if (values.hasOwnProperty(guid)) { - keys.remove(key); - value = values[guid]; - delete values[guid]; - return true; - } else { - return false; - } - }, + Arguments should match those passed to `Ember.addListener`. - /** - Check whether a key is present. + @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); - @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 (!method && 'function' === typeof target) { + method = target; + target = null; + } - return values.hasOwnProperty(guid); - }, + function _removeListener(target, method) { + var actions = actionsFor(obj, eventName), + actionIndex = indexOf(actions, target, method); - /** - Iterate over all the keys and values. Calls the function once - for each key, passing in the key and value, in that order. + // action doesn't exist, give up silently + if (actionIndex === -1) { return; } - The keys are guaranteed to be iterated over in insertion order. + actions.splice(actionIndex, 1); - @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; + if ('function' === typeof obj.didRemoveListener) { + obj.didRemoveListener(eventName, target, method); + } + } - keys.forEach(function(key) { - var guid = guidFor(key); - callback.call(self, key, values[guid]); - }); - }, + if (method) { + _removeListener(target, method); + } else { + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName]; - /** - @method copy - @return {Ember.Map} - */ - copy: function() { - return copyMap(this, new Map()); + if (!actions) { return; } + for (var i = actions.length - 1; i >= 0; i--) { + _removeListener(actions[i][0], actions[i][1]); + } } -}; +} /** - @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` + Suspend listener during callback. + + 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 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 */ -MapWithDefault.create = function(options) { - if (options) { - return new MapWithDefault(options); - } else { - return new Map(); +function suspendListener(obj, eventName, target, method, callback) { + if (!method && 'function' === typeof target) { + method = target; + target = null; } -}; -MapWithDefault.prototype = Ember.create(Map.prototype); + var actions = actionsFor(obj, eventName), + actionIndex = indexOf(actions, target, method), + action; -/** - Retrieve the value associated with a given key. + if (actionIndex !== -1) { + action = actions[actionIndex].slice(); // copy it, otherwise we're modifying a shared object + action[2] |= SUSPENDED; // mark the action as suspended + actions[actionIndex] = action; // replace the shared object with our copy + } - @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); + function tryable() { return callback.call(target); } + function finalizer() { if (action) { action[2] &= ~SUSPENDED; } } - if (hasValue) { - return Map.prototype.get.call(this, key); - } else { - var defaultValue = this.defaultValue(key); - this.set(key, defaultValue); - return defaultValue; - } -}; + return Ember.tryFinally(tryable, finalizer); +} /** - @method copy - @return {Ember.MapWithDefault} -*/ -MapWithDefault.prototype.copy = function() { - return copyMap(this, new MapWithDefault({ - defaultValue: this.defaultValue - })); -}; - -})(); + @private + Suspend listener during callback. + 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. -(function() { -/** -@module ember-metal + @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 */ +function suspendListeners(obj, eventNames, target, method, callback) { + if (!method && 'function' === typeof target) { + method = target; + target = null; + } -var META_KEY = Ember.META_KEY, get, set; + var suspendedActions = [], + eventName, actions, action, i, l; -var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; + for (i=0, l=eventNames.length; i 0) { - ret = meta.values[keyName]; + for (var i = actions.length - 1; i >= 0; i--) { // looping in reverse for once listeners + var action = actions[i]; + if (!action) { continue; } + var target = action[0], method = action[1], flags = action[2]; + 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.) + @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]; - 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. + return !!(actions && actions.length); +} - @method set +/** + @private + @method listenersFor @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 listenersFor(obj, eventName) { + var ret = []; + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName]; - if (!obj || keyName.indexOf('.') !== -1) { - return setPath(obj, keyName, value, tolerant); + if (!actions) { return ret; } + + for (var i = 0, l = actions.length; i < l; i++) { + var target = actions[i][0], + method = actions[i][1]; + ret.push([target, method]); } - Ember.assert("You need to provide an object and key to `set`.", !!obj && keyName !== undefined); - Ember.assert('calling set on destroyed object', !obj.isDestroyed); + return ret; +} - 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); +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; - // 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; +})(); + + + +(function() { +var guidFor = Ember.guidFor, + sendEvent = Ember.sendEvent; + +/* + 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(); +}; + +ObserverSet.prototype.add = function(sender, keyName, eventName) { + var observerSet = this.observerSet, + observers = this.observers, + senderGuid = guidFor(sender), + keySet = observerSet[senderGuid], + index; + + if (!keySet) { + observerSet[senderGuid] = keySet = {}; } - return value; + index = keySet[keyName]; + if (index === undefined) { + index = observers.push({ + sender: sender, + keyName: keyName, + eventName: eventName, + listeners: [] + }) - 1; + keySet[keyName] = index; + } + return observers[index].listeners; }; -// 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; -} +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; } + sendEvent(sender, observer.eventName, [sender, observer.keyName], observer.listeners); + } +}; -function firstKey(path) { - return path.match(FIRST_KEY)[0]; -} +ObserverSet.prototype.clear = function() { + this.observerSet = {}; + this.observers = []; +}; +})(); -// assumes path is already normalized -function normalizeTuple(target, path) { - var hasThis = HAS_THIS.test(path), - isGlobal = !hasThis && IS_GLOBAL_PATH.test(path), - key; - if (!target || isGlobal) target = Ember.lookup; - if (hasThis) path = path.slice(5); - if (target === Ember.lookup) { - key = firstKey(path); - target = get(target, key); - path = path.slice(key.length+1); - } +(function() { +var metaFor = Ember.meta, + guidFor = Ember.guidFor, + tryFinally = Ember.tryFinally, + sendEvent = Ember.sendEvent, + listenersUnion = Ember.listenersUnion, + listenersDiff = Ember.listenersDiff, + ObserverSet = Ember._ObserverSet, + beforeObserverSet = new ObserverSet(), + observerSet = new ObserverSet(), + deferred = 0; - // must return some kind of path to be valid else other things will break. - if (!path || path.length===0) throw new Error('Invalid Path'); +// .......................................................... +// PROPERTY CHANGES +// - return [ target, path ]; -} +/** + 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 getPath(root, path) { - var hasThis, parts, tuple, idx, len; + 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. - // 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); } + @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} +*/ +var propertyWillChange = Ember.propertyWillChange = function(obj, keyName) { + var m = metaFor(obj, false), + watching = m.watching[keyName] > 0 || keyName === 'length', + proto = m.proto, + desc = m.descs[keyName]; - // detect complicated paths and normalize them - hasThis = HAS_THIS.test(path); + 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); +}; - if (!root || hasThis) { - tuple = normalizeTuple(root, path); - root = tuple[0]; - path = tuple[1]; - tuple.length = 0; - } +/** + This function is called just after an object property has changed. + It will notify any observers and clear caches among other things. - parts = path.split("."); - len = parts.length; - for (idx=0; root && idx 0 || keyName === 'length', + proto = m.proto, + desc = m.descs[keyName]; - // get the last part of the path - keyName = path.slice(path.lastIndexOf('.') + 1); + if (proto === obj) { return; } - // get the first part of the part - path = path.slice(0, path.length-(keyName.length+1)); + // shouldn't this mean that we're watching this key? + if (desc && desc.didChange) { desc.didChange(obj, keyName); } + if (!watching && keyName !== 'length') { return; } - // unless the path is this, look up the first part to - // get the root - if (path !== 'this') { - root = getPath(root, path); - } + dependentKeysDidChange(obj, keyName, m); + chainsDidChange(obj, keyName, m); + notifyObservers(obj, keyName); +}; - if (!keyName || keyName.length === 0) { - throw new Error('You passed an empty path'); - } +var WILL_SEEN, DID_SEEN; - if (!root) { - if (tolerant) { return; } - else { throw new Error('Object in path '+path+' could not be found or was destroyed.'); } - } +// 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; } - return set(root, keyName, value); + var seen = WILL_SEEN, top = !seen; + if (top) { seen = WILL_SEEN = {}; } + iterDeps(propertyWillChange, obj, depKey, seen, meta); + if (top) { WILL_SEEN = null; } } -/** - @private +// called whenever a property has just changed to update dependent keys +function dependentKeysDidChange(obj, depKey, meta) { + if (obj.isDestroying) { return; } - 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. + var seen = DID_SEEN, top = !seen; + if (top) { seen = DID_SEEN = {}; } + iterDeps(propertyDidChange, obj, depKey, seen, meta); + if (top) { DID_SEEN = null; } +} - @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. -*/ -Ember.normalizeTuple = function(target, path) { - return normalizeTuple(target, path); +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); + } + } +} + +var chainsWillChange = function(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); + } }; -Ember.getWithDefault = function(root, key, defaultValue) { - var value = get(root, key); +var chainsDidChange = function(obj, keyName, m, arg) { + if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do - if (value === undefined) { return defaultValue; } - return value; + var nodes = m.chainWatchers; + + nodes = nodes[keyName]; + if (!nodes) { return; } + + // 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); + } }; +Ember.overrideChains = function(obj, keyName, m) { + chainsDidChange(obj, keyName, m, true); +}; -Ember.get = get; -Ember.getPath = Ember.deprecateFunc('getPath is deprecated since get now supports paths', Ember.get); +/** + @method beginPropertyChanges + @chainable +*/ +var beginPropertyChanges = Ember.beginPropertyChanges = function() { + deferred++; +}; + +/** + @method endPropertyChanges +*/ +var endPropertyChanges = 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); + }); + ``` + + @method changeProperties + @param {Function} callback + @param [binding] +*/ +Ember.changeProperties = function(cb, binding){ + beginPropertyChanges(); + tryFinally(cb, endPropertyChanges, binding); +}; + +var notifyBeforeObservers = function(obj, keyName) { + if (obj.isDestroying) { return; } + + 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]); + } +}; + +var notifyObservers = function(obj, keyName) { + if (obj.isDestroying) { return; } + + var eventName = keyName + ':change', listeners; + if (deferred) { + listeners = observerSet.add(obj, keyName, eventName); + listenersUnion(obj, eventName, listeners); + } else { + sendEvent(obj, eventName, [obj, keyName]); + } +}; +})(); + + + +(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 `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 + @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; + } + + if (!obj || keyName.indexOf('.') !== -1) { + return setPath(obj, keyName, value, tolerant); + } + + Ember.assert("You need to provide an object and key to `set`.", !!obj && keyName !== undefined); + Ember.assert('calling set on destroyed object', !obj.isDestroyed); + + 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; + } + } + return value; +}; + +// Currently used only by Ember Data tests +if (Ember.config.overrideAccessors) { + Ember.set = set; + Ember.config.overrideAccessors(); + set = Ember.set; +} + +function setPath(root, path, value, tolerant) { + var keyName; + + // 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); + } + + if (!keyName || keyName.length === 0) { + throw new Error('You passed an empty path'); + } + + if (!root) { + if (tolerant) { return; } + else { throw new Error('Object in path '+path+' could not be found or was destroyed.'); } + } + + return set(root, keyName, value); +} + +Ember.set = set; +Ember.setPath = Ember.deprecateFunc('setPath is deprecated since set now supports paths', Ember.set); + +/** + Error-tolerant form of `Ember.set`. Will not blow up if any part of the + 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.trySet = function(root, path, value) { + return set(root, path, value, true); +}; +Ember.trySetPath = Ember.deprecateFunc('trySetPath has been renamed to trySet', Ember.trySet); + +})(); + + + +(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. + + 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. + + Map is mocked out to look like an Ember object, so you can do + `Ember.Map.create()` for symmetry with other Ember classes. +*/ +var get = Ember.get, + set = Ember.set, + guidFor = Ember.guidFor, + indexOf = Ember.ArrayPolyfills.indexOf; + +var copy = function(obj) { + var output = {}; + + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { output[prop] = obj[prop]; } + } + + return output; +}; + +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; +}; + +/** + 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 +*/ +var OrderedSet = Ember.OrderedSet = function() { + this.clear(); +}; + +/** + @method create + @static + @return {Ember.OrderedSet} +*/ +OrderedSet.create = function() { + return new OrderedSet(); +}; + + +OrderedSet.prototype = { + /** + @method clear + */ + clear: function() { + this.presenceSet = {}; + this.list = []; + }, + + /** + @method add + @param obj + */ + add: function(obj) { + var guid = guidFor(obj), + presenceSet = this.presenceSet, + list = this.list; + + if (guid in presenceSet) { return; } + + presenceSet[guid] = true; + list.push(obj); + }, + + /** + @method remove + @param obj + */ + remove: function(obj) { + var guid = guidFor(obj), + presenceSet = this.presenceSet, + list = this.list; + + delete presenceSet[guid]; + + var index = indexOf.call(list, obj); + if (index > -1) { + list.splice(index, 1); + } + }, + + /** + @method isEmpty + @return {Boolean} + */ + isEmpty: function() { + return this.list.length === 0; + }, + + /** + @method has + @param obj + @return {Boolean} + */ + has: function(obj) { + var guid = guidFor(obj), + presenceSet = this.presenceSet; + + 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(); + }, + + /** + @method copy + @return {Ember.OrderedSet} + */ + copy: function() { + var set = new OrderedSet(); + + set.presenceSet = copy(this.presenceSet); + set.list = this.toArray(); + + return set; + } +}; + +/** + A Map stores values indexed by keys. Unlike JavaScript's + default Objects, the keys of a Map can be any JavaScript + object. + + Internally, a Map has two data structures: + + 1. `keys`: an OrderedSet of all of the existing keys + 2. `values`: a JavaScript Object indexed by the `Ember.guidFor(key)` + + 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`. + + @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(); +}; + +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. + + @method get + @param {*} key + @return {*} the value associated with the key, or `undefined` + */ + get: function(key) { + var values = this.values, + guid = guidFor(key); + + return values[guid]; + }, + + /** + 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. + + @method set + @param {*} key + @param {*} value + */ + set: function(key, value) { + var keys = this.keys, + values = this.values, + guid = guidFor(key); + + keys.add(key); + values[guid] = value; + set(this, 'length', keys.list.length); + }, + + /** + Removes a value from the map for an associated key. + + @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 (values.hasOwnProperty(guid)) { + keys.remove(key); + delete values[guid]; + set(this, 'length', keys.list.length); + return true; + } else { + return false; + } + }, + + /** + Check whether a key is present. + + @method has + @param {*} key + @return {Boolean} true if the item was present, false otherwise + */ + has: function(key) { + var values = this.values, + guid = guidFor(key); + + return values.hasOwnProperty(guid); + }, + + /** + Iterate over all the keys and values. Calls the function once + for each key, passing in the key and value, in that order. + + The keys are guaranteed to be iterated over in insertion order. + + @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; + + keys.forEach(function(key) { + var guid = guidFor(key); + callback.call(self, key, values[guid]); + }); + }, + + /** + @method copy + @return {Ember.Map} + */ + copy: function() { + return copyMap(this, new Map()); + } +}; + +/** + @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; +}; + +/** + @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(); + } +}; -Ember.set = set; -Ember.setPath = Ember.deprecateFunc('setPath is deprecated since set now supports paths', Ember.set); +MapWithDefault.prototype = Ember.create(Map.prototype); /** - 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. + Retrieve the value associated with a given key. - @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 + @method get + @param {*} key + @return {*} the value associated with the key, or the default value */ -Ember.trySet = function(root, path, value) { - return set(root, path, value, true); +MapWithDefault.prototype.get = function(key) { + var hasValue = this.has(key); + + if (hasValue) { + return Map.prototype.get.call(this, key); + } else { + var defaultValue = this.defaultValue(key); + this.set(key, defaultValue); + return defaultValue; + } }; -Ember.trySetPath = Ember.deprecateFunc('trySetPath has been renamed to trySet', Ember.trySet); /** - Returns true if the provided path is global (e.g., `MyApp.fooController.bar`) - instead of local (`foo.bar.baz`). - - @method isGlobalPath - @for Ember - @private - @param {String} path - @return Boolean + @method copy + @return {Ember.MapWithDefault} */ -Ember.isGlobalPath = function(path) { - return IS_GLOBAL.test(path); +MapWithDefault.prototype.copy = function() { + return copyMap(this, new MapWithDefault({ + defaultValue: this.defaultValue + })); }; - })(); @@ -2193,7 +2966,7 @@ var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; // /** - Objects of this type can implement an interface to responds requests to + Objects of this type can implement an interface to respond to requests to get and set. The default implementation handles simple properties. You generally won't need to create or subclass this directly. @@ -2203,7 +2976,7 @@ var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; @private @constructor */ -var Descriptor = Ember.Descriptor = function() {}; +Ember.Descriptor = function() {}; // .......................................................... // DEFINING PROPERTIES API @@ -2263,7 +3036,7 @@ var DEFAULT_GETTER_FUNCTION = Ember.DEFAULT_GETTER_FUNCTION = function(name) { @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 + @param {*} [data] something other than a descriptor, that will become the explicit value of this property. */ Ember.defineProperty = function(obj, keyName, desc, data, meta) { @@ -2334,119 +3107,8 @@ Ember.defineProperty = function(obj, keyName, desc, data, meta) { (function() { -// Ember.tryFinally -/** -@module ember-metal -*/ - -var AFTER_OBSERVERS = ':change'; -var BEFORE_OBSERVERS = ':before'; - -var guidFor = Ember.guidFor; - -var deferred = 0; - -/* - this.observerSet = { - [senderGuid]: { // variable name: `keySet` - [keyName]: listIndex - } - }, - this.observers = [ - { - sender: obj, - keyName: keyName, - eventName: eventName, - listeners: [ - [target, method, onceFlag, suspendedFlag] - ] - }, - ... - ] -*/ -function ObserverSet() { - this.clear(); -} - -ObserverSet.prototype.add = function(sender, keyName, eventName) { - var observerSet = this.observerSet, - observers = this.observers, - senderGuid = Ember.guidFor(sender), - keySet = observerSet[senderGuid], - index; - - if (!keySet) { - observerSet[senderGuid] = keySet = {}; - } - index = keySet[keyName]; - if (index === undefined) { - index = observers.push({ - sender: sender, - keyName: keyName, - eventName: eventName, - listeners: [] - }) - 1; - keySet[keyName] = index; - } - 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); - } -}; - -ObserverSet.prototype.clear = function() { - this.observerSet = {}; - this.observers = []; -}; - -var beforeObserverSet = new ObserverSet(), observerSet = new ObserverSet(); - -/** - @method beginPropertyChanges - @chainable -*/ -Ember.beginPropertyChanges = function() { - deferred++; -}; - -/** - @method endPropertyChanges -*/ -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); - }); - ``` - - @method changeProperties - @param {Function} callback - @param [binding] -*/ -Ember.changeProperties = function(cb, binding){ - Ember.beginPropertyChanges(); - Ember.tryFinally(cb, Ember.endPropertyChanges, binding); -}; +var changeProperties = Ember.changeProperties, + set = Ember.set; /** Set a list of properties on an object. These properties are set inside @@ -2459,214 +3121,116 @@ Ember.changeProperties = function(cb, binding){ @return target */ Ember.setProperties = function(self, hash) { - Ember.changeProperties(function(){ + changeProperties(function(){ for(var prop in hash) { - if (hash.hasOwnProperty(prop)) Ember.set(self, prop, hash[prop]); + if (hash.hasOwnProperty(prop)) { set(self, prop, hash[prop]); } } }); return self; }; +})(); -function changeEvent(keyName) { - return keyName+AFTER_OBSERVERS; -} - -function beforeEvent(keyName) { - return keyName+BEFORE_OBSERVERS; -} - -/** - @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; -}; - -Ember.observersFor = function(obj, path) { - return Ember.listenersFor(obj, changeEvent(path)); -}; - -/** - @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; -}; - -/** - @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; -}; -// 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); -}; +(function() { +var metaFor = Ember.meta, // utils.js + typeOf = Ember.typeOf, // utils.js + MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, + o_defineProperty = Ember.platform.defineProperty; -Ember._suspendObserver = function(obj, path, target, method, callback) { - return Ember._suspendListener(obj, changeEvent(path), target, method, callback); -}; +Ember.watchKey = function(obj, keyName) { + // can't watch length on Array - it is special... + if (keyName === 'length' && typeOf(obj) === 'array') { return; } -var map = Ember.ArrayPolyfills.map; + var m = metaFor(obj), watching = m.watching, desc; -Ember._suspendBeforeObservers = function(obj, paths, target, method, callback) { - var events = map.call(paths, beforeEvent); - return Ember._suspendListeners(obj, events, target, method, callback); -}; + // activate watching first time + if (!watching[keyName]) { + watching[keyName] = 1; + desc = m.descs[keyName]; + if (desc && desc.willWatch) { desc.willWatch(obj, keyName); } -Ember._suspendObservers = function(obj, paths, target, method, callback) { - var events = map.call(paths, changeEvent); - return Ember._suspendListeners(obj, events, target, method, callback); -}; + if ('function' === typeof obj.willWatchProperty) { + obj.willWatchProperty(keyName); + } -Ember.beforeObserversFor = function(obj, path) { - return Ember.listenersFor(obj, beforeEvent(path)); + 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; + } }; -/** - @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; -}; -Ember.notifyBeforeObservers = function(obj, keyName) { - if (obj.isDestroying) { return; } +Ember.unwatchKey = function(obj, keyName) { + var m = metaFor(obj), watching = m.watching, desc; - 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]); - } -}; + if (watching[keyName] === 1) { + watching[keyName] = 0; + desc = m.descs[keyName]; -Ember.notifyObservers = function(obj, keyName) { - if (obj.isDestroying) { return; } + if (desc && desc.didUnwatch) { desc.didUnwatch(obj, keyName); } - 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]); + 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 if (watching[keyName] > 1) { + watching[keyName]--; } }; - })(); (function() { -/** -@module ember-metal -*/ - -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 +var metaFor = Ember.meta, // utils.js + get = Ember.get, // property_get.js + normalizeTuple = Ember.normalizeTuple, // property_get.js forEach = Ember.ArrayPolyfills.forEach, // array.js - FIRST_KEY = /^([^\.\*]+)/, - IS_PATH = /[\.\*]/; - -var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, -o_defineProperty = Ember.platform.defineProperty; + warn = Ember.warn, + watchKey = Ember.watchKey, + unwatchKey = Ember.unwatchKey, + propertyWillChange = Ember.propertyWillChange, + propertyDidChange = Ember.propertyDidChange, + FIRST_KEY = /^([^\.\*]+)/; function firstKey(path) { return path.match(FIRST_KEY)[0]; } -// returns true if the passed path is just a keyName -function isKeyName(path) { - return path==='*' || !IS_PATH.test(path); -} - -// .......................................................... -// DEPENDENT KEYS -// - -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); - } - } -} - - -var WILL_SEEN, DID_SEEN; +var pendingQueue = []; -// 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; } +// 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 - var seen = WILL_SEEN, top = !seen; - if (top) { seen = WILL_SEEN = {}; } - iterDeps(propertyWillChange, obj, depKey, seen, meta); - if (top) { WILL_SEEN = null; } -} + var queue = pendingQueue; + pendingQueue = []; -// called whenever a property has just changed to update dependent keys -function dependentKeysDidChange(obj, depKey, meta) { - if (obj.isDestroying) { return; } + forEach.call(queue, function(q) { q[0].add(q[1]); }); - var seen = DID_SEEN, top = !seen; - if (top) { seen = DID_SEEN = {}; } - iterDeps(propertyDidChange, obj, depKey, seen, meta); - if (top) { DID_SEEN = null; } -} + 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); +}; -// .......................................................... -// CHAIN -// function addChainWatcher(obj, keyName, node) { if (!obj || ('object' !== typeof obj)) { return; } // nothing to do @@ -2679,10 +3243,10 @@ function addChainWatcher(obj, keyName, node) { if (!nodes[keyName]) { nodes[keyName] = []; } nodes[keyName].push(node); - Ember.watch(obj, keyName); + watchKey(obj, keyName); } -function removeChainWatcher(obj, keyName, node) { +var removeChainWatcher = Ember.removeChainWatcher = function(obj, keyName, node) { if (!obj || 'object' !== typeof obj) { return; } // nothing to do var m = metaFor(obj, false); @@ -2696,24 +3260,8 @@ function removeChainWatcher(obj, keyName, node) { if (nodes[i] === node) { nodes.splice(i, 1); } } } - Ember.unwatch(obj, keyName); -} - -var pendingQueue = []; - -// 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 - - var queue = pendingQueue; - pendingQueue = []; - - forEach.call(queue, function(q) { q[0].add(q[1]); }); - - 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); -} + unwatchKey(obj, keyName); +}; function isProto(pvalue) { return metaFor(pvalue, false).proto === pvalue; @@ -2722,8 +3270,7 @@ function isProto(pvalue) { // 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; +var ChainNode = Ember._ChainNode = function(parent, key, value) { this._parent = parent; this._key = key; @@ -2895,9 +3442,9 @@ ChainNodePrototype.chainWillChange = function(chain, path, depth) { if (this._parent) { this._parent.chainWillChange(this, path, depth+1); } else { - if (depth > 1) { Ember.propertyWillChange(this.value(), path); } + if (depth > 1) { propertyWillChange(this.value(), path); } path = 'this.' + path; - if (this._paths[path] > 0) { Ember.propertyWillChange(this.value(), path); } + if (this._paths[path] > 0) { propertyWillChange(this.value(), path); } } }; @@ -2906,9 +3453,9 @@ ChainNodePrototype.chainDidChange = function(chain, path, depth) { if (this._parent) { this._parent.chainDidChange(this, path, depth+1); } else { - if (depth > 1) { Ember.propertyDidChange(this.value(), path); } + if (depth > 1) { propertyDidChange(this.value(), path); } path = 'this.' + path; - if (this._paths[path] > 0) { Ember.propertyDidChange(this.value(), path); } + if (this._paths[path] > 0) { propertyDidChange(this.value(), path); } } }; @@ -2944,6 +3491,24 @@ ChainNodePrototype.didChange = function(suppressEvent) { if (this._parent) { this._parent.chainDidChange(this, this._key, 1); } }; +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); + } +}; +})(); + + + +(function() { +var metaFor = Ember.meta, // utils.js + typeOf = Ember.typeOf, // utils.js + ChainNode = Ember._ChainNode; // chains.js + // get the chains for the current object. If the current object has // chains inherited from the proto they will be cloned and reconfigured for // the current object. @@ -2957,240 +3522,123 @@ function chainsFor(obj) { 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 chainsDidChange(obj, keyName, m, arg) { - if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do - - var nodes = m.chainWatchers; - - nodes = nodes[keyName]; - if (!nodes) { return; } - - // 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()` - - @method watch - @for Ember - @param obj - @param {String} keyName -*/ -Ember.watch = function(obj, keyName) { +Ember.watchPath = function(obj, keyPath) { // 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; - - // 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 (keyPath === 'length' && typeOf(obj) === 'array') { return; } - 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); - } + var m = metaFor(obj), watching = m.watching; - } else { - watching[keyName] = (watching[keyName] || 0) + 1; + if (!watching[keyPath]) { // activate watching first time + watching[keyPath] = 1; + chainsFor(obj).add(keyPath); + } else { + watching[keyPath] = (watching[keyPath] || 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; - - 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); - } +Ember.unwatchPath = function(obj, keyPath) { + var m = metaFor(obj), watching = m.watching; - } else if (watching[keyName]>1) { - watching[keyName]--; + if (watching[keyPath] === 1) { + watching[keyPath] = 0; + chainsFor(obj).remove(keyPath); + } else if (watching[keyPath] > 1) { + watching[keyPath]--; } - - 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 +(function() { +/** +@module ember-metal */ -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); - } -}; +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 = /[\.\*]/; -// .......................................................... -// PROPERTY CHANGES -// +// returns true if the passed path is just a keyName +function isKeyName(path) { + return path==='*' || !IS_PATH.test(path); +} /** - 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. + @private - 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. + 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 propertyWillChange + @method watch @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} + @param obj + @param {String} keyName */ -function propertyWillChange(obj, keyName) { - var m = metaFor(obj, false), - watching = m.watching[keyName] > 0 || keyName === 'length', - proto = m.proto, - desc = m.descs[keyName]; +Ember.watch = function(obj, keyPath) { + // can't watch length on Array - it is special... + if (keyPath === 'length' && typeOf(obj) === 'array') { return; } - 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); -} + if (isKeyName(keyPath)) { + watchKey(obj, keyPath); + } else { + watchPath(obj, keyPath); + } +}; + +Ember.isWatching = function isWatching(obj, key) { + var meta = obj[META_KEY]; + return (meta && meta.watching[key]) > 0; +}; + +Ember.watch.flushPending = Ember.flushPendingChains; -Ember.propertyWillChange = propertyWillChange; +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); + } +}; /** - This function is called just after an object property has changed. - It will notify any observers and clear caches among other things. + @private - 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. + 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 propertyDidChange + @method rewatch @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} + @param obj */ -function propertyDidChange(obj, keyName) { - var m = metaFor(obj, false), - watching = m.watching[keyName] > 0 || keyName === 'length', - proto = m.proto, - desc = m.descs[keyName]; - - if (proto === obj) { return; } - - // shouldn't this mean that we're watching this key? - if (desc && desc.didChange) { desc.didChange(obj, keyName); } - if (!watching && keyName !== 'length') { return; } +Ember.rewatch = function(obj) { + var m = metaFor(obj, false), chains = m.chains; - dependentKeysDidChange(obj, keyName, m); - chainsDidChange(obj, keyName, m); - Ember.notifyObservers(obj, keyName); -} + // make sure the object has its own guid. + if (GUID_KEY in obj && !obj.hasOwnProperty(GUID_KEY)) { + generateGuid(obj, 'ember'); + } -Ember.propertyDidChange = propertyDidChange; + // make sure any chained watchers update. + if (chains && chains.value() !== obj) { + m.chains = chains.copy(obj); + } +}; var NODE_STACK = []; @@ -3271,7 +3719,7 @@ var get = Ember.get, This function returns a map of unique dependencies for a given object and key. */ -function keysForDep(obj, depsMeta, depKey) { +function keysForDep(depsMeta, depKey) { var keys = depsMeta[depKey]; if (!keys) { // if there are no dependencies yet for a the given key @@ -3285,8 +3733,8 @@ function keysForDep(obj, depsMeta, depKey) { return keys; } -function metaForDeps(obj, meta) { - return keysForDep(obj, meta, 'deps'); +function metaForDeps(meta) { + return keysForDep(meta, 'deps'); } function addDependentKeys(desc, obj, keyName, meta) { @@ -3295,12 +3743,12 @@ function addDependentKeys(desc, obj, keyName, meta) { var depKeys = desc._dependentKeys, depsMeta, idx, len, depKey, keys; if (!depKeys) return; - depsMeta = metaForDeps(obj, meta); + depsMeta = metaForDeps(meta); for(idx = 0, len = depKeys.length; idx < len; idx++) { depKey = depKeys[idx]; // Lookup keys meta for depKey - keys = keysForDep(obj, depsMeta, depKey); + keys = keysForDep(depsMeta, depKey); // Increment the number of times depKey depends on keyName. keys[keyName] = (keys[keyName] || 0) + 1; // Watch the depKey @@ -3314,12 +3762,12 @@ function removeDependentKeys(desc, obj, keyName, meta) { var depKeys = desc._dependentKeys, depsMeta, idx, len, depKey, keys; if (!depKeys) return; - depsMeta = metaForDeps(obj, meta); + depsMeta = metaForDeps(meta); for(idx = 0, len = depKeys.length; idx < len; idx++) { depKey = depKeys[idx]; // Lookup keys meta for depKey - keys = keysForDep(obj, depsMeta, depKey); + keys = keysForDep(depsMeta, depKey); // Increment the number of times depKey depends on keyName. keys[keyName] = (keys[keyName] || 0) - 1; // Watch the depKey @@ -3350,24 +3798,15 @@ ComputedProperty.prototype = new Ember.Descriptor(); var ComputedPropertyPrototype = ComputedProperty.prototype; -/** - 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. - - ```javascript - MyApp.president = Ember.Object.create({ - fullName: function() { - return this.get('firstName') + ' ' + this.get('lastName'); +/* + Properties are cacheable by default. Computed property will automatically + cache the return value of your function until one of the dependent keys changes. - // 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') - }); - ``` + Call `volatile()` to set it into non-cached mode. When in this mode + the computed property will not automatically cache the return value. - Properties are cacheable by default. + However, if a property is properly observable, there is no reason to disable + caching. @method cacheable @param {Boolean} aFlag optional set to `false` to disable caching @@ -3631,7 +4070,6 @@ ComputedPropertyPrototype.teardown = function(obj, keyName) { 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. @@ -3669,7 +4107,7 @@ Ember.computed = function(func) { @param {Object} obj the object whose property you want to check @param {String} key the name of the property whose cached value you want to return - @return {any} the cached value + @return {*} the cached value */ Ember.cacheFor = function cacheFor(obj, key) { var cache = metaFor(obj, false).cache; @@ -3873,7 +4311,7 @@ registerComputedWithProperties('or', function(properties) { @for Ember @param {String} dependentKey, [dependentKey...] @return {Ember.ComputedProperty} computed property which returns - the first trouthy value of given list of properties. + the first truthy value of given list of properties. */ registerComputedWithProperties('any', function(properties) { for (var key in properties) { @@ -3923,6 +4361,48 @@ Ember.computed.alias = function(dependentKey) { }); }; +/** + @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. + + 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. + + ```javascript + User = Ember.Object.extend({ + firstName: null, + lastName: null, + nickName: Ember.computed.oneWay('firstName') + }); + + user = User.create({ + firstName: 'Teddy', + lastName: 'Zeenny' + }); + + user.get('nickName'); + # 'Teddy' + + user.set('nickName', 'TeddyBear'); + # 'TeddyBear' + + user.get('firstName'); + # 'Teddy' + ``` +*/ +Ember.computed.oneWay = function(dependentKey) { + return Ember.computed(dependentKey, function() { + return get(this, dependentKey); + }); +}; + + /** @method computed.defaultTo @for Ember @@ -3932,7 +4412,6 @@ Ember.computed.alias = function(dependentKey) { */ Ember.computed.defaultTo = function(defaultPath) { return Ember.computed(function(key, newValue, cachedValue) { - var result; if (arguments.length === 1) { return cachedValue != null ? cachedValue : get(this, defaultPath); } @@ -3945,567 +4424,611 @@ Ember.computed.defaultTo = function(defaultPath) { (function() { +// Ember.tryFinally /** @module ember-metal */ -var o_create = Ember.create, - metaFor = Ember.meta, - META_KEY = Ember.META_KEY; - -/* - 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: - - // Object's meta hash - { - listeners: { // variable name: `listenerSet` - "foo:changed": [ // variable name: `actions` - [target, method, onceFlag, suspendedFlag] - ] - } - } - -*/ - -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; } - } - return index; -} - -function actionsFor(obj, eventName) { - var meta = metaFor(obj, true), - actions; - - if (!meta.listeners) { meta.listeners = {}; } - - if (!meta.hasOwnProperty('listeners')) { - // setup inherited copy of the listeners object - meta.listeners = o_create(meta.listeners); - } - - actions = meta.listeners[eventName]; - - // 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] = []; - } - - return actions; -} - -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 - 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); +var AFTER_OBSERVERS = ':change'; +var BEFORE_OBSERVERS = ':before'; - if (actionIndex === -1) { - otherActions.push([target, method, once, suspended]); - } - } +function changeEvent(keyName) { + return keyName+AFTER_OBSERVERS; } -function actionsDiff(obj, eventName, otherActions) { - var meta = obj[META_KEY], - actions = meta && meta.listeners && meta.listeners[eventName], - diffActions = []; - - 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); - - if (actionIndex !== -1) { continue; } - - otherActions.push([target, method, once, suspended]); - diffActions.push([target, method, once, suspended]); - } - - return diffActions; +function beforeEvent(keyName) { + return keyName+BEFORE_OBSERVERS; } /** - Add an event listener - - @method addListener - @for Ember + @method addObserver @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 + @param {String} path + @param {Object|Function} targetOrMethod + @param {Function|String} [method] */ -function addListener(obj, eventName, target, method, once) { - Ember.assert("You must pass at least an object and event name to Ember.addListener", !!obj && !!eventName); - - if (!method && 'function' === typeof target) { - method = target; - target = null; - } - - var actions = actionsFor(obj, eventName), - actionIndex = indexOf(actions, target, method); - - if (actionIndex !== -1) { return; } - - actions.push([target, method, once, undefined]); +Ember.addObserver = function(obj, path, target, method) { + Ember.addListener(obj, changeEvent(path), target, method); + Ember.watch(obj, path); + return this; +}; - if ('function' === typeof obj.didAddListener) { - obj.didAddListener(eventName, target, method); - } -} +Ember.observersFor = function(obj, path) { + return Ember.listenersFor(obj, changeEvent(path)); +}; /** - Remove an event listener - - Arguments should match those passed to {{#crossLink "Ember/addListener"}}{{/crossLink}} - - @method removeListener - @for Ember + @method removeObserver @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 {String} path + @param {Object|Function} targetOrMethod + @param {Function|String} [method] */ -function removeListener(obj, eventName, target, method) { - Ember.assert("You must pass at least an object and event name to Ember.removeListener", !!obj && !!eventName); - - if (!method && 'function' === typeof target) { - method = target; - target = null; - } - - function _removeListener(target, method, once) { - var actions = actionsFor(obj, eventName), - actionIndex = indexOf(actions, target, method); - - // action doesn't exist, give up silently - if (actionIndex === -1) { return; } +Ember.removeObserver = function(obj, path, target, method) { + Ember.unwatch(obj, path); + Ember.removeListener(obj, changeEvent(path), target, method); + return this; +}; - actions.splice(actionIndex, 1); +/** + @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; +}; - if ('function' === typeof obj.didRemoveListener) { - obj.didRemoveListener(eventName, target, method); - } - } +// 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); +}; - if (method) { - _removeListener(target, method); - } else { - var meta = obj[META_KEY], - actions = meta && meta.listeners && meta.listeners[eventName]; +Ember._suspendObserver = function(obj, path, target, method, callback) { + return Ember._suspendListener(obj, changeEvent(path), target, method, callback); +}; - if (!actions) { return; } - for (var i = actions.length - 1; i >= 0; i--) { - _removeListener(actions[i][0], actions[i][1]); - } - } -} +var map = Ember.ArrayPolyfills.map; -/** - @private +Ember._suspendBeforeObservers = function(obj, paths, target, method, callback) { + var events = map.call(paths, beforeEvent); + return Ember._suspendListeners(obj, events, target, method, callback); +}; - Suspend listener during callback. +Ember._suspendObservers = function(obj, paths, target, method, callback) { + var events = map.call(paths, changeEvent); + return Ember._suspendListeners(obj, events, target, method, callback); +}; - 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. +Ember.beforeObserversFor = function(obj, path) { + return Ember.listenersFor(obj, beforeEvent(path)); +}; - @method suspendListener - @for Ember +/** + @method removeBeforeObserver @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 + @param {String} path + @param {Object|Function} targetOrMethod + @param {Function|String} [method] */ -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; - - 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 - } +Ember.removeBeforeObserver = function(obj, path, target, method) { + Ember.unwatch(obj, path); + Ember.removeListener(obj, beforeEvent(path), target, method); + return this; +}; +})(); - function tryable() { return callback.call(target); } - function finalizer() { if (action) { action[3] = undefined; } } - return Ember.tryFinally(tryable, finalizer); -} -/** - @private +(function() { +define("backburner", + ["backburner/deferred_action_queues","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var DeferredActionQueues = __dependency1__.DeferredActionQueues; + + var slice = [].slice, + pop = [].pop, + debouncees = [], + timers = [], + autorun, laterTimer, laterTimerExpiresAt; + + function Backburner(queueNames, options) { + this.queueNames = queueNames; + this.options = options || {}; + if (!this.options.defaultQueue) { + this.options.defaultQueue = queueNames[0]; + } + this.instanceStack = []; + } - Suspend listener during callback. + Backburner.prototype = { + queueNames: null, + options: null, + currentInstance: null, + instanceStack: null, - 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. + begin: function() { + var onBegin = this.options && this.options.onBegin, + previousInstance = this.currentInstance; - @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 -*/ -function suspendListeners(obj, eventNames, target, method, callback) { - if (!method && 'function' === typeof target) { - method = target; - target = null; - } + if (previousInstance) { + this.instanceStack.push(previousInstance); + } - var suspendedActions = [], - eventName, actions, action, i, l; + this.currentInstance = new DeferredActionQueues(this.queueNames, this.options); + if (onBegin) { + onBegin(this.currentInstance, previousInstance); + } + }, - for (i=0, l=eventNames.length; i 2) { + ret = method.apply(target, slice.call(arguments, 2)); + } else { + ret = method.call(target); + } + } finally { + if (!finallyAlreadyCalled) { + finallyAlreadyCalled = true; + this.end(); + } + } + return ret; + }, - if (listeners) { - for(var eventName in listeners) { - if (listeners[eventName]) { ret.push(eventName); } - } - } - return ret; -} + defer: function(queueName, target, method /* , args */) { + if (!method) { + method = target; + target = null; + } -/** - @method sendEvent - @for Ember - @param obj - @param {String} eventName - @param {Array} params - @param {Array} actions - @return true -*/ -function sendEvent(obj, eventName, params, actions) { - // first give object a chance to handle it - if (obj !== Ember && 'function' === typeof obj.sendEvent) { - obj.sendEvent(eventName, params); - } + if (typeof method === 'string') { + method = target[method]; + } - if (!actions) { - var meta = obj[META_KEY]; - actions = meta && meta.listeners && meta.listeners[eventName]; - } + 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); + }, - if (!actions) { return; } + deferOnce: function(queueName, target, method /* , args */) { + if (!method) { + method = target; + target = null; + } - for (var i = actions.length - 1; i >= 0; i--) { // looping in reverse for once listeners - if (!actions[i] || actions[i][3] === true) { continue; } + if (typeof method === 'string') { + method = target[method]; + } - var target = actions[i][0], - method = actions[i][1], - once = actions[i][2]; + 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); + }, - 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.call(target); - } - } - return true; -} + setTimeout: function() { + var self = this, + wait = pop.call(arguments), + target = arguments[0], + method = arguments[1], + executeAt = (+new Date()) + wait; -/** - @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]; + if (!method) { + method = target; + target = null; + } - return !!(actions && actions.length); -} + if (typeof method === 'string') { + method = target[method]; + } -/** - @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 fn, args; + if (arguments.length > 2) { + args = slice.call(arguments, 2); - if (!actions) { return ret; } + fn = function() { + method.apply(target, args); + }; + } else { + fn = function() { + method.call(target); + }; + } - for (var i = 0, l = actions.length; i < l; i++) { - var target = actions[i][0], - method = actions[i][1]; - ret.push([target, method]); - } + // 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; } + } - return ret; -} + timers.splice(i, 0, executeAt, fn); -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; + if (laterTimer && laterTimerExpiresAt < executeAt) { return fn; } -})(); + if (laterTimer) { + clearTimeout(laterTimer); + laterTimer = null; + } + laterTimer = window.setTimeout(function() { + executeTimers(self); + laterTimer = null; + laterTimerExpiresAt = null; + }, wait); + laterTimerExpiresAt = executeAt; + + return fn; + }, + debounce: function(target, method /* , args, wait */) { + var self = this, + args = arguments, + wait = pop.call(args), + debouncee; + for (var i = 0, l = debouncees.length; i < l; i++) { + debouncee = debouncees[i]; + if (debouncee[0] === target && debouncee[1] === method) { return; } // do nothing + } -(function() { -// Ember.Logger -// Ember.watch.flushPending -// Ember.beginPropertyChanges, Ember.endPropertyChanges -// Ember.guidFor, Ember.tryFinally + var timer = window.setTimeout(function() { + self.run.apply(self, args); -/** -@module ember-metal -*/ + // remove debouncee + var index = -1; + for (var i = 0, l = debouncees.length; i < l; i++) { + debouncee = debouncees[i]; + if (debouncee[0] === target && debouncee[1] === method) { + index = i; + break; + } + } -// .......................................................... -// HELPERS -// + if (index > -1) { debouncees.splice(index, 1); } + }, wait); -var slice = [].slice, - forEach = Ember.ArrayPolyfills.forEach; + debouncees.push([target, method, timer]); + }, -// invokes passed params - normalizing so you can pass target/func, -// target/string or just func -function invoke(target, method, args, ignore) { + cancelTimers: function() { + for (var i = 0, l = debouncees.length; i < l; i++) { + clearTimeout(debouncees[i][2]); + } + debouncees = []; - if (method === undefined) { - method = target; - target = undefined; - } + if (laterTimer) { + clearTimeout(laterTimer); + laterTimer = null; + } + timers = []; - if ('string' === typeof method) { method = target[method]; } - if (args && ignore > 0) { - args = args.length > ignore ? slice.call(args, ignore) : null; - } + if (autorun) { + clearTimeout(autorun); + autorun = null; + } + }, - return Ember.handleErrors(function() { - // IE8's Function.prototype.apply doesn't accept undefined/null arguments. - return method.apply(target || this, args || []); - }, this); -} + hasTimers: function() { + return !!timers.length || autorun; + }, + cancel: function(timer) { + if (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; + } + } + } + } + }; -// .......................................................... -// RUNLOOP -// + Backburner.prototype.schedule = Backburner.prototype.defer; + Backburner.prototype.scheduleOnce = Backburner.prototype.deferOnce; + Backburner.prototype.later = Backburner.prototype.setTimeout; -var timerMark; // used by timers... + function createAutorun(backburner) { + backburner.begin(); + autorun = window.setTimeout(function() { + backburner.end(); + autorun = null; + }); + } -/** -Ember RunLoop (Private) + function executeTimers(self) { + var now = +new Date(), + time, fns, i, l; -@class RunLoop -@namespace Ember -@private -@constructor -*/ -var RunLoop = function(prev) { - this._prev = prev || null; - this.onceTimers = {}; -}; + self.run(function() { + // TODO: binary search + for (i = 0, l = timers.length; i < l; i += 2) { + time = timers[i]; + if (time > now) { break; } + } -RunLoop.prototype = { - /** - @method end - */ - end: function() { - this.flush(); - }, + fns = timers.splice(0, i); - /** - @method prev - */ - prev: function() { - return this._prev; - }, + for (i = 1, l = fns.length; i < l; i += 2) { + self.schedule(self.options.defaultQueue, null, fns[i]); + } + }); - // .......................................................... - // Delayed Actions - // + if (timers.length) { + laterTimer = window.setTimeout(function() { + executeTimers(self); + laterTimer = null; + laterTimerExpiresAt = null; + }, timers[0] - now); + laterTimerExpiresAt = timers[0]; + } + } - /** - @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] = []; } - var args = arguments.length > 3 ? slice.call(arguments, 3) : null; - queue.push({ target: target, method: method, args: args }); - return this; - }, + __exports__.Backburner = Backburner; + }); - /** - @method flush - @param {String} queueName - */ - flush: function(queueName) { - var queueNames, idx, len, queue, log; +define("backburner/deferred_action_queues", + ["backburner/queue","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Queue = __dependency1__.Queue; - if (!this._queues) { return this; } // nothing to do + function DeferredActionQueues(queueNames, options) { + var queues = this.queues = {}; + this.queueNames = queueNames = queueNames || []; - function iter(item) { - invoke(item.target, item.method, item.args); + var queueName; + for (var i = 0, l = queueNames.length; i < l; i++) { + queueName = queueNames[i]; + queues[queueName] = new Queue(this, queueName, options[queueName]); + } } - function tryable() { - forEach.call(queue, iter); - } + DeferredActionQueues.prototype = { + queueNames: null, + queues: null, - Ember.watch.flushPending(); // make sure all chained watchers are setup + schedule: function(queueName, target, method, args, onceFlag, stack) { + var queues = this.queues, + queue = queues[queueName]; - if (queueName) { - while (this._queues && (queue = this._queues[queueName])) { - this._queues[queueName] = null; + if (!queue) { throw new Error("You attempted to schedule an action in a queue (" + queueName + ") that doesn't exist"); } - // 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'); } + if (onceFlag) { + return queue.pushUnique(target, method, args, stack); + } else { + return queue.push(target, method, args, stack); + } + }, - Ember.beginPropertyChanges(); + 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._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]; } + + // TODO: error handling + if (args && args.length > 0) { + method.apply(target, args); + } else { + method.call(target); + } - Ember.tryFinally(tryable, Ember.endPropertyChanges); + queueIndex += 4; + } + if (numberOfQueueItems && after) { after(); } - if (log) { Ember.Logger.log('End: Flush Sync Queue'); } + if ((priorQueueNameIndex = indexOfPriorQueueWithActions(this, queueNameIndex)) !== -1) { + queueNameIndex = priorQueueNameIndex; + continue outerloop; + } - } else { - forEach.call(queue, iter); + queueNameIndex++; } } + }; + + 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; } + } - } else { - queueNames = Ember.run.queues; - len = queueNames.length; - idx = 0; + return -1; + } - outerloop: - while (idx < len) { - queueName = queueNames[idx]; - queue = this._queues && this._queues[queueName]; - delete this._queues[queueName]; + __exports__.DeferredActionQueues = DeferredActionQueues; + }); - 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'); } +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}; + }, - Ember.beginPropertyChanges(); + pushUnique: function(target, method, args, stack) { + var queue = this._queue, currentTarget, currentMethod, i, l; - Ember.tryFinally(tryable, Ember.endPropertyChanges); + for (i = 0, l = queue.length; i < l; i += 4) { + currentTarget = queue[i]; + currentMethod = queue[i+1]; - if (log) { Ember.Logger.log('End: Flush Sync Queue'); } - } else { - forEach.call(queue, iter); + 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 } } - // 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; + this._queue.push(target, method, args, stack); + return {queue: this, target: target, method: method}; + }, + + // 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(); } + + // check if new items have been added + if (queue.length > l) { + this._queue = queue.slice(l); + this.flush(); + } else { + this._queue.length = 0; + } + }, + + cancel: function(actionToCancel) { + var queue = this._queue, currentTarget, currentMethod, i, l; + + for (i = 0, l = queue.length; i < l; i += 4) { + currentTarget = queue[i]; + currentMethod = queue[i+1]; - idx++; + if (currentTarget === actionToCancel.target && currentMethod === actionToCancel.method) { + queue.splice(i, 4); + return true; + } + } } - } + }; - timerMark = null; + __exports__.Queue = Queue; + }); - return this; - } +})(); + + +(function() { +var onBegin = function(current) { + Ember.run.currentRunLoop = current; +}; + +var onEnd = function(current, next) { + Ember.run.currentRunLoop = next; }; -Ember.RunLoop = RunLoop; +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; // .......................................................... // Ember.run - this is ideally the only public API the dev sees @@ -4539,20 +5062,76 @@ Ember.RunLoop = RunLoop; @return {Object} return value from invoking the passed function. */ Ember.run = function(target, method) { - var args = arguments; - run.begin(); + var ret; - function tryable() { - if (target || method) { - return invoke(target, method, args, 2); + if (Ember.onerror) { + try { + ret = backburner.run.apply(backburner, arguments); + } catch (e) { + Ember.onerror(e); } + } else { + ret = backburner.run.apply(backburner, arguments); + } + + return ret; +}; + +/** + + 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. + + Please note: This is not for normal usage, and should be used sparingly. + + If invoked when not within a run loop: + + ```javascript + Ember.run.join(function(){ + // creates a new run-loop + }); + ``` + + Alternatively, if called within an existing run loop: + + ```javascript + Ember.run(function(){ + // creates a new run-loop + Ember.run.join(function(){ + // joins with the existing run-loop, and queues for invocation on + // the existing run-loops action queue. + }); + }); + ``` + + @method join + @namespace Ember + @param {Object} [target] target of method to call + @param {Function|String} method Method to invoke. + May be a function or a string. If you pass a string + then it will be looked up on the passed target. + @param {Object} [args*] Any additional arguments you wish to pass to the method. + @return {Object} return value from invoking the passed function. Please note, + when called within an existing loop, no return value is possible. +*/ +Ember.run.join = function(target, method) { + if (!Ember.run.currentRunLoop) { + return Ember.run.apply(Ember.run, arguments); } - return Ember.tryFinally(tryable, run.end); + var args = slice.call(arguments); + args.unshift('actions'); + Ember.run.schedule.apply(Ember.run, args); }; +Ember.run.backburner = backburner; + var run = Ember.run; +Ember.run.currentRunLoop = null; + +Ember.run.queues = backburner.queueNames; /** Begins a new RunLoop. Any deferred actions invoked after the begin will @@ -4569,7 +5148,7 @@ var run = Ember.run; @return {void} */ Ember.run.begin = function() { - run.currentRunLoop = new RunLoop(run.currentRunLoop); + backburner.begin(); }; /** @@ -4587,12 +5166,7 @@ Ember.run.begin = function() { @return {void} */ Ember.run.end = function() { - Ember.assert('must have a current run loop', run.currentRunLoop); - - function tryable() { run.currentRunLoop.end(); } - function finalizer() { run.currentRunLoop = run.currentRunLoop.prev(); } - - Ember.tryFinally(tryable, finalizer); + backburner.end(); }; /** @@ -4605,7 +5179,6 @@ Ember.run.end = function() { @type Array @default ['sync', 'actions', 'destroy'] */ -Ember.run.queues = ['sync', 'actions', 'destroy']; /** Adds the passed target/method and any optional arguments to the named @@ -4615,7 +5188,7 @@ Ember.run.queues = ['sync', 'actions', 'destroy']; 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. + the `Ember.run.queues` property. ```javascript Ember.run.schedule('sync', this, function(){ @@ -4644,57 +5217,18 @@ Ember.run.queues = ['sync', 'actions', 'destroy']; @return {void} */ Ember.run.schedule = function(queue, target, method) { - var loop = run.autorun(); - loop.schedule.apply(loop, arguments); + checkAutoRun(); + backburner.schedule.apply(backburner, arguments); }; -var scheduledAutorun; -function autorun() { - scheduledAutorun = null; - if (run.currentRunLoop) { run.end(); } -} - // Used by global test teardown Ember.run.hasScheduledTimers = function() { - return !!(scheduledAutorun || scheduledLater); + return backburner.hasTimers(); }; // Used by global test teardown Ember.run.cancelTimers = function () { - if (scheduledAutorun) { - clearTimeout(scheduledAutorun); - scheduledAutorun = null; - } - if (scheduledLater) { - clearTimeout(scheduledLater); - scheduledLater = null; - } - timers = {}; -}; - -/** - 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()` - - @method autorun - @example - Ember.run.autorun(); - @return {Ember.RunLoop} the new current RunLoop -*/ -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); - - run.begin(); - - if (!scheduledAutorun) { - scheduledAutorun = setTimeout(autorun, 1); - } - } - - return run.currentRunLoop; + backburner.cancelTimers(); }; /** @@ -4714,42 +5248,9 @@ Ember.run.autorun = function() { @return {void} */ Ember.run.sync = function() { - run.autorun(); - run.currentRunLoop.flush('sync'); + backburner.currentInstance.queues.sync.flush(); }; -// .......................................................... -// TIMERS -// - -var timers = {}; // active timers... - -var scheduledLater, scheduledLaterExpires; -function invokeLaterTimers() { - scheduledLater = null; - run(function() { - 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; } - } - } - } - - // schedule next timeout to fire when the earliest timer expires - if (earliest > 0) { - scheduledLater = setTimeout(invokeLaterTimers, earliest - now); - scheduledLaterExpires = earliest; - } - }); -} - /** 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 @@ -4774,80 +5275,35 @@ function invokeLaterTimers() { @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. + `Ember.run.cancel` later. */ 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(); - } - - expires = (+ new Date()) + wait; - timer = { target: target, method: method, expires: expires, args: args }; - guid = Ember.guidFor(timer); - timers[guid] = timer; - - if(scheduledLater && expires < scheduledLaterExpires) { - // Cancel later timer (then reschedule earlier timer below) - clearTimeout(scheduledLater); - scheduledLater = null; - } - - if (!scheduledLater) { - // Schedule later timers to be run. - scheduledLater = setTimeout(invokeLaterTimers, wait); - scheduledLaterExpires = expires; - } - - return guid; + return backburner.later.apply(backburner, arguments); }; -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; - - if (guid && timers[guid]) { - timers[guid].args = args; // replace args - } else { - timer = { - target: target, - method: method, - args: args, - tguid: tguid, - mguid: mguid - }; - - guid = Ember.guidFor(timer); - timers[guid] = timer; - if (!onceTimers[tguid]) { onceTimers[tguid] = {}; } - onceTimers[tguid][mguid] = guid; // so it isn't scheduled more than once - - run.schedule(queue, timer, invokeOnceTimer, guid, onceTimers); - } +/** + Schedule a function to run one time during the current RunLoop. This is equivalent + to calling `scheduleOnce` with the "actions" queue. - return guid; -} + @method once + @param {Object} [target] The target of the 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) { + checkAutoRun(); + var args = slice.call(arguments); + args.unshift('actions'); + return backburner.scheduleOnce.apply(backburner, args); +}; /** - Schedules an item to run one time during the current RunLoop. Calling - this method with the same target/method combination will have no effect. + Schedules a function to run one time in a given queue of the current RunLoop. + Calling this method with the same queue/target/method combination will have + no effect (past the initial call). Note that although you can pass optional arguments these will not be considered when looking for duplicates. New arguments will replace previous @@ -4855,47 +5311,47 @@ function scheduleOnce(queue, target, method, args) { ```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 sayHi = function() { console.log('hi'); } + Ember.run.scheduleOnce('afterRender', myContext, sayHi); + Ember.run.scheduleOnce('afterRender', myContext, sayHi); + // doFoo will only be executed once, in the afterRender queue of the RunLoop }); ``` - Also note that passing an anonymous function to `Ember.run.once` will + Also note that passing an anonymous function to `Ember.run.scheduleOnce` will not prevent additional calls with an identical anonymous function from scheduling the items multiple times, e.g.: ```javascript function scheduleIt() { - Ember.run.once(myContext, function() { console.log("Closure"); }); + Ember.run.scheduleOnce('actions', myContext, function() { console.log("Closure"); }); } scheduleIt(); scheduleIt(); - // "Closure" will print twice, even though we're using `Ember.run.once`, + // "Closure" will print twice, even though we're using `Ember.run.scheduleOnce`, // because the function we pass to it is anonymous and won't match the // previously scheduled operation. ``` - @method once - @param {Object} [target] target of method to invoke + Available queues, and their order, can be found at `Ember.run.queues` + + @method scheduleOnce + @param {String} [queue] The name of the queue to schedule against. Default queues are 'sync' and 'actions'. + @param {Object} [target] The target of the 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)); +Ember.run.scheduleOnce = function(queue, target, method) { + checkAutoRun(); + return backburner.scheduleOnce.apply(backburner, arguments); }; /** - Schedules an item to run from within a separate run loop, after - control has been returned to the system. This is equivalent to calling + Schedules an item to run from within a separate run loop, after + control has been returned to the system. This is equivalent to calling `Ember.run.later` with a wait time of 1ms. ```javascript @@ -4907,7 +5363,7 @@ Ember.run.scheduleOnce = function(queue, target, method, args) { Multiple operations scheduled with `Ember.run.next` will coalesce into the same later run loop, along with any other operations scheduled by `Ember.run.later` that expire right around the same - time that `Ember.run.next` operations will fire. + time that `Ember.run.next` operations will fire. Note that there are often alternatives to using `Ember.run.next`. For instance, if you'd like to schedule an operation to happen @@ -4933,13 +5389,13 @@ Ember.run.scheduleOnce = function(queue, target, method, args) { One benefit of the above approach compared to using `Ember.run.next` is that you will be able to perform DOM/CSS operations before unprocessed - elements are rendered to the screen, which may prevent flickering or + elements are rendered to the screen, which may prevent flickering or other artifacts caused by delaying processing until after rendering. - The other major benefit to the above approach is that `Ember.run.next` - introduces an element of non-determinism, which can make things much - harder to test, due to its reliance on `setTimeout`; it's much harder - to guarantee the order of scheduled operations when they are scheduled + The other major benefit to the above approach is that `Ember.run.next` + introduces an element of non-determinism, which can make things much + harder to test, due to its reliance on `setTimeout`; it's much harder + to guarantee the order of scheduled operations when they are scheduled outside of the current run loop, i.e. with `Ember.run.next`. @method next @@ -4952,8 +5408,8 @@ Ember.run.scheduleOnce = function(queue, target, method, args) { */ Ember.run.next = function() { var args = slice.call(arguments); - args.push(1); // 1 millisecond wait - return run.later.apply(this, args); + args.push(1); + return backburner.later.apply(backburner, args); }; /** @@ -4982,17 +5438,25 @@ Ember.run.next = function() { @return {void} */ Ember.run.cancel = function(timer) { - delete timers[timer]; + return backburner.cancel(timer); }; +// 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); + } +} + })(); (function() { // Ember.Logger -// get, set, trySet -// guidFor, isArray, meta +// get +// set +// guidFor, meta // addObserver, removeObserver // Ember.run.schedule /** @@ -5018,8 +5482,21 @@ Ember.LOG_BINDINGS = false || !!Ember.ENV.LOG_BINDINGS; var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, - isGlobalPath = Ember.isGlobalPath; + IS_GLOBAL = /^([A-Z$]|([0-9][A-Z$]))/; + +/** + Returns true if the provided path is global (e.g., `MyApp.fooController.bar`) + instead of local (`foo.bar.baz`). + @method isGlobalPath + @for Ember + @private + @param {String} path + @return Boolean +*/ +var isGlobalPath = Ember.isGlobalPath = function(path) { + return IS_GLOBAL.test(path); +}; function getWithGlobals(obj, path) { return get(isGlobalPath(path) ? Ember.lookup : obj, path); @@ -5255,7 +5732,7 @@ function mixinProperties(to, from) { mixinProperties(Binding, { /** - See {{#crossLink "Ember.Binding/from"}}{{/crossLink}} + See `Ember.Binding.from`. @method from @static @@ -5266,7 +5743,7 @@ mixinProperties(Binding, { }, /** - See {{#crossLink "Ember.Binding/to"}}{{/crossLink}} + See `Ember.Binding.to`. @method to @static @@ -5283,7 +5760,7 @@ mixinProperties(Binding, { This means that if you change the "to" side directly, the "from" side may have a different value. - See {{#crossLink "Binding/oneWay"}}{{/crossLink}} + See `Binding.oneWay`. @method oneWay @param {String} from from path. @@ -5311,7 +5788,7 @@ mixinProperties(Binding, { 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"): + (see "One Way Bindings"): ``` valueBinding: "MyApp.someController.title" @@ -5344,7 +5821,7 @@ mixinProperties(Binding, { 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). + to monitor it for changes (such as in the example above). ## Adding Bindings Manually @@ -5611,7 +6088,7 @@ function addNormalizedProperty(base, key, value, meta, descs, values, concats) { } } -function mergeMixins(mixins, m, descs, values, base) { +function mergeMixins(mixins, m, descs, values, base, keys) { var mixin, props, key, concats, meta; function removeKeys(keyName) { @@ -5632,26 +6109,19 @@ function mergeMixins(mixins, m, descs, values, base) { for (key in props) { if (!props.hasOwnProperty(key)) { continue; } + keys.push(key); addNormalizedProperty(base, key, props[key], meta, descs, values, concats); } // manually copy toString() because some JS engines do not enumerate it if (props.hasOwnProperty('toString')) { base.toString = props.toString; } } else if (mixin.mixins) { - mergeMixins(mixin.mixins, m, descs, values, base); + mergeMixins(mixin.mixins, m, descs, values, base, keys); if (mixin._without) { a_forEach.call(mixin._without, removeKeys); } } } } -function writableReq(obj) { - var m = Ember.meta(obj), req = m.required; - if (!req || !m.hasOwnProperty('required')) { - req = m.required = req ? o_create(req) : {}; - } - return req; -} - var IS_BINDING = Ember.IS_BINDING = /^.+Binding$/; function detectBinding(obj, key, value, m) { @@ -5734,7 +6204,7 @@ function replaceObservers(obj, key, observer) { function applyMixin(obj, mixins, partial) { var descs = {}, values = {}, m = Ember.meta(obj), - key, value, desc; + key, value, desc, keys = []; // Go through all mixins and hashes passed in, and: // @@ -5742,10 +6212,11 @@ function applyMixin(obj, mixins, partial) { // * Set up _super wrapping if necessary // * Set up computed property descriptors // * Copying `toString` in broken browsers - mergeMixins(mixins, mixinsMeta(obj), descs, values, obj); + mergeMixins(mixins, mixinsMeta(obj), descs, values, obj, keys); - for(key in values) { - if (key === 'contructor' || !values.hasOwnProperty(key)) { continue; } + for(var i = 0, l = keys.length; i < l; i++) { + key = keys[i]; + if (key === 'constructor' || !values.hasOwnProperty(key)) { continue; } desc = descs[key]; value = values[key]; @@ -5799,7 +6270,7 @@ Ember.mixin = function(obj) { }); // Mix mixins into classes by passing them as the first arguments to - // .extend or .create. + // .extend. App.CommentView = Ember.View.extend(App.Editable, { template: Ember.Handlebars.compile('{{#if isEditing}}...{{else}}...{{/if}}') }); @@ -5818,6 +6289,12 @@ Ember.Mixin = function() { return initMixin(this, arguments); }; Mixin = Ember.Mixin; +Mixin.prototype = { + properties: null, + mixins: null, + ownerConstructor: null +}; + Mixin._apply = applyMixin; Mixin.applyPartial = function(obj) { @@ -6042,7 +6519,7 @@ Ember.alias = function(methodName) { return new Alias(methodName); }; -Ember.deprecateFunc("Ember.alias is deprecated. Please use Ember.aliasMethod or Ember.computed.alias instead.", Ember.alias); +Ember.alias = Ember.deprecateFunc("Ember.alias is deprecated. Please use Ember.aliasMethod or Ember.computed.alias instead.", Ember.alias); /** Makes a method available via an additional name. @@ -6103,6 +6580,29 @@ Ember.immediateObserver = function() { }; /** + When observers fire, they are called with the arguments `obj`, `keyName` + and `value`. In a typical observer, value is the new, post-change value. + + A `beforeObserver` fires before a property changes. The `value` argument contains + the pre-change value. + + A `beforeObserver` is an alternative form of `.observesBefore()`. + + ```javascript + App.PersonView = Ember.View.extend({ + valueWillChange: function (obj, keyName, value) { + this.changingFrom = value; + }.observesBefore('content.value'), + valueDidChange: function(obj, keyName, value) { + // only run if updating a value already in the DOM + if(this.get('state') === 'inDOM') { + var color = value > this.changingFrom ? 'green' : 'red'; + // logic + } + }.observes('content.value') + }); + ``` + @method beforeObserver @for Ember @param {Function} func @@ -6123,21 +6623,64 @@ Ember.beforeObserver = function(func) { /** Ember Metal -@module ember -@submodule ember-metal -*/ +@module ember +@submodule ember-metal +*/ + +})(); + +(function() { +define("rsvp/all", + ["rsvp/defer","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var defer = __dependency1__.defer; + + function all(promises) { + var results = [], deferred = defer(), remaining = promises.length; + + if (remaining === 0) { + deferred.resolve([]); + } + + var resolver = function(index) { + return function(value) { + resolveAll(index, value); + }; + }; + + var resolveAll = function(index, value) { + results[index] = value; + if (--remaining === 0) { + deferred.resolve(results); + } + }; + + var rejectAll = function(error) { + deferred.reject(error); + }; -})(); + for (var i = 0; i < promises.length; i++) { + if (promises[i] && typeof promises[i].then === 'function') { + promises[i].then(resolver(i), rejectAll); + } else { + resolveAll(i, promises[i]); + } + } + return deferred.promise; + } -(function() { -define("rsvp", - [], - function() { + __exports__.all = all; + }); + +define("rsvp/async", + ["exports"], + function(__exports__) { "use strict"; var browserGlobal = (typeof window !== 'undefined') ? window : {}; - var MutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; - var RSVP, async; + var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; + var async; if (typeof process !== 'undefined' && {}.toString.call(process) === '[object process]') { @@ -6146,10 +6689,10 @@ define("rsvp", callback.call(binding); }); }; - } else if (MutationObserver) { + } else if (BrowserMutationObserver) { var queue = []; - var observer = new MutationObserver(function() { + var observer = new BrowserMutationObserver(function() { var toProcess = queue.slice(); queue = []; @@ -6180,6 +6723,47 @@ define("rsvp", }; } + + __exports__.async = async; + }); + +define("rsvp/config", + ["rsvp/async","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var async = __dependency1__.async; + + var config = {}; + config.async = async; + + __exports__.config = config; + }); + +define("rsvp/defer", + ["rsvp/promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__.Promise; + + function defer() { + var deferred = {}; + + var promise = new Promise(function(resolve, reject) { + deferred.resolve = resolve; + deferred.reject = reject; + }); + + deferred.promise = promise; + return deferred; + } + + __exports__.defer = defer; + }); + +define("rsvp/events", + ["exports"], + function(__exports__) { + "use strict"; var Event = function(type, options) { this.type = type; @@ -6274,7 +6858,148 @@ define("rsvp", } }; - var Promise = function() { + + __exports__.EventTarget = EventTarget; + }); + +define("rsvp/hash", + ["rsvp/defer","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var defer = __dependency1__.defer; + + function size(object) { + var size = 0; + + for (var prop in object) { + size++; + } + + return size; + } + + function hash(promises) { + var results = {}, deferred = defer(), remaining = size(promises); + + if (remaining === 0) { + deferred.resolve({}); + } + + var resolver = function(prop) { + return function(value) { + resolveAll(prop, value); + }; + }; + + var resolveAll = function(prop, value) { + results[prop] = value; + if (--remaining === 0) { + deferred.resolve(results); + } + }; + + var rejectAll = function(error) { + deferred.reject(error); + }; + + for (var prop in promises) { + if (promises[prop] && typeof promises[prop].then === 'function') { + promises[prop].then(resolver(prop), rejectAll); + } else { + resolveAll(prop, promises[prop]); + } + } + + return deferred.promise; + } + + __exports__.hash = hash; + }); + +define("rsvp/node", + ["rsvp/promise","rsvp/all","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var Promise = __dependency1__.Promise; + var all = __dependency2__.all; + + function makeNodeCallbackFor(resolve, reject) { + return function (error, value) { + if (error) { + reject(error); + } else if (arguments.length > 2) { + resolve(Array.prototype.slice.call(arguments, 1)); + } else { + resolve(value); + } + }; + } + + function denodeify(nodeFunc) { + return function() { + var nodeArgs = Array.prototype.slice.call(arguments), resolve, reject; + + var promise = new Promise(function(nodeResolve, nodeReject) { + resolve = nodeResolve; + reject = nodeReject; + }); + + all(nodeArgs).then(function(nodeArgs) { + nodeArgs.push(makeNodeCallbackFor(resolve, reject)); + + try { + nodeFunc.apply(this, nodeArgs); + } catch(e) { + reject(e); + } + }); + + 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 isFunction(x){ + return typeof x === "function"; + } + + var Promise = function(resolver) { + var promise = this, + resolved = false; + + if (typeof resolver !== 'function') { + throw new TypeError('You must pass a resolver function as the sole argument to the promise constructor'); + } + + if (!(promise instanceof Promise)) { + return new Promise(resolver); + } + + var resolvePromise = function(value) { + if (resolved) { return; } + resolved = true; + resolve(promise, value); + }; + + var rejectPromise = function(value) { + if (resolved) { return; } + resolved = true; + reject(promise, value); + }; + this.on('promise:resolved', function(event) { this.trigger('success', { detail: event.detail }); }, this); @@ -6282,12 +7007,16 @@ define("rsvp", this.on('promise:failed', function(event) { this.trigger('error', { detail: event.detail }); }, this); - }; - var noop = function() {}; + try { + resolver(resolvePromise, rejectPromise); + } catch(e) { + rejectPromise(e); + } + }; var invokeCallback = function(type, promise, callback, event) { - var hasCallback = typeof callback === 'function', + var hasCallback = isFunction(callback), value, error, succeeded, failed; if (hasCallback) { @@ -6303,34 +7032,34 @@ define("rsvp", succeeded = true; } - if (value && typeof value.then === 'function') { - value.then(function(value) { - promise.resolve(value); - }, function(error) { - promise.reject(error); - }); + if (handleThenable(promise, value)) { + return; } else if (hasCallback && succeeded) { - promise.resolve(value); + resolve(promise, value); } else if (failed) { - promise.reject(error); - } else { - promise[type](value); + reject(promise, error); + } else if (type === 'resolve') { + resolve(promise, value); + } else if (type === 'reject') { + reject(promise, value); } }; Promise.prototype = { + constructor: Promise, + then: function(done, fail) { - var thenPromise = new Promise(); + var thenPromise = new Promise(function() {}); - if (this.isResolved) { - RSVP.async(function() { - invokeCallback('resolve', thenPromise, done, { detail: this.resolvedValue }); + if (this.isFulfilled) { + config.async(function() { + invokeCallback('resolve', thenPromise, done, { detail: this.fulfillmentValue }); }, this); } if (this.isRejected) { - RSVP.async(function() { - invokeCallback('reject', thenPromise, fail, { detail: this.rejectedValue }); + config.async(function() { + invokeCallback('reject', thenPromise, fail, { detail: this.rejectedReason }); }, this); } @@ -6343,75 +7072,162 @@ define("rsvp", }); return thenPromise; - }, + } + }; - resolve: function(value) { - resolve(this, value); + EventTarget.mixin(Promise.prototype); - this.resolve = noop; - this.reject = noop; - }, + function resolve(promise, value) { + if (promise === value) { + fulfill(promise, value); + } else if (!handleThenable(promise, value)) { + fulfill(promise, value); + } + } - reject: function(value) { - reject(this, value); + function handleThenable(promise, value) { + var then = null; + + if (objectOrFunction(value)) { + try { + then = value.then; + } catch(e) { + reject(promise, e); + return true; + } - this.resolve = noop; - this.reject = noop; + if (isFunction(then)) { + try { + then.call(value, function(val) { + if (value !== val) { + resolve(promise, val); + } else { + fulfill(promise, val); + } + }, function(val) { + reject(promise, val); + }); + } catch (e) { + reject(promise, e); + } + return true; + } } - }; - function resolve(promise, value) { - RSVP.async(function() { + return false; + } + + function fulfill(promise, value) { + config.async(function() { promise.trigger('promise:resolved', { detail: value }); - promise.isResolved = true; - promise.resolvedValue = value; + promise.isFulfilled = true; + promise.fulfillmentValue = value; }); } function reject(promise, value) { - RSVP.async(function() { + config.async(function() { promise.trigger('promise:failed', { detail: value }); promise.isRejected = true; - promise.rejectedValue = value; + promise.rejectedReason = value; }); } - function all(promises) { - var i, results = []; - var allPromise = new Promise(); - var remaining = promises.length; - if (remaining === 0) { - allPromise.resolve([]); - } + __exports__.Promise = Promise; + }); - var resolver = function(index) { - return function(value) { - resolve(index, value); - }; - }; +define("rsvp/reject", + ["rsvp/promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__.Promise; - var resolve = function(index, value) { - results[index] = value; - if (--remaining === 0) { - allPromise.resolve(results); - } - }; - var reject = function(error) { - allPromise.reject(error); - }; + function objectOrFunction(x) { + return typeof x === "function" || (typeof x === "object" && x !== null); + } - for (i = 0; i < remaining; i++) { - promises[i].then(resolver(i), reject); - } - return allPromise; + + function reject(reason) { + return new Promise(function (resolve, reject) { + reject(reason); + }); } - EventTarget.mixin(Promise.prototype); - RSVP = { async: async, Promise: Promise, Event: Event, EventTarget: EventTarget, all: all, raiseOnUncaughtExceptions: true }; - return RSVP; + __exports__.reject = reject; + }); + +define("rsvp/resolve", + ["rsvp/promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__.Promise; + + + function objectOrFunction(x) { + return typeof x === "function" || (typeof x === "object" && x !== null); + } + + function resolve(thenable){ + var promise = new Promise(function(resolve, reject){ + var then; + + try { + if ( objectOrFunction(thenable) ) { + then = thenable.then; + + if (typeof then === "function") { + then.call(thenable, resolve, reject); + } else { + resolve(thenable); + } + + } else { + resolve(thenable); + } + + } catch(error) { + reject(error); + } + }); + + return promise; + } + + + __exports__.resolve = resolve; + }); + +define("rsvp", + ["rsvp/events","rsvp/promise","rsvp/node","rsvp/all","rsvp/hash","rsvp/defer","rsvp/config","rsvp/resolve","rsvp/reject","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __exports__) { + "use strict"; + var EventTarget = __dependency1__.EventTarget; + var Promise = __dependency2__.Promise; + var denodeify = __dependency3__.denodeify; + var all = __dependency4__.all; + var hash = __dependency5__.hash; + var defer = __dependency6__.defer; + var config = __dependency7__.config; + var resolve = __dependency8__.resolve; + var reject = __dependency9__.reject; + + function configure(name, value) { + config[name] = value; + } + + + __exports__.Promise = Promise; + __exports__.EventTarget = EventTarget; + __exports__.all = all; + __exports__.hash = hash; + __exports__.defer = defer; + __exports__.denodeify = denodeify; + __exports__.configure = configure; + __exports__.resolve = resolve; + __exports__.reject = reject; }); })(); @@ -6587,10 +7403,6 @@ define("container", this.children = []; - eachDestroyable(this, function(item) { - item.isDestroying = true; - }); - eachDestroyable(this, function(item) { item.destroy(); }); @@ -6657,7 +7469,7 @@ define("container", var factory = factoryFor(container, fullName); var splitName = fullName.split(":"), - type = splitName[0], name = splitName[1], + type = splitName[0], value; if (option(container, fullName, 'instantiate') === false) { @@ -6708,79 +7520,6 @@ define("container", var indexOf = Ember.EnumerableUtils.indexOf; -// ........................................ -// TYPING & ARRAY MESSAGING -// - -var TYPE_MAP = {}; -var t = "Boolean Number String Function Array Date RegExp Object".split(" "); -Ember.ArrayPolyfills.forEach.call(t, function(name) { - TYPE_MAP[ "[object " + name + "]" ] = name.toLowerCase(); -}); - -var toString = Object.prototype.toString; - -/** - Returns a consistent type for the passed item. - - 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: - - | 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' | An Ember class (created using Ember.Object.extend()) | - | 'instance' | An Ember object instance | - | 'error' | An instance of the Error object | - | 'object' | A JavaScript object not inheriting from Ember.Object | - - Examples: - - ```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' - - // "normal" JavaScript object - Ember.typeOf({a: 'b'}); // 'object' - ``` - - @method typeOf - @for Ember - @param {Object} item the item to check - @return {String} the type -*/ -Ember.typeOf = function(item) { - var ret; - - ret = (item === null || item === undefined) ? String(item) : TYPE_MAP[toString.call(item)] || 'object'; - - if (ret === 'function') { - if (Ember.Object && Ember.Object.detect(item)) ret = 'class'; - } else if (ret === 'object') { - if (item instanceof Error) ret = 'error'; - else if (Ember.Object && item instanceof Ember.Object) ret = 'instance'; - else ret = 'object'; - } - - return ret; -}; - /** This will compare two javascript values of possibly different types. It will tell you which one is greater than the other by returning: @@ -7035,10 +7774,15 @@ Ember.ORDER_DEFINITION = Ember.ENV.ORDER_DEFINITION || [ */ Ember.keys = Object.keys; -if (!Ember.keys) { +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; + if (obj.hasOwnProperty(key)) { ret.push(key); } } return ret; @@ -7060,7 +7804,7 @@ var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'n @constructor */ Ember.Error = function() { - var tmp = Error.prototype.constructor.apply(this, arguments); + var tmp = Error.apply(this, arguments); // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. for (var idx = 0; idx < errorProps.length; idx++) { @@ -7139,7 +7883,8 @@ Ember.String = { ``` @method fmt - @param {Object...} [args] + @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) { @@ -7321,10 +8066,12 @@ Ember.String = { /** Returns the Capitalized form of a string - 'innerHTML'.capitalize() // 'InnerHTML' - 'action_name'.capitalize() // 'Action_name' - 'css-class-name'.capitalize() // 'Css-class-name' - 'my favorite items'.capitalize() // 'My favorite items' + ```javascript + 'innerHTML'.capitalize() // 'InnerHTML' + 'action_name'.capitalize() // 'Action_name' + 'css-class-name'.capitalize() // 'Css-class-name' + 'my favorite items'.capitalize() // 'My favorite items' + ``` @method capitalize @param {String} str @@ -7361,7 +8108,7 @@ var fmt = Ember.String.fmt, if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { /** - See {{#crossLink "Ember.String/fmt"}}{{/crossLink}} + See `Ember.String.fmt`. @method fmt @for String @@ -7371,7 +8118,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/w"}}{{/crossLink}} + See `Ember.String.w`. @method w @for String @@ -7381,7 +8128,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/loc"}}{{/crossLink}} + See `Ember.String.loc`. @method loc @for String @@ -7391,7 +8138,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/camelize"}}{{/crossLink}} + See `Ember.String.camelize`. @method camelize @for String @@ -7401,7 +8148,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/decamelize"}}{{/crossLink}} + See `Ember.String.decamelize`. @method decamelize @for String @@ -7411,7 +8158,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/dasherize"}}{{/crossLink}} + See `Ember.String.dasherize`. @method dasherize @for String @@ -7421,7 +8168,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/underscore"}}{{/crossLink}} + See `Ember.String.underscore`. @method underscore @for String @@ -7431,7 +8178,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/classify"}}{{/crossLink}} + See `Ember.String.classify`. @method classify @for String @@ -7441,7 +8188,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/capitalize"}}{{/crossLink}} + See `Ember.String.capitalize`. @method capitalize @for String @@ -7517,8 +8264,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Function) { will instead clear the cache so that it is updated when the next `get` is called on the property. - See {{#crossLink "Ember.ComputedProperty"}}{{/crossLink}}, - {{#crossLink "Ember/computed"}}{{/crossLink}} + See `Ember.ComputedProperty`, `Ember.computed`. @method property @for Function @@ -7545,7 +8291,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Function) { }); ``` - See {{#crossLink "Ember.Observable/observes"}}{{/crossLink}} + See `Ember.Observable.observes`. @method observes @for Function @@ -7572,7 +8318,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Function) { }); ``` - See {{#crossLink "Ember.Observable/observesBefore"}}{{/crossLink}} + See `Ember.Observable.observesBefore`. @method observesBefore @for Function @@ -8469,9 +9215,7 @@ Ember.Enumerable = Ember.Mixin.create({ // HELPERS // -var get = Ember.get, set = Ember.set, map = Ember.EnumerableUtils.map, cacheFor = Ember.cacheFor; - -function none(obj) { return obj===null || obj===undefined; } +var get = Ember.get, set = Ember.set, isNone = Ember.isNone, map = Ember.EnumerableUtils.map, cacheFor = Ember.cacheFor; // .......................................................... // ARRAY @@ -8494,7 +9238,7 @@ function none(obj) { return obj===null || obj===undefined; } 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 + membership of an array changes by changing the syntax of the property to `.observes('*myProperty.[]')`. To support `Ember.Array` in your own class, you must override two @@ -8511,9 +9255,6 @@ function none(obj) { return obj===null || obj===undefined; } */ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.prototype */ { - // compatibility - isSCArray: true, - /** Your array must support the `length` property. Your replace methods should set this property whenever it changes. @@ -8542,7 +9283,7 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot @method objectAt @param {Number} idx The index of the item to return. - @return {any} item at index or undefined + @return {*} item at index or undefined */ objectAt: function(idx) { if ((idx < 0) || (idx>=get(this, 'length'))) return undefined ; @@ -8621,8 +9362,8 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot slice: function(beginIndex, endIndex) { var ret = Ember.A([]); var length = get(this, 'length') ; - if (none(beginIndex)) beginIndex = 0 ; - if (none(endIndex) || (endIndex > length)) endIndex = length ; + if (isNone(beginIndex)) beginIndex = 0 ; + if (isNone(endIndex) || (endIndex > length)) endIndex = length ; if (beginIndex < 0) beginIndex = length + beginIndex; if (endIndex < 0) endIndex = length + endIndex; @@ -8782,7 +9523,7 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot @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 + @param {Number} addAmt The number of items that will be added. If you pass `null` assumes 0. @return {Ember.Array} receiver */ @@ -8816,6 +9557,20 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot return this; }, + /** + If you are implementing an object that supports `Ember.Array`, call this + method just after the array content changes to notify any observers and + invalidate any related properties. Pass the starting index of the change + as well as a delta of the amounts to change. + + @method arrayContentDidChange + @param {Number} startIdx The starting index in the array that did change. + @param {Number} removeAmt The number of items that were removed. If you + pass `null` assumes 0 + @param {Number} addAmt The number of items that were added. If you + pass `null` assumes 0. + @return {Ember.Array} receiver + */ arrayContentDidChange: function(startIdx, removeAmt, addAmt) { // if no args are passed assume everything changes @@ -8956,8 +9711,7 @@ var get = Ember.get, set = Ember.set; @extends Ember.Mixin @since Ember 0.9 */ -Ember.Copyable = Ember.Mixin.create( -/** @scope Ember.Copyable.prototype */ { +Ember.Copyable = Ember.Mixin.create(/** @scope Ember.Copyable.prototype */ { /** Override to return a copy of the receiver. Default implementation raises @@ -9062,8 +9816,7 @@ var get = Ember.get, set = Ember.set; @extends Ember.Mixin @since Ember 0.9 */ -Ember.Freezable = Ember.Mixin.create( -/** @scope Ember.Freezable.prototype */ { +Ember.Freezable = Ember.Mixin.create(/** @scope Ember.Freezable.prototype */ { /** Set to `true` when the object is frozen. Use this property to detect @@ -9116,7 +9869,7 @@ var forEach = Ember.EnumerableUtils.forEach; 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. + already present and is of a type supported by the enumerable. ```javascript set.addObject(contact); @@ -9124,8 +9877,8 @@ var forEach = Ember.EnumerableUtils.forEach; ## Removing Objects - 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 + 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. ```javascript @@ -9152,7 +9905,7 @@ Ember.MutableEnumerable = Ember.Mixin.create(Ember.Enumerable, { 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 + If the passed object is of a type not supported by the receiver, then this method should raise an exception. @method addObject @@ -9179,10 +9932,10 @@ Ember.MutableEnumerable = Ember.Mixin.create(Ember.Enumerable, { __Required.__ You must implement this method to apply this mixin. 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, + object is present in the collection. If the object is not present, this method has no effect. - If the passed object is of a type not supported by the receiver + If the passed object is of a type not supported by the receiver, then this method should raise an exception. @method removeObject @@ -9193,7 +9946,7 @@ Ember.MutableEnumerable = Ember.Mixin.create(Ember.Enumerable, { /** - Removes each objects in the passed enumerable from the receiver. + Removes each object in the passed enumerable from the receiver. @method removeObjects @param {Ember.Enumerable} objects the objects to remove @@ -9244,8 +9997,7 @@ var get = Ember.get, set = Ember.set; @uses Ember.Array @uses Ember.MutableEnumerable */ -Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, - /** @scope Ember.MutableArray.prototype */ { +Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable,/** @scope Ember.MutableArray.prototype */ { /** __Required.__ You must implement this method to apply this mixin. @@ -9351,8 +10103,8 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, ``` @method pushObject - @param {anything} obj object to push - @return {any} the same obj passed as param + @param {*} obj object to push + @return {*} the same obj passed as param */ pushObject: function(obj) { this.insertAt(get(this, 'length'), obj) ; @@ -9431,8 +10183,8 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, ``` @method unshiftObject - @param {anything} obj object to unshift - @return {any} the same obj passed as param + @param {*} obj object to unshift + @return {*} the same obj passed as param */ unshiftObject: function(obj) { this.insertAt(0, obj) ; @@ -9516,7 +10268,6 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, }); - })(); @@ -9564,7 +10315,7 @@ var get = Ember.get, set = Ember.set; For example: ```javascript - Ember.Object.create({ + Ember.Object.extend({ valueObserver: function() { // Executes whenever the "value" property changes }.observes('value') @@ -9583,8 +10334,8 @@ var get = Ember.get, set = Ember.set; object.addObserver('propertyKey', targetObject, targetAction) ``` - This will call the `targetAction` method on the `targetObject` to be called - whenever the value of the `propertyKey` changes. + This will call the `targetAction` method on the `targetObject` whenever + the value of the `propertyKey` changes. Note that if `propertyKey` is a computed property, the observer will be called when any of the property dependencies are changed, even if the @@ -9844,8 +10595,8 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { This is the core method used to register an observer for a property. - Once you call this method, anytime the key's value is set, your observer - will be notified. Note that the observers are triggered anytime the + 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. @@ -9970,11 +10721,11 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { @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 + @param {Number} increment The amount to increment by. Defaults to 1 + @return {Number} The new property value */ incrementProperty: function(keyName, increment) { - if (!increment) { increment = 1; } + if (Ember.isNone(increment)) { increment = 1; } set(this, keyName, (get(this, keyName) || 0)+increment); return get(this, keyName); }, @@ -9989,12 +10740,12 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { @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 + @param {Number} decrement The amount to decrement by. Defaults to 1 + @return {Number} The new property value */ - decrementProperty: function(keyName, increment) { - if (!increment) { increment = 1; } - set(this, keyName, (get(this, keyName) || 0)-increment); + decrementProperty: function(keyName, decrement) { + if (Ember.isNone(decrement)) { decrement = 1; } + set(this, keyName, (get(this, keyName) || 0)-decrement); return get(this, keyName); }, @@ -10003,7 +10754,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { current value. ```javascript - starship.toggleProperty('warpDriveEnaged'); + starship.toggleProperty('warpDriveEngaged'); ``` @method toggleProperty @@ -10035,7 +10786,6 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { } }); - })(); @@ -10049,6 +10799,15 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { var get = Ember.get, set = Ember.set; /** +`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. + +See also `Ember.ViewTargetActionSupport`, which has +view-aware defaults for target and actionContext. + @class TargetActionSupport @namespace Ember @extends Ember.Mixin @@ -10056,6 +10815,7 @@ var get = Ember.get, set = Ember.set; Ember.TargetActionSupport = Ember.Mixin.create({ target: null, action: null, + actionContext: null, targetObject: Ember.computed(function() { var target = get(this, 'target'); @@ -10069,21 +10829,86 @@ Ember.TargetActionSupport = Ember.Mixin.create({ } }).property('target'), - triggerAction: function() { - var action = get(this, 'action'), - target = get(this, 'targetObject'); + actionContextObject: Ember.computed(function() { + var actionContext = get(this, 'actionContext'); + + if (Ember.typeOf(actionContext) === "string") { + var value = get(this, actionContext); + if (value === undefined) { value = get(Ember.lookup, actionContext); } + return value; + } else { + return actionContext; + } + }).property('actionContext'), + + /** + Send an "action" with an "actionContext" to a "target". The action, actionContext + and target will be retrieved from properties of the object. For example: + + ```javascript + 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 + } + }); + ``` + + The `target`, `action`, and `actionContext` can be provided as properties of + an optional object argument to `triggerAction` as well. + + ```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 + } + }); + ``` + + 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: + + ```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 + } + }); + ``` + + @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; if (target && action) { var ret; - if (typeof target.send === 'function') { - ret = target.send(action, this); + if (target.send) { + ret = target.send.apply(target, [action, actionContext]); } else { - if (typeof action === 'string') { - action = target[action]; - } - ret = action.call(target, this); + 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; @@ -10255,9 +11080,9 @@ Ember.Evented = Ember.Mixin.create({ (function() { var RSVP = requireModule("rsvp"); -RSVP.async = function(callback, binding) { +RSVP.configure('async', function(callback, binding) { Ember.run.schedule('actions', binding, callback); -}; +}); /** @module ember @@ -10279,9 +11104,22 @@ Ember.DeferredMixin = Ember.Mixin.create({ @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); + 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); + } + } + + return promise.then(resolve && fulfillmentHandler, reject); }, /** @@ -10290,7 +11128,16 @@ Ember.DeferredMixin = Ember.Mixin.create({ @method resolve */ resolve: function(value) { - get(this, 'promise').resolve(value); + var deferred, promise; + + deferred = get(this, '_deferred'); + promise = deferred.promise; + + if (value === this){ + deferred.resolve(promise); + } else { + deferred.resolve(value); + } }, /** @@ -10299,11 +11146,11 @@ Ember.DeferredMixin = Ember.Mixin.create({ @method reject */ reject: function(value) { - get(this, 'promise').reject(value); + get(this, '_deferred').reject(value); }, - promise: Ember.computed(function() { - return new RSVP.Promise(); + _deferred: Ember.computed(function() { + return RSVP.defer(); }) }); @@ -10393,6 +11240,9 @@ function makeCtor() { for (var i = 0, l = props.length; i < l; i++) { var properties = props[i]; + + Ember.assert("Ember.Object.create no longer supports mixing in other definitions, use createWithMixins instead.", !(properties instanceof Ember.Mixin)); + for (var keyName in properties) { if (!properties.hasOwnProperty(keyName)) { continue; } @@ -10514,8 +11364,6 @@ CoreObject.PrototypeMixin = Mixin.create({ do important setup work, and you'll see strange behavior in your application. - ``` - @method init */ init: function() {}, @@ -10615,23 +11463,25 @@ CoreObject.PrototypeMixin = Mixin.create({ raised. Note that destruction is scheduled for the end of the run loop and does not - happen immediately. + happen immediately. It will set an isDestroying flag immediately. @method destroy @return {Ember.Object} receiver */ destroy: function() { - if (this._didCallDestroy) { return; } - + if (this.isDestroying) { return; } this.isDestroying = true; - this._didCallDestroy = true; - - if (this.willDestroy) { this.willDestroy(); } + schedule('actions', this, this.willDestroy); schedule('destroy', this, this._scheduledDestroy); return this; }, + /** + Override to implement teardown. + */ + willDestroy: Ember.K, + /** @private @@ -10641,10 +11491,9 @@ CoreObject.PrototypeMixin = Mixin.create({ @method _scheduledDestroy */ _scheduledDestroy: function() { + if (this.isDestroyed) { return; } destroy(this); - set(this, 'isDestroyed', true); - - if (this.didDestroy) { this.didDestroy(); } + this.isDestroyed = true; }, bind: function(to, from) { @@ -10991,7 +11840,7 @@ function findNamespaces() { for (var prop in lookup) { // These don't raise exceptions but can cause warnings - if (prop === "parent" || prop === "top" || prop === "frameElement") { continue; } + if (prop === "parent" || prop === "top" || prop === "frameElement" || prop === "webkitStorageInfo") { continue; } // 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 @@ -11135,8 +11984,7 @@ var get = Ember.get, set = Ember.set; @extends Ember.Object @uses Ember.MutableArray */ -Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray, -/** @scope Ember.ArrayProxy.prototype */ { +Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray,/** @scope Ember.ArrayProxy.prototype */ { /** The content array. Must be an object that implements `Ember.Array` and/or @@ -11412,7 +12260,6 @@ Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray, } }); - })(); @@ -11512,8 +12359,7 @@ function contentPropertyDidChange(content, contentKey) { @namespace Ember @extends Ember.Object */ -Ember.ObjectProxy = Ember.Object.extend( -/** @scope Ember.ObjectProxy.prototype */ { +Ember.ObjectProxy = Ember.Object.extend(/** @scope Ember.ObjectProxy.prototype */ { /** The object whose properties will be forwarded. @@ -11679,7 +12525,7 @@ Ember.EachProxy = Ember.Object.extend({ @method unknownProperty @param keyName {String} - @param value {anything} + @param value {*} */ unknownProperty: function(keyName, value) { var ret; @@ -11903,9 +12749,8 @@ if (ignore.length>0) { @namespace Ember @extends Ember.Mixin @uses Ember.MutableArray - @uses Ember.MutableEnumerable + @uses Ember.Observable @uses Ember.Copyable - @uses Ember.Freezable */ Ember.NativeArray = NativeArray; @@ -11952,7 +12797,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) { @submodule ember-runtime */ -var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, none = Ember.isNone, fmt = Ember.String.fmt; +var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, isNone = Ember.isNone, fmt = Ember.String.fmt; /** An unordered collection of objects. @@ -12310,7 +13155,7 @@ Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Emb // implements Ember.MutableEnumerable addObject: function(obj) { if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); - if (none(obj)) return this; // nothing to do + if (isNone(obj)) return this; // nothing to do var guid = guidFor(obj), idx = this[guid], @@ -12338,7 +13183,7 @@ Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Emb // implements Ember.MutableEnumerable removeObject: function(obj) { if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); - if (none(obj)) return this; // nothing to do + if (isNone(obj)) return this; // nothing to do var guid = guidFor(obj), idx = this[guid], @@ -12413,7 +13258,7 @@ Deferred.reopenClass({ promise: function(callback, binding) { var deferred = Deferred.create(); callback.call(binding, deferred); - return get(deferred, 'promise'); + return deferred; } }); @@ -12424,6 +13269,8 @@ Ember.Deferred = Deferred; (function() { +var forEach = Ember.ArrayPolyfills.forEach; + /** @module ember @submodule ember-runtime @@ -12456,12 +13303,10 @@ Ember.onLoad = function(name, callback) { @param object {Object} object to pass to callbacks */ Ember.runLoadHooks = function(name, object) { - var hooks; - loaded[name] = object; - if (hooks = loadHooks[name]) { - loadHooks[name].forEach(function(callback) { + if (loadHooks[name]) { + forEach.call(loadHooks[name], function(callback) { callback(object); }); } @@ -12535,6 +13380,8 @@ Ember.ControllerMixin = Ember.Mixin.create({ container: null, + parentController: null, + store: null, model: Ember.computed.alias('content'), @@ -12877,6 +13724,10 @@ var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach, }); ``` + The itemController instances will have a `parentController` property set to + either the the `parentController` property of the `ArrayController` + or to the `ArrayController` instance itself. + @class ArrayController @namespace Ember @extends Ember.ArrayProxy @@ -12916,9 +13767,9 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, }); ``` - @method - @type String - @default null + @method lookupItemController + @param {Object} object + @return {String} */ lookupItemController: function(object) { return get(this, 'itemController'); @@ -12967,8 +13818,8 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, }, init: function() { - this._super(); if (!this.get('content')) { Ember.defineProperty(this, 'content', undefined, Ember.A()); } + this._super(); this.set('_subControllers', Ember.A()); }, @@ -12987,6 +13838,7 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, } subController.set('target', this); + subController.set('parentController', get(this, 'parentController') || this); subController.set('content', object); return subController; @@ -12996,10 +13848,11 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, _resetSubControllers: function() { var subControllers = get(this, '_subControllers'); - - forEach(subControllers, function(subController) { - if (subController) { subController.destroy(); } - }); + if (subControllers) { + forEach(subControllers, function(subController) { + if (subController) { subController.destroy(); } + }); + } this.set('_subControllers', Ember.A()); } @@ -13059,7 +13912,7 @@ Ember Runtime */ var jQuery = Ember.imports.jQuery; -Ember.assert("Ember Views require jQuery 1.8, 1.9 or 2.0", jQuery && (jQuery().jquery.match(/^((1\.(8|9))|2.0)(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_JQUERY)); +Ember.assert("Ember Views require jQuery 1.8, 1.9, 1.10, or 2.0", jQuery && (jQuery().jquery.match(/^((1\.(8|9|10))|2.0)(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_JQUERY)); /** Alias for jQuery @@ -13099,7 +13952,7 @@ if (Ember.$) { @submodule ember-views */ -/*** BEGIN METAMORPH HELPERS ***/ +/* BEGIN METAMORPH HELPERS */ // Internet Explorer prior to 9 does not allow setting innerHTML if the first element // is a "zero-scope" element. This problem can be worked around by making @@ -13172,7 +14025,7 @@ var setInnerHTMLWithoutFix = function(element, html) { } }; -/*** END METAMORPH HELPERS */ +/* END METAMORPH HELPERS */ var innerHTMLTags = {}; @@ -13276,7 +14129,7 @@ Ember.RenderBuffer = function(tagName) { Ember._RenderBuffer = function(tagName) { this.tagNames = [tagName || null]; - this.buffer = []; + this.buffer = ""; }; Ember._RenderBuffer.prototype = @@ -13285,6 +14138,8 @@ Ember._RenderBuffer.prototype = // The root view's element _element: null, + _hasElement: true, + /** @private @@ -13400,7 +14255,7 @@ Ember._RenderBuffer.prototype = @chainable */ push: function(string) { - this.buffer.push(string); + this.buffer += string; return this; }, @@ -13533,7 +14388,7 @@ Ember._RenderBuffer.prototype = var tagName = this.currentTagName(); if (!tagName) { return; } - if (!this._element && this.buffer.length === 0) { + if (this._hasElement && !this._element && this.buffer.length === 0) { this._element = this.generateElement(); return; } @@ -13546,27 +14401,27 @@ Ember._RenderBuffer.prototype = style = this.elementStyle, attr, prop; - buffer.push('<' + tagName); + buffer += '<' + tagName; if (id) { - buffer.push(' id="' + this._escapeAttribute(id) + '"'); + buffer += ' id="' + this._escapeAttribute(id) + '"'; this.elementId = null; } if (classes) { - buffer.push(' class="' + this._escapeAttribute(classes.join(' ')) + '"'); + buffer += ' class="' + this._escapeAttribute(classes.join(' ')) + '"'; this.classes = null; } if (style) { - buffer.push(' style="'); + buffer += ' style="'; for (prop in style) { if (style.hasOwnProperty(prop)) { - buffer.push(prop + ':' + this._escapeAttribute(style[prop]) + ';'); + buffer += prop + ':' + this._escapeAttribute(style[prop]) + ';'; } } - buffer.push('"'); + buffer += '"'; this.elementStyle = null; } @@ -13574,7 +14429,7 @@ Ember._RenderBuffer.prototype = if (attrs) { for (attr in attrs) { if (attrs.hasOwnProperty(attr)) { - buffer.push(' ' + attr + '="' + this._escapeAttribute(attrs[attr]) + '"'); + buffer += ' ' + attr + '="' + this._escapeAttribute(attrs[attr]) + '"'; } } @@ -13587,9 +14442,9 @@ Ember._RenderBuffer.prototype = var value = props[prop]; if (value || typeof(value) === 'number') { if (value === true) { - buffer.push(' ' + prop + '="' + prop + '"'); + buffer += ' ' + prop + '="' + prop + '"'; } else { - buffer.push(' ' + prop + '="' + this._escapeAttribute(props[prop]) + '"'); + buffer += ' ' + prop + '="' + this._escapeAttribute(props[prop]) + '"'; } } } @@ -13598,12 +14453,13 @@ Ember._RenderBuffer.prototype = this.elementProperties = null; } - buffer.push('>'); + buffer += '>'; + this.buffer = buffer; }, pushClosingTag: function() { var tagName = this.tagNames.pop(); - if (tagName) { this.buffer.push(''); } + if (tagName) { this.buffer += ''; } }, currentTagName: function() { @@ -13687,17 +14543,20 @@ Ember._RenderBuffer.prototype = @return {String} The generated HTML */ string: function() { - if (this._element) { + if (this._hasElement && this._element) { // Firefox versions < 11 do not have support for element.outerHTML. - return this.element().outerHTML || - new XMLSerializer().serializeToString(this.element()); + 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.join(''); + return this.buffer; }, _escapeAttribute: function(value) { @@ -13749,8 +14608,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; @private @extends Ember.Object */ -Ember.EventDispatcher = Ember.Object.extend( -/** @scope Ember.EventDispatcher.prototype */{ +Ember.EventDispatcher = Ember.Object.extend(/** @scope Ember.EventDispatcher.prototype */{ /** @private @@ -13782,7 +14640,7 @@ Ember.EventDispatcher = Ember.Object.extend( @method setup @param addedEvents {Hash} */ - setup: function(addedEvents) { + setup: function(addedEvents, rootElement) { var event, events = { touchstart : 'touchStart', touchmove : 'touchMove', @@ -13815,7 +14673,12 @@ Ember.EventDispatcher = Ember.Object.extend( Ember.$.extend(events, addedEvents || {}); - var rootElement = Ember.$(get(this, 'rootElement')); + + 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); @@ -13857,7 +14720,7 @@ Ember.EventDispatcher = Ember.Object.extend( setupHandler: function(rootElement, event, eventName) { var self = this; - rootElement.delegate('.ember-view', event + '.ember', function(evt, triggeringManager) { + rootElement.on(event + '.ember', '.ember-view', function(evt, triggeringManager) { return Ember.handleErrors(function() { var view = Ember.View.views[this.id], result = true, manager = null; @@ -13876,7 +14739,7 @@ Ember.EventDispatcher = Ember.Object.extend( }, this); }); - rootElement.delegate('[data-ember-action]', event + '.ember', function(evt) { + 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]; @@ -13928,7 +14791,7 @@ Ember.EventDispatcher = Ember.Object.extend( destroy: function() { var rootElement = get(this, 'rootElement'); - Ember.$(rootElement).undelegate('.ember').removeClass('ember-application'); + Ember.$(rootElement).off('.ember', '**').removeClass('ember-application'); return this._super(); } }); @@ -14060,18 +14923,6 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { init: function() { this._super(); - - // 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; - } - - this.addBeforeObserver('elementId', function() { - throw new Error("Changing a view's elementId after creation is not allowed"); - }); - this.transitionTo('preRender'); }, @@ -14101,7 +14952,7 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { concreteView: Ember.computed(function() { if (!this.isVirtual) { return this; } else { return get(this, 'parentView'); } - }).property('parentView').volatile(), + }).property('parentView'), instrumentName: 'core_view', @@ -14139,8 +14990,6 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { }, _renderToBuffer: function(parentBuffer, bufferOperation) { - Ember.run.sync(); - // 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 @@ -14186,9 +15035,11 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { return Ember.typeOf(this[name]) === 'function' || this._super(name); }, - willDestroy: function() { + 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(); } @@ -14198,10 +15049,9 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { // the DOM again. if (parent) { parent.removeChild(this); } - this.transitionTo('destroyed'); + this.transitionTo('destroying', false); - // next remove view from global hash - if (!this.isVirtual) delete Ember.View.views[this.elementId]; + return this; }, clearRenderedChildren: Ember.K, @@ -14211,6 +15061,68 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { 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 @@ -14682,7 +15594,7 @@ class: eventManager: Ember.Object.create({ mouseEnter: function(event, view){ // view might be instance of either - // OutsideView or InnerView depending on + // OuterView or InnerView depending on // where on the page the user interaction occured } }) @@ -14838,14 +15750,6 @@ Ember.View = Ember.CoreView.extend( return template || get(this, 'defaultTemplate'); }).property('templateName'), - container: Ember.computed(function() { - var parentView = get(this, '_parentView'); - - if (parentView) { return get(parentView, 'container'); } - - return Ember.Container && Ember.Container.defaultContainer; - }), - /** The controller managing this view. If this property is set, it will be made available for use by the template. @@ -14883,14 +15787,11 @@ Ember.View = Ember.CoreView.extend( templateForName: function(name, type) { if (!name) { return; } - Ember.assert("templateNames are not allowed to contain periods: "+name, name.indexOf('.') === -1); - var container = get(this, 'container'); - - if (container) { - return container.lookup('template:' + name); - } + // the defaultContainer is deprecated + var container = this.container || (Ember.Container && Ember.Container.defaultContainer); + return container && container.lookup('template:' + name); }, /** @@ -14981,7 +15882,7 @@ Ember.View = Ember.CoreView.extend( */ childViews: childViewsProperty, - _childViews: [], + _childViews: EMPTY_ARRAY, // When it's a virtual view, we need to notify the parent that their // childViews will change. @@ -15279,7 +16180,7 @@ Ember.View = Ember.CoreView.extend( @param {Ember.RenderBuffer} buffer */ _applyAttributeBindings: function(buffer, attributeBindings) { - var attributeValue, elem, type; + var attributeValue, elem; a_forEach(attributeBindings, function(binding) { var split = binding.split(':'), @@ -15370,7 +16271,7 @@ Ember.View = Ember.CoreView.extend( while(--idx >= 0) { view = childViews[idx]; - callback.call(this, view, idx); + callback(this, view, idx); } return this; @@ -15384,9 +16285,9 @@ Ember.View = Ember.CoreView.extend( var len = childViews.length, view, idx; - for(idx = 0; idx < len; idx++) { + for (idx = 0; idx < len; idx++) { view = childViews[idx]; - callback.call(this, view); + callback(view); } return this; @@ -15582,13 +16483,15 @@ Ember.View = Ember.CoreView.extend( /** @private - Run this callback on the current view and recursively on child views. + 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) { - var childViews = [this], currentViews, view; + invokeRecursively: function(fn, includeSelf) { + var childViews = (includeSelf === false) ? this._childViews : [this]; + var currentViews, view; while (childViews.length) { currentViews = childViews.slice(); @@ -15596,7 +16499,7 @@ Ember.View = Ember.CoreView.extend( for (var i=0, l=currentViews.length; i=0; i--) { @@ -15998,27 +16914,16 @@ Ember.View = Ember.CoreView.extend( } // 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); - } + if (viewName && nonVirtualParentView) { + nonVirtualParentView.set(viewName, null); } - // 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'); - childLen = childViews.length; for (i=childLen-1; i>=0; i--) { childViews[i].destroy(); } - // next remove view from global hash - if (!this.isVirtual) delete Ember.View.views[get(this, 'elementId')]; + return this; }, /** @@ -16039,6 +16944,7 @@ Ember.View = Ember.CoreView.extend( if (Ember.CoreView.detect(view)) { attrs = attrs || {}; attrs._parentView = this; + attrs.container = this.container; attrs.templateData = attrs.templateData || get(this, 'templateData'); view = view.create(attrs); @@ -16133,9 +17039,13 @@ Ember.View = Ember.CoreView.extend( }, transitionTo: function(state, children) { - this.currentState = this.states[state]; + 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); @@ -16350,7 +17260,7 @@ Ember.View.reopenClass({ // If the value is not false, undefined, or null, return the current // value of the property. - } else if (val !== false && val !== undefined && val !== null) { + } else if (val !== false && val != null) { return val; // Nothing to display. Return null so that the old class is removed @@ -16479,15 +17389,18 @@ Ember.merge(preRender, { // created (createElement). insertElement: function(view, fn) { view.createElement(); - view.triggerRecursively('willInsertElement'); + var viewCollection = view.viewHierarchyCollection(); + + viewCollection.trigger('willInsertElement'); // after createElement, the view will be in the hasElement state. fn.call(view); - view.transitionTo('inDOM'); - view.triggerRecursively('didInsertElement'); + viewCollection.transitionTo('inDOM', false); + viewCollection.trigger('didInsertElement'); }, - renderToBufferIfNeeded: function(view) { - return view.renderToBuffer(); + renderToBufferIfNeeded: function(view, buffer) { + view.renderToBuffer(buffer); + return true; }, empty: Ember.K, @@ -16534,10 +17447,11 @@ Ember.merge(inBuffer, { // view will render that view and append the resulting // buffer into its buffer. appendChild: function(view, childView, options) { - var buffer = view.buffer; + var buffer = view.buffer, _childViews = view._childViews; childView = view.createChildView(childView, options); - view._childViews.push(childView); + if (!_childViews.length) { _childViews = view._childViews = _childViews.slice(); } + _childViews.push(childView); childView.renderToBuffer(buffer); @@ -16551,8 +17465,8 @@ Ember.merge(inBuffer, { // state back into the preRender state. destroyElement: function(view) { view.clearBuffer(); - view._notifyWillDestroyElement(); - view.transitionTo('preRender'); + var viewCollection = view._notifyWillDestroyElement(); + viewCollection.transitionTo('preRender', false); return view; }, @@ -16561,8 +17475,8 @@ Ember.merge(inBuffer, { 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) { - return view.buffer; + renderToBufferIfNeeded: function (view, buffer) { + return false; }, // It should be impossible for a rendered view to be scheduled for @@ -16681,6 +17595,23 @@ Ember.merge(hasElement, { var inDOM = Ember.View.states.inDOM = Ember.create(hasElement); Ember.merge(inDOM, { + enter: function(view) { + // Register the view for event handling. This hash is used by + // Ember.EventDispatcher to dispatch incoming events. + if (!view.isVirtual) { + Ember.assert("Attempted to register a view with an id already in use: "+view.elementId, !Ember.View.views[view.elementId]); + Ember.View.views[view.elementId] = view; + } + + view.addBeforeObserver('elementId', function() { + throw new 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"; } @@ -16696,30 +17627,30 @@ Ember.merge(inDOM, { @submodule ember-views */ -var destroyedError = "You can't call %@ on a destroyed view", fmt = Ember.String.fmt; +var destroyingError = "You can't call %@ on a view being destroyed", fmt = Ember.String.fmt; -var destroyed = Ember.View.states.destroyed = Ember.create(Ember.View.states._default); +var destroying = Ember.View.states.destroying = Ember.create(Ember.View.states._default); -Ember.merge(destroyed, { +Ember.merge(destroying, { appendChild: function() { - throw fmt(destroyedError, ['appendChild']); + throw fmt(destroyingError, ['appendChild']); }, rerender: function() { - throw fmt(destroyedError, ['rerender']); + throw fmt(destroyingError, ['rerender']); }, destroyElement: function() { - throw fmt(destroyedError, ['destroyElement']); + throw fmt(destroyingError, ['destroyElement']); }, empty: function() { - throw fmt(destroyedError, ['empty']); + throw fmt(destroyingError, ['empty']); }, setElement: function() { - throw fmt(destroyedError, ["set('element', ...)"]); + throw fmt(destroyingError, ["set('element', ...)"]); }, renderToBufferIfNeeded: function() { - throw fmt(destroyedError, ["renderToBufferIfNeeded"]); + return false; }, // Since element insertion is scheduled, don't do anything if @@ -16738,7 +17669,7 @@ Ember.View.cloneStates = function(from) { into._default = {}; into.preRender = Ember.create(into._default); - into.destroyed = 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); @@ -16765,6 +17696,7 @@ var states = Ember.View.cloneStates(Ember.View.states); 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` @@ -16969,12 +17901,15 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { 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); @@ -16983,6 +17918,7 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { 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); } @@ -17014,7 +17950,7 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { }); }, - instrumentName: 'render.container', + instrumentName: 'container', /** @private @@ -17094,6 +18030,7 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { _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'), @@ -17127,26 +18064,48 @@ Ember.merge(states.hasElement, { }, ensureChildrenAreInDOM: function(view) { - var childViews = view._childViews, i, len, childView, previous, buffer; + var childViews = view._childViews, i, len, childView, previous, buffer, viewCollection = new ViewCollection(); + 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'); + + if (!buffer) { buffer = Ember.RenderBuffer(); buffer._hasElement = false; } + + if (childView.renderToBufferIfNeeded(buffer)) { + viewCollection.push(childView); + } else if (viewCollection.length) { + insertViewCollection(view, viewCollection, previous, buffer); + buffer = null; + previous = childView; + viewCollection.clear(); + } else { + previous = childView; } - previous = childView; + } + + if (viewCollection.length) { + insertViewCollection(view, viewCollection, previous, buffer); } } }); +function insertViewCollection(view, viewCollection, previous, buffer) { + viewCollection.triggerRecursively('willInsertElement'); + + if (previous) { + previous.domManager.after(previous, buffer.string()); + } else { + view.domManager.prepend(view, buffer.string()); + } + + viewCollection.forEach(function(v) { + v.transitionTo('inDOM'); + v.propertyDidChange('element'); + v.triggerRecursively('didInsertElement'); + }); +} + + })(); @@ -17161,7 +18120,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; /** `Ember.CollectionView` is an `Ember.View` descendent responsible for managing - a collection (an array or array-like object) by maintaing a child view object + a collection (an array or array-like object) by maintaining a child view object and associated DOM representation for each item in the array and ensuring that child views and their associated rendered HTML are updated when items in the array are added, removed, or replaced. @@ -17312,8 +18271,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; @extends Ember.ContainerView @since Ember 0.9 */ -Ember.CollectionView = Ember.ContainerView.extend( -/** @scope Ember.CollectionView.prototype */ { +Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionView.prototype */ { /** A list of items to be displayed by the `Ember.CollectionView`. @@ -17387,15 +18345,17 @@ Ember.CollectionView = Ember.ContainerView.extend( this.arrayDidChange(content, 0, null, len); }, 'content'), - willDestroy: function() { + destroy: function() { + if (!this._super()) { return; } + var content = get(this, 'content'); if (content) { content.removeArrayObserver(this); } - this._super(); - if (this._createdEmptyView) { this._createdEmptyView.destroy(); } + + return this; }, arrayWillChange: function(content, start, removedCount) { @@ -17417,11 +18377,13 @@ Ember.CollectionView = Ember.ContainerView.extend( 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]; - if (removingAll) { childView.removedFromDOM = true; } childView.destroy(); } }, @@ -17435,9 +18397,10 @@ Ember.CollectionView = Ember.ContainerView.extend( This array observer is added in `contentDidChange`. @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 + @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 itemViewClass = get(this, 'itemViewClass'), @@ -17488,26 +18451,89 @@ Ember.CollectionView = Ember.ContainerView.extend( } }); -/** - 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 +/** + 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() { + +})(); + + + +(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.CollectionView.CONTAINER_MAP = { - ul: 'li', - ol: 'li', - table: 'tr', - thead: 'tr', - tbody: 'tr', - tfoot: 'tr', - tr: 'td', - select: 'option' -}; +Ember.ViewTargetActionSupport = Ember.Mixin.create(Ember.TargetActionSupport, { + /** + @property target + */ + target: Ember.computed.alias('controller'), + /** + @property actionContext + */ + actionContext: Ember.computed.alias('context') +}); })(); @@ -18012,7 +19038,8 @@ if(!Handlebars && typeof require === 'function') { Handlebars = require('handlebars'); } -Ember.assert("Ember Handlebars requires Handlebars 1.0.0-rc.3 or greater. Include a SCRIPT tag in the HTML HEAD linking to the Handlebars file before you link to Ember.", Handlebars && Handlebars.COMPILER_REVISION === 2); +Ember.assert("Ember Handlebars requires Handlebars version 1.0.0-rc.4. 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-rc.4, COMPILER_REVISION expected: 3, got: " + Handlebars.COMPILER_REVISION + " – Please note: Builds of master may have other COMPILER_REVISION values.", Handlebars.COMPILER_REVISION === 3); /** Prepares the Handlebars templating library for use inside Ember's view @@ -18030,6 +19057,32 @@ Ember.assert("Ember Handlebars requires Handlebars 1.0.0-rc.3 or greater. Includ */ 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]; + } + } +} + +Ember.Handlebars.helper = function(name, value) { + if (Ember.View.detect(value)) { + Ember.Handlebars.registerHelper(name, function(options) { + Ember.assert("You can only pass attributes as parameters (not values) to a application-defined helper", 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 @@ -18398,15 +19451,15 @@ Ember.Handlebars.registerHelper('helperMissing', function(path, options) { }); ``` - Which allows for template syntax such as {{concatenate prop1 prop2}} or - {{concatenate prop1 prop2 prop3}}. If any of the properties change, + 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 + The `{{unbound}}` helper can be used with bound helper invocations to render them in their unbound form, e.g. ```handlebars @@ -18416,6 +19469,10 @@ Ember.Handlebars.registerHelper('helperMissing', function(path, options) { In this example, if the name property changes, the helper will not re-render. + ## Use with blocks not supported + + Bound helpers do not support use with Handlebars blocks or + the addition of child views of any kind. @method registerBoundHelper @for Ember.Handlebars @@ -18439,6 +19496,8 @@ Ember.Handlebars.registerBoundHelper = function(name, fn) { pathRoot, path, loc, hashOption; + Ember.assert("registerBoundHelper-generated helpers do not support use with Handlebars blocks.", !options.fn); + // Detect bound options (e.g. countBinding="otherCount") hash.boundOptions = {}; for (hashOption in hash) { @@ -18499,6 +19558,7 @@ Ember.Handlebars.registerBoundHelper = function(name, fn) { Renders the unbound form of an otherwise bound helper function. + @method evaluateMultiPropertyBoundHelper @param {Function} fn @param {Object} context @param {Array} normalizedProperties @@ -18515,7 +19575,7 @@ function evaluateMultiPropertyBoundHelper(context, fn, normalizedProperties, opt bindView = new Ember._SimpleHandlebarsView(null, null, !hash.unescaped, data); bindView.normalizedValue = function() { - var args = [], value, boundOption; + var args = [], boundOption; // Copy over bound options. for (boundOption in boundOptions) { @@ -18560,6 +19620,7 @@ function evaluateMultiPropertyBoundHelper(context, fn, normalizedProperties, opt Renders the unbound form of an otherwise bound helper function. + @method evaluateUnboundHelper @param {Function} fn @param {Object} context @param {Array} normalizedProperties @@ -18597,7 +19658,6 @@ Ember.Handlebars.template = function(spec){ return t; }; - })(); @@ -18617,7 +19677,7 @@ var htmlSafe = Ember.String.htmlSafe; if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { /** - See {{#crossLink "Ember.String/htmlSafe"}}{{/crossLink}} + See `Ember.String.htmlSafe`. @method htmlSafe @for String @@ -18698,13 +19758,18 @@ var DOMManager = { var buffer = view.renderToBuffer(); view.invokeRecursively(function(view) { - view.propertyDidChange('element'); + view.propertyWillChange('element'); }); - view.triggerRecursively('willInsertElement'); + morph.replaceWith(buffer.string()); view.transitionTo('inDOM'); + + view.invokeRecursively(function(view) { + view.propertyDidChange('element'); + }); view.triggerRecursively('didInsertElement'); + notifyMutationListeners(); }); }, @@ -18728,7 +19793,7 @@ Ember._Metamorph = Ember.Mixin.create({ isVirtual: true, tagName: '', - instrumentName: 'render.metamorph', + instrumentName: 'metamorph', init: function() { this._super(); @@ -18813,6 +19878,8 @@ SimpleHandlebarsView.prototype = { this.morph = null; }, + propertyWillChange: Ember.K, + propertyDidChange: Ember.K, normalizedValue: function() { @@ -18863,7 +19930,7 @@ SimpleHandlebarsView.prototype = { rerender: function() { switch(this.state) { case 'preRender': - case 'destroyed': + case 'destroying': break; case 'inBuffer': throw new Ember.Error("Something you did tried to replace an {{expression}} before it was inserted into the DOM."); @@ -18894,7 +19961,7 @@ merge(states._default, { merge(states.inDOM, { rerenderIfNeeded: function(view) { - if (get(view, 'normalizedValue') !== view._lastNormalizedValue) { + if (view.normalizedValue() !== view._lastNormalizedValue) { view.rerender(); } } @@ -18915,7 +19982,7 @@ merge(states.inDOM, { @private */ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ - instrumentName: 'render.boundHandlebars', + instrumentName: 'boundHandlebars', states: states, /** @@ -18998,7 +20065,7 @@ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ */ pathRoot: null, - normalizedValue: Ember.computed(function() { + normalizedValue: function() { var path = get(this, 'path'), pathRoot = get(this, 'pathRoot'), valueNormalizer = get(this, 'valueNormalizerFunc'), @@ -19016,7 +20083,7 @@ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ } return valueNormalizer ? valueNormalizer(result) : result; - }).property('path', 'pathRoot', 'valueNormalizerFunc').volatile(), + }, rerenderIfNeeded: function() { this.currentState.rerenderIfNeeded(this); @@ -19051,7 +20118,7 @@ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ var inverseTemplate = get(this, 'inverseTemplate'), displayTemplate = get(this, 'displayTemplate'); - var result = get(this, 'normalizedValue'); + var result = this.normalizedValue(); this._lastNormalizedValue = result; // First, test the conditional to see if we should @@ -19114,6 +20181,10 @@ var forEach = Ember.ArrayPolyfills.forEach; var EmberHandlebars = Ember.Handlebars, helpers = EmberHandlebars.helpers; +function exists(value){ + return !Ember.isNone(value); +} + // Binds a property into the DOM. This will create a hook in DOM that the // KVO system will look for and update if the property changes. function bind(property, options, preserveContext, shouldDisplay, valueNormalizer, childProperties) { @@ -19292,9 +20363,7 @@ EmberHandlebars.registerHelper('bind', function(property, options) { return simpleBind.call(context, property, options); } - return bind.call(context, property, options, false, function(result) { - return !Ember.isNone(result); - }); + return bind.call(context, property, options, false, exists); }); /** @@ -19366,9 +20435,7 @@ EmberHandlebars.registerHelper('with', function(context, options) { Ember.bind(options.data.keywords, keywordName, contextPath); } - return bind.call(this, path, options, true, function(result) { - return !Ember.isNone(result); - }); + return bind.call(this, path, options, true, exists); } else { Ember.assert("You must pass exactly one argument to the with helper", arguments.length === 2); Ember.assert("You must pass a block to the with helper", options.fn && options.fn !== Handlebars.VM.noop); @@ -19520,7 +20587,7 @@ EmberHandlebars.registerHelper('unless', function(context, options) { Results in the following rendered output: ```html - + ``` All three strategies - string return value, boolean return value, and @@ -19552,7 +20619,7 @@ EmberHandlebars.registerHelper('bindAttr', function(options) { // Handle classes differently, as we can bind multiple classes var classBindings = attrs['class']; - if (classBindings !== null && classBindings !== undefined) { + if (classBindings != null) { var classResults = EmberHandlebars.bindClasses(this, classBindings, view, dataId, options); ret.push('class="' + Handlebars.Utils.escapeExpression(classResults.join(' ')) + '"'); @@ -19859,11 +20926,8 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ }, helper: function(thisContext, path, options) { - var inverse = options.inverse, - data = options.data, - view = data.view, + var data = options.data, fn = options.fn, - hash = options.hash, newView; if ('string' === typeof path) { @@ -19877,7 +20941,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ var viewOptions = this.propertiesFromHTMLOptions(options, thisContext); var currentView = data.view; - viewOptions.templateData = options.data; + viewOptions.templateData = data; var newViewProto = newView.proto ? newView.proto() : newView; if (fn) { @@ -20004,9 +21068,8 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ {{/view}} ``` - The first argument can also be a relative path. Ember will search for the - view class starting at the `Ember.View` of the template where `{{view}}` was - used as the root object: + The first argument can also be a relative path accessible from the current + context. ```javascript MyApp = Ember.Application.create({}); @@ -20014,7 +21077,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ innerViewClass: Ember.View.extend({ classNames: ['a-custom-view-class-as-property'] }), - template: Ember.Handlebars.compile('{{#view "innerViewClass"}} hi {{/view}}') + template: Ember.Handlebars.compile('{{#view "view.innerViewClass"}} hi {{/view}}') }); MyApp.OuterView.create().appendTo('body'); @@ -20259,8 +21322,6 @@ Ember.Handlebars.registerHelper('collection', function(path, options) { } } - var tagName = hash.tagName || collectionPrototype.tagName; - if (fn) { itemHash.template = fn; delete options.fn; @@ -20282,8 +21343,6 @@ Ember.Handlebars.registerHelper('collection', function(path, options) { itemHash._context = Ember.computed.alias('content'); } - var viewString = view.toString(); - var viewOptions = Ember.Handlebars.ViewHelper.propertiesFromHTMLOptions({ data: data, hash: itemHash }, this); hash.itemViewClass = itemViewClass.extend(viewOptions); @@ -20412,6 +21471,7 @@ Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { set(controller, 'container', get(this, 'controller.container')); set(controller, '_eachView', this); set(controller, 'target', get(this, 'controller')); + set(controller, 'parentController', get(this, 'controller')); this.disableContentObservers(function() { set(this, 'content', controller); @@ -20473,14 +21533,16 @@ Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { return view; }, - willDestroy: function() { + destroy: function() { + if (!this._super()) { return; } + var arrayController = get(this, '_arrayController'); if (arrayController) { arrayController.destroy(); } - return this._super(); + return this; } }); @@ -20683,6 +21745,9 @@ GroupedEach.prototype = { {{/each}} ``` + Each itemController will receive a reference to the current controller as + a `parentController` property. + @method each @for Ember.Handlebars.helpers @param [name] {String} name for item (used with `in`) @@ -20704,6 +21769,11 @@ Ember.Handlebars.registerHelper('each', function(path, options) { 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; @@ -20797,6 +21867,7 @@ Ember.Handlebars.registerHelper('template', function(name, options) { {{partial user_info}} {{/with}} + ``` The `data-template-name` attribute of a partial template is prefixed with an underscore. @@ -20821,7 +21892,7 @@ Ember.Handlebars.registerHelper('partial', function(name, options) { var view = options.data.view, underscoredName = nameParts.join("/"), template = view.templateForName(underscoredName), - deprecatedTemplate = view.templateForName(name); + deprecatedTemplate = !template && view.templateForName(name); Ember.deprecate("You tried to render the partial " + name + ", which should be at '" + underscoredName + "', but Ember found '" + name + "'. Please use a leading underscore in your partials", template); Ember.assert("Unable to find partial with name '"+name+"'.", template || deprecatedTemplate); @@ -20947,7 +22018,7 @@ var set = Ember.set, get = Ember.get; You can add a `label` tag yourself in the template where the `Ember.Checkbox` is being used. - ```html + ```handlebars