summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Wintermeyer <stefan.wintermeyer@amooma.de>2013-03-20 17:08:14 +0100
committerStefan Wintermeyer <stefan.wintermeyer@amooma.de>2013-04-03 22:09:33 +0200
commit595e5f9d32c9dda8f7b6c0dd5e7e4fba4693eca4 (patch)
treee2e4c5c463536b2295e722b98020ce6378482640
parentedf06bf6f7d4f20d5a9145a5bbf8db409fe6ab1b (diff)
basic Ember.js setup
-rw-r--r--app/controllers/switchboards_controller.rb3
-rw-r--r--app/views/switchboards/app.js8
-rw-r--r--app/views/switchboards/show-old.html.haml (renamed from app/views/switchboards/show.html.haml)0
-rw-r--r--app/views/switchboards/show.html.erb23
-rw-r--r--public/js/app.js16
-rw-r--r--public/js/libs/ember-data.js7793
-rw-r--r--public/js/libs/ember.js26839
-rw-r--r--public/js/libs/handlebars.js2201
-rw-r--r--public/js/libs/new-ember-data.js8431
-rw-r--r--public/js/models.js0
-rw-r--r--public/js/old-app.js58
11 files changed, 45372 insertions, 0 deletions
diff --git a/app/controllers/switchboards_controller.rb b/app/controllers/switchboards_controller.rb
index 98008c1..c499c69 100644
--- a/app/controllers/switchboards_controller.rb
+++ b/app/controllers/switchboards_controller.rb
@@ -8,6 +8,9 @@ class SwitchboardsController < ApplicationController
end
def show
+ if @user.nil?
+ @user = current_user
+ end
@switchboard = @user.switchboards.find(params[:id])
@switchboard_entries = @switchboard.switchboard_entries
spread_breadcrumbs
diff --git a/app/views/switchboards/app.js b/app/views/switchboards/app.js
new file mode 100644
index 0000000..a4aa100
--- /dev/null
+++ b/app/views/switchboards/app.js
@@ -0,0 +1,8 @@
+window.App = Ember.Application.create({
+ rootElement: '#xxxyyy',
+
+ ready: function() {
+ App.view.appendTo('#xxxyyy');
+ }
+});
+
diff --git a/app/views/switchboards/show.html.haml b/app/views/switchboards/show-old.html.haml
index a825806..a825806 100644
--- a/app/views/switchboards/show.html.haml
+++ b/app/views/switchboards/show-old.html.haml
diff --git a/app/views/switchboards/show.html.erb b/app/views/switchboards/show.html.erb
new file mode 100644
index 0000000..337d00c
--- /dev/null
+++ b/app/views/switchboards/show.html.erb
@@ -0,0 +1,23 @@
+<% content_for :title, "Switchboard #{@switchboard.name}" %>
+
+<div class='row'>
+ <div class='span12'>
+ <div id='container'>
+
+ </div>
+
+ <script type="text/x-handlebars">
+ <h1>{{appName}}</h1>
+
+ <h2>{{title}}</h2>
+ </script>
+
+
+
+ </div>
+</div>
+
+<script src="/js/libs/handlebars.js"></script>
+<script src="/js/libs/ember.js"></script>
+<script src="/js/libs/ember-data.js"></script>
+<script src="/js/app.js"></script>
diff --git a/public/js/app.js b/public/js/app.js
new file mode 100644
index 0000000..238f29e
--- /dev/null
+++ b/public/js/app.js
@@ -0,0 +1,16 @@
+App = Ember.Application.create({
+ LOG_TRANSITIONS: true,
+ rootElement: '#container'
+});
+
+
+App.ApplicationRoute = Ember.Route.extend({
+ setupController: function(controller) {
+ // `controller` is the instance of ApplicationController
+ controller.set('title', "Hello world!");
+ }
+});
+
+App.ApplicationController = Ember.Controller.extend({
+ appName: 'My First Example'
+}); \ No newline at end of file
diff --git a/public/js/libs/ember-data.js b/public/js/libs/ember-data.js
new file mode 100644
index 0000000..6db1c0f
--- /dev/null
+++ b/public/js/libs/ember-data.js
@@ -0,0 +1,7793 @@
+(function() {
+window.DS = Ember.Namespace.create({
+ // this one goes to 11
+ CURRENT_API_REVISION: 11
+});
+
+})();
+
+
+
+(function() {
+var DeferredMixin = Ember.DeferredMixin, // ember-runtime/mixins/deferred
+ Evented = Ember.Evented, // ember-runtime/mixins/evented
+ run = Ember.run, // ember-metal/run-loop
+ get = Ember.get; // ember-metal/accessors
+
+var LoadPromise = Ember.Mixin.create(Evented, DeferredMixin, {
+ init: function() {
+ this._super.apply(this, arguments);
+ this.one('didLoad', function() {
+ run(this, 'resolve', this);
+ });
+
+ if (get(this, 'isLoaded')) {
+ this.trigger('didLoad');
+ }
+ }
+});
+
+DS.LoadPromise = LoadPromise;
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set;
+
+var LoadPromise = DS.LoadPromise; // system/mixins/load_promise
+
+/**
+ A record array is an array that contains records of a certain type. The record
+ array materializes records as needed when they are retrieved for the first
+ time. You should not create record arrays yourself. Instead, an instance of
+ DS.RecordArray or its subclasses will be returned by your application's store
+ in response to queries.
+*/
+
+DS.RecordArray = Ember.ArrayProxy.extend(Ember.Evented, LoadPromise, {
+ /**
+ The model type contained by this record array.
+
+ @type DS.Model
+ */
+ type: null,
+
+ // The array of client ids backing the record array. When a
+ // record is requested from the record array, the record
+ // for the client id at the same index is materialized, if
+ // necessary, by the store.
+ content: null,
+
+ isLoaded: false,
+ isUpdating: false,
+
+ // The store that created this record array.
+ store: null,
+
+ objectAtContent: function(index) {
+ var content = get(this, 'content'),
+ reference = content.objectAt(index),
+ store = get(this, 'store');
+
+ if (reference) {
+ return store.recordForReference(reference);
+ }
+ },
+
+ materializedObjectAt: function(index) {
+ var reference = get(this, 'content').objectAt(index);
+ if (!reference) { return; }
+
+ if (get(this, 'store').recordIsMaterialized(reference)) {
+ return this.objectAt(index);
+ }
+ },
+
+ update: function() {
+ if (get(this, 'isUpdating')) { return; }
+
+ var store = get(this, 'store'),
+ type = get(this, 'type');
+
+ store.fetchAll(type, this);
+ },
+
+ addReference: function(reference) {
+ get(this, 'content').addObject(reference);
+ },
+
+ removeReference: function(reference) {
+ get(this, 'content').removeObject(reference);
+ }
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get;
+
+DS.FilteredRecordArray = DS.RecordArray.extend({
+ filterFunction: null,
+ isLoaded: true,
+
+ replace: function() {
+ var type = get(this, 'type').toString();
+ throw new Error("The result of a client-side filter (on " + type + ") is immutable.");
+ },
+
+ updateFilter: Ember.observer(function() {
+ var store = get(this, 'store');
+ store.updateRecordArrayFilter(this, get(this, 'type'), get(this, 'filterFunction'));
+ }, 'filterFunction')
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set;
+
+DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({
+ query: null,
+
+ replace: function() {
+ var type = get(this, 'type').toString();
+ throw new Error("The result of a server query (on " + type + ") is immutable.");
+ },
+
+ load: function(references) {
+ var store = get(this, 'store'), type = get(this, 'type');
+
+ this.beginPropertyChanges();
+ set(this, 'content', Ember.A(references));
+ set(this, 'isLoaded', true);
+ this.endPropertyChanges();
+
+ var self = this;
+ // TODO: does triggering didLoad event should be the last action of the runLoop?
+ Ember.run.once(function() {
+ self.trigger('didLoad');
+ });
+ }
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set;
+
+/**
+ A ManyArray is a RecordArray that represents the contents of a has-many
+ relationship.
+
+ The ManyArray is instantiated lazily the first time the relationship is
+ requested.
+
+ ### Inverses
+
+ Often, the relationships in Ember Data applications will have
+ an inverse. For example, imagine the following models are
+ defined:
+
+ App.Post = DS.Model.extend({
+ comments: DS.hasMany('App.Comment')
+ });
+
+ App.Comment = DS.Model.extend({
+ post: DS.belongsTo('App.Post')
+ });
+
+ If you created a new instance of `App.Post` and added
+ a `App.Comment` record to its `comments` has-many
+ relationship, you would expect the comment's `post`
+ property to be set to the post that contained
+ the has-many.
+
+ We call the record to which a relationship belongs the
+ relationship's _owner_.
+*/
+DS.ManyArray = DS.RecordArray.extend({
+ init: function() {
+ this._super.apply(this, arguments);
+ this._changesToSync = Ember.OrderedSet.create();
+ },
+
+ /**
+ @private
+
+ The record to which this relationship belongs.
+
+ @property {DS.Model}
+ */
+ owner: null,
+
+ // LOADING STATE
+
+ isLoaded: false,
+
+ loadingRecordsCount: function(count) {
+ this.loadingRecordsCount = count;
+ },
+
+ loadedRecord: function() {
+ this.loadingRecordsCount--;
+ if (this.loadingRecordsCount === 0) {
+ set(this, 'isLoaded', true);
+ this.trigger('didLoad');
+ }
+ },
+
+ fetch: function() {
+ var references = get(this, 'content'),
+ store = get(this, 'store'),
+ type = get(this, 'type'),
+ owner = get(this, 'owner');
+
+ store.fetchUnloadedReferences(type, 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));
+ return get(record, '_reference');
+ }, this);
+
+ this._super(index, removed, added);
+ },
+
+ arrangedContentDidChange: function() {
+ this.fetch();
+ },
+
+ arrayContentWillChange: function(index, removed, added) {
+ var owner = get(this, 'owner'),
+ name = get(this, 'name');
+
+ if (!owner._suspendedRelationships) {
+ // This code is the first half of code that continues inside
+ // of arrayContentDidChange. It gets or creates a change from
+ // the child object, adds the current owner as the old
+ // parent if this is the first time the object was removed
+ // from a ManyArray, and sets `newParent` to null.
+ //
+ // Later, if the object is added to another ManyArray,
+ // the `arrayContentDidChange` will set `newParent` on
+ // the change.
+ for (var i=index; i<index+removed; i++) {
+ var reference = get(this, 'content').objectAt(i);
+
+ var change = DS.RelationshipChange.createChange(owner.get('_reference'), reference, get(this, 'store'), {
+ parentType: owner.constructor,
+ changeType: "remove",
+ kind: "hasMany",
+ key: name
+ });
+
+ this._changesToSync.add(change);
+ }
+ }
+
+ return this._super.apply(this, arguments);
+ },
+
+ arrayContentDidChange: function(index, removed, added) {
+ this._super.apply(this, arguments);
+
+ var owner = get(this, 'owner'),
+ name = get(this, 'name'),
+ store = get(this, 'store');
+
+ if (!owner._suspendedRelationships) {
+ // This code is the second half of code that started in
+ // `arrayContentWillChange`. It gets or creates a change
+ // from the child object, and adds the current owner as
+ // the new parent.
+ for (var i=index; i<index+added; i++) {
+ var reference = get(this, 'content').objectAt(i);
+
+ var change = DS.RelationshipChange.createChange(owner.get('_reference'), reference, store, {
+ parentType: owner.constructor,
+ changeType: "add",
+ kind:"hasMany",
+ key: name
+ });
+ change.hasManyName = name;
+
+ this._changesToSync.add(change);
+ }
+
+ // We wait until the array has finished being
+ // mutated before syncing the OneToManyChanges created
+ // in arrayContentWillChange, so that the array
+ // membership test in the sync() logic operates
+ // on the final results.
+ this._changesToSync.forEach(function(change) {
+ change.sync();
+ });
+ DS.OneToManyChange.ensureSameTransaction(this._changesToSync, store);
+ this._changesToSync.clear();
+ }
+ },
+
+ // Create a child record within the owner
+ createRecord: function(hash, transaction) {
+ var owner = get(this, 'owner'),
+ store = get(owner, 'store'),
+ type = get(this, 'type'),
+ record;
+
+ transaction = transaction || get(owner, 'transaction');
+
+ record = store.createRecord.call(store, type, hash, transaction);
+ this.pushObject(record);
+
+ return record;
+ }
+
+});
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt,
+ removeObject = Ember.EnumerableUtils.removeObject, forEach = Ember.EnumerableUtils.forEach;
+
+/**
+ A transaction allows you to collect multiple records into a unit of work
+ that can be committed or rolled back as a group.
+
+ For example, if a record has local modifications that have not yet
+ been saved, calling `commit()` on its transaction will cause those
+ modifications to be sent to the adapter to be saved. Calling
+ `rollback()` on its transaction would cause all of the modifications to
+ be discarded and the record to return to the last known state before
+ changes were made.
+
+ If a newly created record's transaction is rolled back, it will
+ immediately transition to the deleted state.
+
+ If you do not explicitly create a transaction, a record is assigned to
+ an implicit transaction called the default transaction. In these cases,
+ you can treat your application's instance of `DS.Store` as a transaction
+ and call the `commit()` and `rollback()` methods on the store itself.
+
+ Once a record has been successfully committed or rolled back, it will
+ be moved back to the implicit transaction. Because it will now be in
+ a clean state, it can be moved to a new transaction if you wish.
+
+ ### Creating a Transaction
+
+ To create a new transaction, call the `transaction()` method of your
+ application's `DS.Store` instance:
+
+ var transaction = App.store.transaction();
+
+ This will return a new instance of `DS.Transaction` with no records
+ yet assigned to it.
+
+ ### Adding Existing Records
+
+ Add records to a transaction using the `add()` method:
+
+ record = App.store.find(App.Person, 1);
+ transaction.add(record);
+
+ Note that only records whose `isDirty` flag is `false` may be added
+ to a transaction. Once modifications to a record have been made
+ (its `isDirty` flag is `true`), it is not longer able to be added to
+ a transaction.
+
+ ### Creating New Records
+
+ Because newly created records are dirty from the time they are created,
+ and because dirty records can not be added to a transaction, you must
+ use the `createRecord()` method to assign new records to a transaction.
+
+ For example, instead of this:
+
+ var transaction = store.transaction();
+ var person = App.Person.createRecord({ name: "Steve" });
+
+ // won't work because person is dirty
+ transaction.add(person);
+
+ Call `createRecord()` on the transaction directly:
+
+ var transaction = store.transaction();
+ transaction.createRecord(App.Person, { name: "Steve" });
+
+ ### Asynchronous Commits
+
+ Typically, all of the records in a transaction will be committed
+ together. However, new records that have a dependency on other new
+ records need to wait for their parent record to be saved and assigned an
+ ID. In that case, the child record will continue to live in the
+ transaction until its parent is saved, at which time the transaction will
+ attempt to commit again.
+
+ For this reason, you should not re-use transactions once you have committed
+ them. Always make a new transaction and move the desired records to it before
+ calling commit.
+*/
+
+var arrayDefault = function() { return []; };
+
+DS.Transaction = Ember.Object.extend({
+ /**
+ @private
+
+ Creates the bucket data structure used to segregate records by
+ 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());
+ },
+
+ /**
+ Creates a new record of the given type and assigns it to the transaction
+ on which the method was called.
+
+ This is useful as only clean records can be added to a transaction and
+ new records created using other methods immediately become dirty.
+
+ @param {DS.Model} type the model type to create
+ @param {Object} hash the data hash to assign the new record
+ */
+ createRecord: function(type, hash) {
+ var store = get(this, 'store');
+
+ return store.createRecord(type, hash, this);
+ },
+
+ isEqualOrDefault: function(other) {
+ if (this === other || other === get(this, 'store.defaultTransaction')) {
+ return true;
+ }
+ },
+
+ isDefault: Ember.computed(function() {
+ return this === get(this, 'store.defaultTransaction');
+ }),
+
+ /**
+ Adds an existing record to this transaction. Only records without
+ modficiations (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
+ */
+ 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');
+
+ // Make `add` idempotent
+ if (recordTransaction === this) { return; }
+
+ // XXX it should be possible to move a dirty transaction from the default transaction
+
+ // 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'));
+
+ Ember.assert("Models cannot belong to more than one transaction at a time.", recordTransaction === defaultTransaction);
+
+ this.adoptRecord(record);
+ },
+
+ relationshipBecameDirty: function(relationship) {
+ get(this, 'relationships').add(relationship);
+ },
+
+ relationshipBecameClean: function(relationship) {
+ get(this, 'relationships').remove(relationship);
+ },
+
+ /**
+ Commits the transaction, which causes all of the modified records that
+ belong to the transaction to be sent to the adapter to be saved.
+
+ Once you call `commit()` on a transaction, you should not re-use it.
+
+ When a record is saved, it will be removed from this transaction and
+ moved back to the store's default transaction.
+ */
+ 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) {
+ 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); }
+ }
+
+ // Once we've committed the transaction, there is no need to
+ // keep the OneToManyChanges around. Destroy them so they
+ // can be garbage collected.
+ relationships.forEach(function(relationship) {
+ relationship.destroy();
+ });
+ },
+
+ /**
+ Rolling back a transaction resets the records that belong to
+ that transaction.
+
+ Updated records have their properties reset to the last known
+ value from the persistence layer. Deleted records are reverted
+ to a clean, non-deleted state. Newly created records immediately
+ become deleted, and are not sent to the adapter to be persisted.
+
+ After the transaction is rolled back, any records that belong
+ to it will return to the store's default transaction, and the
+ 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);
+
+ // Now that all records in the transaction are guaranteed to be
+ // clean, migrate them all to the store's default transaction.
+ this.removeCleanRecords();
+ },
+
+ /**
+ @private
+
+ Removes a record from this transaction and back to the store's
+ default transaction.
+
+ Note: This method is private for now, but should probably be exposed
+ in the future once we have stricter error checking (for example, in the
+ case of the record being dirty).
+
+ @param {DS.Model} record
+ */
+ remove: function(record) {
+ var defaultTransaction = get(this, 'store.defaultTransaction');
+ defaultTransaction.adoptRecord(record);
+ },
+
+ /**
+ @private
+
+ 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);
+ },
+
+ /**
+ @private
+
+ This method moves a record into a different transaction without the normal
+ checks that ensure that the user is not doing something weird, like moving
+ a dirty record into a new transaction.
+
+ It is designed for internal use, such as when we are moving a clean record
+ into a new transaction when the transaction is committed.
+
+ This method must not be called unless the record is clean.
+
+ @param {DS.Model} record
+ */
+ adoptRecord: function(record) {
+ var oldTransaction = get(record, 'transaction');
+
+ if (oldTransaction) {
+ oldTransaction.removeFromBucket('clean', record);
+ }
+
+ this.addToBucket('clean', 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.
+
+ @param {String} bucketType one of `created`, `updated`, or `deleted`
+ */
+ 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.
+
+ @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);
+ },
+
+ /**
+ @private
+
+ 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.
+
+ @param {String} bucketType one of `created`, `updated`, or `deleted`
+ */
+ recordBecameClean: function(kind, record) {
+ this.removeFromBucket(kind, record);
+ this.remove(record);
+ }
+});
+
+})();
+
+
+
+(function() {
+var classify = Ember.String.classify, get = Ember.get;
+
+/**
+@private
+
+ The Mappable mixin is designed for classes that would like to
+ behave as a map for configuration purposes.
+
+ For example, the DS.Adapter class can behave like a map, with
+ more semantic API, via the `map` API:
+
+ DS.Adapter.map('App.Person', { firstName: { keyName: 'FIRST' } });
+
+ Class configuration via a map-like API has a few common requirements
+ that differentiate it from the standard Ember.Map implementation.
+
+ First, values often are provided as strings that should be normalized
+ into classes the first time the configuration options are used.
+
+ Second, the values configured on parent classes should also be taken
+ into account.
+
+ Finally, setting the value of a key sometimes should merge with the
+ previous value, rather than replacing it.
+
+ This mixin provides a instance method, `createInstanceMapFor`, that
+ will reify all of the configuration options set on an instance's
+ constructor and provide it for the instance to use.
+
+ Classes can implement certain hooks that allow them to customize
+ the requirements listed above:
+
+ * `resolveMapConflict` - called when a value is set for an existing
+ value
+ * `transformMapKey` - allows a key name (for example, a global path
+ to a class) to be normalized
+ * `transformMapValue` - allows a value (for example, a class that
+ should be instantiated) to be normalized
+
+ Classes that implement this mixin should also implement a class
+ method built using the `generateMapFunctionFor` method:
+
+ DS.Adapter.reopenClass({
+ map: DS.Mappable.generateMapFunctionFor('attributes', function(key, newValue, map) {
+ var existingValue = map.get(key);
+
+ for (var prop in newValue) {
+ if (!newValue.hasOwnProperty(prop)) { continue; }
+ existingValue[prop] = newValue[prop];
+ }
+ })
+ });
+
+ The function passed to `generateMapFunctionFor` is invoked every time a
+ new value is added to the map.
+**/
+
+var resolveMapConflict = function(oldValue, newValue, mappingsKey) {
+ return oldValue;
+};
+
+var transformMapKey = function(key, value) {
+ return key;
+};
+
+var transformMapValue = function(key, value) {
+ return value;
+};
+
+DS._Mappable = Ember.Mixin.create({
+ createInstanceMapFor: function(mapName) {
+ var instanceMeta = Ember.metaPath(this, ['DS.Mappable'], true);
+
+ instanceMeta.values = instanceMeta.values || {};
+
+ if (instanceMeta.values[mapName]) { return instanceMeta.values[mapName]; }
+
+ var instanceMap = instanceMeta.values[mapName] = new Ember.Map();
+
+ var klass = this.constructor;
+
+ while (klass && klass !== DS.Store) {
+ this._copyMap(mapName, klass, instanceMap);
+ klass = klass.superclass;
+ }
+
+ instanceMeta.values[mapName] = instanceMap;
+ return instanceMap;
+ },
+
+ _copyMap: function(mapName, klass, instanceMap) {
+ var classMeta = Ember.metaPath(klass, ['DS.Mappable'], true);
+
+ var classMap = classMeta[mapName];
+ if (classMap) {
+ classMap.forEach(eachMap, this);
+ }
+
+ function eachMap(key, value) {
+ var transformedKey = (klass.transformMapKey || transformMapKey)(key, value);
+ var transformedValue = (klass.transformMapValue || transformMapValue)(key, value);
+
+ var oldValue = instanceMap.get(transformedKey);
+ var newValue = transformedValue;
+
+ if (oldValue) {
+ newValue = (this.constructor.resolveMapConflict || resolveMapConflict)(oldValue, newValue, mapName);
+ }
+
+ instanceMap.set(transformedKey, newValue);
+ }
+ },
+
+
+});
+
+DS._Mappable.generateMapFunctionFor = function(mapName, transform) {
+ return function(key, value) {
+ var meta = Ember.metaPath(this, ['DS.Mappable'], true);
+ var map = meta[mapName] || Ember.MapWithDefault.create({
+ defaultValue: function() { return {}; }
+ });
+
+ transform.call(this, key, value, map);
+
+ meta[mapName] = map;
+ };
+};
+
+})();
+
+
+
+(function() {
+/*globals Ember*/
+/*jshint eqnull:true*/
+var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt, once = Ember.run.once;
+var forEach = Ember.EnumerableUtils.forEach;
+// These values are used in the data cache when clientIds are
+// needed but the underlying data has not yet been loaded by
+// the server.
+var UNLOADED = 'unloaded';
+var LOADING = 'loading';
+var MATERIALIZED = { materialized: true };
+var CREATED = { created: true };
+
+// Implementors Note:
+//
+// The variables in this file are consistently named according to the following
+// scheme:
+//
+// * +id+ means an identifier managed by an external source, provided inside
+// the data provided by that source.
+// * +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.
+// * +type+ means a subclass of DS.Model.
+
+// Used by the store to normalize IDs entering the store. Despite the fact
+// that developers may provide IDs as numbers (e.g., `store.find(Person, 1)`),
+// it is important that internally we use strings, since IDs may be serialized
+// and lose type information. For example, Ember's router may put a record's
+// ID into the URL, and if we later try to deserialize that URL and find the
+// corresponding record, we will not know if it is a string or a number.
+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
+ the individual data for a record, so that they can be bound to in your
+ Handlebars templates.
+
+ Create a new store like this:
+
+ MyApp.store = DS.Store.create();
+
+ 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 record = MyApp.store.find(MyApp.Contact, 123);
+
+ By default, the store will talk to your backend using a standard REST mechanism.
+ You can customize how the store talks to your backend by specifying a custom adapter:
+
+ MyApp.store = DS.Store.create({
+ adapter: 'MyApp.CustomAdapter'
+ });
+
+ You can learn more about writing a custom adapter by reading the `DS.Adapter`
+ documentation.
+*/
+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.
+
+ 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.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 = {};
+
+ set(this, 'defaultTransaction', this.transaction());
+ },
+
+ /**
+ Returns a new transaction scoped to this store. This delegates
+ responsibility for invoking the adapter's commit mechanism to
+ a transaction.
+
+ Transaction are responsible for tracking changes to records
+ added to them, and supporting `commit` and `rollback`
+ functionality. Committing a transaction invokes the store's
+ adapter, while rolling back a transaction reverses all
+ changes made to records added to the transaction.
+
+ A store has an implicit (default) transaction, which tracks changes
+ made to records not explicitly added to a transaction.
+
+ @see {DS.Transaction}
+ @returns DS.Transaction
+ */
+ transaction: function() {
+ 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
+
+ Instructs the store to materialize the data for a given record.
+
+ To materialize a record, the store first retrieves the opaque data that was
+ passed to either `load()` or `loadMany()`. Then, the data and the record
+ are passed to the adapter's `materialize()` method, which allows the adapter
+ to translate arbitrary data structures from the adapter into the normalized
+ form the record expects.
+
+ The adapter's `materialize()` method will invoke `materializeAttribute()`,
+ `materializeHasMany()` and `materializeBelongsTo()` on the record to
+ populate it with normalized values.
+
+ @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 prematerialized = this.clientIdToPrematerializedData[clientId];
+
+ // 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);
+ }
+ },
+
+ /**
+ @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.
+
+ This can be specified as an instance, a class, or a property path that specifies
+ where the adapter can be located.
+
+ @property {DS.Adapter|String}
+ */
+ adapter: 'DS.RESTAdapter',
+
+ /**
+ @private
+
+ Returns a JSON representation of the record using the adapter's
+ serialization strategy. This method exists primarily to enable
+ a record, which has access to its store (but not the store's
+ adapter) to provide a `serialize()` convenience.
+
+ The available options are:
+
+ * `includeId`: `true` if the record's ID should be included in
+ the JSON representation
+
+ @param {DS.Model} record the record to serialize
+ @param {Object} options an options hash
+ */
+ serialize: function(record, options) {
+ return this.adapterForType(record.constructor).serialize(record, options);
+ },
+
+ /**
+ @private
+
+ This property returns the adapter, after resolving a possible
+ property path.
+
+ If the supplied `adapter` was a class, or a String property
+ path resolved to a class, this property will instantiate the
+ class.
+
+ This property is cacheable, so the same instance of a specified
+ adapter class should be used for the lifetime of the store.
+
+ @returns DS.Adapter
+ */
+ _adapter: Ember.computed(function() {
+ var adapter = get(this, 'adapter');
+ if (typeof adapter === 'string') {
+ adapter = get(this, adapter, false) || get(Ember.lookup, adapter);
+ }
+
+ if (DS.Adapter.detect(adapter)) {
+ adapter = adapter.create();
+ }
+
+ return adapter;
+ }).property('adapter'),
+
+ /**
+ @private
+
+ A monotonically increasing number to be used to uniquely identify
+ data and records.
+
+ It starts at 1 so other parts of the code can test for truthiness
+ when provided a `clientId` instead of having to explicitly test
+ for undefined.
+ */
+ clientIdCounter: 1,
+
+ // .....................
+ // . CREATE NEW RECORD .
+ // .....................
+
+ /**
+ Create a new record in the current store. The properties passed
+ to this method are set on the newly created record.
+
+ Note: The third `transaction` property is for internal use only.
+ If you want to create a record inside of a given transaction,
+ use `transaction.createRecord()` instead of `store.createRecord()`.
+
+ @param {subclass of DS.Model} type
+ @param {Object} properties a hash of properties to set on the
+ newly created record.
+ @returns DS.Model
+ */
+ createRecord: function(type, properties, transaction) {
+ properties = properties || {};
+
+ // Create a new instance of the model `type` and put it
+ // into the specified `transaction`. If no transaction is
+ // specified, the default transaction will be used.
+ var record = type._create({
+ store: this
+ });
+
+ transaction = transaction || get(this, 'defaultTransaction');
+
+ // adoptRecord is an internal API that allows records to move
+ // into a transaction without assertions designed for app
+ // code. It is used here to ensure that regardless of new
+ // restrictions on the use of the public `transaction.add()`
+ // API, we will always be able to insert new records into
+ // their transaction.
+ transaction.adoptRecord(record);
+
+ // `id` is a special property that may not be a `DS.attr`
+ var id = properties.id;
+
+ // If the passed properties do not include a primary key,
+ // give the adapter an opportunity to generate one. Typically,
+ // client-side ID generators will use something like uuid.js
+ // to avoid conflicts.
+ var adapter;
+ if (Ember.isNone(id)) {
+ adapter = this.adapterForType(type);
+ if (adapter && adapter.generateIdForRecord) {
+ id = coerceId(adapter.generateIdForRecord(this, record));
+ properties.id = id;
+ }
+ }
+
+ id = coerceId(id);
+
+ // Create a new `clientId` and associate it with the
+ // specified (or generated) `id`. Since we don't have
+ // any data for the server yet (by definition), store
+ // 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);
+
+ // Now that we have a clientId, attach it to the record we
+ // just created.
+ set(record, 'clientId', clientId);
+
+ // 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);
+
+ // Resolve record promise
+ Ember.run(record, 'resolve', record);
+
+ return record;
+ },
+
+ // .................
+ // . DELETE RECORD .
+ // .................
+
+ /**
+ For symmetry, a record can be deleted via the store.
+
+ @param {DS.Model} record
+ */
+ deleteRecord: function(record) {
+ record.deleteRecord();
+ },
+
+ /**
+ For symmetry, a record can be unloaded via the store.
+
+ @param {DS.Model} record
+ */
+ unloadRecord: function(record) {
+ record.unloadRecord();
+ },
+
+ // ................
+ // . FIND RECORDS .
+ // ................
+
+ /**
+ This is the main entry point into finding records. The first parameter to
+ this method is always a subclass of `DS.Model`.
+
+ You can use the `find` method on a subclass of `DS.Model` directly if your
+ application only has one store. For example, instead of
+ `store.find(App.Person, 1)`, you could say `App.Person.find(1)`.
+
+ ---
+
+ To find a record by ID, pass the `id` as the second parameter:
+
+ store.find(App.Person, 1);
+ App.Person.find(1);
+
+ If the record with that `id` had not previously been loaded, the store will
+ return an empty record immediately and ask the adapter to find the data by
+ calling the adapter's `find` method.
+
+ The `find` method will always return the same object for a given type and
+ `id`. To check whether the adapter has populated a record, you can check
+ its `isLoaded` property.
+
+ ---
+
+ To find all records for a type, call `find` with no additional parameters:
+
+ store.find(App.Person);
+ App.Person.find();
+
+ This will return a `RecordArray` representing all known records for the
+ given type and kick off a request to the adapter's `findAll` method to load
+ any additional records for the type.
+
+ The `RecordArray` returned by `find()` is live. If any more records for the
+ type are added at a later time through any mechanism, it will automatically
+ update to reflect the change.
+
+ ---
+
+ To find a record by a query, call `find` with a hash as the second
+ parameter:
+
+ store.find(App.Person, { page: 1 });
+ App.Person.find({ page: 1 });
+
+ This will return a `RecordArray` immediately, but it will always be an
+ empty `RecordArray` at first. It will call the adapter's `findQuery`
+ method, which will populate the `RecordArray` once the server has returned
+ results.
+
+ You can check whether a query results `RecordArray` has loaded by checking
+ its `isLoaded` property.
+ */
+ find: function(type, id) {
+ if (id === undefined) {
+ return this.findAll(type);
+ }
+
+ // We are passed a query instead of an id.
+ if (Ember.typeOf(id) === 'object') {
+ return this.findQuery(type, id);
+ }
+
+ return this.findById(type, coerceId(id));
+ },
+
+ /**
+ @private
+
+ This method returns a record for a given type and id combination.
+
+ If the store has never seen this combination of type and id before, it
+ creates a new `clientId` with the LOADING sentinel and asks the adapter to
+ load the data.
+
+ If the store has seen the combination, this method delegates to
+ `getByReference`.
+ */
+ findById: function(type, id) {
+ var clientId = this.typeMapFor(type).idToCid[id];
+
+ if (clientId) {
+ return this.findByClientId(type, clientId);
+ }
+
+ clientId = this.pushData(LOADING, id, type);
+
+ // create a new instance of the model type in the
+ // 'isLoading' state
+ var record = this.materializeRecord(type, clientId, id);
+
+ // 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"; }
+
+ return record;
+ },
+
+ reloadRecord: function(record) {
+ var type = record.constructor,
+ adapter = this.adapterForType(type),
+ 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);
+ },
+
+ /**
+ @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.
+
+ 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
+
+ In short, it's a convenient way to get a record for a known
+ clientId, materializing it if necessary.
+
+ @param {Class} type
+ @param {Number|String} clientId
+ */
+ findByClientId: function(type, clientId) {
+ var cidToData, record, id;
+
+ record = this.recordCache[clientId];
+
+ 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();
+ }
+ }
+
+ return record;
+ },
+
+ /**
+ @private
+
+ Given a type and array of `clientId`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;
+
+ for (var i=0, l=references.length; i<l; i++) {
+ reference = references[i];
+
+ if (cidToData[reference.clientId] === UNLOADED) {
+ neededReferences.push(reference);
+ cidToData[reference.clientId] = LOADING;
+ }
+ }
+
+ return neededReferences;
+ },
+
+ /**
+ @private
+
+ This method is the entry point that relationships use to update
+ themselves when their underlying data changes.
+
+ First, it determines which of its `clientId`s are still unloaded,
+ then converts the needed `clientId`s to IDs and invokes `findMany`
+ on the adapter.
+ */
+ fetchUnloadedReferences: function(type, references, owner) {
+ var neededReferences = this.neededReferences(type, references);
+ this.fetchMany(type, neededReferences, owner);
+ },
+
+ /**
+ @private
+
+ This method takes a type and list of `clientId`s, converts the
+ `clientId`s into IDs, and then invokes the adapter's `findMany`
+ method.
+
+ It is used both by a brand new relationship (via the `findMany`
+ method) or when the data underlying an existing relationship
+ changes (via the `fetchUnloadedReferences` method).
+ */
+ fetchMany: function(type, references, owner) {
+ if (!references.length) { return; }
+
+ var ids = map(references, function(reference) {
+ return reference.id;
+ });
+
+ var adapter = this.adapterForType(type);
+ if (adapter && adapter.findMany) { adapter.findMany(this, type, ids, owner); }
+ else { throw "Adapter is either null or does not implement `findMany` method"; }
+ },
+
+ referenceForId: function(type, id) {
+ var clientId = this.clientIdForId(type, id);
+ return this.referenceForClientId(clientId);
+ },
+
+ referenceForClientId: function(clientId) {
+ var references = this.recordReferences;
+
+ if (references[clientId]) {
+ return references[clientId];
+ }
+
+ var type = this.clientIdToType[clientId];
+
+ return references[clientId] = {
+ id: this.idForClientId(clientId),
+ clientId: clientId,
+ type: type
+ };
+ },
+
+ recordForReference: function(reference) {
+ return this.findByClientId(reference.type, reference.clientId);
+ },
+
+ /**
+ @private
+
+ `findMany` is the entry point that relationships use to generate a
+ new `ManyArray` for the list of IDs specified by the server for
+ the relationship.
+
+ Its responsibilities are:
+
+ * convert the IDs into clientIds
+ * determine which of the clientIds still need to be loaded
+ * create a new ManyArray whose content is *all* of the clientIds
+ * notify the ManyArray of the number of its elements that are
+ already loaded
+ * insert the unloaded clientIds into the `loadingRecordArrays`
+ bookkeeping structure, which will allow the `ManyArray` to know
+ when all of its loading elements are loaded from the server.
+ * ask the adapter to load the unloaded elements, by invoking
+ findMany with the still-unloaded IDs.
+ */
+ findMany: function(type, ids, record, relationship) {
+ // 1. Convert ids to client ids
+ // 2. Determine which of the client ids need to be loaded
+ // 3. Create a new ManyArray whose content is ALL of the clientIds
+ // 4. Decrement the ManyArray's counter by the number of loaded clientIds
+ // 5. Put the ManyArray into our bookkeeping data structure, keyed on
+ // the needed clientIds
+ // 6. Ask the adapter to load the records for the unloaded clientIds (but
+ // convert them back to ids)
+
+ if (!Ember.isArray(ids)) {
+ var adapter = this.adapterForType(type);
+ if (adapter && adapter.findHasMany) { adapter.findHasMany(this, record, relationship, ids); }
+ else { throw fmt("Adapter is either null or does not implement `findHasMany` method", this); }
+
+ return this.createManyArray(type, Ember.A());
+ }
+
+ // Coerce server IDs into Record Reference
+ var references = map(ids, function(reference) {
+ if (typeof reference !== 'object' && reference !== null) {
+ return this.referenceForId(type, reference);
+ }
+
+ return reference;
+ }, this);
+
+ var neededReferences = this.neededReferences(type, references),
+ manyArray = this.createManyArray(type, Ember.A(references)),
+ loadingRecordArrays = this.loadingRecordArrays,
+ reference, clientId, i, l;
+
+ // Start the decrementing counter on the ManyArray at the number of
+ // records we need to load from the adapter
+ manyArray.loadingRecordsCount(neededReferences.length);
+
+ if (neededReferences.length) {
+ for (i=0, l=neededReferences.length; i<l; i++) {
+ reference = neededReferences[i];
+ clientId = reference.clientId;
+
+ // keep track of the record arrays that a given loading record
+ // is part of. This way, if the same record is in multiple
+ // ManyArrays, all of their loading records counters will be
+ // decremented when the adapter provides the data.
+ if (loadingRecordArrays[clientId]) {
+ loadingRecordArrays[clientId].push(manyArray);
+ } else {
+ this.loadingRecordArrays[clientId] = [ manyArray ];
+ }
+ }
+
+ this.fetchMany(type, neededReferences, record);
+ } else {
+ // all requested records are available
+ manyArray.set('isLoaded', true);
+
+ Ember.run.once(function() {
+ manyArray.trigger('didLoad');
+ });
+ }
+
+ return manyArray;
+ },
+
+ /**
+ @private
+
+ This method delegates a query to the adapter. This is the one place where
+ adapter-level semantics are exposed to the application.
+
+ Exposing queries this way seems preferable to creating an abstract query
+ language for all server-side queries, and then require all adapters to
+ implement them.
+
+ @param {Class} type
+ @param {Object} query an opaque query to be used by the adapter
+ @return {DS.AdapterPopulatedRecordArray}
+ */
+ findQuery: function(type, query) {
+ var array = DS.AdapterPopulatedRecordArray.create({ type: type, query: query, content: Ember.A([]), store: this });
+ var adapter = this.adapterForType(type);
+ if (adapter && adapter.findQuery) { adapter.findQuery(this, type, query, array); }
+ else { throw "Adapter is either null or does not implement `findQuery` method"; }
+ return array;
+ },
+
+ /**
+ @private
+
+ This method returns an array of all records adapter can find.
+ It triggers the adapter's `findAll` method to give it an opportunity to populate
+ the array with records of that type.
+
+ @param {Class} type
+ @return {DS.AdapterPopulatedRecordArray}
+ */
+ findAll: function(type) {
+ var array = this.all(type);
+ this.fetchAll(type, array);
+ return array;
+ },
+
+ /**
+ @private
+ */
+ fetchAll: function(type, array) {
+ var sinceToken = this.typeMapFor(type).sinceToken,
+ adapter = this.adapterForType(type);
+
+ set(array, 'isUpdating', true);
+
+ if (adapter && adapter.findAll) { adapter.findAll(this, type, sinceToken); }
+ else { throw "Adapter is either null or does not implement `findAll` method"; }
+ },
+
+ /**
+ */
+ sinceForType: function(type, sinceToken) {
+ this.typeMapFor(type).sinceToken = sinceToken;
+ },
+
+ /**
+ */
+ didUpdateAll: function(type) {
+ var findAllCache = this.typeMapFor(type).findAllCache;
+ set(findAllCache, 'isUpdating', false);
+ },
+
+ /**
+ This method returns a filtered array that contains all of the known records
+ for a given type.
+
+ Note that because it's just a filter, it will have any locally
+ created records of the type.
+
+ Also note that multiple calls to `all` for a given type will always
+ return the same RecordArray.
+
+ @param {Class} type
+ @return {DS.RecordArray}
+ */
+ all: function(type) {
+ var typeMap = this.typeMapFor(type),
+ findAllCache = typeMap.findAllCache;
+
+ if (findAllCache) { return findAllCache; }
+
+ var array = DS.RecordArray.create({ type: type, content: Ember.A([]), store: this, isLoaded: true });
+ this.registerRecordArray(array, type);
+
+ typeMap.findAllCache = array;
+ return array;
+ },
+
+ /**
+ Takes a type and filter function, and returns a live RecordArray that
+ remains up to date as new records are loaded into the store or created
+ locally.
+
+ The callback function takes a materialized record, and returns true
+ if the record should be included in the filter and false if it should
+ not.
+
+ The filter function is called once on all records for the type when
+ it is created, and then once on each newly loaded or created record.
+
+ If any of a record's properties change, or if it changes state, the
+ filter function will be invoked again to determine whether it should
+ still be in the array.
+
+ Note that the existence of a filter on a type will trigger immediate
+ materialization of all loaded data for a given type, so you might
+ not want to use filters for a type if you are loading many records
+ into the store, many of which are not active at any given time.
+
+ In this scenario, you might want to consider filtering the raw
+ data before loading it into the store.
+
+ @param {Class} type
+ @param {Function} filter
+
+ @return {DS.FilteredRecordArray}
+ */
+ filter: function(type, query, filter) {
+ // allow an optional server query
+ if (arguments.length === 3) {
+ this.findQuery(type, query);
+ } else if (arguments.length === 2) {
+ filter = query;
+ }
+
+ var array = DS.FilteredRecordArray.create({ type: type, content: Ember.A([]), store: this, filterFunction: filter });
+
+ this.registerRecordArray(array, type, filter);
+
+ return array;
+ },
+
+ /**
+ This method returns if a certain record is already loaded
+ in the store. Use this function to know beforehand if a find()
+ will result in a request or that it will be a cache hit.
+
+ @param {Class} type
+ @param {string} id
+ @return {boolean}
+ */
+ recordIsLoaded: function(type, id) {
+ return !Ember.isNone(this.typeMapFor(type).idToCid[id]);
+ },
+
+ // ............
+ // . UPDATING .
+ // ............
+
+ /**
+ @private
+
+ If the adapter updates attributes or acknowledges creation
+ or deletion, the record will notify the store to update its
+ membership in any filters.
+
+ To avoid thrashing, this method is invoked only once per
+ run loop per record.
+
+ @param {Class} type
+ @param {Number|String} clientId
+ @param {DS.Model} record
+ */
+ dataWasUpdated: function(type, reference, record) {
+ // Because data updates are invoked at the end of the run loop,
+ // it is possible that a record might be deleted after its data
+ // has been modified and this method was scheduled to be called.
+ //
+ // If that's the case, the record would have already been removed
+ // from all record arrays; calling updateRecordArrays would just
+ // add it back. If the record is deleted, just bail. It shouldn't
+ // give us any more trouble after this.
+
+ if (get(record, 'isDeleted')) { return; }
+
+ var cidToData = this.clientIdToData,
+ clientId = reference.clientId,
+ data = cidToData[clientId];
+
+ if (typeof data === "object") {
+ this.updateRecordArrays(type, clientId);
+ }
+ },
+
+ // ..............
+ // . PERSISTING .
+ // ..............
+
+ /**
+ This method delegates committing to the store's implicit
+ transaction.
+
+ Calling this method is essentially a request to persist
+ any changes to records that were not explicitly added to
+ a transaction.
+ */
+ commit: function() {
+ get(this, 'defaultTransaction').commit();
+ },
+
+ /**
+ Adapters should call this method if they would like to acknowledge
+ that all changes related to a record (other than relationship
+ changes) have persisted.
+
+ Because relationship changes affect multiple records, the adapter
+ is responsible for acknowledging the change to the relationship
+ directly (using `store.didUpdateRelationship`) when all aspects
+ of the relationship change have persisted.
+
+ It can be called for created, deleted or updated records.
+
+ If the adapter supplies new data, that data will become the new
+ canonical data for the record. That will result in blowing away
+ all local changes and rematerializing the record with the new
+ data (the "sledgehammer" approach).
+
+ Alternatively, if the adapter does not supply new data, the record
+ will collapse all local changes into its saved data. Subsequent
+ rollbacks of the record will roll back to this point.
+
+ If an adapter is acknowledging receipt of a newly created record
+ that did not generate an id in the client, it *must* either
+ provide data or explicitly invoke `store.didReceiveId` with
+ the server-provided id.
+
+ Note that an adapter may not supply new data when acknowledging
+ a deleted record.
+
+ @see DS.Store#didUpdateRelationship
+
+ @param {DS.Model} record the in-flight record
+ @param {Object} data optional data (see above)
+ */
+ didSaveRecord: function(record, data) {
+ record.adapterDidCommit();
+
+ if (data) {
+ this.updateId(record, data);
+ this.updateRecordData(record, data);
+ } else {
+ this.didUpdateAttributes(record);
+ }
+ },
+
+ /**
+ For convenience, if an adapter is performing a bulk commit, it can also
+ acknowledge all of the records at once.
+
+ If the adapter supplies an array of data, they must be in the same order as
+ the array of records passed in as the first parameter.
+
+ @param {#forEach} list a list of records whose changes the
+ adapter is acknowledging. You can pass any object that
+ has an ES5-like `forEach` method, including the
+ `OrderedSet` objects passed into the adapter at commit
+ time.
+ @param {Array[Object]} dataList an Array of data. This
+ parameter must be an integer-indexed Array-like.
+ */
+ didSaveRecords: function(list, dataList) {
+ var i = 0;
+ list.forEach(function(record) {
+ this.didSaveRecord(record, dataList && dataList[i++]);
+ }, this);
+ },
+
+ /**
+ This method allows the adapter to specify that a record
+ could not be saved because it had backend-supplied validation
+ errors.
+
+ The errors object must have keys that correspond to the
+ attribute names. Once each of the specified attributes have
+ changed, the record will automatically move out of the
+ invalid state and be ready to commit again.
+
+ TODO: We should probably automate the process of converting
+ server names to attribute names using the existing serializer
+ infrastructure.
+
+ @param {DS.Model} record
+ @param {Object} errors
+ */
+ recordWasInvalid: function(record, errors) {
+ record.adapterDidInvalidate(errors);
+ },
+
+ /**
+ This method allows the adapter to specify that a record
+ could not be saved because the server returned an unhandled
+ error.
+
+ @param {DS.Model} record
+ */
+ recordWasError: function(record) {
+ record.adapterDidError();
+ },
+
+ /**
+ This is a lower-level API than `didSaveRecord` that allows an
+ adapter to acknowledge the persistence of a single attribute.
+
+ This is useful if an adapter needs to make multiple asynchronous
+ calls to fully persist a record. The record will keep track of
+ which attributes and relationships are still outstanding and
+ automatically move into the `saved` state once the adapter has
+ acknowledged everything.
+
+ If a value is provided, it clobbers the locally specified value.
+ Otherwise, the local value becomes the record's last known
+ saved value (which is used when rolling back a record).
+
+ Note that the specified attributeName is the normalized name
+ specified in the definition of the `DS.Model`, not a key in
+ the server-provided data.
+
+ Also note that the adapter is responsible for performing any
+ transformations on the value using the serializer API.
+
+ @param {DS.Model} record
+ @param {String} attributeName
+ @param {Object} value
+ */
+ didUpdateAttribute: function(record, attributeName, value) {
+ record.adapterDidUpdateAttribute(attributeName, value);
+ },
+
+ /**
+ This method allows an adapter to acknowledge persistence
+ of all attributes of a record but not relationships or
+ other factors.
+
+ It loops through the record's defined attributes and
+ notifies the record that they are all acknowledged.
+
+ This method does not take optional values, because
+ the adapter is unlikely to have a hash of normalized
+ keys and transformed values, and instead of building
+ one up, it should just call `didUpdateAttribute` as
+ needed.
+
+ This method is intended as a middle-ground between
+ `didSaveRecord`, which acknowledges all changes to
+ a record, and `didUpdateAttribute`, which allows an
+ adapter fine-grained control over updates.
+
+ @param {DS.Model} record
+ */
+ didUpdateAttributes: function(record) {
+ record.eachAttribute(function(attributeName) {
+ this.didUpdateAttribute(record, attributeName);
+ }, this);
+ },
+
+ /**
+ This allows an adapter to acknowledge that it has saved all
+ necessary aspects of a relationship change.
+
+ This is separated from acknowledging the record itself
+ (via `didSaveRecord`) because a relationship change can
+ involve as many as three separate records. Records should
+ only move out of the in-flight state once the server has
+ acknowledged all of their relationships, and this differs
+ based upon the adapter's semantics.
+
+ There are three basic scenarios by which an adapter can
+ save a relationship.
+
+ ### Foreign Key
+
+ An adapter can save all relationship changes by updating
+ a foreign key on the child record. If it does this, it
+ should acknowledge the changes when the child record is
+ saved.
+
+ record.eachRelationship(function(name, meta) {
+ if (meta.kind === 'belongsTo') {
+ store.didUpdateRelationship(record, name);
+ }
+ });
+
+ store.didSaveRecord(record, data);
+
+ ### Embedded in Parent
+
+ An adapter can save one-to-many relationships by embedding
+ IDs (or records) in the parent object. In this case, the
+ relationship is not considered acknowledged until both the
+ old parent and new parent have acknowledged the change.
+
+ In this case, the adapter should keep track of the old
+ parent and new parent, and acknowledge the relationship
+ change once both have acknowledged. If one of the two
+ sides does not exist (e.g. the new parent does not exist
+ because of nulling out the belongs-to relationship),
+ the adapter should acknowledge the relationship once
+ the other side has acknowledged.
+
+ ### Separate Entity
+
+ An adapter can save relationships as separate entities
+ on the server. In this case, they should acknowledge
+ the relationship as saved once the server has
+ acknowledged the entity.
+
+ @see DS.Store#didSaveRecord
+
+ @param {DS.Model} record
+ @param {DS.Model} relationshipName
+ */
+ didUpdateRelationship: function(record, relationshipName) {
+ var relationship = this.relationshipChangeFor(get(record, 'clientId'), relationshipName);
+ //TODO(Igor)
+ if (relationship) { relationship.adapterDidUpdate(); }
+ },
+
+ /**
+ This allows an adapter to acknowledge all relationship changes
+ for a given record.
+
+ Like `didUpdateAttributes`, this is intended as a middle ground
+ between `didSaveRecord` and fine-grained control via the
+ `didUpdateRelationship` API.
+ */
+ didUpdateRelationships: function(record) {
+ var changes = this.relationshipChangesFor(get(record, '_reference'));
+
+ for (var name in changes) {
+ if (!changes.hasOwnProperty(name)) { continue; }
+ changes[name].adapterDidUpdate();
+ }
+ },
+
+ /**
+ When acknowledging the creation of a locally created record,
+ adapters must supply an id (if they did not implement
+ `generateIdForRecord` to generate an id locally).
+
+ If an adapter does not use `didSaveRecord` and supply a hash
+ (for example, if it needs to make multiple HTTP requests to
+ create and then update the record), it will need to invoke
+ `didReceiveId` with the backend-supplied id.
+
+ When not using `didSaveRecord`, an adapter will need to
+ invoke:
+
+ * didReceiveId (unless the id was generated locally)
+ * didCreateRecord
+ * didUpdateAttribute(s)
+ * didUpdateRelationship(s)
+
+ @param {DS.Model} record
+ @param {Number|String} id
+ */
+ didReceiveId: function(record, id) {
+ var typeMap = this.typeMapFor(record.constructor),
+ clientId = get(record, 'clientId'),
+ oldId = get(record, 'id');
+
+ Ember.assert("An adapter cannot assign a new id to a record that already has an id. " + record + " had id: " + oldId + " and you tried to update it with " + id + ". This likely happened because your server returned data in response to a find or update that had a different id than the one you sent.", oldId === undefined || id === oldId);
+
+ typeMap.idToCid[id] = clientId;
+ this.clientIdToId[clientId] = id;
+ },
+
+ /**
+ @private
+
+ This method re-indexes the data by its clientId in the store
+ and then notifies the record that it should rematerialize
+ itself.
+
+ @param {DS.Model} record
+ @param {Object} data
+ */
+ updateRecordData: function(record, data) {
+ var clientId = get(record, 'clientId'),
+ cidToData = this.clientIdToData;
+
+ cidToData[clientId] = data;
+
+ record.didChangeData();
+ },
+
+ /**
+ @private
+
+ If an adapter invokes `didSaveRecord` with data, this method
+ extracts the id from the supplied data (using the adapter's
+ `extractId()` method) and indexes the clientId with that id.
+
+ @param {DS.Model} record
+ @param {Object} data
+ */
+ updateId: function(record, data) {
+ var typeMap = this.typeMapFor(record.constructor),
+ clientId = get(record, 'clientId'),
+ oldId = get(record, 'id'),
+ type = record.constructor,
+ id = this.preprocessData(type, data);
+
+ Ember.assert("An adapter cannot assign a new id to a record that already has an id. " + record + " had id: " + oldId + " and you tried to update it with " + id + ". This likely happened because your server returned data in response to a find or update that had a different id than the one you sent.", oldId === null || id === oldId);
+
+ typeMap.idToCid[id] = clientId;
+ this.clientIdToId[clientId] = id;
+ this.referenceForClientId(clientId).id = id;
+ },
+
+ /**
+ @private
+
+ This method receives opaque data provided by the adapter and
+ preprocesses it, returning an ID.
+
+ The actual preprocessing takes place in the adapter. If you would
+ like to change the default behavior, you should override the
+ appropriate hooks in `DS.Serializer`.
+
+ @see {DS.Serializer}
+ @return {String} id the id represented by the data
+ */
+ preprocessData: function(type, data) {
+ return this.adapterForType(type).extractId(type, data);
+ },
+
+ // .................
+ // . RECORD ARRAYS .
+ // .................
+
+ /**
+ @private
+
+ Register a RecordArray for a given type to be backed by
+ a filter function. This will cause the array to update
+ automatically when records of that type change attribute
+ values or states.
+
+ @param {DS.RecordArray} array
+ @param {Class} type
+ @param {Function} filter
+ */
+ registerRecordArray: function(array, type, filter) {
+ var recordArrays = this.typeMapFor(type).recordArrays;
+
+ recordArrays.push(array);
+
+ this.updateRecordArrayFilter(array, type, filter);
+ },
+
+ /**
+ @private
+
+ Create a `DS.ManyArray` for a type and list of clientIds
+ and index the `ManyArray` under each clientId. This allows
+ us to efficiently remove records from `ManyArray`s when
+ they are deleted.
+
+ @param {Class} type
+ @param {Array} clientIds
+
+ @return {DS.ManyArray}
+ */
+ createManyArray: function(type, clientIds) {
+ var array = DS.ManyArray.create({ type: type, content: clientIds, store: this });
+
+ clientIds.forEach(function(clientId) {
+ var recordArrays = this.recordArraysForClientId(clientId);
+ recordArrays.add(array);
+ }, this);
+
+ return array;
+ },
+
+ /**
+ @private
+
+ This method is invoked if the `filterFunction` property is
+ changed on a `DS.FilteredRecordArray`.
+
+ It essentially re-runs the filter from scratch. This same
+ method is invoked when the filter is created in th first place.
+ */
+ updateRecordArrayFilter: function(array, type, filter) {
+ var typeMap = this.typeMapFor(type),
+ cidToData = this.clientIdToData,
+ clientIds = typeMap.clientIds,
+ clientId, data, shouldFilter, record;
+
+ for (var i=0, l=clientIds.length; i<l; i++) {
+ clientId = clientIds[i];
+ shouldFilter = false;
+
+ data = cidToData[clientId];
+
+ if (typeof data === 'object') {
+ if (record = this.recordCache[clientId]) {
+ if (!get(record, 'isDeleted')) { shouldFilter = true; }
+ } else {
+ shouldFilter = true;
+ }
+
+ if (shouldFilter) {
+ this.updateRecordArray(array, filter, type, clientId);
+ }
+ }
+ }
+ },
+
+ updateRecordArraysLater: function(type, clientId) {
+ Ember.run.once(this, function() {
+ this.updateRecordArrays(type, clientId);
+ });
+ },
+
+ /**
+ @private
+
+ This method is invoked whenever data is loaded into the store
+ by the adapter or updated by the adapter, or when an attribute
+ changes on a record.
+
+ It updates all filters that a record belongs to.
+
+ To avoid thrashing, it only runs once per run loop per record.
+
+ @param {Class} type
+ @param {Number|String} clientId
+ */
+ updateRecordArrays: function(type, clientId) {
+ var recordArrays = this.typeMapFor(type).recordArrays,
+ filter;
+
+ recordArrays.forEach(function(array) {
+ filter = get(array, 'filterFunction');
+ this.updateRecordArray(array, filter, type, clientId);
+ }, this);
+
+ // loop through all manyArrays containing an unloaded copy of this
+ // clientId and notify them that the record was loaded.
+ var manyArrays = this.loadingRecordArrays[clientId];
+
+ if (manyArrays) {
+ for (var i=0, l=manyArrays.length; i<l; i++) {
+ manyArrays[i].loadedRecord();
+ }
+
+ this.loadingRecordArrays[clientId] = null;
+ }
+ },
+
+ /**
+ @private
+
+ Update an individual filter.
+
+ @param {DS.FilteredRecordArray} array
+ @param {Function} filter
+ @param {Class} type
+ @param {Number|String} clientId
+ */
+ updateRecordArray: function(array, filter, type, clientId) {
+ var shouldBeInArray, record;
+
+ if (!filter) {
+ shouldBeInArray = true;
+ } else {
+ record = this.findByClientId(type, clientId);
+ shouldBeInArray = filter(record);
+ }
+
+ var content = get(array, 'content');
+ var alreadyInArray = content.indexOf(clientId) !== -1;
+
+ var recordArrays = this.recordArraysForClientId(clientId);
+ var reference = this.referenceForClientId(clientId);
+
+ if (shouldBeInArray) {
+ recordArrays.add(array);
+ array.addReference(reference);
+ } else if (!shouldBeInArray) {
+ recordArrays.remove(array);
+ array.removeReference(reference);
+ }
+ },
+
+ /**
+ @private
+
+ When a record is deleted, it is removed from all its
+ record arrays.
+
+ @param {DS.Model} record
+ */
+ removeFromRecordArrays: function(record) {
+ var reference = get(record, '_reference');
+ var recordArrays = this.recordArraysForClientId(reference.clientId);
+
+ recordArrays.forEach(function(array) {
+ array.removeReference(reference);
+ });
+ },
+
+ // ............
+ // . INDEXING .
+ // ............
+
+ /**
+ @private
+
+ Return a list of all `DS.RecordArray`s a clientId is
+ part of.
+
+ @return {Object(clientId: Ember.OrderedSet)}
+ */
+ recordArraysForClientId: function(clientId) {
+ var recordArrays = get(this, 'recordArraysByClientId');
+ var ret = recordArrays[clientId];
+
+ if (!ret) {
+ ret = recordArrays[clientId] = Ember.OrderedSet.create();
+ }
+
+ return ret;
+ },
+
+ typeMapFor: function(type) {
+ var typeMaps = get(this, 'typeMaps');
+ var guidForType = Ember.guidFor(type);
+
+ var typeMap = typeMaps[guidForType];
+
+ if (typeMap) {
+ return typeMap;
+ } else {
+ return (typeMaps[guidForType] =
+ {
+ idToCid: {},
+ clientIds: [],
+ recordArrays: []
+ });
+ }
+ },
+
+ /** @private
+
+ For a given type and id combination, returns the client id used by the store.
+ If no client id has been assigned yet, one will be created and returned.
+
+ @param {DS.Model} type
+ @param {String|Number} id
+ */
+ clientIdForId: function(type, id) {
+ id = coerceId(id);
+
+ var clientId = this.typeMapFor(type).idToCid[id];
+ if (clientId !== undefined) { return clientId; }
+
+ return this.pushData(UNLOADED, id, type);
+ },
+
+ /**
+ @private
+
+ This method works exactly like `clientIdForId`, but does not
+ require looking up the `typeMap` for every `clientId` and
+ invoking a method per `clientId`.
+ */
+ clientIdsForIds: function(type, ids) {
+ var typeMap = this.typeMapFor(type),
+ idToClientIdMap = typeMap.idToCid;
+
+ return map(ids, function(id) {
+ id = coerceId(id);
+
+ var clientId = idToClientIdMap[id];
+ if (clientId) { return clientId; }
+ return this.pushData(UNLOADED, id, type);
+ }, this);
+ },
+
+ typeForClientId: function(clientId) {
+ return this.clientIdToType[clientId];
+ },
+
+ idForClientId: function(clientId) {
+ return this.clientIdToId[clientId];
+ },
+
+ // ................
+ // . LOADING DATA .
+ // ................
+
+ /**
+ Load new data into the store for a given id and type combination.
+ If data for that record had been loaded previously, the new information
+ overwrites the old.
+
+ If the record you are loading data for has outstanding changes that have not
+ yet been saved, an exception will be thrown.
+
+ @param {DS.Model} type
+ @param {String|Number} id
+ @param {Object} data the data to load
+ */
+ load: function(type, data, prematerialized) {
+ var id;
+
+ if (typeof data === 'number' || typeof data === 'string') {
+ id = data;
+ data = prematerialized;
+ prematerialized = null;
+ }
+
+ if (prematerialized && prematerialized.id) {
+ id = prematerialized.id;
+ } else if (id === undefined) {
+ var adapter = this.adapterForType(type);
+ id = this.preprocessData(type, data);
+ }
+
+ id = coerceId(id);
+
+ var typeMap = this.typeMapFor(type),
+ cidToData = this.clientIdToData,
+ clientId = typeMap.idToCid[id],
+ cidToPrematerialized = this.clientIdToPrematerializedData;
+
+ if (clientId !== undefined) {
+ cidToData[clientId] = data;
+ cidToPrematerialized[clientId] = prematerialized;
+
+ var record = this.recordCache[clientId];
+ if (record) {
+ once(record, 'loadedData');
+ }
+ } else {
+ clientId = this.pushData(data, id, type);
+ cidToPrematerialized[clientId] = prematerialized;
+ }
+
+ this.updateRecordArraysLater(type, clientId);
+
+ return this.referenceForClientId(clientId);
+ },
+
+ prematerialize: function(reference, prematerialized) {
+ this.clientIdToPrematerializedData[reference.clientId] = prematerialized;
+ },
+
+ loadMany: function(type, ids, dataList) {
+ if (dataList === undefined) {
+ dataList = ids;
+ ids = map(dataList, function(data) {
+ return this.preprocessData(type, data);
+ }, this);
+ }
+
+ return map(ids, function(id, i) {
+ return this.load(type, id, dataList[i]);
+ }, this);
+ },
+
+ loadHasMany: function(record, key, ids) {
+ record.materializeHasMany(key, ids);
+
+ // Update any existing many arrays that use the previous IDs,
+ // if necessary.
+ record.hasManyDidChange(key);
+
+ var relationship = record.cacheFor(key);
+
+ // TODO (tomdale) this assumes that loadHasMany *always* means
+ // that the records for the provided IDs are loaded.
+ if (relationship) { set(relationship, 'isLoaded', true); }
+ },
+
+ /** @private
+
+ Stores data for the specified type and id combination and returns
+ the client id.
+
+ @param {Object} data
+ @param {String|Number} id
+ @param {DS.Model} type
+ @returns {Number}
+ */
+ pushData: function(data, id, type) {
+ var typeMap = this.typeMapFor(type);
+
+ var idToClientIdMap = typeMap.idToCid,
+ clientIdToIdMap = this.clientIdToId,
+ clientIdToTypeMap = this.clientIdToType,
+ clientIds = typeMap.clientIds,
+ cidToData = this.clientIdToData;
+
+ var clientId = ++this.clientIdCounter;
+
+ cidToData[clientId] = data;
+ clientIdToTypeMap[clientId] = type;
+
+ // if we're creating an item, this process will be done
+ // later, once the object has been persisted.
+ if (id) {
+ idToClientIdMap[id] = clientId;
+ clientIdToIdMap[clientId] = id;
+ }
+
+ clientIds.push(clientId);
+
+ return clientId;
+ },
+
+ // ..........................
+ // . RECORD MATERIALIZATION .
+ // ..........................
+
+ materializeRecord: function(type, clientId, id) {
+ var record;
+
+ this.recordCache[clientId] = record = type._create({
+ store: this,
+ clientId: clientId,
+ });
+
+ set(record, 'id', id);
+
+ get(this, 'defaultTransaction').adoptRecord(record);
+
+ record.loadingData();
+ return record;
+ },
+
+ dematerializeRecord: function(record) {
+ var id = get(record, 'id'),
+ clientId = get(record, 'clientId'),
+ type = this.typeForClientId(clientId),
+ typeMap = this.typeMapFor(type);
+
+ record.updateRecordArrays();
+
+ delete this.recordCache[clientId];
+ delete this.clientIdToId[clientId];
+ delete this.clientIdToType[clientId];
+ delete this.clientIdToData[clientId];
+ delete this.recordArraysByClientId[clientId];
+
+ if (id) { delete typeMap.idToCid[id]; }
+ },
+
+ destroy: function() {
+ if (get(DS, 'defaultStore') === this) {
+ set(DS, 'defaultStore', null);
+ }
+
+ return this._super();
+ },
+
+ // ........................
+ // . RELATIONSHIP CHANGES .
+ // ........................
+
+ addRelationshipChangeFor: function(clientReference, childKey, parentReference, parentKey, change) {
+ var clientId = clientReference.clientId,
+ parentClientId = parentReference ? parentReference.clientId : parentReference;
+ var key = childKey + parentKey;
+ var changes = this.relationshipChanges;
+ if (!(clientId in changes)) {
+ changes[clientId] = {};
+ }
+ if (!(parentClientId in changes[clientId])) {
+ changes[clientId][parentClientId] = {};
+ }
+ if (!(key in changes[clientId][parentClientId])) {
+ changes[clientId][parentClientId][key] = {};
+ }
+ changes[clientId][parentClientId][key][change.changeType] = change;
+ },
+
+ removeRelationshipChangeFor: function(clientReference, childKey, parentReference, parentKey, type) {
+ var clientId = clientReference.clientId,
+ parentClientId = parentReference ? parentReference.clientId : parentReference;
+ var changes = this.relationshipChanges;
+ var key = childKey + parentKey;
+ if (!(clientId in changes) || !(parentClientId in changes[clientId]) || !(key in changes[clientId][parentClientId])){
+ return;
+ }
+ delete changes[clientId][parentClientId][key][type];
+ },
+
+ relationshipChangeFor: function(clientId, childKey, parentClientId, parentKey, type) {
+ var changes = this.relationshipChanges;
+ var key = childKey + parentKey;
+ if (!(clientId in changes) || !(parentClientId in changes[clientId])){
+ return;
+ }
+ if(type){
+ return changes[clientId][parentClientId][key][type];
+ }
+ else{
+ //TODO(Igor) what if both present
+ return changes[clientId][parentClientId][key]["add"] || changes[clientId][parentClientId][key]["remove"];
+ }
+ },
+
+ relationshipChangePairsFor: function(reference){
+ var toReturn = [];
+
+ if( !reference ) { return toReturn; }
+
+ //TODO(Igor) What about the other side
+ var changesObject = this.relationshipChanges[reference.clientId];
+ for (var objKey in changesObject){
+ if(changesObject.hasOwnProperty(objKey)){
+ for (var changeKey in changesObject[objKey]){
+ if(changesObject[objKey].hasOwnProperty(changeKey)){
+ toReturn.push(changesObject[objKey][changeKey]);
+ }
+ }
+ }
+ }
+ return toReturn;
+ },
+
+ relationshipChangesFor: function(reference) {
+ var toReturn = [];
+
+ if( !reference ) { return toReturn; }
+
+ var relationshipPairs = this.relationshipChangePairsFor(reference);
+ forEach(relationshipPairs, function(pair){
+ var addedChange = pair["add"];
+ var removedChange = pair["remove"];
+ if(addedChange){
+ toReturn.push(addedChange);
+ }
+ if(removedChange){
+ toReturn.push(removedChange);
+ }
+ });
+ return toReturn;
+ },
+ // ......................
+ // . PER-TYPE ADAPTERS
+ // ......................
+
+ adapterForType: function(type) {
+ this._adaptersMap = this.createInstanceMapFor('adapters');
+
+ var adapter = this._adaptersMap.get(type);
+ if (adapter) { return adapter; }
+
+ return this.get('_adapter');
+ },
+
+ // ..............................
+ // . RECORD CHANGE NOTIFICATION .
+ // ..............................
+
+ recordAttributeDidChange: function(reference, attributeName, newValue, oldValue) {
+ var record = this.recordForReference(reference),
+ dirtySet = new Ember.OrderedSet(),
+ adapter = this.adapterForType(record.constructor);
+
+ if (adapter.dirtyRecordsForAttributeChange) {
+ adapter.dirtyRecordsForAttributeChange(dirtySet, record, attributeName, newValue, oldValue);
+ }
+
+ dirtySet.forEach(function(record) {
+ record.adapterDidDirty();
+ });
+ },
+
+ recordBelongsToDidChange: function(dirtySet, child, relationship) {
+ var adapter = this.adapterForType(child.constructor);
+
+ if (adapter.dirtyRecordsForBelongsToChange) {
+ adapter.dirtyRecordsForBelongsToChange(dirtySet, child, relationship);
+ }
+
+ // adapterDidDirty is called by the RelationshipChange that created
+ // the dirtySet.
+ },
+
+ recordHasManyDidChange: function(dirtySet, parent, relationship) {
+ var adapter = this.adapterForType(parent.constructor);
+
+ if (adapter.dirtyRecordsForHasManyChange) {
+ adapter.dirtyRecordsForHasManyChange(dirtySet, parent, relationship);
+ }
+
+ // adapterDidDirty is called by the RelationshipChange that created
+ // the dirtySet.
+ }
+});
+
+DS.Store.reopenClass({
+ registerAdapter: DS._Mappable.generateMapFunctionFor('adapters', function(type, adapter, map) {
+ map.set(type, adapter);
+ }),
+
+ transformMapKey: function(key) {
+ if (typeof key === 'string') {
+ var transformedKey;
+ transformedKey = get(Ember.lookup, key);
+ Ember.assert("Could not find model at path " + key, transformedKey);
+ return transformedKey;
+ } else {
+ return key;
+ }
+ },
+
+ transformMapValue: function(key, value) {
+ if (Ember.Object.detect(value)) {
+ return value.create();
+ }
+
+ return value;
+ }
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor,
+ once = Ember.run.once, arrayMap = Ember.ArrayPolyfills.map;
+
+/**
+ This file encapsulates the various states that a record can transition
+ through during its lifecycle.
+
+ ### State Manager
+
+ A record's state manager explicitly tracks what state a record is in
+ at any given time. For instance, if a record is newly created and has
+ not yet been sent to the adapter to be saved, it would be in the
+ `created.uncommitted` state. If a record has had local modifications
+ made to it that are in the process of being saved, the record would be
+ in the `updated.inFlight` state. (These state paths will be explained
+ in more detail below.)
+
+ Events are sent by the record or its store to the record's state manager.
+ How the state manager reacts to these events is dependent on which state
+ it is in. In some states, certain events will be invalid and will cause
+ an exception to be raised.
+
+ States are hierarchical. For example, a record can be in the
+ `deleted.start` state, then transition into the `deleted.inFlight` state.
+ If a child state does not implement an event handler, the state manager
+ will attempt to invoke the event on all parent states until the root state is
+ reached. The state hierarchy of a record is described in terms of a path
+ string. You can determine a record's current state by getting its manager's
+ current state path:
+
+ record.get('stateManager.currentPath');
+ //=> "created.uncommitted"
+
+ The `DS.Model` states are themselves stateless. What we mean is that,
+ though each instance of a record also has a unique instance of a
+ `DS.StateManager`, the hierarchical states that each of *those* points
+ to is a shared data structure. For performance reasons, instead of each
+ record getting its own copy of the hierarchy of states, each state
+ manager points to this global, immutable shared instance. How does a
+ state know which record it should be acting on? We pass a reference to
+ the current state manager as the first parameter to every method invoked
+ on a state.
+
+ The state manager passed as the first parameter is where you should stash
+ state about the record if needed; you should never store data on the state
+ object itself. If you need access to the record being acted on, you can
+ retrieve the state manager's `record` property. For example, if you had
+ an event handler `myEvent`:
+
+ myEvent: function(manager) {
+ var record = manager.get('record');
+ record.doSomething();
+ }
+
+ For more information about state managers in general, see the Ember.js
+ documentation on `Ember.StateManager`.
+
+ ### Events, Flags, and Transitions
+
+ A state may implement zero or more events, flags, or transitions.
+
+ #### Events
+
+ Events are named functions that are invoked when sent to a record. The
+ state manager will first look for a method with the given name on the
+ current state. If no method is found, it will search the current state's
+ parent, and then its grandparent, and so on until reaching the top of
+ the hierarchy. If the root is reached without an event handler being found,
+ an exception will be raised. This can be very helpful when debugging new
+ features.
+
+ Here's an example implementation of a state with a `myEvent` event handler:
+
+ aState: DS.State.create({
+ myEvent: function(manager, param) {
+ console.log("Received myEvent with "+param);
+ }
+ })
+
+ To trigger this event:
+
+ record.send('myEvent', 'foo');
+ //=> "Received myEvent with foo"
+
+ Note that an optional parameter can be sent to a record's `send()` method,
+ which will be passed as the second parameter to the event handler.
+
+ Events should transition to a different state if appropriate. This can be
+ done by calling the state manager's `transitionTo()` method with a path to the
+ desired state. The state manager will attempt to resolve the state path
+ relative to the current state. If no state is found at that path, it will
+ attempt to resolve it relative to the current state's parent, and then its
+ parent, and so on until the root is reached. For example, imagine a hierarchy
+ like this:
+
+ * created
+ * start <-- currentState
+ * inFlight
+ * updated
+ * inFlight
+
+ If we are currently in the `start` state, calling
+ `transitionTo('inFlight')` would transition to the `created.inFlight` state,
+ while calling `transitionTo('updated.inFlight')` would transition to
+ the `updated.inFlight` state.
+
+ Remember that *only events* should ever cause a state transition. You should
+ never call `transitionTo()` from outside a state's event handler. If you are
+ tempted to do so, create a new event and send that to the state manager.
+
+ #### Flags
+
+ Flags are Boolean values that can be used to introspect a record's current
+ state in a more user-friendly way than examining its state path. For example,
+ instead of doing this:
+
+ var statePath = record.get('stateManager.currentPath');
+ if (statePath === 'created.inFlight') {
+ doSomething();
+ }
+
+ You can say:
+
+ if (record.get('isNew') && record.get('isSaving')) {
+ doSomething();
+ }
+
+ If your state does not set a value for a given flag, the value will
+ be inherited from its parent (or the first place in the state hierarchy
+ where it is defined).
+
+ The current set of flags are defined below. If you want to add a new flag,
+ in addition to the area below, you will also need to declare it in the
+ `DS.Model` class.
+
+ #### Transitions
+
+ Transitions are like event handlers but are called automatically upon
+ entering or exiting a state. To implement a transition, just call a method
+ either `enter` or `exit`:
+
+ myState: DS.State.create({
+ // Gets called automatically when entering
+ // this state.
+ enter: function(manager) {
+ console.log("Entered myState");
+ }
+ })
+
+ Note that enter and exit events are called once per transition. If the
+ current state changes, but changes to another child state of the parent,
+ the transition event on the parent will not be triggered.
+*/
+
+var stateProperty = Ember.computed(function(key) {
+ var parent = get(this, 'parentState');
+ if (parent) {
+ return get(parent, key);
+ }
+}).property();
+
+var isEmptyObject = function(object) {
+ for (var name in object) {
+ if (object.hasOwnProperty(name)) { return false; }
+ }
+
+ return true;
+};
+
+var hasDefinedProperties = function(object) {
+ for (var name in object) {
+ if (object.hasOwnProperty(name) && object[name]) { return true; }
+ }
+
+ return false;
+};
+
+var didChangeData = function(manager) {
+ var record = get(manager, 'record');
+ record.materializeData();
+};
+
+var willSetProperty = function(manager, context) {
+ context.oldValue = get(get(manager, 'record'), context.name);
+
+ var change = DS.AttributeChange.createChange(context);
+ get(manager, 'record')._changesToSync[context.attributeName] = change;
+};
+
+var didSetProperty = function(manager, context) {
+ var change = get(manager, 'record')._changesToSync[context.attributeName];
+ change.value = get(get(manager, 'record'), context.name);
+ change.sync();
+};
+
+// Whenever a property is set, recompute all dependent filters
+var updateRecordArrays = function(manager) {
+ var record = manager.get('record');
+ record.updateRecordArraysLater();
+};
+
+DS.State = Ember.State.extend({
+ isLoaded: stateProperty,
+ isReloading: stateProperty,
+ isDirty: stateProperty,
+ isSaving: stateProperty,
+ isDeleted: stateProperty,
+ isError: stateProperty,
+ isNew: stateProperty,
+ isValid: stateProperty,
+
+ // For states that are substates of a
+ // DirtyState (updated or created), it is
+ // useful to be able to determine which
+ // type of dirty state it is.
+ dirtyType: stateProperty
+});
+
+// Implementation notes:
+//
+// Each state has a boolean value for all of the following flags:
+//
+// * isLoaded: The record has a populated `data` property. When a
+// record is loaded via `store.find`, `isLoaded` is false
+// until the adapter sets it. When a record is created locally,
+// its `isLoaded` property is always true.
+// * isDirty: The record has local changes that have not yet been
+// saved by the adapter. This includes records that have been
+// created (but not yet saved) or deleted.
+// * isSaving: The record's transaction has been committed, but
+// the adapter has not yet acknowledged that the changes have
+// been persisted to the backend.
+// * isDeleted: The record was marked for deletion. When `isDeleted`
+// is true and `isDirty` is true, the record is deleted locally
+// but the deletion was not yet persisted. When `isSaving` is
+// true, the change is in-flight. When both `isDirty` and
+// `isSaving` are false, the change has persisted.
+// * isError: The adapter reported that it was unable to save
+// local changes to the backend. This may also result in the
+// record having its `isValid` property become false if the
+// adapter reported that server-side validations failed.
+// * isNew: The record was created on the client and the adapter
+// did not yet report that it was successfully saved.
+// * isValid: No client-side validations have failed and the
+// adapter did not report any server-side validation failures.
+
+// The dirty state is a abstract state whose functionality is
+// shared between the `created` and `updated` states.
+//
+// The deleted state shares the `isDirty` flag with the
+// subclasses of `DirtyState`, but with a very different
+// implementation.
+//
+// Dirty states have three child states:
+//
+// `uncommitted`: the store has not yet handed off the record
+// to be saved.
+// `inFlight`: the store has handed off the record to be saved,
+// but the adapter has not yet acknowledged success.
+// `invalid`: the record has invalid information and cannot be
+// send to the adapter yet.
+var DirtyState = DS.State.extend({
+ initialState: 'uncommitted',
+
+ // FLAGS
+ isDirty: true,
+
+ // SUBSTATES
+
+ // When a record first becomes dirty, it is `uncommitted`.
+ // This means that there are local pending changes, but they
+ // have not yet begun to be saved, and are not invalid.
+ uncommitted: DS.State.extend({
+ // TRANSITIONS
+ enter: function(manager) {
+ var dirtyType = get(this, 'dirtyType'),
+ record = get(manager, 'record');
+
+ record.withTransaction(function (t) {
+ t.recordBecameDirty(dirtyType, record);
+ });
+ },
+
+ // EVENTS
+ willSetProperty: willSetProperty,
+ didSetProperty: didSetProperty,
+
+ becomeDirty: Ember.K,
+
+ willCommit: function(manager) {
+ manager.transitionTo('inFlight');
+ },
+
+ becameClean: function(manager) {
+ var record = get(manager, 'record'),
+ dirtyType = get(this, 'dirtyType');
+
+ record.withTransaction(function(t) {
+ t.recordBecameClean(dirtyType, record);
+ });
+
+ manager.transitionTo('loaded.materializing');
+ },
+
+ becameInvalid: function(manager) {
+ var dirtyType = get(this, 'dirtyType'),
+ record = get(manager, 'record');
+
+ record.withTransaction(function (t) {
+ t.recordBecameInFlight(dirtyType, record);
+ });
+
+ manager.transitionTo('invalid');
+ },
+
+ rollback: function(manager) {
+ get(manager, 'record').rollback();
+ }
+ }),
+
+ // Once a record has been handed off to the adapter to be
+ // saved, it is in the 'in flight' state. Changes to the
+ // record cannot be made during this window.
+ inFlight: DS.State.extend({
+ // FLAGS
+ isSaving: true,
+
+ // TRANSITIONS
+ enter: function(manager) {
+ var dirtyType = get(this, 'dirtyType'),
+ record = get(manager, 'record');
+
+ record.becameInFlight();
+
+ record.withTransaction(function (t) {
+ t.recordBecameInFlight(dirtyType, record);
+ });
+ },
+
+ // EVENTS
+ didCommit: function(manager) {
+ var dirtyType = get(this, 'dirtyType'),
+ record = get(manager, 'record');
+
+ record.withTransaction(function(t) {
+ t.recordBecameClean('inflight', record);
+ });
+
+ manager.transitionTo('saved');
+ manager.send('invokeLifecycleCallbacks', dirtyType);
+ },
+
+ becameInvalid: function(manager, errors) {
+ var record = get(manager, 'record');
+
+ set(record, 'errors', errors);
+
+ manager.transitionTo('invalid');
+ manager.send('invokeLifecycleCallbacks');
+ },
+
+ becameError: function(manager) {
+ manager.transitionTo('error');
+ manager.send('invokeLifecycleCallbacks');
+ }
+ }),
+
+ // A record is in the `invalid` state when its client-side
+ // invalidations have failed, or if the adapter has indicated
+ // the the record failed server-side invalidations.
+ invalid: DS.State.extend({
+ // FLAGS
+ isValid: false,
+
+ exit: function(manager) {
+ var record = get(manager, 'record');
+
+ record.withTransaction(function (t) {
+ t.recordBecameClean('inflight', record);
+ });
+ },
+
+ // EVENTS
+ deleteRecord: function(manager) {
+ manager.transitionTo('deleted');
+ get(manager, 'record').clearRelationships();
+ },
+
+ willSetProperty: willSetProperty,
+
+ didSetProperty: function(manager, context) {
+ var record = get(manager, 'record'),
+ errors = get(record, 'errors'),
+ key = context.name;
+
+ set(errors, key, null);
+
+ if (!hasDefinedProperties(errors)) {
+ manager.send('becameValid');
+ }
+
+ didSetProperty(manager, context);
+ },
+
+ becomeDirty: Ember.K,
+
+ rollback: function(manager) {
+ manager.send('becameValid');
+ manager.send('rollback');
+ },
+
+ becameValid: function(manager) {
+ manager.transitionTo('uncommitted');
+ },
+
+ invokeLifecycleCallbacks: function(manager) {
+ var record = get(manager, 'record');
+ record.trigger('becameInvalid', record);
+ }
+ })
+});
+
+// The created and updated states are created outside the state
+// chart so we can reopen their substates and add mixins as
+// necessary.
+
+var createdState = DirtyState.create({
+ dirtyType: 'created',
+
+ // FLAGS
+ isNew: true
+});
+
+var updatedState = DirtyState.create({
+ dirtyType: 'updated'
+});
+
+createdState.states.uncommitted.reopen({
+ deleteRecord: function(manager) {
+ var record = get(manager, 'record');
+
+ record.withTransaction(function(t) {
+ t.recordIsMoving('created', record);
+ });
+
+ record.clearRelationships();
+ manager.transitionTo('deleted.saved');
+ }
+});
+
+createdState.states.uncommitted.reopen({
+ rollback: function(manager) {
+ this._super(manager);
+ manager.transitionTo('deleted.saved');
+ }
+});
+
+updatedState.states.uncommitted.reopen({
+ deleteRecord: function(manager) {
+ var record = get(manager, 'record');
+
+ record.withTransaction(function(t) {
+ t.recordIsMoving('updated', record);
+ });
+
+ manager.transitionTo('deleted');
+ get(manager, 'record').clearRelationships();
+ }
+});
+
+var states = {
+ rootState: Ember.State.create({
+ // FLAGS
+ isLoaded: false,
+ isReloading: false,
+ isDirty: false,
+ isSaving: false,
+ isDeleted: false,
+ isError: false,
+ isNew: false,
+ isValid: true,
+
+ // SUBSTATES
+
+ // A record begins its lifecycle in the `empty` state.
+ // If its data will come from the adapter, it will
+ // transition into the `loading` state. Otherwise, if
+ // the record is being created on the client, it will
+ // transition into the `created` state.
+ empty: DS.State.create({
+ // EVENTS
+ loadingData: function(manager) {
+ manager.transitionTo('loading');
+ },
+
+ loadedData: function(manager) {
+ manager.transitionTo('loaded.created');
+ }
+ }),
+
+ // A record enters this state when the store askes
+ // the adapter for its data. It remains in this state
+ // until the adapter provides the requested data.
+ //
+ // Usually, this process is asynchronous, using an
+ // XHR to retrieve the data.
+ loading: DS.State.create({
+ // EVENTS
+ loadedData: didChangeData,
+
+ materializingData: function(manager) {
+ manager.transitionTo('loaded.materializing.firstTime');
+ }
+ }),
+
+ // A record enters this state when its data is populated.
+ // Most of a record's lifecycle is spent inside substates
+ // of the `loaded` state.
+ loaded: DS.State.create({
+ initialState: 'saved',
+
+ // FLAGS
+ isLoaded: true,
+
+ // SUBSTATES
+
+ materializing: DS.State.create({
+ // FLAGS
+ isLoaded: false,
+
+ // EVENTS
+ willSetProperty: Ember.K,
+ didSetProperty: Ember.K,
+
+ didChangeData: didChangeData,
+
+ finishedMaterializing: function(manager) {
+ manager.transitionTo('loaded.saved');
+ },
+
+ // SUBSTATES
+ firstTime: DS.State.create({
+ exit: function(manager) {
+ var record = get(manager, 'record');
+
+ Ember.run.once(function() {
+ record.trigger('didLoad');
+ });
+ }
+ })
+ }),
+
+ reloading: DS.State.create({
+ // FLAGS
+ isReloading: true,
+
+ // TRANSITIONS
+ enter: function(manager) {
+ var record = get(manager, 'record'),
+ store = get(record, 'store');
+
+ store.reloadRecord(record);
+ },
+
+ exit: function(manager) {
+ var record = get(manager, 'record');
+
+ once(record, 'trigger', 'didReload');
+ },
+
+ // EVENTS
+ loadedData: didChangeData,
+
+ materializingData: function(manager) {
+ manager.transitionTo('loaded.materializing');
+ }
+ }),
+
+ // If there are no local changes to a record, it remains
+ // in the `saved` state.
+ saved: DS.State.create({
+ // EVENTS
+ willSetProperty: willSetProperty,
+ didSetProperty: didSetProperty,
+
+ didChangeData: didChangeData,
+ loadedData: didChangeData,
+
+ reloadRecord: function(manager) {
+ manager.transitionTo('loaded.reloading');
+ },
+
+ materializingData: function(manager) {
+ manager.transitionTo('loaded.materializing');
+ },
+
+ becomeDirty: function(manager) {
+ manager.transitionTo('updated');
+ },
+
+ deleteRecord: function(manager) {
+ manager.transitionTo('deleted');
+ get(manager, 'record').clearRelationships();
+ },
+
+ unloadRecord: function(manager) {
+ manager.transitionTo('deleted.saved');
+ get(manager, 'record').clearRelationships();
+ },
+
+ invokeLifecycleCallbacks: function(manager, dirtyType) {
+ var record = get(manager, 'record');
+ if (dirtyType === 'created') {
+ record.trigger('didCreate', record);
+ } else {
+ record.trigger('didUpdate', record);
+ }
+ }
+ }),
+
+ // A record is in this state after it has been locally
+ // created but before the adapter has indicated that
+ // it has been saved.
+ created: createdState,
+
+ // A record is in this state if it has already been
+ // saved to the server, but there are new local changes
+ // that have not yet been saved.
+ updated: updatedState
+ }),
+
+ // A record is in this state if it was deleted from the store.
+ deleted: DS.State.create({
+ initialState: 'uncommitted',
+ dirtyType: 'deleted',
+
+ // FLAGS
+ isDeleted: true,
+ isLoaded: true,
+ isDirty: true,
+
+ // TRANSITIONS
+ setup: function(manager) {
+ var record = get(manager, 'record'),
+ store = get(record, 'store');
+
+ store.removeFromRecordArrays(record);
+ },
+
+ // SUBSTATES
+
+ // When a record is deleted, it enters the `start`
+ // state. It will exit this state when the record's
+ // transaction starts to commit.
+ uncommitted: DS.State.create({
+ // TRANSITIONS
+ enter: function(manager) {
+ var record = get(manager, 'record');
+
+ record.withTransaction(function(t) {
+ t.recordBecameDirty('deleted', record);
+ });
+ },
+
+ // EVENTS
+ willCommit: function(manager) {
+ manager.transitionTo('inFlight');
+ },
+
+ rollback: function(manager) {
+ get(manager, 'record').rollback();
+ },
+
+ becomeDirty: Ember.K,
+
+ becameClean: function(manager) {
+ var record = get(manager, 'record');
+
+ record.withTransaction(function(t) {
+ t.recordBecameClean('deleted', record);
+ });
+
+ manager.transitionTo('loaded.materializing');
+ }
+ }),
+
+ // After a record's transaction is committing, but
+ // before the adapter indicates that the deletion
+ // has saved to the server, a record is in the
+ // `inFlight` substate of `deleted`.
+ inFlight: DS.State.create({
+ // FLAGS
+ isSaving: true,
+
+ // TRANSITIONS
+ enter: function(manager) {
+ var record = get(manager, 'record');
+
+ record.becameInFlight();
+
+ record.withTransaction(function (t) {
+ t.recordBecameInFlight('deleted', record);
+ });
+ },
+
+ // EVENTS
+ didCommit: function(manager) {
+ var record = get(manager, 'record');
+
+ record.withTransaction(function(t) {
+ t.recordBecameClean('inflight', record);
+ });
+
+ manager.transitionTo('saved');
+
+ manager.send('invokeLifecycleCallbacks');
+ }
+ }),
+
+ // Once the adapter indicates that the deletion has
+ // been saved, the record enters the `saved` substate
+ // of `deleted`.
+ saved: DS.State.create({
+ // FLAGS
+ isDirty: false,
+
+ setup: function(manager) {
+ var record = get(manager, 'record'),
+ store = get(record, 'store');
+
+ store.dematerializeRecord(record);
+ },
+
+ invokeLifecycleCallbacks: function(manager) {
+ var record = get(manager, 'record');
+ record.trigger('didDelete', record);
+ }
+ })
+ }),
+
+ // If the adapter indicates that there was an unknown
+ // error saving a record, the record enters the `error`
+ // state.
+ error: DS.State.create({
+ isError: true,
+
+ // EVENTS
+
+ invokeLifecycleCallbacks: function(manager) {
+ var record = get(manager, 'record');
+ record.trigger('becameError', record);
+ }
+ })
+ })
+};
+
+DS.StateManager = Ember.StateManager.extend({
+ record: null,
+ initialState: 'rootState',
+ states: states,
+ unhandledEvent: function(manager, originalEvent) {
+ var record = manager.get('record'),
+ contexts = [].slice.call(arguments, 2),
+ errorMessage;
+ errorMessage = "Attempted to handle event `" + originalEvent + "` ";
+ errorMessage += "on " + record.toString() + " while in state ";
+ errorMessage += get(manager, 'currentState.path') + ". Called with ";
+ errorMessage += arrayMap.call(contexts, function(context){
+ return Ember.inspect(context);
+ }).join(', ');
+ throw new Ember.Error(errorMessage);
+ }
+});
+
+})();
+
+
+
+(function() {
+var LoadPromise = DS.LoadPromise; // system/mixins/load_promise
+
+var get = Ember.get, set = Ember.set, none = Ember.isNone, map = Ember.EnumerableUtils.map;
+
+var retrieveFromCurrentState = Ember.computed(function(key) {
+ return get(get(this, 'stateManager.currentState'), key);
+}).property('stateManager.currentState');
+
+DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, {
+ isLoaded: retrieveFromCurrentState,
+ isReloading: retrieveFromCurrentState,
+ isDirty: retrieveFromCurrentState,
+ isSaving: retrieveFromCurrentState,
+ isDeleted: retrieveFromCurrentState,
+ isError: retrieveFromCurrentState,
+ isNew: retrieveFromCurrentState,
+ isValid: retrieveFromCurrentState,
+
+ clientId: null,
+ id: null,
+ transaction: null,
+ stateManager: null,
+ errors: null,
+
+ /**
+ Create a JSON representation of the record, using the serialization
+ strategy of the store's adapter.
+
+ Available options:
+
+ * `includeId`: `true` if the record's ID should be included in the
+ JSON representation.
+
+ @param {Object} options
+ @returns {Object} an object whose values are primitive JSON values only
+ */
+ serialize: function(options) {
+ var store = get(this, 'store');
+ return store.serialize(this, options);
+ },
+
+ didLoad: Ember.K,
+ didReload: Ember.K,
+ didUpdate: Ember.K,
+ didCreate: Ember.K,
+ didDelete: Ember.K,
+ becameInvalid: Ember.K,
+ becameError: Ember.K,
+
+ data: Ember.computed(function() {
+ if (!this._data) {
+ this.materializeData();
+ }
+
+ return this._data;
+ }).property(),
+
+ materializeData: function() {
+ this.send('materializingData');
+
+ get(this, 'store').materializeData(this);
+
+ this.suspendRelationshipObservers(function() {
+ this.notifyPropertyChange('data');
+ });
+ },
+
+ _data: null,
+
+ init: function() {
+ this._super();
+
+ var stateManager = DS.StateManager.create({ record: this });
+ set(this, 'stateManager', stateManager);
+
+ this._setup();
+
+ stateManager.goToState('empty');
+ },
+
+ _setup: function() {
+ this._relationshipChanges = {};
+ this._changesToSync = {};
+ },
+
+ send: function(name, context) {
+ return get(this, 'stateManager').send(name, context);
+ },
+
+ withTransaction: function(fn) {
+ var transaction = get(this, 'transaction');
+ if (transaction) { fn(transaction); }
+ },
+
+ loadingData: function() {
+ this.send('loadingData');
+ },
+
+ loadedData: function() {
+ this.send('loadedData');
+ },
+
+ didChangeData: function() {
+ this.send('didChangeData');
+ },
+
+ setProperty: function(key, value, oldValue) {
+ this.send('setProperty', { key: key, value: value, oldValue: oldValue });
+ },
+
+ /**
+ 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`).
+ */
+ reload: function() {
+ this.send('reloadRecord');
+ },
+
+ deleteRecord: function() {
+ this.send('deleteRecord');
+ },
+
+ unloadRecord: function() {
+ Ember.assert("You can only unload a loaded, non-dirty record.", !get(this, 'isDirty'));
+
+ this.send('unloadRecord');
+ },
+
+ clearRelationships: function() {
+ this.eachRelationship(function(name, relationship) {
+ if (relationship.kind === 'belongsTo') {
+ set(this, name, null);
+ } else if (relationship.kind === 'hasMany') {
+ get(this, name).clear();
+ }
+ }, this);
+ },
+
+ updateRecordArrays: function() {
+ var store = get(this, 'store');
+ if (store) {
+ store.dataWasUpdated(this.constructor, get(this, '_reference'), this);
+ }
+ },
+
+ /**
+ If the adapter did not return a hash in response to a commit,
+ merge the changed attributes and relationships into the existing
+ saved data.
+ */
+ adapterDidCommit: function() {
+ var attributes = get(this, 'data').attributes;
+
+ get(this.constructor, 'attributes').forEach(function(name, meta) {
+ attributes[name] = get(this, name);
+ }, this);
+
+ this.send('didCommit');
+ this.updateRecordArraysLater();
+ },
+
+ adapterDidDirty: function() {
+ this.send('becomeDirty');
+ this.updateRecordArraysLater();
+ },
+
+ dataDidChange: Ember.observer(function() {
+ var relationships = get(this.constructor, 'relationshipsByName');
+
+ this.updateRecordArraysLater();
+
+ relationships.forEach(function(name, relationship) {
+ if (relationship.kind === 'hasMany') {
+ this.hasManyDidChange(relationship.key);
+ }
+ }, this);
+
+ this.send('finishedMaterializing');
+ }, 'data'),
+
+ hasManyDidChange: function(key) {
+ var cachedValue = this.cacheFor(key);
+
+ if (cachedValue) {
+ var type = get(this.constructor, 'relationshipsByName').get(key).type;
+ var store = get(this, 'store');
+ var ids = this._data.hasMany[key] || [];
+
+ var references = map(ids, function(id) {
+ // if it was already a reference, return the reference
+ if (typeof id === 'object') { return id; }
+ return store.referenceForId(type, id);
+ });
+
+ set(cachedValue, 'content', Ember.A(references));
+ }
+ },
+
+ updateRecordArraysLater: function() {
+ Ember.run.once(this, this.updateRecordArrays);
+ },
+
+ setupData: function(prematerialized) {
+ this._data = {
+ attributes: {},
+ belongsTo: {},
+ hasMany: {},
+ id: null
+ };
+ },
+
+ materializeId: function(id) {
+ set(this, 'id', id);
+ },
+
+ materializeAttributes: function(attributes) {
+ Ember.assert("Must pass a hash of attributes to materializeAttributes", !!attributes);
+ this._data.attributes = attributes;
+ },
+
+ materializeAttribute: function(name, value) {
+ this._data.attributes[name] = value;
+ },
+
+ materializeHasMany: function(name, ids) {
+ this._data.hasMany[name] = ids;
+ },
+
+ materializeBelongsTo: function(name, id) {
+ this._data.belongsTo[name] = id;
+ },
+
+ rollback: function() {
+ this._setup();
+ this.send('becameClean');
+
+ this.suspendRelationshipObservers(function() {
+ this.notifyPropertyChange('data');
+ });
+ },
+
+ toStringExtension: function() {
+ return get(this, 'id');
+ },
+
+ /**
+ @private
+
+ The goal of this method is to temporarily disable specific observers
+ that take action in response to application changes.
+
+ This allows the system to make changes (such as materialization and
+ rollback) that should not trigger secondary behavior (such as setting an
+ inverse relationship or marking records as dirty).
+
+ The specific implementation will likely change as Ember proper provides
+ better infrastructure for suspending groups of observers, and if Array
+ observation becomes more unified with regular observers.
+ */
+ suspendRelationshipObservers: function(callback, binding) {
+ var observers = get(this.constructor, 'relationshipNames').belongsTo;
+ var self = this;
+
+ try {
+ this._suspendedRelationships = true;
+ Ember._suspendObservers(self, observers, null, 'belongsToDidChange', function() {
+ Ember._suspendBeforeObservers(self, observers, null, 'belongsToWillChange', function() {
+ callback.call(binding || self);
+ });
+ });
+ } finally {
+ this._suspendedRelationships = false;
+ }
+ },
+
+ becameInFlight: function() {
+ },
+
+ // FOR USE DURING COMMIT PROCESS
+
+ adapterDidUpdateAttribute: function(attributeName, value) {
+
+ // If a value is passed in, update the internal attributes and clear
+ // the attribute cache so it picks up the new value. Otherwise,
+ // collapse the current value into the internal attributes because
+ // the adapter has acknowledged it.
+ if (value !== undefined) {
+ get(this, 'data.attributes')[attributeName] = value;
+ this.notifyPropertyChange(attributeName);
+ } else {
+ value = get(this, attributeName);
+ get(this, 'data.attributes')[attributeName] = value;
+ }
+
+ this.updateRecordArraysLater();
+ },
+
+ _reference: Ember.computed(function() {
+ return get(this, 'store').referenceForClientId(get(this, 'clientId'));
+ }),
+
+ adapterDidInvalidate: function(errors) {
+ this.send('becameInvalid', errors);
+ },
+
+ adapterDidError: function() {
+ this.send('becameError');
+ },
+
+ /**
+ @private
+
+ Override the default event firing from Ember.Evented to
+ also call methods with the given name.
+ */
+ trigger: function(name) {
+ Ember.tryInvoke(this, name, [].slice.call(arguments, 1));
+ this._super.apply(this, arguments);
+ }
+});
+
+// Helper function to generate store aliases.
+// This returns a function that invokes the named alias
+// on the default store, but injects the class as the
+// first parameter.
+var storeAlias = function(methodName) {
+ return function() {
+ var store = get(DS, 'defaultStore'),
+ args = [].slice.call(arguments);
+
+ args.unshift(this);
+ return store[methodName].apply(store, args);
+ };
+};
+
+DS.Model.reopenClass({
+ isLoaded: storeAlias('recordIsLoaded'),
+ find: storeAlias('find'),
+ all: storeAlias('all'),
+ filter: storeAlias('filter'),
+
+ _create: DS.Model.create,
+
+ 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.");
+ },
+
+ createRecord: storeAlias('createRecord')
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get;
+DS.Model.reopenClass({
+ attributes: Ember.computed(function() {
+ var map = Ember.Map.create();
+
+ this.eachComputedProperty(function(name, meta) {
+ if (meta.isAttribute) {
+ Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('<type>')` from " + this.toString(), name !== 'id');
+
+ meta.name = name;
+ map.set(name, meta);
+ }
+ });
+
+ return map;
+ })
+});
+
+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) {
+ get(this.constructor, 'attributes').forEach(function(name, meta) {
+ callback.call(binding, name, meta);
+ }, binding);
+ },
+
+ attributeWillChange: Ember.beforeObserver(function(record, key) {
+ var reference = get(record, '_reference'),
+ store = get(record, 'store');
+
+ record.send('willSetProperty', { reference: reference, store: store, name: key });
+ }),
+
+ attributeDidChange: Ember.observer(function(record, key) {
+ record.send('didSetProperty', { name: key });
+ })
+});
+
+function getAttr(record, options, key) {
+ var attributes = get(record, 'data').attributes;
+ var value = attributes[key];
+
+ if (value === undefined) {
+ value = options.defaultValue;
+ }
+
+ return value;
+}
+
+DS.attr = function(type, options) {
+ options = options || {};
+
+ var meta = {
+ type: type,
+ isAttribute: true,
+ options: options
+ };
+
+ return Ember.computed(function(key, value, oldValue) {
+ var data;
+
+ if (arguments.length > 1) {
+ Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('<type>')` from " + this.constructor.toString(), key !== 'id');
+ } else {
+ value = getAttr(this, options, key);
+ }
+
+ return value;
+ // `data` is never set directly. However, it may be
+ // invalidated from the state manager's setData
+ // event.
+ }).property('data').meta(meta);
+};
+
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(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 || {};
+
+ var meta = { type: type, isRelationship: true, options: options, kind: 'belongsTo' };
+
+ return Ember.computed(function(key, value) {
+ if (arguments.length === 2) {
+ return value === undefined ? null : value;
+ }
+
+ var data = get(this, 'data').belongsTo,
+ store = get(this, 'store'), id;
+
+ if (typeof type === 'string') {
+ type = get(this, type, false) || get(Ember.lookup, type);
+ }
+
+ id = data[key];
+
+ if(!id) {
+ return null;
+ } else if (typeof id === 'object') {
+ return store.recordForReference(id);
+ } else {
+ return store.find(type, id);
+ }
+ }).property('data').meta(meta);
+};
+
+/**
+ 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;
+ }
+ }
+ }),
+
+ /** @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];
+ })
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set;
+var hasRelationship = function(type, options) {
+ options = options || {};
+
+ var meta = { type: type, isRelationship: true, options: options, kind: 'hasMany' };
+
+ return Ember.computed(function(key, value) {
+ var data = get(this, 'data').hasMany,
+ store = get(this, 'store'),
+ ids, relationship;
+
+ if (typeof type === 'string') {
+ type = get(this, type, false) || get(Ember.lookup, type);
+ }
+
+ ids = data[key];
+ relationship = store.findMany(type, ids || [], this, meta);
+ set(relationship, 'owner', this);
+ set(relationship, 'name', key);
+
+ return relationship;
+ }).property().meta(meta);
+};
+
+DS.hasMany = function(type, options) {
+ Ember.assert("The type passed to DS.hasMany must be defined", !!type);
+ return hasRelationship(type, options);
+};
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set;
+
+/**
+ @private
+
+ This file defines several extensions to the base `DS.Model` class that
+ add support for one-to-many relationships.
+*/
+
+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) {
+
+ // If it is, get the metadata for the relationship. This is
+ // populated by the `DS.belongsTo` helper when it is creating
+ // the computed property.
+ var meta = value.meta();
+
+ if (meta.isRelationship && meta.kind === 'belongsTo') {
+ Ember.addObserver(proto, key, null, 'belongsToDidChange');
+ Ember.addBeforeObserver(proto, key, null, 'belongsToWillChange');
+ }
+
+ if (meta.isAttribute) {
+ Ember.addObserver(proto, key, null, 'attributeDidChange');
+ Ember.addBeforeObserver(proto, key, null, 'attributeWillChange');
+ }
+
+ meta.parentType = proto.constructor;
+ }
+ }
+});
+
+/**
+ These DS.Model extensions add class methods that provide relationship
+ introspection abilities about relationships.
+
+ A note about the computed properties contained here:
+
+ **These properties are effectively sealed once called for the first time.**
+ To avoid repeatedly doing expensive iteration over a model's fields, these
+ values are computed once and then cached for the remainder of the runtime of
+ your application.
+
+ If your application needs to modify a class after its initial definition
+ (for example, using `reopen()` to add additional attributes), make sure you
+ do it before using your model with the store, which uses these properties
+ extensively.
+*/
+
+DS.Model.reopenClass({
+ /**
+ For a given relationship name, returns the model type of the relationship.
+
+ For example, if you define a model like this:
+
+ App.Post = DS.Model.extend({
+ comments: DS.hasMany(App.Comment)
+ });
+
+ Calling `App.Post.typeForRelationship('comments')` will return `App.Comment`.
+
+ @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;
+ },
+
+ /**
+ The model's relationships as a map, keyed on the type of the
+ relationship. The value of each entry is an array containing a descriptor
+ for each relationship with that type, describing the name of the relationship
+ as well as the type.
+
+ For example, given the following model definition:
+
+ App.Blog = DS.Model.extend({
+ users: DS.hasMany(App.User),
+ owner: DS.belongsTo(App.User),
+
+ posts: DS.hasMany(App.Post)
+ });
+
+ This computed property would return a map describing these
+ relationships, like this:
+
+ var relationships = Ember.get(App.Blog, 'relationships');
+ associatons.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 []; }
+ });
+
+ // Loop through each computed property on the class
+ this.eachComputedProperty(function(name, meta) {
+
+ // If the computed property is a relationship, add
+ // it to the map.
+ if (meta.isRelationship) {
+ if (typeof meta.type === 'string') {
+ meta.type = Ember.get(Ember.lookup, meta.type);
+ }
+
+ var relationshipsForType = map.get(meta.type);
+
+ relationshipsForType.push({ name: name, kind: meta.kind });
+ }
+ });
+
+ return map;
+ }),
+
+ /**
+ A hash containing lists of the model's relationships, grouped
+ by the relationship kind. For example, given a model with this
+ definition:
+
+ App.Blog = DS.Model.extend({
+ users: DS.hasMany(App.User),
+ owner: DS.belongsTo(App.User),
+
+ posts: DS.hasMany(App.Post)
+ });
+
+ This property would contain the following:
+
+ var relationshipNames = Ember.get(App.Blog, 'relationshipNames');
+ relationshipNames.hasMany;
+ //=> ['users', 'posts']
+ relationshipNames.belongsTo;
+ //=> ['owner']
+
+ @type Object
+ @readOnly
+ */
+ relationshipNames: Ember.computed(function() {
+ var names = { hasMany: [], belongsTo: [] };
+
+ this.eachComputedProperty(function(name, meta) {
+ if (meta.isRelationship) {
+ names[meta.kind].push(name);
+ }
+ });
+
+ return names;
+ }),
+
+ /**
+ A map whose keys are the relationships of a model and whose values are
+ relationship descriptors.
+
+ For example, given a model with this
+ definition:
+
+ App.Blog = DS.Model.extend({
+ users: DS.hasMany(App.User),
+ owner: DS.belongsTo(App.User),
+
+ posts: DS.hasMany(App.Post)
+ });
+
+ This property would contain the following:
+
+ var relationshipsByName = Ember.get(App.Blog, 'relationshipsByName');
+ relationshipsByName.get('users');
+ //=> { key: 'users', kind: 'hasMany', type: App.User }
+ relationshipsByName.get('owner');
+ //=> { key: 'owner', kind: 'belongsTo', type: App.User }
+
+ @type Ember.Map
+ @readOnly
+ */
+ relationshipsByName: Ember.computed(function() {
+ var map = Ember.Map.create(), type;
+
+ this.eachComputedProperty(function(name, meta) {
+ if (meta.isRelationship) {
+ meta.key = name;
+ type = meta.type;
+
+ if (typeof type === 'string') {
+ type = get(this, type, false) || get(Ember.lookup, type);
+ meta.type = type;
+ }
+
+ map.set(name, meta);
+ }
+ });
+
+ return map;
+ }),
+
+ /**
+ A map whose keys are the fields of the model and whose values are strings
+ describing the kind of the field. A model's fields are the union of all of its
+ attributes and relationships.
+
+ For example:
+
+ App.Blog = DS.Model.extend({
+ users: DS.hasMany(App.User),
+ owner: DS.belongsTo(App.User),
+
+ posts: DS.hasMany(App.Post),
+
+ title: DS.attr('string')
+ });
+
+ var fields = Ember.get(App.Blog, 'fields');
+ fields.forEach(function(field, kind) {
+ console.log(field, kind);
+ });
+
+ // prints:
+ // users, hasMany
+ // owner, belongsTo
+ // posts, hasMany
+ // title, attribute
+
+ @type Ember.Map
+ @readOnly
+ */
+ fields: Ember.computed(function() {
+ var map = Ember.Map.create(), type;
+
+ this.eachComputedProperty(function(name, meta) {
+ if (meta.isRelationship) {
+ map.set(name, meta.kind);
+ } else if (meta.isAttribute) {
+ map.set(name, 'attribute');
+ }
+ });
+
+ return map;
+ }),
+
+ /**
+ Given a callback, iterates over each of the relationships in the model,
+ invoking the callback with the name of each relationship and its relationship
+ descriptor.
+
+ @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);
+ });
+ }
+});
+
+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.
+
+ @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);
+ }
+});
+
+/**
+ @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.
+
+ 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.
+
+ For example, imagine the following two associated models:
+
+ App.Post = DS.Model.extend({
+ comments: DS.hasMany('App.Comment')
+ });
+
+ App.Comment = DS.Model.extend({
+ post: DS.belongsTo('App.Post')
+ });
+
+ If the `post` property of a `Comment` was modified, Ember Data would invoke
+ this helper like this:
+
+ DS._inverseNameFor(App.Comment, App.Post, 'hasMany');
+ //=> 'comments'
+
+ 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];
+};
+
+/**
+ @private
+
+ Given a model and a relationship name, returns the model type of
+ the named relationship.
+
+ App.Post = DS.Model.extend({
+ comments: DS.hasMany('App.Comment')
+ });
+
+ DS._inverseTypeFor(App.Post, 'comments');
+ //=> App.Comment
+ @param {DS.Model class} modelType
+ @param {String} relationshipName
+ @return {DS.Model class}
+*/
+DS._inverseTypeFor = function(modelType, relationshipName) {
+ var relationships = get(modelType, 'relationshipsByName'),
+ relationship = relationships.get(relationshipName);
+
+ if (relationship) { return relationship.type; }
+};
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set;
+var forEach = Ember.EnumerableUtils.forEach;
+
+DS.RelationshipChange = function(options) {
+ 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;
+};
+
+DS.RelationshipChangeAdd = function(options){
+ DS.RelationshipChange.call(this, options);
+};
+
+DS.RelationshipChangeRemove = function(options){
+ DS.RelationshipChange.call(this, options);
+};
+
+/** @private */
+DS.RelationshipChange.create = function(options) {
+ return new DS.RelationshipChange(options);
+};
+
+/** @private */
+DS.RelationshipChangeAdd.create = function(options) {
+ return new DS.RelationshipChangeAdd(options);
+};
+
+/** @private */
+DS.RelationshipChangeRemove.create = function(options) {
+ return new DS.RelationshipChangeRemove(options);
+};
+
+DS.OneToManyChange = {};
+DS.OneToNoneChange = {};
+DS.ManyToNoneChange = {};
+DS.OneToOneChange = {};
+DS.ManyToManyChange = {};
+
+DS.RelationshipChange._createChange = function(options){
+ if(options.changeType === "add"){
+ return DS.RelationshipChangeAdd.create(options);
+ }
+ if(options.changeType === "remove"){
+ return DS.RelationshipChangeRemove.create(options);
+ }
+};
+
+
+DS.RelationshipChange.determineRelationshipType = function(recordType, knownSide){
+ var knownKey = knownSide.key, key, 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";
+ }
+ }
+
+};
+
+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, {}, store, options);
+ }
+ else if (changeType === "manyToNone"){
+ return DS.ManyToNoneChange.createChange(firstRecordReference, {}, 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);
+ }
+};
+
+/** @private */
+DS.OneToNoneChange.createChange = function(childReference, parentReference, store, options) {
+ var key = options.key;
+ var change = DS.RelationshipChange._createChange({
+ firstRecordReference: childReference,
+ store: store,
+ changeType: options.changeType,
+ firstRecordName: key,
+ firstRecordKind: "belongsTo"
+ });
+
+ store.addRelationshipChangeFor(childReference, key, parentReference, null, change);
+
+ return change;
+};
+
+/** @private */
+DS.ManyToNoneChange.createChange = function(childReference, parentReference, store, options) {
+ var key = options.key;
+ var change = DS.RelationshipChange._createChange({
+ 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) {
+ // Get the type of the child based on the child's client ID
+ var childType = childReference.type, key;
+
+ // If the name of the belongsTo side of the relationship is specified,
+ // use that
+ // If the type of the parent is specified, look it up on the child's type
+ // definition.
+ key = options.key;
+
+ var change = DS.RelationshipChange._createChange({
+ firstRecordReference: childReference,
+ secondRecordReference: parentReference,
+ firstRecordKind: "hasMany",
+ secondRecordKind: "hasMany",
+ store: store,
+ changeType: options.changeType,
+ firstRecordName: key
+ });
+
+ store.addRelationshipChangeFor(childReference, key, parentReference, null, change);
+
+
+ return change;
+};
+
+/** @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 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 change = DS.RelationshipChange._createChange({
+ firstRecordReference: childReference,
+ secondRecordReference: parentReference,
+ firstRecordKind: "belongsTo",
+ secondRecordKind: "belongsTo",
+ store: store,
+ changeType: options.changeType,
+ firstRecordName: key
+ });
+
+ store.addRelationshipChangeFor(childReference, key, parentReference, null, change);
+
+
+ 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();
+ }
+ }
+};
+
+/** @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;
+
+ // 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 change = DS.RelationshipChange._createChange({
+ firstRecordReference: childReference,
+ secondRecordReference: parentReference,
+ firstRecordKind: "belongsTo",
+ secondRecordKind: "hasMany",
+ store: store,
+ changeType: options.changeType,
+ firstRecordName: key
+ });
+
+ store.addRelationshipChangeFor(childReference, key, parentReference, null, change);
+
+
+ return change;
+};
+
+
+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
+ });
+ 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;
+ });
+};
+
+DS.RelationshipChange.prototype = {
+
+ getSecondRecordName: function() {
+ var name = this.secondRecordName, store = this.store, parent;
+
+ if (!name) {
+ parent = this.secondRecordReference;
+ if (!parent) { return; }
+
+ var childType = this.firstRecordReference.type;
+ var inverseType = DS._inverseTypeFor(childType, this.firstRecordName);
+ name = inverseHasManyName(inverseType, childType, this.firstRecordName);
+ this.secondRecordName = name;
+ }
+
+ return name;
+ },
+
+ /**
+ Get the name of the relationship on the belongsTo side.
+
+ @returns {String}
+ */
+ getFirstRecordName: function() {
+ var name = this.firstRecordName, store = this.store, parent, child;
+
+ if (!name) {
+ parent = this.secondRecordReference;
+ child = this.firstRecordReference;
+ if (!(child && parent)) { return; }
+
+ name = DS._inverseRelationshipFor(child.type, parent.type).name;
+
+ this.firstRecordName = name;
+ }
+
+ return name;
+ },
+
+ /** @private */
+ destroy: function() {
+ var childReference = this.firstRecordReference,
+ belongsToName = this.getFirstRecordName(),
+ hasManyName = this.getSecondRecordName(),
+ store = this.store,
+ child, oldParent, newParent, lastParent, transaction;
+
+ store.removeRelationshipChangeFor(childReference, belongsToName, this.secondRecordReference, hasManyName, this.changeType);
+
+ if (transaction = this.transaction) {
+ transaction.relationshipBecameClean(this);
+ }
+ },
+
+ /** @private */
+ getByReference: function(reference) {
+ var store = this.store;
+
+ // return null or undefined if the original reference was null or undefined
+ if (!reference) { return reference; }
+
+ if (store.recordIsMaterialized(reference)) {
+ return store.recordForReference(reference);
+ }
+ },
+
+ 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 = this.store.ensureSameTransaction([child, parentRecord]);
+
+ this.transaction = transaction;
+ return transaction;
+ },
+
+ callChangeEvents: function(){
+ var hasManyName = this.getSecondRecordName(),
+ belongsToName = this.getFirstRecordName(),
+ child = this.getFirstRecord(),
+ parentRecord = this.getSecondRecord();
+
+ var dirtySet = new Ember.OrderedSet();
+
+ // 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);
+ }
+
+ if (child) {
+ this.store.recordBelongsToDidChange(dirtySet, child, this);
+ }
+
+ dirtySet.forEach(function(record) {
+ record.adapterDidDirty();
+ });
+ },
+
+ 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();
+ }
+ });
+ }
+};
+
+DS.RelationshipChangeAdd.prototype = Ember.create(DS.RelationshipChange.create({}));
+DS.RelationshipChangeRemove.prototype = Ember.create(DS.RelationshipChange.create({}));
+
+DS.RelationshipChangeAdd.prototype.changeType = "add";
+DS.RelationshipChangeAdd.prototype.sync = function() {
+ var secondRecordName = this.getSecondRecordName(),
+ firstRecordName = this.getFirstRecordName(),
+ firstRecord = this.getFirstRecord(),
+ secondRecord = this.getSecondRecord();
+
+ //Ember.assert("You specified a hasMany (" + hasManyName + ") on " + (!belongsToName && (newParent || oldParent || this.lastParent).constructor) + " but did not specify an inverse belongsTo on " + child.constructor, belongsToName);
+ //Ember.assert("You specified a belongsTo (" + belongsToName + ") on " + child.constructor + " but did not specify an inverse hasMany on " + (!hasManyName && (newParent || oldParent || this.lastParentRecord).constructor), hasManyName);
+
+ var transaction = this.ensureSameTransaction();
+ transaction.relationshipBecameDirty(this);
+
+ this.callChangeEvents();
+
+ if (secondRecord && firstRecord) {
+ if(this.secondRecordKind === "belongsTo"){
+ secondRecord.suspendRelationshipObservers(function(){
+ set(secondRecord, secondRecordName, firstRecord);
+ });
+
+ }
+ 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();
+
+ //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(firstRecord, secondRecord, secondRecordName, firstRecordName);
+ transaction.relationshipBecameDirty(this);
+
+ this.callChangeEvents();
+
+ if (secondRecord && firstRecord) {
+ if(this.secondRecordKind === "belongsTo"){
+ set(secondRecord, secondRecordName, null);
+ }
+ else if(this.secondRecordKind === "hasMany"){
+ secondRecord.suspendRelationshipObservers(function(){
+ get(secondRecord, secondRecordName).removeObject(firstRecord);
+ });
+ }
+ }
+
+ 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);
+ });
+ }
+ }
+
+ this.coalesce();
+};
+
+function inverseBelongsToName(parentType, childType, hasManyName) {
+ // Get the options passed to the parent's DS.hasMany()
+ var options = parentType.metaForProperty(hasManyName).options;
+ var belongsToName;
+
+ if (belongsToName = options.inverse) {
+ return belongsToName;
+ }
+
+ return DS._inverseRelationshipFor(childType, parentType).name;
+}
+
+function inverseHasManyName(parentType, childType, belongsToName) {
+ var options = childType.metaForProperty(belongsToName).options;
+ var hasManyName;
+
+ if (hasManyName = options.inverse) {
+ return hasManyName;
+ }
+
+ return DS._inverseRelationshipFor(parentType, childType).name;
+}
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(function() {
+var set = Ember.set;
+
+/**
+ This code registers an injection for Ember.Application.
+
+ If an Ember.js developer defines a subclass of DS.Store on their application,
+ this code will automatically instantiate it and make it available on the
+ router.
+
+ Additionally, after an application's controllers have been injected, they will
+ each have the store made available to them.
+
+ For example, imagine an Ember.js application with the following classes:
+
+ App.Store = DS.Store.extend({
+ adapter: 'App.MyCustomAdapter'
+ });
+
+ App.PostsController = Ember.ArrayController.extend({
+ // ...
+ });
+
+ When the application is initialized, `App.Store` will automatically be
+ instantiated, and the instance of `App.PostsController` will have its `store`
+ property set to that instance.
+
+ Note that this code will only be run if the `ember-application` package is
+ loaded. If Ember Data is being used in an environment other than a
+ typical application (e.g., node.js where only `ember-runtime` is available),
+ this code will be ignored.
+*/
+
+Ember.onLoad('Ember.Application', function(Application) {
+ if (Application.registerInjection) {
+ Application.registerInjection({
+ name: "store",
+ before: "controllers",
+
+ // If a store subclass is defined, like App.Store,
+ // instantiate it and inject it into the router.
+ injection: function(app, stateManager, property) {
+ if (!stateManager) { return; }
+ if (property === 'Store') {
+ set(stateManager, 'store', app[property].create());
+ }
+ }
+ });
+
+ Application.registerInjection({
+ name: "giveStoreToControllers",
+ after: ['store','controllers'],
+
+ // For each controller, set its `store` property
+ // to the DS.Store instance we created above.
+ injection: function(app, stateManager, property) {
+ if (!stateManager) { return; }
+ if (/^[A-Z].*Controller$/.test(property)) {
+ var controllerName = property.charAt(0).toLowerCase() + property.substr(1);
+ var store = stateManager.get('store');
+ var controller = stateManager.get(controllerName);
+ if(!controller) { return; }
+
+ controller.set('store', store);
+ }
+ }
+ });
+ } else if (Application.initializer) {
+ Application.initializer({
+ name: "store",
+
+ initialize: function(container, application) {
+ container.register('store', 'main', application.Store);
+
+ // Eagerly generate the store so defaultStore is populated.
+ // TODO: Do this in a finisher hook
+ container.lookup('store:main');
+ }
+ });
+
+ Application.initializer({
+ name: "injectStore",
+
+ initialize: function(container) {
+ container.typeInjection('controller', 'store', 'store:main');
+ container.typeInjection('route', 'store', 'store:main');
+ }
+ });
+ }
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set, map = Ember.ArrayPolyfills.map, isNone = Ember.isNone;
+
+function mustImplement(name) {
+ return function() {
+ throw new Ember.Error("Your serializer " + this.toString() + " does not implement the required method " + name);
+ };
+}
+
+/**
+ A serializer is responsible for serializing and deserializing a group of
+ records.
+
+ `DS.Serializer` is an abstract base class designed to help you build a
+ serializer that can read to and write from any serialized form. While most
+ applications will use `DS.JSONSerializer`, which reads and writes JSON, the
+ serializer architecture allows your adapter to transmit things like XML,
+ strings, or custom binary data.
+
+ Typically, your application's `DS.Adapter` is responsible for both creating a
+ serializer as well as calling the appropriate methods when it needs to
+ materialize data or serialize a record.
+
+ The serializer API is designed as a series of layered hooks that you can
+ override to customize any of the individual steps of serialization and
+ deserialization.
+
+ The hooks are organized by the three responsibilities of the serializer:
+
+ 1. Determining naming conventions
+ 2. Serializing records into a serialized form
+ 3. Deserializing records from a serialized form
+
+ Because Ember Data lazily materializes records, the deserialization
+ step, and therefore the hooks you implement, are split into two phases:
+
+ 1. Extraction, where the serialized forms for multiple records are
+ extracted from a single payload. The IDs of each record are also
+ extracted for indexing.
+ 2. Materialization, where a newly-created record has its attributes
+ and relationships initialized based on the serialized form loaded
+ by the adapter.
+
+ Additionally, a serializer can convert values from their JavaScript
+ versions into their serialized versions via a declarative API.
+
+ ## Naming Conventions
+
+ One of the most common uses of the serializer is to map attribute names
+ from the serialized form to your `DS.Model`. For example, in your model,
+ you may have an attribute called `firstName`:
+
+ ```javascript
+ App.Person = DS.Model.extend({
+ firstName: DS.attr('string')
+ });
+ ```
+
+ However, because the web API your adapter is communicating with is
+ legacy, it calls this attribute `FIRST_NAME`.
+
+ You can determine the attribute name used in the serialized form
+ by implementing `keyForAttributeName`:
+
+ ```javascript
+ keyForAttributeName: function(type, name) {
+ return name.underscore.toUpperCase();
+ }
+ ```
+
+ If your attribute names are not predictable, you can re-map them
+ one-by-one using the `map` API:
+
+ ```javascript
+ App.Person.map('App.Person', {
+ firstName: { key: '*API_USER_FIRST_NAME*' }
+ });
+ ```
+
+ ## Serialization
+
+ During the serialization process, a record or records are converted
+ from Ember.js objects into their serialized form.
+
+ These methods are designed in layers, like a delicious 7-layer
+ cake (but with fewer layers).
+
+ The main entry point for serialization is the `serialize`
+ method, which takes the record and options.
+
+ The `serialize` method is responsible for:
+
+ * turning the record's attributes (`DS.attr`) into
+ attributes on the JSON object.
+ * optionally adding the record's ID onto the hash
+ * adding relationships (`DS.hasMany` and `DS.belongsTo`)
+ to the JSON object.
+
+ Depending on the backend, the serializer can choose
+ whether to include the `hasMany` or `belongsTo`
+ relationships on the JSON hash.
+
+ For very custom serialization, you can implement your
+ own `serialize` method. In general, however, you will want
+ to override the hooks described below.
+
+ ### Adding the ID
+
+ The default `serialize` will optionally call your serializer's
+ `addId` method with the JSON hash it is creating, the
+ record's type, and the record's ID. The `serialize` method
+ will not call `addId` if the record's ID is undefined.
+
+ Your adapter must specifically request ID inclusion by
+ passing `{ includeId: true }` as an option to `serialize`.
+
+ NOTE: You may not want to include the ID when updating an
+ existing record, because your server will likely disallow
+ changing an ID after it is created, and the PUT request
+ itself will include the record's identification.
+
+ By default, `addId` will:
+
+ 1. Get the primary key name for the record by calling
+ the serializer's `primaryKey` with the record's type.
+ Unless you override the `primaryKey` method, this
+ will be `'id'`.
+ 2. Assign the record's ID to the primary key in the
+ JSON hash being built.
+
+ If your backend expects a JSON object with the primary
+ key at the root, you can just override the `primaryKey`
+ method on your serializer subclass.
+
+ Otherwise, you can override the `addId` method for
+ more specialized handling.
+
+ ### Adding Attributes
+
+ By default, the serializer's `serialize` method will call
+ `addAttributes` with the JSON object it is creating
+ and the record to serialize.
+
+ The `addAttributes` method will then call `addAttribute`
+ in turn, with the JSON object, the record to serialize,
+ the attribute's name and its type.
+
+ Finally, the `addAttribute` method will serialize the
+ attribute:
+
+ 1. It will call `keyForAttributeName` to determine
+ the key to use in the JSON hash.
+ 2. It will get the value from the record.
+ 3. It will call `serializeValue` with the attribute's
+ value and attribute type to convert it into a
+ JSON-compatible value. For example, it will convert a
+ Date into a String.
+
+ If your backend expects a JSON object with attributes as
+ keys at the root, you can just override the `serializeValue`
+ and `keyForAttributeName` methods in your serializer
+ subclass and let the base class do the heavy lifting.
+
+ If you need something more specialized, you can probably
+ override `addAttribute` and let the default `addAttributes`
+ handle the nitty gritty.
+
+ ### Adding Relationships
+
+ By default, `serialize` will call your serializer's
+ `addRelationships` method with the JSON object that is
+ being built and the record being serialized. The default
+ implementation of this method is to loop over all of the
+ relationships defined on your record type and:
+
+ * If the relationship is a `DS.hasMany` relationship,
+ call `addHasMany` with the JSON object, the record
+ and a description of the relationship.
+ * If the relationship is a `DS.belongsTo` relationship,
+ call `addBelongsTo` with the JSON object, the record
+ and a description of the relationship.
+
+ The relationship description has the following keys:
+
+ * `type`: the class of the associated information (the
+ first parameter to `DS.hasMany` or `DS.belongsTo`)
+ * `kind`: either `hasMany` or `belongsTo`
+
+ The relationship description may get additional
+ information in the future if more capabilities or
+ relationship types are added. However, it will
+ remain backwards-compatible, so the mere existence
+ of new features should not break existing adapters.
+*/
+DS.Serializer = Ember.Object.extend({
+ init: function() {
+ this.mappings = Ember.Map.create();
+ this.configurations = Ember.Map.create();
+ this.globalConfigurations = {};
+ },
+
+ extract: mustImplement('extract'),
+ extractMany: mustImplement('extractMany'),
+
+ extractRecordRepresentation: function(loader, type, json, shouldSideload) {
+ var mapping = this.mappingForType(type);
+ var embeddedData, prematerialized = {}, reference;
+
+ if (shouldSideload) {
+ reference = loader.sideload(type, json);
+ } else {
+ reference = loader.load(type, json);
+ }
+
+ this.eachEmbeddedHasMany(type, function(name, relationship) {
+ var embeddedData = json[this.keyFor(relationship)];
+ if (!isNone(embeddedData)) {
+ this.extractEmbeddedHasMany(loader, relationship, embeddedData, reference, prematerialized);
+ }
+ }, this);
+
+ this.eachEmbeddedBelongsTo(type, function(name, relationship) {
+ var embeddedData = json[this.keyFor(relationship)];
+ if (!isNone(embeddedData)) {
+ this.extractEmbeddedBelongsTo(loader, relationship, embeddedData, reference, prematerialized);
+ }
+ }, this);
+
+ loader.prematerialize(reference, prematerialized);
+
+ return reference;
+ },
+
+ extractEmbeddedHasMany: function(loader, relationship, array, parent, prematerialized) {
+ var references = map.call(array, function(item) {
+ if (!item) { return; }
+
+ var reference = this.extractRecordRepresentation(loader, relationship.type, item, true);
+
+ // If the embedded record should also be saved back when serializing the parent,
+ // make sure we set its parent since it will not have an ID.
+ var embeddedType = this.embeddedType(parent.type, relationship.key);
+ if (embeddedType === 'always') {
+ reference.parent = parent;
+ }
+
+ return reference;
+ }, this);
+
+ prematerialized[relationship.key] = references;
+ },
+
+ extractEmbeddedBelongsTo: function(loader, relationship, data, parent, prematerialized) {
+ var reference = this.extractRecordRepresentation(loader, relationship.type, data, true);
+ prematerialized[relationship.key] = reference;
+
+ // If the embedded record should also be saved back when serializing the parent,
+ // make sure we set its parent since it will not have an ID.
+ var embeddedType = this.embeddedType(parent.type, relationship.key);
+ if (embeddedType === 'always') {
+ reference.parent = parent;
+ }
+ },
+
+ //.......................
+ //. SERIALIZATION HOOKS
+ //.......................
+
+ /**
+ The main entry point for serializing a record. While you can consider this
+ a hook that can be overridden in your serializer, you will have to manually
+ handle serialization. For most cases, there are more granular hooks that you
+ can override.
+
+ If overriding this method, these are the responsibilities that you will need
+ to implement yourself:
+
+ * If the option hash contains `includeId`, add the record's ID to the serialized form.
+ By default, `serialize` calls `addId` if appropriate.
+ * Add the record's attributes to the serialized form. By default, `serialize` calls
+ `addAttributes`.
+ * Add the record's relationships to the serialized form. By default, `serialize` calls
+ `addRelationships`.
+
+ @param {DS.Model} record the record to serialize
+ @param {Object} [options] a hash of options
+ @returns {any} the serialized form of the record
+ */
+ serialize: function(record, options) {
+ options = options || {};
+
+ var serialized = this.createSerializedForm(), id;
+
+ if (options.includeId) {
+ if (id = get(record, 'id')) {
+ this._addId(serialized, record.constructor, id);
+ }
+ }
+
+ this.addAttributes(serialized, record);
+ this.addRelationships(serialized, record);
+
+ return serialized;
+ },
+
+ /**
+ @private
+
+ Given an attribute type and value, convert the value into the
+ serialized form using the transform registered for that type.
+
+ @param {any} value the value to convert to the serialized form
+ @param {String} attributeType the registered type (e.g. `string`
+ or `boolean`)
+ @returns {any} the serialized form of the value
+ */
+ serializeValue: function(value, attributeType) {
+ var transform = this.transforms ? this.transforms[attributeType] : null;
+
+ Ember.assert("You tried to use an attribute type (" + attributeType + ") that has not been registered", transform);
+ return transform.serialize(value);
+ },
+
+ /**
+ A hook you can use to normalize IDs before adding them to the
+ serialized representation.
+
+ Because the store coerces all IDs to strings for consistency,
+ this is the opportunity for the serializer to, for example,
+ convert numerical IDs back into number form.
+
+ @param {String} id the id from the record
+ @returns {any} the serialized representation of the id
+ */
+ serializeId: function(id) {
+ if (isNaN(id)) { return id; }
+ return +id;
+ },
+
+ /**
+ A hook you can use to change how attributes are added to the serialized
+ representation of a record.
+
+ By default, `addAttributes` simply loops over all of the attributes of the
+ passed record, maps the attribute name to the key for the serialized form,
+ and invokes any registered transforms on the value. It then invokes the
+ more granular `addAttribute` with the key and transformed value.
+
+ Since you can override `keyForAttributeName`, `addAttribute`, and register
+ custom tranforms, you should rarely need to override this hook.
+
+ @param {any} data the serialized representation that is being built
+ @param {DS.Model} record the record to serialize
+ */
+ addAttributes: function(data, record) {
+ record.eachAttribute(function(name, attribute) {
+ this._addAttribute(data, record, name, attribute.type);
+ }, this);
+ },
+
+ /**
+ A hook you can use to customize how the key/value pair is added to
+ the serialized data.
+
+ @param {any} serialized the serialized form being built
+ @param {String} key the key to add to the serialized data
+ @param {any} value the value to add to the serialized data
+ */
+ addAttribute: Ember.K,
+
+ /**
+ A hook you can use to customize how the record's id is added to
+ the serialized data.
+
+ The `addId` hook is called with:
+
+ * the serialized representation being built
+ * the resolved primary key (taking configurations and the
+ `primaryKey` hook into consideration)
+ * the serialized id (after calling the `serializeId` hook)
+
+ @param {any} data the serialized representation that is being built
+ @param {String} key the resolved primary key
+ @param {id} id the serialized id
+ */
+ addId: Ember.K,
+
+ /**
+ A hook you can use to change how relationships are added to the serialized
+ representation of a record.
+
+ By default, `addAttributes` loops over all of the relationships of the
+ passed record, maps the relationship names to the key for the serialized form,
+ and then invokes the public `addBelongsTo` and `addHasMany` hooks.
+
+ Since you can override `keyForBelongsTo`, `keyForHasMany`, `addBelongsTo`,
+ `addHasMany`, and register mappings, you should rarely need to override this
+ hook.
+
+ @param {any} data the serialized representation that is being built
+ @param {DS.Model} record the record to serialize
+ */
+ addRelationships: function(data, record) {
+ record.eachRelationship(function(name, relationship) {
+ if (relationship.kind === 'belongsTo') {
+ this._addBelongsTo(data, record, name, relationship);
+ } else if (relationship.kind === 'hasMany') {
+ this._addHasMany(data, record, name, relationship);
+ }
+ }, this);
+ },
+
+ /**
+ A hook you can use to add a `belongsTo` relationship to the
+ serialized representation.
+
+ The specifics of this hook are very adapter-specific, so there
+ is no default implementation. You can see `DS.JSONSerializer`
+ for an example of an implementation of the `addBelongsTo` hook.
+
+ The `belongsTo` relationship object has the following properties:
+
+ * **type** a subclass of DS.Model that is the type of the
+ relationship. This is the first parameter to DS.belongsTo
+ * **options** the options passed to the call to DS.belongsTo
+ * **kind** always `belongsTo`
+
+ Additional properties may be added in the future.
+
+ @param {any} data the serialized representation that is being built
+ @param {DS.Model} record the record to serialize
+ @param {String} key the key for the serialized object
+ @param {Object} relationship an object representing the relationship
+ */
+ addBelongsTo: Ember.K,
+
+ /**
+ A hook you can use to add a `hasMany` relationship to the
+ serialized representation.
+
+ The specifics of this hook are very adapter-specific, so there
+ is no default implementation. You may not need to implement this,
+ for example, if your backend only expects relationships on the
+ child of a one to many relationship.
+
+ The `hasMany` relationship object has the following properties:
+
+ * **type** a subclass of DS.Model that is the type of the
+ relationship. This is the first parameter to DS.hasMany
+ * **options** the options passed to the call to DS.hasMany
+ * **kind** always `hasMany`
+
+ Additional properties may be added in the future.
+
+ @param {any} data the serialized representation that is being built
+ @param {DS.Model} record the record to serialize
+ @param {String} key the key for the serialized object
+ @param {Object} relationship an object representing the relationship
+ */
+ addHasMany: Ember.K,
+
+ /**
+ NAMING CONVENTIONS
+
+ The most commonly overridden APIs of the serializer are
+ the naming convention methods:
+
+ * `keyForAttributeName`: converts a camelized attribute name
+ into a key in the adapter-provided data hash. For example,
+ if the model's attribute name was `firstName`, and the
+ server used underscored names, you would return `first_name`.
+ * `primaryKey`: returns the key that should be used to
+ extract the id from the adapter-provided data hash. It is
+ also used when serializing a record.
+ */
+
+ /**
+ A hook you can use in your serializer subclass to customize
+ how an unmapped attribute name is converted into a key.
+
+ By default, this method returns the `name` parameter.
+
+ For example, if the attribute names in your JSON are underscored,
+ you will want to convert them into JavaScript conventional
+ camelcase:
+
+ ```javascript
+ App.MySerializer = DS.Serializer.extend({
+ // ...
+
+ keyForAttributeName: function(type, name) {
+ return name.camelize();
+ }
+ });
+ ```
+
+ @param {DS.Model subclass} type the type of the record with
+ the attribute name `name`
+ @param {String} name the attribute name to convert into a key
+
+ @returns {String} the key
+ */
+ keyForAttributeName: function(type, name) {
+ return name;
+ },
+
+ /**
+ A hook you can use in your serializer to specify a conventional
+ primary key.
+
+ By default, this method will return the string `id`.
+
+ In general, you should not override this hook to specify a special
+ primary key for an individual type; use `configure` instead.
+
+ For example, if your primary key is always `__id__`:
+
+ ```javascript
+ App.MySerializer = DS.Serializer.extend({
+ // ...
+ primaryKey: function(type) {
+ return '__id__';
+ }
+ });
+ ```
+
+ In another example, if the primary key always includes the
+ underscored version of the type before the string `id`:
+
+ ```javascript
+ App.MySerializer = DS.Serializer.extend({
+ // ...
+ primaryKey: function(type) {
+ // If the type is `BlogPost`, this will return
+ // `blog_post_id`.
+ var typeString = type.toString.split(".")[1].underscore();
+ return typeString + "_id";
+ }
+ });
+ ```
+
+ @param {DS.Model subclass} type
+ @returns {String} the primary key for the type
+ */
+ primaryKey: function(type) {
+ return "id";
+ },
+
+ /**
+ A hook you can use in your serializer subclass to customize
+ how an unmapped `belongsTo` relationship is converted into
+ a key.
+
+ By default, this method calls `keyForAttributeName`, so if
+ your naming convention is uniform across attributes and
+ relationships, you can use the default here and override
+ just `keyForAttributeName` as needed.
+
+ For example, if the `belongsTo` names in your JSON always
+ begin with `BT_` (e.g. `BT_posts`), you can strip out the
+ `BT_` prefix:"
+
+ ```javascript
+ App.MySerializer = DS.Serializer.extend({
+ // ...
+ keyForBelongsTo: function(type, name) {
+ return name.match(/^BT_(.*)$/)[1].camelize();
+ }
+ });
+ ```
+
+ @param {DS.Model subclass} type the type of the record with
+ the `belongsTo` relationship.
+ @param {String} name the relationship name to convert into a key
+
+ @returns {String} the key
+ */
+ keyForBelongsTo: function(type, name) {
+ return this.keyForAttributeName(type, name);
+ },
+
+ /**
+ A hook you can use in your serializer subclass to customize
+ how an unmapped `hasMany` relationship is converted into
+ a key.
+
+ By default, this method calls `keyForAttributeName`, so if
+ your naming convention is uniform across attributes and
+ relationships, you can use the default here and override
+ just `keyForAttributeName` as needed.
+
+ For example, if the `hasMany` names in your JSON always
+ begin with the "table name" for the current type (e.g.
+ `post_comments`), you can strip out the prefix:"
+
+ ```javascript
+ App.MySerializer = DS.Serializer.extend({
+ // ...
+ keyForHasMany: function(type, name) {
+ // if your App.BlogPost has many App.BlogComment, the key from
+ // the server would look like: `blog_post_blog_comments`
+ //
+ // 1. Convert the type into a string and underscore the
+ // second part (App.BlogPost -> blog_post)
+ // 2. Extract the part after `blog_post_` (`blog_comments`)
+ // 3. Underscore it, to become `blogComments`
+ var typeString = type.toString().split(".")[1].underscore();
+ return name.match(new RegExp("^" + typeString + "_(.*)$"))[1].camelize();
+ }
+ });
+ ```
+
+ @param {DS.Model subclass} type the type of the record with
+ the `belongsTo` relationship.
+ @param {String} name the relationship name to convert into a key
+
+ @returns {String} the key
+ */
+ keyForHasMany: function(type, name) {
+ return this.keyForAttributeName(type, name);
+ },
+
+ //.........................
+ //. MATERIALIZATION HOOKS
+ //.........................
+
+ materialize: function(record, serialized, prematerialized) {
+ var id;
+ if (Ember.isNone(get(record, 'id'))) {
+ if (prematerialized && prematerialized.hasOwnProperty('id')) {
+ id = prematerialized.id;
+ } else {
+ id = this.extractId(record.constructor, serialized);
+ }
+ record.materializeId(id);
+ }
+
+ this.materializeAttributes(record, serialized, prematerialized);
+ this.materializeRelationships(record, serialized, prematerialized);
+ },
+
+ deserializeValue: function(value, attributeType) {
+ var transform = this.transforms ? this.transforms[attributeType] : null;
+
+ Ember.assert("You tried to use a attribute type (" + attributeType + ") that has not been registered", transform);
+ return transform.deserialize(value);
+ },
+
+ materializeAttributes: function(record, serialized, prematerialized) {
+ record.eachAttribute(function(name, attribute) {
+ if (prematerialized && prematerialized.hasOwnProperty(name)) {
+ record.materializeAttribute(name, prematerialized[name]);
+ } else {
+ this.materializeAttribute(record, serialized, name, attribute.type);
+ }
+ }, this);
+ },
+
+ materializeAttribute: function(record, serialized, attributeName, attributeType) {
+ var value = this.extractAttribute(record.constructor, serialized, attributeName);
+ value = this.deserializeValue(value, attributeType);
+
+ record.materializeAttribute(attributeName, value);
+ },
+
+ materializeRelationships: function(record, hash, prematerialized) {
+ record.eachRelationship(function(name, relationship) {
+ if (relationship.kind === 'hasMany') {
+ if (prematerialized && prematerialized.hasOwnProperty(name)) {
+ record.materializeHasMany(name, prematerialized[name]);
+ } else {
+ this.materializeHasMany(name, record, hash, relationship, prematerialized);
+ }
+ } else if (relationship.kind === 'belongsTo') {
+ if (prematerialized && prematerialized.hasOwnProperty(name)) {
+ record.materializeBelongsTo(name, prematerialized[name]);
+ } else {
+ this.materializeBelongsTo(name, record, hash, relationship, prematerialized);
+ }
+ }
+ }, this);
+ },
+
+ materializeHasMany: function(name, record, hash, relationship) {
+ var key = this._keyForHasMany(record.constructor, relationship.key);
+ record.materializeHasMany(name, this.extractHasMany(record.constructor, hash, key));
+ },
+
+ materializeBelongsTo: function(name, record, hash, relationship) {
+ var key = this._keyForBelongsTo(record.constructor, relationship.key);
+ record.materializeBelongsTo(name, this.extractBelongsTo(record.constructor, hash, key));
+ },
+
+ _extractEmbeddedRelationship: function(type, hash, name, relationshipType) {
+ var key = this['_keyFor' + relationshipType](type, name);
+
+ if (this.embeddedType(type, name)) {
+ return this['extractEmbedded' + relationshipType](type, hash, key);
+ }
+ },
+
+ _extractEmbeddedBelongsTo: function(type, hash, name) {
+ return this._extractEmbeddedRelationship(type, hash, name, 'BelongsTo');
+ },
+
+ _extractEmbeddedHasMany: function(type, hash, name) {
+ return this._extractEmbeddedRelationship(type, hash, name, 'HasMany');
+ },
+
+ /**
+ @private
+
+ This method is called to get the primary key for a given
+ type.
+
+ If a primary key configuration exists for this type, this
+ method will return the configured value. Otherwise, it will
+ call the public `primaryKey` hook.
+
+ @param {DS.Model subclass} type
+ @returns {String} the primary key for the type
+ */
+ _primaryKey: function(type) {
+ var config = this.configurationForType(type),
+ primaryKey = config && config.primaryKey;
+
+ if (primaryKey) {
+ return primaryKey;
+ } else {
+ return this.primaryKey(type);
+ }
+ },
+
+ /**
+ @private
+
+ This method looks up the key for the attribute name and transforms the
+ attribute's value using registered transforms.
+
+ Specifically:
+
+ 1. Look up the key for the attribute name. If available, this will use
+ any registered mappings. Otherwise, it will invoke the public
+ `keyForAttributeName` hook.
+ 2. Get the value from the record using the `attributeName`.
+ 3. Transform the value using registered transforms for the `attributeType`.
+ 4. Invoke the public `addAttribute` hook with the hash, key, and
+ transformed value.
+
+ @param {any} data the serialized representation being built
+ @param {DS.Model} record the record to serialize
+ @param {String} attributeName the name of the attribute on the record
+ @param {String} attributeType the type of the attribute (e.g. `string`
+ or `boolean`)
+ */
+ _addAttribute: function(data, record, attributeName, attributeType) {
+ var key = this._keyForAttributeName(record.constructor, attributeName);
+ var value = get(record, attributeName);
+
+ this.addAttribute(data, key, this.serializeValue(value, attributeType));
+ },
+
+ /**
+ @private
+
+ This method looks up the primary key for the `type` and invokes
+ `serializeId` on the `id`.
+
+ It then invokes the public `addId` hook with the primary key and
+ the serialized id.
+
+ @param {any} data the serialized representation that is being built
+ @param {Ember.Model subclass} type
+ @param {any} id the materialized id from the record
+ */
+ _addId: function(hash, type, id) {
+ var primaryKey = this._primaryKey(type);
+
+ this.addId(hash, primaryKey, this.serializeId(id));
+ },
+
+ /**
+ @private
+
+ This method is called to get a key used in the data from
+ an attribute name. It first checks for any mappings before
+ calling the public hook `keyForAttributeName`.
+
+ @param {DS.Model subclass} type the type of the record with
+ the attribute name `name`
+ @param {String} name the attribute name to convert into a key
+
+ @returns {String} the key
+ */
+ _keyForAttributeName: function(type, name) {
+ return this._keyFromMappingOrHook('keyForAttributeName', type, name);
+ },
+
+ /**
+ @private
+
+ This method is called to get a key used in the data from
+ a belongsTo relationship. It first checks for any mappings before
+ calling the public hook `keyForBelongsTo`.
+
+ @param {DS.Model subclass} type the type of the record with
+ the `belongsTo` relationship.
+ @param {String} name the relationship name to convert into a key
+
+ @returns {String} the key
+ */
+ _keyForBelongsTo: function(type, name) {
+ return this._keyFromMappingOrHook('keyForBelongsTo', type, name);
+ },
+
+ keyFor: function(description) {
+ var type = description.parentType,
+ name = description.key;
+
+ switch (description.kind) {
+ case 'belongsTo':
+ return this._keyForBelongsTo(type, name);
+ case 'hasMany':
+ return this._keyForHasMany(type, name);
+ }
+ },
+
+ /**
+ @private
+
+ This method is called to get a key used in the data from
+ a hasMany relationship. It first checks for any mappings before
+ calling the public hook `keyForHasMany`.
+
+ @param {DS.Model subclass} type the type of the record with
+ the `hasMany` relationship.
+ @param {String} name the relationship name to convert into a key
+
+ @returns {String} the key
+ */
+ _keyForHasMany: function(type, name) {
+ return this._keyFromMappingOrHook('keyForHasMany', type, name);
+ },
+ /**
+ @private
+
+ This method converts the relationship name to a key for serialization,
+ and then invokes the public `addBelongsTo` hook.
+
+ @param {any} data the serialized representation that is being built
+ @param {DS.Model} record the record to serialize
+ @param {String} name the relationship name
+ @param {Object} relationship an object representing the relationship
+ */
+ _addBelongsTo: function(data, record, name, relationship) {
+ var key = this._keyForBelongsTo(record.constructor, name);
+ this.addBelongsTo(data, record, key, relationship);
+ },
+
+ /**
+ @private
+
+ This method converts the relationship name to a key for serialization,
+ and then invokes the public `addHasMany` hook.
+
+ @param {any} data the serialized representation that is being built
+ @param {DS.Model} record the record to serialize
+ @param {String} name the relationship name
+ @param {Object} relationship an object representing the relationship
+ */
+ _addHasMany: function(data, record, name, relationship) {
+ var key = this._keyForHasMany(record.constructor, name);
+ this.addHasMany(data, record, key, relationship);
+ },
+
+ /**
+ @private
+
+ An internal method that handles checking whether a mapping
+ exists for a particular attribute or relationship name before
+ calling the public hooks.
+
+ If a mapping is found, and the mapping has a key defined,
+ use that instead of invoking the hook.
+
+ @param {String} publicMethod the public hook to invoke if
+ a mapping is not found (e.g. `keyForAttributeName`)
+ @param {DS.Model subclass} type the type of the record with
+ the attribute or relationship name.
+ @param {String} name the attribute or relationship name to
+ convert into a key
+ */
+ _keyFromMappingOrHook: function(publicMethod, type, name) {
+ var key = this.mappingOption(type, name, 'key');
+
+ if (key) {
+ return key;
+ } else {
+ return this[publicMethod](type, name);
+ }
+ },
+
+ /**
+ TRANSFORMS
+ */
+
+ registerTransform: function(type, transform) {
+ this.transforms[type] = transform;
+ },
+
+ registerEnumTransform: function(type, objects) {
+ var transform = {
+ deserialize: function(deserialized) {
+ return objects.objectAt(deserialized);
+ },
+ serialize: function(serialized) {
+ return objects.indexOf(serialized);
+ },
+ values: objects
+ };
+ this.registerTransform(type, transform);
+ },
+
+ /**
+ MAPPING CONVENIENCE
+ */
+
+ map: function(type, mappings) {
+ this.mappings.set(type, mappings);
+ },
+
+ configure: function(type, configuration) {
+ if (type && !configuration) {
+ Ember.merge(this.globalConfigurations, type);
+ return;
+ }
+
+ var config = Ember.create(this.globalConfigurations);
+ Ember.merge(config, configuration);
+
+ this.configurations.set(type, config);
+ },
+
+ mappingForType: function(type) {
+ this._reifyMappings();
+ return this.mappings.get(type) || {};
+ },
+
+ configurationForType: function(type) {
+ this._reifyConfigurations();
+ return this.configurations.get(type) || this.globalConfigurations;
+ },
+
+ _reifyMappings: function() {
+ if (this._didReifyMappings) { return; }
+
+ var mappings = this.mappings,
+ reifiedMappings = Ember.Map.create();
+
+ mappings.forEach(function(key, mapping) {
+ if (typeof key === 'string') {
+ var type = Ember.get(Ember.lookup, key);
+ Ember.assert("Could not find model at path " + key, type);
+
+ reifiedMappings.set(type, mapping);
+ } else {
+ reifiedMappings.set(key, mapping);
+ }
+ });
+
+ this.mappings = reifiedMappings;
+
+ this._didReifyMappings = true;
+ },
+
+ _reifyConfigurations: function() {
+ if (this._didReifyConfigurations) { return; }
+
+ var configurations = this.configurations,
+ reifiedConfigurations = Ember.Map.create();
+
+ configurations.forEach(function(key, mapping) {
+ if (typeof key === 'string' && key !== 'plurals') {
+ var type = Ember.get(Ember.lookup, key);
+ Ember.assert("Could not find model at path " + key, type);
+
+ reifiedConfigurations.set(type, mapping);
+ } else {
+ reifiedConfigurations.set(key, mapping);
+ }
+ });
+
+ this.configurations = reifiedConfigurations;
+
+ this._didReifyConfigurations = true;
+ },
+
+ mappingOption: function(type, name, option) {
+ var mapping = this.mappingForType(type)[name];
+
+ return mapping && mapping[option];
+ },
+
+ configOption: function(type, option) {
+ var config = this.configurationForType(type);
+
+ return config[option];
+ },
+
+ // EMBEDDED HELPERS
+
+ embeddedType: function(type, name) {
+ return this.mappingOption(type, name, 'embedded');
+ },
+
+ eachEmbeddedRecord: function(record, callback, binding) {
+ this.eachEmbeddedBelongsToRecord(record, callback, binding);
+ this.eachEmbeddedHasManyRecord(record, callback, binding);
+ },
+
+ eachEmbeddedBelongsToRecord: function(record, callback, binding) {
+ var type = record.constructor;
+
+ this.eachEmbeddedBelongsTo(record.constructor, function(name, relationship, embeddedType) {
+ var embeddedRecord = get(record, name);
+ if (embeddedRecord) { callback.call(binding, embeddedRecord, embeddedType); }
+ });
+ },
+
+ eachEmbeddedHasManyRecord: function(record, callback, binding) {
+ var type = record.constructor;
+
+ this.eachEmbeddedHasMany(record.constructor, function(name, relationship, embeddedType) {
+ var array = get(record, name);
+ for (var i=0, l=get(array, 'length'); i<l; i++) {
+ callback.call(binding, array.objectAt(i), embeddedType);
+ }
+ });
+ },
+
+ eachEmbeddedHasMany: function(type, callback, binding) {
+ this.eachEmbeddedRelationship(type, 'hasMany', callback, binding);
+ },
+
+ eachEmbeddedBelongsTo: function(type, callback, binding) {
+ this.eachEmbeddedRelationship(type, 'belongsTo', callback, binding);
+ },
+
+ eachEmbeddedRelationship: function(type, kind, callback, binding) {
+ type.eachRelationship(function(name, relationship) {
+ var embeddedType = this.embeddedType(type, name);
+
+ if (embeddedType) {
+ if (relationship.kind === kind) {
+ callback.call(binding, name, relationship, embeddedType);
+ }
+ }
+ }, this);
+ }
+});
+
+
+})();
+
+
+
+(function() {
+var none = Ember.isNone;
+
+/**
+ DS.Transforms is a hash of transforms used by DS.Serializer.
+*/
+DS.JSONTransforms = {
+ string: {
+ deserialize: function(serialized) {
+ return none(serialized) ? null : String(serialized);
+ },
+
+ serialize: function(deserialized) {
+ return none(deserialized) ? null : String(deserialized);
+ }
+ },
+
+ number: {
+ deserialize: function(serialized) {
+ return none(serialized) ? null : Number(serialized);
+ },
+
+ serialize: function(deserialized) {
+ return none(deserialized) ? null : Number(deserialized);
+ }
+ },
+
+ // Handles the following boolean inputs:
+ // "TrUe", "t", "f", "FALSE", 0, (non-zero), or boolean true/false
+ 'boolean': {
+ deserialize: function(serialized) {
+ var type = typeof serialized;
+
+ if (type === "boolean") {
+ return serialized;
+ } else if (type === "string") {
+ return serialized.match(/^true$|^t$|^1$/i) !== null;
+ } else if (type === "number") {
+ return serialized === 1;
+ } else {
+ return false;
+ }
+ },
+
+ serialize: function(deserialized) {
+ return Boolean(deserialized);
+ }
+ },
+
+ date: {
+ deserialize: function(serialized) {
+ var type = typeof serialized;
+ var date = null;
+
+ if (type === "string" || type === "number") {
+ // this is a fix for Safari 5.1.5 on Mac which does not accept timestamps as yyyy-mm-dd
+ if (type === "string" && serialized.search(/^\d{4}-\d{2}-\d{2}$/) !== -1) {
+ serialized += "T00:00:00Z";
+ }
+
+ date = new Date(serialized);
+
+ // this is a fix for IE8 which does not accept timestamps in ISO 8601 format
+ if (type === "string" && isNaN(date)) {
+ date = new Date(Date.parse(serialized.replace(/\-/ig, '/').replace(/Z$/, '').split('.')[0]));
+ }
+
+ return date;
+ } else if (serialized === null || serialized === undefined) {
+ // if the value is not present in the data,
+ // return undefined, not null.
+ return serialized;
+ } else {
+ return null;
+ }
+ },
+
+ serialize: function(date) {
+ if (date instanceof Date) {
+ var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+ var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+
+ var pad = function(num) {
+ return num < 10 ? "0"+num : ""+num;
+ };
+
+ var utcYear = date.getUTCFullYear(),
+ utcMonth = date.getUTCMonth(),
+ utcDayOfMonth = date.getUTCDate(),
+ utcDay = date.getUTCDay(),
+ utcHours = date.getUTCHours(),
+ utcMinutes = date.getUTCMinutes(),
+ utcSeconds = date.getUTCSeconds();
+
+
+ var dayOfWeek = days[utcDay];
+ var dayOfMonth = pad(utcDayOfMonth);
+ var month = months[utcMonth];
+
+ return dayOfWeek + ", " + dayOfMonth + " " + month + " " + utcYear + " " +
+ pad(utcHours) + ":" + pad(utcMinutes) + ":" + pad(utcSeconds) + " GMT";
+ } else if (date === undefined) {
+ return undefined;
+ } else {
+ return null;
+ }
+ }
+ }
+};
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set;
+
+var generatedId = 0;
+
+DS.JSONSerializer = DS.Serializer.extend({
+ init: function() {
+ this._super();
+
+ if (!get(this, 'transforms')) {
+ this.set('transforms', DS.JSONTransforms);
+ }
+
+ this.sideloadMapping = Ember.Map.create();
+
+ this.configure({
+ meta: 'meta',
+ since: 'since'
+ });
+ },
+
+ configure: function(type, configuration) {
+ if (type && !configuration) {
+ return this._super(type);
+ }
+
+ var sideloadAs = configuration.sideloadAs;
+
+ if (sideloadAs) {
+ this.sideloadMapping.set(sideloadAs, type);
+ delete configuration.sideloadAs;
+ }
+
+ this._super.apply(this, arguments);
+ },
+
+ addId: function(data, key, id) {
+ data[key] = id;
+ },
+
+ /**
+ A hook you can use to customize how the key/value pair is added to
+ the serialized data.
+
+ @param {any} hash the JSON hash being built
+ @param {String} key the key to add to the serialized data
+ @param {any} value the value to add to the serialized data
+ */
+ addAttribute: function(hash, key, value) {
+ 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];
+ },
+
+ extractId: function(type, hash) {
+ var primaryKey = this._primaryKey(type);
+
+ if (hash.hasOwnProperty(primaryKey)) {
+ // Ensure that we coerce IDs to strings so that record
+ // IDs remain consistent between application runs; especially
+ // if the ID is serialized and later deserialized from the URL,
+ // when type information will have been lost.
+ return hash[primaryKey]+'';
+ } else {
+ return null;
+ }
+ },
+
+ extractHasMany: function(type, hash, key) {
+ return hash[key];
+ },
+
+ extractBelongsTo: function(type, hash, key) {
+ return hash[key];
+ },
+
+ addBelongsTo: function(hash, record, key, relationship) {
+ var type = record.constructor,
+ name = relationship.key,
+ value = null,
+ embeddedChild;
+
+ if (this.embeddedType(type, name)) {
+ if (embeddedChild = get(record, name)) {
+ value = this.serialize(embeddedChild, { include: true });
+ }
+
+ hash[key] = value;
+ } else {
+ var id = get(record, relationship.key+'.id');
+ if (!Ember.isNone(id)) { hash[key] = id; }
+ }
+ },
+
+ /**
+ Adds a has-many relationship to the JSON hash being built.
+
+ The default REST semantics are to only add a has-many relationship if it
+ is embedded. If the relationship was initially loaded by ID, we assume that
+ that was done as a performance optimization, and that changes to the
+ has-many should be saved as foreign key changes on the child's belongs-to
+ relationship.
+
+ @param {Object} hash the JSON being built
+ @param {DS.Model} record the record being serialized
+ @param {String} key the JSON key into which the serialized relationship
+ 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 = [],
+ manyArray, embeddedType;
+
+ // If the has-many is not embedded, there is nothing to do.
+ embeddedType = this.embeddedType(type, name);
+ if (embeddedType !== 'always') { return; }
+
+ // Get the DS.ManyArray for the relationship off the record
+ manyArray = get(record, name);
+
+ // Build up the array of serialized records
+ manyArray.forEach(function (record) {
+ serializedHasMany.push(this.serialize(record, { includeId: true }));
+ }, this);
+
+ // Set the appropriate property of the serialized JSON to the
+ // array of serialized embedded records
+ hash[key] = serializedHasMany;
+ },
+
+ // EXTRACTION
+
+ extract: function(loader, json, type, record) {
+ var root = this.rootForType(type);
+
+ this.sideload(loader, type, json, root);
+ this.extractMeta(loader, type, json);
+
+ if (json[root]) {
+ if (record) { loader.updateId(record, json[root]); }
+ this.extractRecordRepresentation(loader, type, json[root]);
+ }
+ },
+
+ extractMany: function(loader, json, type, records) {
+ var root = this.rootForType(type);
+ root = this.pluralize(root);
+
+ this.sideload(loader, type, json, root);
+ this.extractMeta(loader, type, json);
+
+ if (json[root]) {
+ var objects = json[root], references = [];
+ if (records) { records = records.toArray(); }
+
+ for (var i = 0; i < objects.length; i++) {
+ if (records) { loader.updateId(records[i], objects[i]); }
+ var reference = this.extractRecordRepresentation(loader, type, objects[i]);
+ references.push(reference);
+ }
+
+ loader.populateArray(references);
+ }
+ },
+
+ extractMeta: function(loader, type, json) {
+ var meta = json[this.configOption(type, 'meta')], since;
+ if (!meta) { return; }
+
+ if (since = meta[this.configOption(type, 'since')]) {
+ loader.sinceForType(type, since);
+ }
+ },
+
+ sideload: function(loader, type, json, root) {
+ var sideloadedType, mappings, loaded = {};
+
+ loaded[root] = true;
+
+ for (var prop in json) {
+ if (!json.hasOwnProperty(prop)) { continue; }
+ if (prop === root) { continue; }
+ if (prop === this.configOption(type, 'meta')) { continue; }
+
+ sideloadedType = type.typeForRelationship(prop);
+
+ if (!sideloadedType) {
+ sideloadedType = this.sideloadMapping.get(prop);
+
+ if (typeof sideloadedType === 'string') {
+ sideloadedType = get(Ember.lookup, sideloadedType);
+ }
+
+ Ember.assert("Your server returned a hash with the key " + prop + " but you have no mapping for it", !!sideloadedType);
+ }
+
+ this.sideloadRelationships(loader, sideloadedType, json, prop, loaded);
+ }
+ },
+
+ sideloadRelationships: function(loader, type, json, prop, loaded) {
+ loaded[prop] = true;
+
+ get(type, 'relationshipsByName').forEach(function(key, meta) {
+ key = meta.key || key;
+ if (meta.kind === 'belongsTo') {
+ key = this.pluralize(key);
+ }
+ if (json[key] && !loaded[key]) {
+ this.sideloadRelationships(loader, meta.type, json, key, loaded);
+ }
+ }, this);
+
+ this.loadValue(loader, type, json[prop]);
+ },
+
+ loadValue: function(loader, type, value) {
+ if (value instanceof Array) {
+ for (var i=0; i < value.length; i++) {
+ loader.sideload(type, value[i]);
+ }
+ } else {
+ loader.sideload(type, value);
+ }
+ },
+
+ // HELPERS
+
+ // 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";
+ },
+
+ rootForType: function(type) {
+ var typeString = type.toString();
+
+ Ember.assert("Your model must not be anonymous. It was " + type, typeString.charAt(0) !== '(');
+
+ // use the last part of the name as the URL
+ var parts = typeString.split(".");
+ var name = parts[parts.length - 1];
+ return name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
+ }
+});
+
+})();
+
+
+
+(function() {
+function loaderFor(store) {
+ return {
+ load: function(type, data, prematerialized) {
+ return store.load(type, data, prematerialized);
+ },
+
+ loadMany: function(type, array) {
+ return store.loadMany(type, array);
+ },
+
+ updateId: function(record, data) {
+ return store.updateId(record, data);
+ },
+
+ populateArray: Ember.K,
+
+ sideload: function(type, data) {
+ return store.load(type, data);
+ },
+
+ sideloadMany: function(type, array) {
+ return store.loadMany(type, array);
+ },
+
+ prematerialize: function(reference, prematerialized) {
+ store.prematerialize(reference, prematerialized);
+ },
+
+ sinceForType: function(type, since) {
+ store.sinceForType(type, since);
+ }
+ };
+}
+
+DS.loaderFor = loaderFor;
+
+/**
+ An adapter is an object that receives requests from a store and
+ translates them into the appropriate action to take against your
+ persistence layer. The persistence layer is usually an HTTP API, but may
+ be anything, such as the browser's local storage.
+
+ ### Creating an Adapter
+
+ First, create a new subclass of `DS.Adapter`:
+
+ App.MyAdapter = DS.Adapter.extend({
+ // ...your code here
+ });
+
+ To tell your store which adapter to use, set its `adapter` property:
+
+ App.store = DS.Store.create({
+ revision: 3,
+ adapter: App.MyAdapter.create()
+ });
+
+ `DS.Adapter` is an abstract base class that you should override in your
+ application to customize it for your backend. The minimum set of methods
+ that you should implement is:
+
+ * `find()`
+ * `createRecord()`
+ * `updateRecord()`
+ * `deleteRecord()`
+
+ 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;
+
+DS.Adapter = Ember.Object.extend(DS._Mappable, {
+
+ init: function() {
+ var serializer = get(this, 'serializer');
+
+ if (Ember.Object.detect(serializer)) {
+ serializer = serializer.create();
+ set(this, 'serializer', serializer);
+ }
+
+ this._attributesMap = this.createInstanceMapFor('attributes');
+ this._configurationsMap = this.createInstanceMapFor('configurations');
+
+ this._outstandingOperations = new Ember.MapWithDefault({
+ defaultValue: function() { return 0; }
+ });
+
+ this._dependencies = new Ember.MapWithDefault({
+ defaultValue: function() { return new Ember.OrderedSet(); }
+ });
+
+ this.registerSerializerTransforms(this.constructor, serializer, {});
+ this.registerSerializerMappings(serializer);
+ },
+
+ /**
+ Loads a payload for a record into the store.
+
+ This method asks the serializer to break the payload into
+ constituent parts, and then loads them into the store. For example,
+ if you have a payload that contains embedded records, they will be
+ extracted by the serializer and loaded into the store.
+
+ For example:
+
+ ```javascript
+ 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`.
+
+ @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);
+ },
+
+ /**
+ Acknowledges that the adapter has finished creating a record.
+
+ Your adapter should call this method from `createRecord` when
+ it has saved a new record to its persistent storage and 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.
+
+ 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.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} record
+ @param {any} payload
+ */
+ didCreateRecord: function(store, type, record, payload) {
+ store.didSaveRecord(record);
+
+ if (payload) {
+ var loader = DS.loaderFor(store);
+ var serializer = get(this, 'serializer');
+
+ loader.load = function(type, data, prematerialized) {
+ store.updateId(record, data);
+ return store.load(type, data, prematerialized);
+ };
+
+ get(this, 'serializer').extract(loader, payload, type);
+ }
+ },
+
+ /**
+ 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
+ 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.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} record
+ @param {any} payload
+ */
+ didCreateRecords: function(store, type, records, payload) {
+ records.forEach(function(record) {
+ store.didSaveRecord(record);
+ }, this);
+
+ if (payload) {
+ var loader = DS.loaderFor(store);
+ get(this, 'serializer').extractMany(loader, payload, type, records);
+ }
+ },
+
+ /**
+ @private
+
+ Acknowledges that the adapter has finished updating or deleting a record.
+
+ Your adapter should call this method from `updateRecord` or `deleteRecord`
+ when it has updated or deleted a record to its persistent storage and
+ received an acknowledgement.
+
+ If the persistent storage returns a new payload in response to the
+ update or delete, and you want to update the existing record with the
+ new information, pass the payload as the fourth parameter.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} record
+ @param {any} payload
+ */
+ didSaveRecord: function(store, type, record, payload) {
+ store.didSaveRecord(record);
+
+ var serializer = get(this, 'serializer'),
+ mappings = serializer.mappingForType(type);
+
+ serializer.eachEmbeddedRecord(record, function(embeddedRecord, embeddedType) {
+ if (embeddedType === 'load') { return; }
+
+ this.didSaveRecord(store, embeddedRecord.constructor, embeddedRecord);
+ }, this);
+
+ if (payload) {
+ var loader = DS.loaderFor(store);
+ serializer.extract(loader, payload, type);
+ }
+ },
+
+ /**
+ Acknowledges that the adapter has finished updating a record.
+
+ Your adapter should call this method from `updateRecord` when it
+ has updated a record to its persistent storage and received an
+ acknowledgement.
+
+ If the persistent storage returns a new payload in response to the
+ update, pass the payload as the fourth parameter.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} record
+ @param {any} payload
+ */
+ didUpdateRecord: function() {
+ this.didSaveRecord.apply(this, arguments);
+ },
+
+ /**
+ Acknowledges that the adapter has finished deleting a record.
+
+ Your adapter should call this method from `deleteRecord` when it
+ has deleted a record from its persistent storage and received an
+ acknowledgement.
+
+ If the persistent storage returns a new payload in response to the
+ deletion, pass the payload as the fourth parameter.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} record
+ @param {any} payload
+ */
+ didDeleteRecord: function() {
+ this.didSaveRecord.apply(this, arguments);
+ },
+
+ /**
+ Acknowledges that the adapter has finished updating or deleting
+ multiple records.
+
+ Your adapter should call this method from its `updateRecords` or
+ `deleteRecords` when it has updated or deleted multiple records
+ to its persistent storage and received an acknowledgement.
+
+ If the persistent storage returns a new payload in response to the
+ creation, pass the payload as the fourth parameter.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} records
+ @param {any} payload
+ */
+ didSaveRecords: function(store, type, records, payload) {
+ records.forEach(function(record) {
+ store.didSaveRecord(record);
+ }, this);
+
+ if (payload) {
+ var loader = DS.loaderFor(store);
+ get(this, 'serializer').extractMany(loader, payload, type);
+ }
+ },
+
+ /**
+ Acknowledges that the adapter has finished updating multiple records.
+
+ Your adapter should call this method from its `updateRecords` when
+ it has updated multiple records to its persistent storage and
+ received an acknowledgement.
+
+ If the persistent storage returns a new payload in response to the
+ update, pass the payload as the fourth parameter.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} records
+ @param {any} payload
+ */
+ didUpdateRecords: function() {
+ this.didSaveRecords.apply(this, arguments);
+ },
+
+ /**
+ Acknowledges that the adapter has finished updating multiple records.
+
+ Your adapter should call this method from its `deleteRecords` when
+ it has deleted multiple records to its persistent storage and
+ received an acknowledgement.
+
+ If the persistent storage returns a new payload in response to the
+ deletion, pass the payload as the fourth parameter.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} records
+ @param {any} payload
+ */
+ didDeleteRecords: function() {
+ this.didSaveRecords.apply(this, arguments);
+ },
+
+ /**
+ Loads the response to a request for a record by ID.
+
+ Your adapter should call this method from its `find` method
+ with the response from the backend.
+
+ You should pass the same ID to this method that was given
+ to your find method so that the store knows which record
+ to associate the new data with.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {any} payload
+ @param {String} id
+ */
+ didFindRecord: function(store, type, payload, id) {
+ var loader = DS.loaderFor(store);
+
+ loader.load = function(type, data, prematerialized) {
+ prematerialized = prematerialized || {};
+ prematerialized.id = id;
+
+ return store.load(type, data, prematerialized);
+ };
+
+ get(this, 'serializer').extract(loader, payload, type);
+ },
+
+ /**
+ Loads the response to a request for all records by type.
+
+ You adapter should call this method from its `findAll`
+ method with the response from the backend.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {any} payload
+ */
+ didFindAll: function(store, type, payload) {
+ var loader = DS.loaderFor(store),
+ serializer = get(this, 'serializer');
+
+ store.didUpdateAll(type);
+
+ serializer.extractMany(loader, payload, type);
+ },
+
+ /**
+ Loads the response to a request for records by query.
+
+ Your adapter should call this method from its `findQuery`
+ method with the response from the backend.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {any} payload
+ @param {DS.AdapterPopulatedRecordArray} recordArray
+ */
+ didFindQuery: function(store, type, payload, recordArray) {
+ var loader = DS.loaderFor(store);
+
+ loader.populateArray = function(data) {
+ recordArray.load(data);
+ };
+
+ get(this, 'serializer').extractMany(loader, payload, type);
+ },
+
+ /**
+ Loads the response to a request for many records by ID.
+
+ You adapter should call this method from its `findMany`
+ method with the response from the backend.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {any} payload
+ */
+ didFindMany: function(store, type, payload) {
+ var loader = DS.loaderFor(store);
+
+ get(this, 'serializer').extractMany(loader, payload, type);
+ },
+
+ /**
+ Notifies the store that a request to the backend returned
+ an error.
+
+ Your adapter should call this method to indicate that the
+ backend returned an error for a request.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} record
+ */
+ didError: function(store, type, record) {
+ store.recordWasError(record);
+ },
+
+ dirtyRecordsForAttributeChange: function(dirtySet, record, attributeName, newValue, oldValue) {
+ if (newValue !== oldValue) {
+ // If this record is embedded, add its parent
+ // to the dirty set.
+ this.dirtyRecordsForRecordChange(dirtySet, record);
+ }
+ },
+
+ dirtyRecordsForRecordChange: function(dirtySet, record) {
+ dirtySet.add(record);
+ },
+
+ dirtyRecordsForBelongsToChange: function(dirtySet, child) {
+ this.dirtyRecordsForRecordChange(dirtySet, child);
+ },
+
+ dirtyRecordsForHasManyChange: function(dirtySet, parent) {
+ this.dirtyRecordsForRecordChange(dirtySet, parent);
+ },
+
+ /**
+ @private
+
+ This method recursively climbs the superclass hierarchy and
+ registers any class-registered transforms on the adapter's
+ serializer.
+
+ Once it registers a transform for a given type, it ignores
+ subsequent transforms for the same attribute type.
+
+ @param {Class} klass the DS.Adapter subclass to extract the
+ transforms from
+ @param {DS.Serializer} serializer the serializer to register
+ the transforms onto
+ @param {Object} seen a hash of attributes already seen
+ */
+ registerSerializerTransforms: function(klass, serializer, seen) {
+ var transforms = klass._registeredTransforms, superclass, prop;
+
+ for (prop in transforms) {
+ if (!transforms.hasOwnProperty(prop) || prop in seen) { continue; }
+ seen[prop] = true;
+
+ serializer.registerTransform(prop, transforms[prop]);
+ }
+
+ if (superclass = klass.superclass) {
+ this.registerSerializerTransforms(superclass, serializer, seen);
+ }
+ },
+
+ /**
+ @private
+
+ This method recursively climbs the superclass hierarchy and
+ registers any class-registered mappings on the adapter's
+ serializer.
+
+ @param {Class} klass the DS.Adapter subclass to extract the
+ transforms from
+ @param {DS.Serializer} serializer the serializer to register the
+ mappings onto
+ */
+ registerSerializerMappings: function(serializer) {
+ var mappings = this._attributesMap,
+ configurations = this._configurationsMap;
+
+ mappings.forEach(serializer.map, serializer);
+ configurations.forEach(serializer.configure, serializer);
+ },
+
+ /**
+ The `find()` method is invoked when the store is asked for a record that
+ has not previously been loaded. In response to `find()` being called, you
+ should query your persistence layer for a record with the given ID. Once
+ found, you can asynchronously call the store's `load()` method to load
+ the record.
+
+ Here is an example `find` implementation:
+
+ 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);
+ });
+ }
+ */
+ find: null,
+
+ serializer: DS.JSONSerializer,
+
+ registerTransform: function(attributeType, transform) {
+ get(this, 'serializer').registerTransform(attributeType, transform);
+ },
+
+ /**
+ 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.
+
+ Eg: Say you want to utilize "low","medium","high" text strings
+ 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']);
+
+ You would then refer to the 'priority' DS.attr in your model:
+ 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} }
+
+ @param {String} type of the transform
+ @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) {
+ get(this, 'serializer').registerEnumTransform(attributeType, objects);
+ },
+
+ /**
+ If the globally unique IDs for your records should be generated on the client,
+ implement the `generateIdForRecord()` method. This method will be invoked
+ each time you create a new record, and the value returned from it will be
+ assigned to the record's `primaryKey`.
+
+ Most traditional REST-like HTTP APIs will not use this method. Instead, the ID
+ of the record will be set by the server, and your adapter will update the store
+ with the new ID when it calls `didCreateRecord()`. Only implement this method if
+ you intend to generate record IDs on the client-side.
+
+ The `generateIdForRecord()` method will be invoked with the requesting store as
+ the first parameter and the newly created record as the second parameter:
+
+ generateIdForRecord: function(store, record) {
+ var uuid = App.generateUUIDWithStatisticallyLowOddsOfCollision();
+ return uuid;
+ }
+ */
+ generateIdForRecord: null,
+
+ materialize: function(record, data, prematerialized) {
+ get(this, 'serializer').materialize(record, data, prematerialized);
+ },
+
+ serialize: function(record, options) {
+ return get(this, 'serializer').serialize(record, options);
+ },
+
+ extractId: function(type, data) {
+ return get(this, 'serializer').extractId(type, data);
+ },
+
+ groupByType: function(enumerable) {
+ var map = Ember.MapWithDefault.create({
+ defaultValue: function() { return Ember.OrderedSet.create(); }
+ });
+
+ enumerable.forEach(function(item) {
+ map.get(item.constructor).add(item);
+ });
+
+ return map;
+ },
+
+ commit: function(store, commitDetails) {
+ this.save(store, commitDetails);
+ },
+
+ save: function(store, commitDetails) {
+ var adapter = this;
+
+ function filter(records) {
+ var filteredSet = Ember.OrderedSet.create();
+
+ records.forEach(function(record) {
+ if (adapter.shouldSave(record)) {
+ filteredSet.add(record);
+ }
+ });
+
+ return filteredSet;
+ }
+
+ this.groupByType(commitDetails.created).forEach(function(type, set) {
+ this.createRecords(store, type, filter(set));
+ }, this);
+
+ this.groupByType(commitDetails.updated).forEach(function(type, set) {
+ this.updateRecords(store, type, filter(set));
+ }, this);
+
+ this.groupByType(commitDetails.deleted).forEach(function(type, set) {
+ this.deleteRecords(store, type, filter(set));
+ }, this);
+ },
+
+ shouldSave: Ember.K,
+
+ createRecords: function(store, type, records) {
+ records.forEach(function(record) {
+ this.createRecord(store, type, record);
+ }, this);
+ },
+
+ updateRecords: function(store, type, records) {
+ records.forEach(function(record) {
+ this.updateRecord(store, type, record);
+ }, this);
+ },
+
+ deleteRecords: function(store, type, records) {
+ records.forEach(function(record) {
+ this.deleteRecord(store, type, record);
+ }, this);
+ },
+
+ findMany: function(store, type, ids) {
+ ids.forEach(function(id) {
+ this.find(store, type, id);
+ }, this);
+ }
+});
+
+DS.Adapter.reopenClass({
+ registerTransform: function(attributeType, transform) {
+ var registeredTransforms = this._registeredTransforms || {};
+
+ registeredTransforms[attributeType] = transform;
+
+ this._registeredTransforms = registeredTransforms;
+ },
+
+ map: DS._Mappable.generateMapFunctionFor('attributes', function(key, newValue, map) {
+ var existingValue = map.get(key);
+
+ merge(existingValue, newValue);
+ }),
+
+ configure: DS._Mappable.generateMapFunctionFor('configurations', function(key, newValue, map) {
+ var existingValue = map.get(key);
+
+ // If a mapping configuration is provided, peel it off and apply it
+ // using the DS.Adapter.map API.
+ var mappings = newValue && newValue.mappings;
+ if (mappings) {
+ this.map(key, mappings);
+ delete newValue.mappings;
+ }
+
+ merge(existingValue, newValue);
+ }),
+
+ resolveMapConflict: function(oldValue, newValue, mappingsKey) {
+ merge(newValue, oldValue);
+
+ return newValue;
+ }
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get;
+
+DS.FixtureAdapter = DS.Adapter.extend({
+
+ simulateRemoteResponse: true,
+
+ latency: 50,
+
+ /*
+ Implement this method in order to provide data associated with a type
+ */
+ fixturesForType: function(type) {
+ if (type.FIXTURES) {
+ var fixtures = Ember.A(type.FIXTURES);
+ return fixtures.map(function(fixture){
+ if(!fixture.id){
+ throw new Error('the id property must be defined for fixture %@'.fmt(fixture));
+ }
+ fixture.id = fixture.id + '';
+ return fixture;
+ });
+ }
+ return null;
+ },
+
+ /*
+ Implement this method in order to query fixtures data
+ */
+ queryFixtures: function(fixtures, query, type) {
+ return fixtures;
+ },
+
+ /*
+ Implement this method in order to provide provide json for CRUD methods
+ */
+ mockJSON: function(type, record) {
+ return this.serialize(record, { includeId: true });
+ },
+
+ /*
+ Adapter methods
+ */
+ generateIdForRecord: function(store, record) {
+ return Ember.guidFor(record);
+ },
+
+ find: function(store, type, id) {
+ var fixtures = this.fixturesForType(type);
+
+ Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures);
+
+ if (fixtures) {
+ fixtures = fixtures.findProperty('id', id);
+ }
+
+ if (fixtures) {
+ this.simulateRemoteCall(function() {
+ store.load(type, fixtures);
+ }, store, type);
+ }
+ },
+
+ findMany: function(store, type, ids) {
+ var fixtures = this.fixturesForType(type);
+
+ Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures);
+
+ if (fixtures) {
+ fixtures = fixtures.filter(function(item) {
+ return ids.indexOf(item.id) !== -1;
+ });
+ }
+
+ if (fixtures) {
+ this.simulateRemoteCall(function() {
+ store.loadMany(type, fixtures);
+ }, store, type);
+ }
+ },
+
+ findAll: function(store, type) {
+ var fixtures = this.fixturesForType(type);
+
+ Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures);
+
+ this.simulateRemoteCall(function() {
+ store.loadMany(type, fixtures);
+ store.didUpdateAll(type);
+ }, store, type);
+ },
+
+ findQuery: function(store, type, query, array) {
+ var fixtures = this.fixturesForType(type);
+
+ Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures);
+
+ fixtures = this.queryFixtures(fixtures, query, type);
+
+ if (fixtures) {
+ this.simulateRemoteCall(function() {
+ array.load(fixtures);
+ }, store, type);
+ }
+ },
+
+ createRecord: function(store, type, record) {
+ var fixture = this.mockJSON(type, record);
+
+ fixture.id = this.generateIdForRecord(store, record);
+
+ this.simulateRemoteCall(function() {
+ store.didSaveRecord(record, fixture);
+ }, store, type, record);
+ },
+
+ updateRecord: function(store, type, record) {
+ var fixture = this.mockJSON(type, record);
+
+ this.simulateRemoteCall(function() {
+ store.didSaveRecord(record, fixture);
+ }, store, type, record);
+ },
+
+ deleteRecord: function(store, type, record) {
+ this.simulateRemoteCall(function() {
+ store.didSaveRecord(record);
+ }, store, type, record);
+ },
+
+ /*
+ @private
+ */
+ simulateRemoteCall: function(callback, store, type, record) {
+ if (get(this, 'simulateRemoteResponse')) {
+ setTimeout(callback, get(this, 'latency'));
+ } else {
+ callback();
+ }
+ }
+});
+
+})();
+
+
+
+(function() {
+DS.RESTSerializer = DS.JSONSerializer.extend({
+ keyForAttributeName: function(type, name) {
+ return Ember.String.decamelize(name);
+ },
+
+ keyForBelongsTo: function(type, name) {
+ var key = this.keyForAttributeName(type, name);
+
+ if (this.embeddedType(type, name)) {
+ return key;
+ }
+
+ return key + "_id";
+ }
+});
+
+})();
+
+
+
+(function() {
+/*global jQuery*/
+
+var get = Ember.get, set = Ember.set, merge = Ember.merge;
+
+/**
+ The REST adapter allows your store to communicate with an HTTP server by
+ transmitting JSON via XHR. Most Ember.js apps that consume a JSON API
+ should use the REST adapter.
+
+ This adapter is designed around the idea that the JSON exchanged with
+ the server should be conventional.
+
+ ## JSON Structure
+
+ The REST adapter expects the JSON returned from your server to follow
+ these conventions.
+
+ ### Object Root
+
+ The JSON payload should be an object that contains the record inside a
+ root property. For example, in response to a `GET` request for
+ `/posts/1`, the JSON should look like this:
+
+ ```js
+ {
+ "post": {
+ title: "I'm Running to Reform the W3C's Tag",
+ author: "Yehuda Katz"
+ }
+ }
+ ```
+
+ ### Conventional Names
+
+ Attribute names in your JSON payload should be the underscored versions of
+ the attributes in your Ember.js models.
+
+ For example, if you have a `Person` model:
+
+ ```js
+ App.Person = DS.Model.extend({
+ firstName: DS.attr('string'),
+ lastName: DS.attr('string'),
+ occupation: DS.attr('string')
+ });
+ ```
+
+ The JSON returned should look like this:
+
+ ```js
+ {
+ "person": {
+ "first_name": "Barack",
+ "last_name": "Obama",
+ "occupation": "President"
+ }
+ }
+ ```
+*/
+DS.RESTAdapter = DS.Adapter.extend({
+ bulkCommit: false,
+ since: 'since',
+
+ serializer: DS.RESTSerializer,
+
+ init: function() {
+ this._super.apply(this, arguments);
+ },
+
+ shouldSave: function(record) {
+ var reference = get(record, '_reference');
+
+ 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) {
+ dirtySet.add(record);
+
+ get(this, 'serializer').eachEmbeddedRecord(record, function(embeddedRecord, embeddedType) {
+ if (embeddedType !== 'always') { return; }
+ if (dirtySet.has(embeddedRecord)) { return; }
+ this.dirtyRecordsForRecordChange(dirtySet, embeddedRecord);
+ }, this);
+
+ var reference = record.get('_reference');
+
+ if (reference.parent) {
+ var store = get(record, 'store');
+ var parent = store.recordForReference(reference.parent);
+ this.dirtyRecordsForRecordChange(dirtySet, parent);
+ }
+ },
+
+ dirtyRecordsForHasManyChange: Ember.K,
+
+ createRecords: function(store, type, records) {
+ if (get(this, 'bulkCommit') === false) {
+ return this._super(store, type, records);
+ }
+
+ var root = this.rootForType(type),
+ plural = this.pluralize(root);
+
+ var data = {};
+ data[plural] = [];
+ records.forEach(function(record) {
+ 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);
+ });
+ }
+ });
+ },
+
+ updateRecord: function(store, type, record) {
+ var id = get(record, 'id');
+ var root = this.rootForType(type);
+
+ var 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);
+ }
+ });
+ },
+
+ updateRecords: function(store, type, records) {
+ if (get(this, 'bulkCommit') === false) {
+ return this._super(store, type, records);
+ }
+
+ var root = this.rootForType(type),
+ plural = this.pluralize(root);
+
+ 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);
+ });
+ }
+ });
+ },
+
+ deleteRecord: function(store, type, record) {
+ var id = get(record, 'id');
+ var root = this.rootForType(type);
+
+ this.ajax(this.buildURL(root, id), "DELETE", {
+ context: this,
+ success: function(json) {
+ Ember.run(this, function(){
+ this.didSaveRecord(store, type, record, json);
+ });
+ }
+ });
+ },
+
+ deleteRecords: function(store, type, records) {
+ if (get(this, 'bulkCommit') === false) {
+ return this._super(store, type, records);
+ }
+
+ var root = this.rootForType(type),
+ plural = this.pluralize(root),
+ serializer = get(this, 'serializer');
+
+ 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);
+ });
+ }
+ });
+ },
+
+ find: function(store, type, id) {
+ var root = this.rootForType(type);
+
+ this.ajax(this.buildURL(root, id), "GET", {
+ success: function(json) {
+ Ember.run(this, function(){
+ this.didFindRecord(store, type, json, id);
+ });
+ }
+ });
+ },
+
+ findAll: function(store, type, since) {
+ var root = this.rootForType(type);
+
+ this.ajax(this.buildURL(root), "GET", {
+ data: this.sinceQuery(since),
+ success: function(json) {
+ Ember.run(this, function(){
+ this.didFindAll(store, type, json);
+ });
+ }
+ });
+ },
+
+ findQuery: function(store, type, query, recordArray) {
+ var root = this.rootForType(type);
+
+ this.ajax(this.buildURL(root), "GET", {
+ data: query,
+ success: function(json) {
+ Ember.run(this, function(){
+ this.didFindQuery(store, type, json, recordArray);
+ });
+ }
+ });
+ },
+
+ findMany: function(store, type, ids, owner) {
+ var root = this.rootForType(type);
+ 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);
+ });
+ }
+ });
+ },
+
+ /**
+ @private
+
+ This method serializes a list of IDs using `serializeId`
+
+ @returns {Array} an array of serialized IDs
+ */
+ serializeIds: function(ids) {
+ var serializer = get(this, 'serializer');
+
+ return Ember.EnumerableUtils.map(ids, function(id) {
+ return serializer.serializeId(id);
+ });
+ },
+
+ didError: function(store, type, record, xhr) {
+ if (xhr.status === 422) {
+ var data = JSON.parse(xhr.responseText);
+ store.recordWasInvalid(record, data['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;
+
+ if (hash.data && type !== 'GET') {
+ hash.data = JSON.stringify(hash.data);
+ }
+
+ jQuery.ajax(hash);
+ },
+
+ url: "",
+
+ rootForType: function(type) {
+ var serializer = get(this, 'serializer');
+ return serializer.rootForType(type);
+ },
+
+ pluralize: function(string) {
+ var serializer = get(this, 'serializer');
+ return serializer.pluralize(string);
+ },
+
+ buildURL: function(record, suffix) {
+ var url = [this.url];
+
+ Ember.assert("Namespace URL (" + this.namespace + ") must not start with slash", !this.namespace || this.namespace.toString().charAt(0) !== "/");
+ 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) {
+ url.push(this.namespace);
+ }
+
+ url.push(this.pluralize(record));
+ if (suffix !== undefined) {
+ url.push(suffix);
+ }
+
+ return url.join("/");
+ },
+
+ sinceQuery: function(since) {
+ var query = {};
+ query[get(this, 'since')] = since;
+ return since ? query : null;
+ }
+});
+
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(function() {
+//Copyright (C) 2011 by Living Social, Inc.
+
+//Permission is hereby granted, free of charge, to any person obtaining a copy of
+//this software and associated documentation files (the "Software"), to deal in
+//the Software without restriction, including without limitation the rights to
+//use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+//of the Software, and to permit persons to whom the Software is furnished to do
+//so, subject to the following conditions:
+
+//The above copyright notice and this permission notice shall be included in all
+//copies or substantial portions of the Software.
+
+//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+//SOFTWARE.
+
+})();
+
diff --git a/public/js/libs/ember.js b/public/js/libs/ember.js
new file mode 100644
index 0000000..e566b10
--- /dev/null
+++ b/public/js/libs/ember.js
@@ -0,0 +1,26839 @@
+// Version: v1.0.0-rc.1
+// Last commit: 8b061b4 (2013-02-15 12:10:22 -0800)
+
+
+(function() {
+/*global __fail__*/
+
+/**
+Ember Debug
+
+@module ember
+@submodule ember-debug
+*/
+
+/**
+@class Ember
+*/
+
+if ('undefined' === typeof Ember) {
+ Ember = {};
+
+ if ('undefined' !== typeof window) {
+ window.Em = window.Ember = Em = Ember;
+ }
+}
+
+Ember.ENV = 'undefined' === typeof ENV ? {} : ENV;
+
+if (!('MANDATORY_SETTER' in Ember.ENV)) {
+ Ember.ENV.MANDATORY_SETTER = true; // default to true for debug dist
+}
+
+/**
+ Define an assertion that will throw an exception if the condition is not
+ met. Ember build tools will remove any calls to `Ember.assert()` when
+ doing a production build. Example:
+
+ ```javascript
+ // Test for truthiness
+ Ember.assert('Must pass a valid object', obj);
+ // Fail unconditionally
+ Ember.assert('This code path should never be run')
+ ```
+
+ @method assert
+ @param {String} desc A description of the assertion. This will become
+ the text of the Error thrown if the assertion fails.
+ @param {Boolean} test Must be truthy for the assertion to pass. If
+ falsy, an exception will be thrown.
+*/
+Ember.assert = function(desc, test) {
+ if (!test) throw new Error("assertion failed: "+desc);
+};
+
+
+/**
+ Display a warning with the provided message. Ember build tools will
+ remove any calls to `Ember.warn()` when doing a production build.
+
+ @method warn
+ @param {String} message A warning to display.
+ @param {Boolean} test An optional boolean. If falsy, the warning
+ will be displayed.
+*/
+Ember.warn = function(message, test) {
+ if (!test) {
+ Ember.Logger.warn("WARNING: "+message);
+ if ('trace' in Ember.Logger) Ember.Logger.trace();
+ }
+};
+
+/**
+ Display a debug notice. Ember build tools will remove any calls to
+ `Ember.debug()` when doing a production build.
+
+ ```javascript
+ Ember.debug("I'm a debug notice!");
+ ```
+
+ @method debug
+ @param {String} message A debug message to display.
+*/
+Ember.debug = function(message) {
+ Ember.Logger.debug("DEBUG: "+message);
+};
+
+/**
+ Display a deprecation warning with the provided message and a stack trace
+ (Chrome and Firefox only). Ember build tools will remove any calls to
+ `Ember.deprecate()` when doing a production build.
+
+ @method deprecate
+ @param {String} message A description of the deprecation.
+ @param {Boolean} test An optional boolean. If falsy, the deprecation
+ will be displayed.
+*/
+Ember.deprecate = function(message, test) {
+ if (Ember && Ember.TESTING_DEPRECATION) { return; }
+
+ if (arguments.length === 1) { test = false; }
+ if (test) { return; }
+
+ if (Ember && Ember.ENV.RAISE_ON_DEPRECATION) { throw new Error(message); }
+
+ var error;
+
+ // When using new Error, we can't do the arguments check for Chrome. Alternatives are welcome
+ try { __fail__.fail(); } catch (e) { error = e; }
+
+ if (Ember.LOG_STACKTRACE_ON_DEPRECATION && error.stack) {
+ var stack, stackStr = '';
+ if (error['arguments']) {
+ // Chrome
+ stack = error.stack.replace(/^\s+at\s+/gm, '').
+ replace(/^([^\(]+?)([\n$])/gm, '{anonymous}($1)$2').
+ replace(/^Object.<anonymous>\s*\(([^\)]+)\)/gm, '{anonymous}($1)').split('\n');
+ stack.shift();
+ } else {
+ // Firefox
+ stack = error.stack.replace(/(?:\n@:0)?\s+$/m, '').
+ replace(/^\(/gm, '{anonymous}(').split('\n');
+ }
+
+ stackStr = "\n " + stack.slice(2).join("\n ");
+ message = message + stackStr;
+ }
+
+ Ember.Logger.warn("DEPRECATION: "+message);
+};
+
+
+
+/**
+ Display a deprecation warning with the provided message and a stack trace
+ (Chrome and Firefox only) when the wrapped method is called.
+
+ Ember build tools will not remove calls to `Ember.deprecateFunc()`, though
+ no warnings will be shown in production.
+
+ @method deprecateFunc
+ @param {String} message A description of the deprecation.
+ @param {Function} func The function to be deprecated.
+*/
+Ember.deprecateFunc = function(message, func) {
+ return function() {
+ Ember.deprecate(message);
+ return func.apply(this, arguments);
+ };
+};
+
+})();
+
+// Version: v1.0.0-rc.1
+// Last commit: 8b061b4 (2013-02-15 12:10:22 -0800)
+
+
+(function() {
+var define, requireModule;
+
+(function() {
+ var registry = {}, seen = {};
+
+ define = function(name, deps, callback) {
+ registry[name] = { deps: deps, callback: callback };
+ };
+
+ requireModule = function(name) {
+ if (seen[name]) { return seen[name]; }
+ seen[name] = {};
+
+ var mod = registry[name],
+ deps = mod.deps,
+ callback = mod.callback,
+ reified = [],
+ exports;
+
+ for (var i=0, l=deps.length; i<l; i++) {
+ if (deps[i] === 'exports') {
+ reified.push(exports = {});
+ } else {
+ reified.push(requireModule(deps[i]));
+ }
+ }
+
+ var value = callback.apply(this, reified);
+ return seen[name] = exports || value;
+ };
+})();
+(function() {
+/*globals Em:true ENV */
+
+/**
+@module ember
+@submodule ember-metal
+*/
+
+/**
+ All Ember methods and functions are defined inside of this namespace. You
+ generally should not add new properties to this namespace as it may be
+ overwritten by future versions of Ember.
+
+ You can also use the shorthand `Em` instead of `Ember`.
+
+ Ember-Runtime is a framework that provides core functions for Ember including
+ cross-platform functions, support for property observing and objects. Its
+ focus is on small size and performance. You can use this in place of or
+ along-side other cross-platform libraries such as jQuery.
+
+ The core Runtime framework is based on the jQuery API with a number of
+ performance optimizations.
+
+ @class Ember
+ @static
+ @version 1.0.0-rc.1
+*/
+
+if ('undefined' === typeof Ember) {
+ // Create core object. Make it act like an instance of Ember.Namespace so that
+ // objects assigned to it are given a sane string representation.
+ Ember = {};
+}
+
+// Default imports, exports and lookup to the global object;
+var imports = Ember.imports = Ember.imports || this;
+var exports = Ember.exports = Ember.exports || this;
+var lookup = Ember.lookup = Ember.lookup || this;
+
+// aliases needed to keep minifiers from removing the global context
+exports.Em = exports.Ember = Em = Ember;
+
+// Make sure these are set whether Ember was already defined or not
+
+Ember.isNamespace = true;
+
+Ember.toString = function() { return "Ember"; };
+
+
+/**
+ @property VERSION
+ @type String
+ @default '1.0.0-rc.1'
+ @final
+*/
+Ember.VERSION = '1.0.0-rc.1';
+
+/**
+ Standard environmental variables. You can define these in a global `ENV`
+ variable before loading Ember to control various configuration
+ settings.
+
+ @property ENV
+ @type Hash
+*/
+Ember.ENV = Ember.ENV || ('undefined' === typeof ENV ? {} : ENV);
+
+Ember.config = Ember.config || {};
+
+// ..........................................................
+// BOOTSTRAP
+//
+
+/**
+ Determines whether Ember should enhances some built-in object prototypes to
+ provide a more friendly API. If enabled, a few methods will be added to
+ `Function`, `String`, and `Array`. `Object.prototype` will not be enhanced,
+ which is the one that causes most trouble for people.
+
+ In general we recommend leaving this option set to true since it rarely
+ conflicts with other code. If you need to turn it off however, you can
+ define an `ENV.EXTEND_PROTOTYPES` config to disable it.
+
+ @property EXTEND_PROTOTYPES
+ @type Boolean
+ @default true
+*/
+Ember.EXTEND_PROTOTYPES = Ember.ENV.EXTEND_PROTOTYPES;
+
+if (typeof Ember.EXTEND_PROTOTYPES === 'undefined') {
+ Ember.EXTEND_PROTOTYPES = true;
+}
+
+/**
+ Determines whether Ember logs a full stack trace during deprecation warnings
+
+ @property LOG_STACKTRACE_ON_DEPRECATION
+ @type Boolean
+ @default true
+*/
+Ember.LOG_STACKTRACE_ON_DEPRECATION = (Ember.ENV.LOG_STACKTRACE_ON_DEPRECATION !== false);
+
+/**
+ Determines whether Ember should add ECMAScript 5 shims to older browsers.
+
+ @property SHIM_ES5
+ @type Boolean
+ @default Ember.EXTEND_PROTOTYPES
+*/
+Ember.SHIM_ES5 = (Ember.ENV.SHIM_ES5 === false) ? false : Ember.EXTEND_PROTOTYPES;
+
+/**
+ Empty function. Useful for some operations.
+
+ @method K
+ @private
+ @return {Object}
+*/
+Ember.K = function() { return this; };
+
+
+// Stub out the methods defined by the ember-debug package in case it's not loaded
+
+if ('undefined' === typeof Ember.assert) { Ember.assert = Ember.K; }
+if ('undefined' === typeof Ember.warn) { Ember.warn = Ember.K; }
+if ('undefined' === typeof Ember.debug) { Ember.debug = Ember.K; }
+if ('undefined' === typeof Ember.deprecate) { Ember.deprecate = Ember.K; }
+if ('undefined' === typeof Ember.deprecateFunc) {
+ Ember.deprecateFunc = function(_, func) { return func; };
+}
+
+/**
+ Previously we used `Ember.$.uuid`, however `$.uuid` has been removed from
+ jQuery master. We'll just bootstrap our own uuid now.
+
+ @property uuid
+ @type Number
+ @private
+*/
+Ember.uuid = 0;
+
+// ..........................................................
+// LOGGER
+//
+
+function consoleMethod(name) {
+ if (imports.console && imports.console[name]) {
+ // Older IE doesn't support apply, but Chrome needs it
+ if (imports.console[name].apply) {
+ return function() {
+ imports.console[name].apply(imports.console, arguments);
+ };
+ } else {
+ return function() {
+ var message = Array.prototype.join.call(arguments, ', ');
+ imports.console[name](message);
+ };
+ }
+ }
+}
+
+/**
+ Inside Ember-Metal, simply uses the methods from `imports.console`.
+ Override this to provide more robust logging functionality.
+
+ @class Logger
+ @namespace Ember
+*/
+Ember.Logger = {
+ log: consoleMethod('log') || Ember.K,
+ warn: consoleMethod('warn') || Ember.K,
+ error: consoleMethod('error') || Ember.K,
+ info: consoleMethod('info') || Ember.K,
+ debug: consoleMethod('debug') || consoleMethod('info') || Ember.K
+};
+
+
+// ..........................................................
+// ERROR HANDLING
+//
+
+/**
+ A function may be assigned to `Ember.onerror` to be called when Ember
+ internals encounter an error. This is useful for specialized error handling
+ and reporting code.
+
+ @event onerror
+ @for Ember
+ @param {Exception} error the error object
+*/
+Ember.onerror = null;
+
+/**
+ @private
+
+ Wrap code block in a try/catch if {{#crossLink "Ember/onerror"}}{{/crossLink}} is set.
+
+ @method handleErrors
+ @for Ember
+ @param {Function} func
+ @param [context]
+*/
+Ember.handleErrors = function(func, context) {
+ // Unfortunately in some browsers we lose the backtrace if we rethrow the existing error,
+ // so in the event that we don't have an `onerror` handler we don't wrap in a try/catch
+ if ('function' === typeof Ember.onerror) {
+ try {
+ return func.apply(context || this);
+ } catch (error) {
+ Ember.onerror(error);
+ }
+ } else {
+ return func.apply(context || this);
+ }
+};
+
+Ember.merge = function(original, updates) {
+ for (var prop in updates) {
+ if (!updates.hasOwnProperty(prop)) { continue; }
+ original[prop] = updates[prop];
+ }
+};
+
+})();
+
+
+
+(function() {
+/*globals Node */
+/**
+@module ember-metal
+*/
+
+/**
+ Platform specific methods and feature detectors needed by the framework.
+
+ @class platform
+ @namespace Ember
+ @static
+*/
+var platform = Ember.platform = {};
+
+
+/**
+ Identical to `Object.create()`. Implements if not available natively.
+
+ @method create
+ @for Ember
+*/
+Ember.create = Object.create;
+
+// STUB_OBJECT_CREATE allows us to override other libraries that stub
+// Object.create different than we would prefer
+if (!Ember.create || Ember.ENV.STUB_OBJECT_CREATE) {
+ var K = function() {};
+
+ Ember.create = function(obj, props) {
+ K.prototype = obj;
+ obj = new K();
+ if (props) {
+ K.prototype = obj;
+ for (var prop in props) {
+ K.prototype[prop] = props[prop].value;
+ }
+ obj = new K();
+ }
+ K.prototype = null;
+
+ return obj;
+ };
+
+ Ember.create.isSimulated = true;
+}
+
+var defineProperty = Object.defineProperty;
+var canRedefineProperties, canDefinePropertyOnDOM;
+
+// Catch IE8 where Object.defineProperty exists but only works on DOM elements
+if (defineProperty) {
+ try {
+ defineProperty({}, 'a',{get:function(){}});
+ } catch (e) {
+ defineProperty = null;
+ }
+}
+
+if (defineProperty) {
+ // Detects a bug in Android <3.2 where you cannot redefine a property using
+ // Object.defineProperty once accessors have already been set.
+ canRedefineProperties = (function() {
+ var obj = {};
+
+ defineProperty(obj, 'a', {
+ configurable: true,
+ enumerable: true,
+ get: function() { },
+ set: function() { }
+ });
+
+ defineProperty(obj, 'a', {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: true
+ });
+
+ return obj.a === true;
+ })();
+
+ // This is for Safari 5.0, which supports Object.defineProperty, but not
+ // on DOM nodes.
+ canDefinePropertyOnDOM = (function(){
+ try {
+ defineProperty(document.createElement('div'), 'definePropertyOnDOM', {});
+ return true;
+ } catch(e) { }
+
+ return false;
+ })();
+
+ if (!canRedefineProperties) {
+ defineProperty = null;
+ } else if (!canDefinePropertyOnDOM) {
+ defineProperty = function(obj, keyName, desc){
+ var isNode;
+
+ if (typeof Node === "object") {
+ isNode = obj instanceof Node;
+ } else {
+ isNode = typeof obj === "object" && typeof obj.nodeType === "number" && typeof obj.nodeName === "string";
+ }
+
+ if (isNode) {
+ // TODO: Should we have a warning here?
+ return (obj[keyName] = desc.value);
+ } else {
+ return Object.defineProperty(obj, keyName, desc);
+ }
+ };
+ }
+}
+
+/**
+@class platform
+@namespace Ember
+*/
+
+/**
+ Identical to `Object.defineProperty()`. Implements as much functionality
+ as possible if not available natively.
+
+ @method defineProperty
+ @param {Object} obj The object to modify
+ @param {String} keyName property name to modify
+ @param {Object} desc descriptor hash
+ @return {void}
+*/
+platform.defineProperty = defineProperty;
+
+/**
+ Set to true if the platform supports native getters and setters.
+
+ @property hasPropertyAccessors
+ @final
+*/
+platform.hasPropertyAccessors = true;
+
+if (!platform.defineProperty) {
+ platform.hasPropertyAccessors = false;
+
+ platform.defineProperty = function(obj, keyName, desc) {
+ if (!desc.get) { obj[keyName] = desc.value; }
+ };
+
+ platform.defineProperty.isSimulated = true;
+}
+
+if (Ember.ENV.MANDATORY_SETTER && !platform.hasPropertyAccessors) {
+ Ember.ENV.MANDATORY_SETTER = false;
+}
+
+})();
+
+
+
+(function() {
+/**
+@module ember-metal
+*/
+
+
+var o_defineProperty = Ember.platform.defineProperty,
+ o_create = Ember.create,
+ // Used for guid generation...
+ GUID_KEY = '__ember'+ (+ new Date()),
+ uuid = 0,
+ numberCache = [],
+ stringCache = {};
+
+var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER;
+
+/**
+ @private
+
+ A unique key used to assign guids and other private metadata to objects.
+ If you inspect an object in your browser debugger you will often see these.
+ They can be safely ignored.
+
+ On browsers that support it, these properties are added with enumeration
+ disabled so they won't show up when you iterate over your properties.
+
+ @property GUID_KEY
+ @for Ember
+ @type String
+ @final
+*/
+Ember.GUID_KEY = GUID_KEY;
+
+var GUID_DESC = {
+ writable: false,
+ configurable: false,
+ enumerable: false,
+ value: null
+};
+
+/**
+ @private
+
+ Generates a new guid, optionally saving the guid to the object that you
+ pass in. You will rarely need to use this method. Instead you should
+ call `Ember.guidFor(obj)`, which return an existing guid if available.
+
+ @method generateGuid
+ @for Ember
+ @param {Object} [obj] Object the guid will be used for. If passed in, the guid will
+ be saved on the object and reused whenever you pass the same object
+ again.
+
+ If no object is passed, just generate a new guid.
+ @param {String} [prefix] Prefix to place in front of the guid. Useful when you want to
+ separate the guid into separate namespaces.
+ @return {String} the guid
+*/
+Ember.generateGuid = function generateGuid(obj, prefix) {
+ if (!prefix) prefix = 'ember';
+ var ret = (prefix + (uuid++));
+ if (obj) {
+ GUID_DESC.value = ret;
+ o_defineProperty(obj, GUID_KEY, GUID_DESC);
+ }
+ return ret ;
+};
+
+/**
+ @private
+
+ Returns a unique id for the object. If the object does not yet have a guid,
+ one will be assigned to it. You can call this on any object,
+ `Ember.Object`-based or not, but be aware that it will add a `_guid`
+ property.
+
+ You can also use this method on DOM Element objects.
+
+ @method guidFor
+ @for Ember
+ @param obj {Object} any object, string, number, Element, or primitive
+ @return {String} the unique guid for this instance.
+*/
+Ember.guidFor = function guidFor(obj) {
+
+ // special cases where we don't want to add a key to object
+ if (obj === undefined) return "(undefined)";
+ if (obj === null) return "(null)";
+
+ var cache, ret;
+ var type = typeof obj;
+
+ // Don't allow prototype changes to String etc. to change the guidFor
+ switch(type) {
+ case 'number':
+ ret = numberCache[obj];
+ if (!ret) ret = numberCache[obj] = 'nu'+obj;
+ return ret;
+
+ case 'string':
+ ret = stringCache[obj];
+ if (!ret) ret = stringCache[obj] = 'st'+(uuid++);
+ return ret;
+
+ case 'boolean':
+ return obj ? '(true)' : '(false)';
+
+ default:
+ if (obj[GUID_KEY]) return obj[GUID_KEY];
+ if (obj === Object) return '(Object)';
+ if (obj === Array) return '(Array)';
+ ret = 'ember'+(uuid++);
+ GUID_DESC.value = ret;
+ o_defineProperty(obj, GUID_KEY, GUID_DESC);
+ return ret;
+ }
+};
+
+// ..........................................................
+// META
+//
+
+var META_DESC = {
+ writable: true,
+ configurable: false,
+ enumerable: false,
+ value: null
+};
+
+var META_KEY = Ember.GUID_KEY+'_meta';
+
+/**
+ The key used to store meta information on object for property observing.
+
+ @property META_KEY
+ @for Ember
+ @private
+ @final
+ @type String
+*/
+Ember.META_KEY = META_KEY;
+
+// Placeholder for non-writable metas.
+var EMPTY_META = {
+ descs: {},
+ watching: {}
+};
+
+if (MANDATORY_SETTER) { EMPTY_META.values = {}; }
+
+Ember.EMPTY_META = EMPTY_META;
+
+if (Object.freeze) Object.freeze(EMPTY_META);
+
+var isDefinePropertySimulated = Ember.platform.defineProperty.isSimulated;
+
+function Meta(obj) {
+ this.descs = {};
+ this.watching = {};
+ this.cache = {};
+ this.source = obj;
+}
+
+if (isDefinePropertySimulated) {
+ // on platforms that don't support enumerable false
+ // make meta fail jQuery.isPlainObject() to hide from
+ // jQuery.extend() by having a property that fails
+ // hasOwnProperty check.
+ Meta.prototype.__preventPlainObject__ = true;
+
+ // Without non-enumerable properties, meta objects will be output in JSON
+ // unless explicitly suppressed
+ Meta.prototype.toJSON = function () { };
+}
+
+/**
+ Retrieves the meta hash for an object. If `writable` is true ensures the
+ hash is writable for this object as well.
+
+ The meta object contains information about computed property descriptors as
+ well as any watched properties and other information. You generally will
+ not access this information directly but instead work with higher level
+ methods that manipulate this hash indirectly.
+
+ @method meta
+ @for Ember
+ @private
+
+ @param {Object} obj The object to retrieve meta for
+ @param {Boolean} [writable=true] Pass `false` if you do not intend to modify
+ the meta hash, allowing the method to avoid making an unnecessary copy.
+ @return {Hash}
+*/
+Ember.meta = function meta(obj, writable) {
+
+ var ret = obj[META_KEY];
+ if (writable===false) return ret || EMPTY_META;
+
+ if (!ret) {
+ if (!isDefinePropertySimulated) o_defineProperty(obj, META_KEY, META_DESC);
+
+ ret = new Meta(obj);
+
+ if (MANDATORY_SETTER) { ret.values = {}; }
+
+ obj[META_KEY] = ret;
+
+ // make sure we don't accidentally try to create constructor like desc
+ ret.descs.constructor = null;
+
+ } else if (ret.source !== obj) {
+ if (!isDefinePropertySimulated) o_defineProperty(obj, META_KEY, META_DESC);
+
+ ret = o_create(ret);
+ ret.descs = o_create(ret.descs);
+ ret.watching = o_create(ret.watching);
+ ret.cache = {};
+ ret.source = obj;
+
+ if (MANDATORY_SETTER) { ret.values = o_create(ret.values); }
+
+ obj[META_KEY] = ret;
+ }
+ return ret;
+};
+
+Ember.getMeta = function getMeta(obj, property) {
+ var meta = Ember.meta(obj, false);
+ return meta[property];
+};
+
+Ember.setMeta = function setMeta(obj, property, value) {
+ var meta = Ember.meta(obj, true);
+ meta[property] = value;
+ return value;
+};
+
+/**
+ @private
+
+ In order to store defaults for a class, a prototype may need to create
+ a default meta object, which will be inherited by any objects instantiated
+ from the class's constructor.
+
+ However, the properties of that meta object are only shallow-cloned,
+ so if a property is a hash (like the event system's `listeners` hash),
+ it will by default be shared across all instances of that class.
+
+ This method allows extensions to deeply clone a series of nested hashes or
+ other complex objects. For instance, the event system might pass
+ `['listeners', 'foo:change', 'ember157']` to `prepareMetaPath`, which will
+ walk down the keys provided.
+
+ For each key, if the key does not exist, it is created. If it already
+ exists and it was inherited from its constructor, the constructor's
+ key is cloned.
+
+ You can also pass false for `writable`, which will simply return
+ undefined if `prepareMetaPath` discovers any part of the path that
+ shared or undefined.
+
+ @method metaPath
+ @for Ember
+ @param {Object} obj The object whose meta we are examining
+ @param {Array} path An array of keys to walk down
+ @param {Boolean} writable whether or not to create a new meta
+ (or meta property) if one does not already exist or if it's
+ shared with its constructor
+*/
+Ember.metaPath = function metaPath(obj, path, writable) {
+ var meta = Ember.meta(obj, writable), keyName, value;
+
+ for (var i=0, l=path.length; i<l; i++) {
+ keyName = path[i];
+ value = meta[keyName];
+
+ if (!value) {
+ if (!writable) { return undefined; }
+ value = meta[keyName] = { __ember_source__: obj };
+ } else if (value.__ember_source__ !== obj) {
+ if (!writable) { return undefined; }
+ value = meta[keyName] = o_create(value);
+ value.__ember_source__ = obj;
+ }
+
+ meta = value;
+ }
+
+ return value;
+};
+
+/**
+ @private
+
+ Wraps the passed function so that `this._super` will point to the superFunc
+ when the function is invoked. This is the primitive we use to implement
+ calls to super.
+
+ @method wrap
+ @for Ember
+ @param {Function} func The function to call
+ @param {Function} superFunc The super function.
+ @return {Function} wrapped function.
+*/
+Ember.wrap = function(func, superFunc) {
+ function K() {}
+
+ function superWrapper() {
+ var ret, sup = this._super;
+ this._super = superFunc || K;
+ ret = func.apply(this, arguments);
+ this._super = sup;
+ return ret;
+ }
+
+ superWrapper.wrappedFunction = func;
+ superWrapper.__ember_observes__ = func.__ember_observes__;
+ superWrapper.__ember_observesBefore__ = func.__ember_observesBefore__;
+
+ return superWrapper;
+};
+
+/**
+ Returns true if the passed object is an array or Array-like.
+
+ Ember Array Protocol:
+
+ - the object has an objectAt property
+ - the object is a native Array
+ - the object is an Object, and has a length property
+
+ Unlike `Ember.typeOf` this method returns true even if the passed object is
+ not formally array but appears to be array-like (i.e. implements `Ember.Array`)
+
+ ```javascript
+ Ember.isArray(); // false
+ Ember.isArray([]); // true
+ Ember.isArray( Ember.ArrayProxy.create({ content: [] }) ); // true
+ ```
+
+ @method isArray
+ @for Ember
+ @param {Object} obj The object to test
+ @return {Boolean}
+*/
+Ember.isArray = function(obj) {
+ if (!obj || obj.setInterval) { return false; }
+ if (Array.isArray && Array.isArray(obj)) { return true; }
+ if (Ember.Array && Ember.Array.detect(obj)) { return true; }
+ if ((obj.length !== undefined) && 'object'===typeof obj) { return true; }
+ return false;
+};
+
+/**
+ Forces the passed object to be part of an array. If the object is already
+ an array or array-like, returns the object. Otherwise adds the object to
+ an array. If obj is `null` or `undefined`, returns an empty array.
+
+ ```javascript
+ Ember.makeArray(); // []
+ Ember.makeArray(null); // []
+ Ember.makeArray(undefined); // []
+ Ember.makeArray('lindsay'); // ['lindsay']
+ Ember.makeArray([1,2,42]); // [1,2,42]
+
+ var controller = Ember.ArrayProxy.create({ content: [] });
+ Ember.makeArray(controller) === controller; // true
+ ```
+
+ @method makeArray
+ @for Ember
+ @param {Object} obj the object
+ @return {Array}
+*/
+Ember.makeArray = function(obj) {
+ if (obj === null || obj === undefined) { return []; }
+ return Ember.isArray(obj) ? obj : [obj];
+};
+
+function canInvoke(obj, methodName) {
+ return !!(obj && typeof obj[methodName] === 'function');
+}
+
+/**
+ Checks to see if the `methodName` exists on the `obj`.
+
+ @method canInvoke
+ @for Ember
+ @param {Object} obj The object to check for the method
+ @param {String} methodName The method name to check for
+*/
+Ember.canInvoke = canInvoke;
+
+/**
+ Checks to see if the `methodName` exists on the `obj`,
+ and if it does, invokes it with the arguments passed.
+
+ @method tryInvoke
+ @for Ember
+ @param {Object} obj The object to check for the method
+ @param {String} methodName The method name to check for
+ @param {Array} [args] The arguments to pass to the method
+ @return {anything} the return value of the invoked method or undefined if it cannot be invoked
+*/
+Ember.tryInvoke = function(obj, methodName, args) {
+ if (canInvoke(obj, methodName)) {
+ return obj[methodName].apply(obj, args || []);
+ }
+};
+
+// https://github.com/emberjs/ember.js/pull/1617
+var needsFinallyFix = (function() {
+ var count = 0;
+ try{
+ try { }
+ finally {
+ count++;
+ throw new Error('needsFinallyFixTest');
+ }
+ } catch (e) {}
+
+ return count !== 1;
+})();
+
+/**
+ Provides try { } finally { } functionality, while working
+ around Safari's double finally bug.
+
+ @method tryFinally
+ @for Ember
+ @param {Function} function The function to run the try callback
+ @param {Function} function The function to run the finally callback
+ @param [binding]
+ @return {anything} The return value is the that of the finalizer,
+ unless that valueis undefined, in which case it is the return value
+ of the tryable
+*/
+
+if (needsFinallyFix) {
+ Ember.tryFinally = function(tryable, finalizer, binding) {
+ var result, finalResult, finalError;
+
+ binding = binding || this;
+
+ try {
+ result = tryable.call(binding);
+ } finally {
+ try {
+ finalResult = finalizer.call(binding);
+ } catch (e){
+ finalError = e;
+ }
+ }
+
+ if (finalError) { throw finalError; }
+
+ return (finalResult === undefined) ? result : finalResult;
+ };
+} else {
+ Ember.tryFinally = function(tryable, finalizer, binding) {
+ var result, finalResult;
+
+ binding = binding || this;
+
+ try {
+ result = tryable.call(binding);
+ } finally {
+ finalResult = finalizer.call(binding);
+ }
+
+ return (finalResult === undefined) ? result : finalResult;
+ };
+}
+
+/**
+ Provides try { } catch finally { } functionality, while working
+ around Safari's double finally bug.
+
+ @method tryCatchFinally
+ @for Ember
+ @param {Function} function The function to run the try callback
+ @param {Function} function The function to run the catchable callback
+ @param {Function} function The function to run the finally callback
+ @param [binding]
+ @return {anything} The return value is the that of the finalizer,
+ unless that value is undefined, in which case it is the return value
+ of the tryable.
+*/
+if (needsFinallyFix) {
+ Ember.tryCatchFinally = function(tryable, catchable, finalizer, binding) {
+ var result, finalResult, finalError, finalReturn;
+
+ binding = binding || this;
+
+ try {
+ result = tryable.call(binding);
+ } catch(error) {
+ result = catchable.call(binding, error);
+ } finally {
+ try {
+ finalResult = finalizer.call(binding);
+ } catch (e){
+ finalError = e;
+ }
+ }
+
+ if (finalError) { throw finalError; }
+
+ return (finalResult === undefined) ? result : finalResult;
+ };
+} else {
+ Ember.tryCatchFinally = function(tryable, catchable, finalizer, binding) {
+ var result, finalResult;
+
+ binding = binding || this;
+
+ try {
+ result = tryable.call(binding);
+ } catch(error) {
+ result = catchable.call(binding, error);
+ } finally {
+ finalResult = finalizer.call(binding);
+ }
+
+ return (finalResult === undefined) ? result : finalResult;
+ };
+}
+
+})();
+
+
+
+(function() {
+// Ember.tryCatchFinally
+
+/**
+ The purpose of the Ember Instrumentation module is
+ to provide efficient, general-purpose instrumentation
+ for Ember.
+
+ Subscribe to a listener by using `Ember.subscribe`:
+
+ ```javascript
+ Ember.subscribe("render", {
+ before: function(name, timestamp, payload) {
+
+ },
+
+ after: function(name, timestamp, payload) {
+
+ }
+ });
+ ```
+
+ If you return a value from the `before` callback, that same
+ value will be passed as a fourth parameter to the `after`
+ callback.
+
+ Instrument a block of code by using `Ember.instrument`:
+
+ ```javascript
+ Ember.instrument("render.handlebars", payload, function() {
+ // rendering logic
+ }, binding);
+ ```
+
+ Event names passed to `Ember.instrument` are namespaced
+ by periods, from more general to more specific. Subscribers
+ can listen for events by whatever level of granularity they
+ are interested in.
+
+ In the above example, the event is `render.handlebars`,
+ and the subscriber listened for all events beginning with
+ `render`. It would receive callbacks for events named
+ `render`, `render.handlebars`, `render.container`, or
+ even `render.handlebars.layout`.
+
+ @class Instrumentation
+ @namespace Ember
+ @static
+*/
+Ember.Instrumentation = {};
+
+var subscribers = [], cache = {};
+
+var populateListeners = function(name) {
+ var listeners = [], subscriber;
+
+ for (var i=0, l=subscribers.length; i<l; i++) {
+ subscriber = subscribers[i];
+ if (subscriber.regex.test(name)) {
+ listeners.push(subscriber.object);
+ }
+ }
+
+ cache[name] = listeners;
+ return listeners;
+};
+
+var time = (function() {
+ var perf = 'undefined' !== typeof window ? window.performance || {} : {};
+ var fn = perf.now || perf.mozNow || perf.webkitNow || perf.msNow || perf.oNow;
+ // fn.bind will be available in all the browsers that support the advanced window.performance... ;-)
+ return fn ? fn.bind(perf) : function() { return +new Date(); };
+})();
+
+
+Ember.Instrumentation.instrument = function(name, payload, callback, binding) {
+ var listeners = cache[name], timeName, ret;
+
+ if (Ember.STRUCTURED_PROFILE) {
+ timeName = name + ": " + payload.object;
+ console.time(timeName);
+ }
+
+ if (!listeners) {
+ listeners = populateListeners(name);
+ }
+
+ if (listeners.length === 0) {
+ ret = callback.call(binding);
+ if (Ember.STRUCTURED_PROFILE) { console.timeEnd(timeName); }
+ return ret;
+ }
+
+ var beforeValues = [], listener, i, l;
+
+ function tryable(){
+ for (i=0, l=listeners.length; i<l; i++) {
+ listener = listeners[i];
+ beforeValues[i] = listener.before(name, time(), payload);
+ }
+
+ return callback.call(binding);
+ }
+
+ function catchable(e){
+ payload = payload || {};
+ payload.exception = e;
+ }
+
+ function finalizer() {
+ for (i=0, l=listeners.length; i<l; i++) {
+ listener = listeners[i];
+ listener.after(name, time(), payload, beforeValues[i]);
+ }
+
+ if (Ember.STRUCTURED_PROFILE) {
+ console.timeEnd(timeName);
+ }
+ }
+
+ return Ember.tryCatchFinally(tryable, catchable, finalizer);
+};
+
+Ember.Instrumentation.subscribe = function(pattern, object) {
+ var paths = pattern.split("."), path, regex = [];
+
+ for (var i=0, l=paths.length; i<l; i++) {
+ path = paths[i];
+ if (path === "*") {
+ regex.push("[^\\.]*");
+ } else {
+ regex.push(path);
+ }
+ }
+
+ regex = regex.join("\\.");
+ regex = regex + "(\\..*)?";
+
+ var subscriber = {
+ pattern: pattern,
+ regex: new RegExp("^" + regex + "$"),
+ object: object
+ };
+
+ subscribers.push(subscriber);
+ cache = {};
+
+ return subscriber;
+};
+
+Ember.Instrumentation.unsubscribe = function(subscriber) {
+ var index;
+
+ for (var i=0, l=subscribers.length; i<l; i++) {
+ if (subscribers[i] === subscriber) {
+ index = i;
+ }
+ }
+
+ subscribers.splice(index, 1);
+ cache = {};
+};
+
+Ember.Instrumentation.reset = function() {
+ subscribers = [];
+ cache = {};
+};
+
+Ember.instrument = Ember.Instrumentation.instrument;
+Ember.subscribe = Ember.Instrumentation.subscribe;
+
+})();
+
+
+
+(function() {
+var utils = Ember.EnumerableUtils = {
+ map: function(obj, callback, thisArg) {
+ return obj.map ? obj.map.call(obj, callback, thisArg) : Array.prototype.map.call(obj, callback, thisArg);
+ },
+
+ forEach: function(obj, callback, thisArg) {
+ return obj.forEach ? obj.forEach.call(obj, callback, thisArg) : Array.prototype.forEach.call(obj, callback, thisArg);
+ },
+
+ indexOf: function(obj, element, index) {
+ return obj.indexOf ? obj.indexOf.call(obj, element, index) : Array.prototype.indexOf.call(obj, element, index);
+ },
+
+ indexesOf: function(obj, elements) {
+ return elements === undefined ? [] : utils.map(elements, function(item) {
+ return utils.indexOf(obj, item);
+ });
+ },
+
+ addObject: function(array, item) {
+ var index = utils.indexOf(array, item);
+ if (index === -1) { array.push(item); }
+ },
+
+ removeObject: function(array, item) {
+ var index = utils.indexOf(array, item);
+ if (index !== -1) { array.splice(index, 1); }
+ },
+
+ replace: function(array, idx, amt, objects) {
+ if (array.replace) {
+ return array.replace(idx, amt, objects);
+ } else {
+ var args = Array.prototype.concat.apply([idx, amt], objects);
+ return array.splice.apply(array, args);
+ }
+ },
+
+ intersection: function(array1, array2) {
+ var intersection = [];
+
+ array1.forEach(function(element) {
+ if (array2.indexOf(element) >= 0) {
+ intersection.push(element);
+ }
+ });
+
+ return intersection;
+ }
+};
+
+})();
+
+
+
+(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;
+};
+
+// 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
+*/
+
+/*
+ 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 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;
+
+ 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} function
+ @param target
+ */
+ forEach: function(fn, self) {
+ // allow mutation during iteration
+ var list = this.list.slice();
+
+ 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.list.slice();
+
+ 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 = {
+ /**
+ Retrieve the value associated with a given key.
+
+ @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);
+
+ 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 {anything} key
+ @param {anything} value
+ */
+ set: function(key, value) {
+ var keys = this.keys,
+ values = this.values,
+ guid = guidFor(key);
+
+ keys.add(key);
+ values[guid] = value;
+ },
+
+ /**
+ Removes a value from the map for an associated key.
+
+ @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;
+
+ if (values.hasOwnProperty(guid)) {
+ keys.remove(key);
+ value = values[guid];
+ delete values[guid];
+ return true;
+ } else {
+ return false;
+ }
+ },
+
+ /**
+ Check whether a key is present.
+
+ @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);
+
+ 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 {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;
+
+ 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 {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`
+*/
+MapWithDefault.create = function(options) {
+ if (options) {
+ return new MapWithDefault(options);
+ } else {
+ return new Map();
+ }
+};
+
+MapWithDefault.prototype = Ember.create(Map.prototype);
+
+/**
+ Retrieve the value associated with a given key.
+
+ @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);
+
+ if (hasValue) {
+ return Map.prototype.get.call(this, key);
+ } else {
+ var defaultValue = this.defaultValue(key);
+ this.set(key, defaultValue);
+ return defaultValue;
+ }
+};
+
+/**
+ @method copy
+ @return {Ember.MapWithDefault}
+*/
+MapWithDefault.prototype.copy = function() {
+ return copyMap(this, new MapWithDefault({
+ defaultValue: this.defaultValue
+ }));
+};
+
+})();
+
+
+
+(function() {
+/**
+@module ember-metal
+*/
+
+var META_KEY = Ember.META_KEY, get, set;
+
+var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER;
+
+var IS_GLOBAL = /^([A-Z$]|([0-9][A-Z$]))/;
+var IS_GLOBAL_PATH = /^([A-Z$]|([0-9][A-Z$])).*[\.\*]/;
+var HAS_THIS = /^this[\.\*]/;
+var FIRST_KEY = /^([^\.\*]+)/;
+
+// ..........................................................
+// GET AND SET
+//
+// If we are on a platform that supports accessors we can get use those.
+// Otherwise simulate accessors by looking up the property directly on the
+// object.
+
+/**
+ 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.
+
+ 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.)
+
+ 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.
+
+ Note that if the object itself is `undefined`, this method will throw
+ an error.
+
+ @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;
+ }
+
+ if (!keyName && 'string'===typeof obj) {
+ keyName = obj;
+ obj = null;
+ }
+
+ if (!obj || keyName.indexOf('.') !== -1) {
+ Ember.assert("Cannot call get with '"+ keyName +"' on an undefined object.", obj !== undefined);
+ return getPath(obj, keyName);
+ }
+
+ Ember.assert("You need to provide an object and key to `get`.", !!obj && keyName);
+
+ 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 (ret === undefined &&
+ 'object' === typeof obj && !(keyName in obj) && 'function' === typeof obj.unknownProperty) {
+ return obj.unknownProperty(keyName);
+ }
+
+ return ret;
+ }
+};
+
+/**
+ 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.
+*/
+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.get = get;
+ Ember.set = set;
+ Ember.config.overrideAccessors();
+ get = Ember.get;
+ set = Ember.set;
+}
+
+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);
+
+ if (target === Ember.lookup) {
+ key = firstKey(path);
+ target = get(target, key);
+ path = path.slice(key.length+1);
+ }
+
+ // must return some kind of path to be valid else other things will break.
+ if (!path || path.length===0) throw new Error('Invalid Path');
+
+ return [ target, path ];
+}
+
+function getPath(root, path) {
+ var hasThis, parts, tuple, idx, len;
+
+ // If there is no root and path is a key name, return that
+ // property from the global object.
+ // E.g. get('Ember') -> Ember
+ if (root === null && path.indexOf('.') === -1) { return get(Ember.lookup, path); }
+
+ // detect complicated paths and normalize them
+ hasThis = HAS_THIS.test(path);
+
+ if (!root || hasThis) {
+ tuple = normalizeTuple(root, path);
+ root = tuple[0];
+ path = tuple[1];
+ tuple.length = 0;
+ }
+
+ parts = path.split(".");
+ len = parts.length;
+ for (idx=0; root && idx<len; idx++) {
+ root = get(root, parts[idx], true);
+ if (root && root.isDestroyed) { return undefined; }
+ }
+ return root;
+}
+
+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);
+}
+
+/**
+ @private
+
+ Normalizes a target/path pair to reflect that actual target/path that should
+ be observed, etc. This takes into account passing in global property
+ paths (i.e. a path beginning with a captial letter not defined on the
+ target) and * separators.
+
+ @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);
+};
+
+Ember.getWithDefault = function(root, key, defaultValue) {
+ var value = get(root, key);
+
+ if (value === undefined) { return defaultValue; }
+ return value;
+};
+
+
+Ember.get = get;
+Ember.getPath = Ember.deprecateFunc('getPath is deprecated since get now supports paths', Ember.get);
+
+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} keyName The property key to set
+ @param {Object} value The value to set
+*/
+Ember.trySet = function(root, path, value) {
+ return set(root, path, value, true);
+};
+Ember.trySetPath = Ember.deprecateFunc('trySetPath has been renamed to trySet', Ember.trySet);
+
+/**
+ 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
+*/
+Ember.isGlobalPath = function(path) {
+ return IS_GLOBAL.test(path);
+};
+
+
+})();
+
+
+
+(function() {
+/**
+@module ember-metal
+*/
+
+var GUID_KEY = Ember.GUID_KEY,
+ META_KEY = Ember.META_KEY,
+ EMPTY_META = Ember.EMPTY_META,
+ metaFor = Ember.meta,
+ o_create = Ember.create,
+ objectDefineProperty = Ember.platform.defineProperty;
+
+var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER;
+
+// ..........................................................
+// DESCRIPTOR
+//
+
+/**
+ Objects of this type can implement an interface to responds requests to
+ get and set. The default implementation handles simple properties.
+
+ You generally won't need to create or subclass this directly.
+
+ @class Descriptor
+ @namespace Ember
+ @private
+ @constructor
+*/
+var Descriptor = Ember.Descriptor = function() {};
+
+// ..........................................................
+// DEFINING PROPERTIES API
+//
+
+var MANDATORY_SETTER_FUNCTION = Ember.MANDATORY_SETTER_FUNCTION = function(value) {
+ Ember.assert("You must use Ember.set() to access this property (of " + this + ")", false);
+};
+
+var DEFAULT_GETTER_FUNCTION = Ember.DEFAULT_GETTER_FUNCTION = function(name) {
+ return function() {
+ var meta = this[META_KEY];
+ return meta && meta.values[name];
+ };
+};
+
+/**
+ @private
+
+ NOTE: This is a low-level method used by other parts of the API. You almost
+ never want to call this method directly. Instead you should use
+ `Ember.mixin()` to define new properties.
+
+ Defines a property on an object. This method works much like the ES5
+ `Object.defineProperty()` method except that it can also accept computed
+ properties and other special descriptors.
+
+ Normally this method takes only three parameters. However if you pass an
+ instance of `Ember.Descriptor` as the third param then you can pass an
+ optional value as the fourth parameter. This is often more efficient than
+ creating new descriptor hashes for each property.
+
+ ## Examples
+
+ ```javascript
+ // ES5 compatible mode
+ Ember.defineProperty(contact, 'firstName', {
+ writable: true,
+ configurable: false,
+ enumerable: true,
+ value: 'Charles'
+ });
+
+ // define a simple property
+ Ember.defineProperty(contact, 'lastName', undefined, 'Jolley');
+
+ // define a computed property
+ Ember.defineProperty(contact, 'fullName', Ember.computed(function() {
+ return this.firstName+' '+this.lastName;
+ }).property('firstName', 'lastName'));
+ ```
+
+ @method defineProperty
+ @for Ember
+ @param {Object} obj the object to define this property on. This may be a prototype.
+ @param {String} keyName the name of the property
+ @param {Ember.Descriptor} [desc] an instance of `Ember.Descriptor` (typically a
+ computed property) or an ES5 descriptor.
+ You must provide this or `data` but not both.
+ @param {anything} [data] something other than a descriptor, that will
+ become the explicit value of this property.
+*/
+Ember.defineProperty = function(obj, keyName, desc, data, meta) {
+ var descs, existingDesc, watching, value;
+
+ if (!meta) meta = metaFor(obj);
+ descs = meta.descs;
+ existingDesc = meta.descs[keyName];
+ watching = meta.watching[keyName] > 0;
+
+ if (existingDesc instanceof Ember.Descriptor) {
+ existingDesc.teardown(obj, keyName);
+ }
+
+ if (desc instanceof Ember.Descriptor) {
+ value = desc;
+
+ descs[keyName] = desc;
+ if (MANDATORY_SETTER && watching) {
+ objectDefineProperty(obj, keyName, {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: undefined // make enumerable
+ });
+ } else {
+ obj[keyName] = undefined; // make enumerable
+ }
+ desc.setup(obj, keyName);
+ } else {
+ descs[keyName] = undefined; // shadow descriptor in proto
+ if (desc == null) {
+ value = data;
+
+ if (MANDATORY_SETTER && watching) {
+ meta.values[keyName] = data;
+ objectDefineProperty(obj, keyName, {
+ configurable: true,
+ enumerable: true,
+ set: MANDATORY_SETTER_FUNCTION,
+ get: DEFAULT_GETTER_FUNCTION(keyName)
+ });
+ } else {
+ obj[keyName] = data;
+ }
+ } else {
+ value = desc;
+
+ // compatibility with ES5
+ objectDefineProperty(obj, keyName, desc);
+ }
+ }
+
+ // if key is being watched, override chains that
+ // were initialized with the prototype
+ if (watching) { Ember.overrideChains(obj, keyName, meta); }
+
+ // The `value` passed to the `didDefineProperty` hook is
+ // either the descriptor or data, whichever was passed.
+ if (obj.didDefineProperty) { obj.didDefineProperty(obj, keyName, value); }
+
+ return this;
+};
+
+
+})();
+
+
+
+(function() {
+// 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);
+};
+
+/**
+ Set a list of properties on an object. These properties are set inside
+ a single `beginPropertyChanges` and `endPropertyChanges` batch, so
+ observers will be buffered.
+
+ @method setProperties
+ @param target
+ @param {Hash} properties
+ @return target
+*/
+Ember.setProperties = function(self, hash) {
+ Ember.changeProperties(function(){
+ for(var prop in hash) {
+ if (hash.hasOwnProperty(prop)) Ember.set(self, prop, hash[prop]);
+ }
+ });
+ return self;
+};
+
+
+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);
+};
+
+Ember._suspendObserver = function(obj, path, target, method, callback) {
+ return Ember._suspendListener(obj, changeEvent(path), target, method, callback);
+};
+
+var map = Ember.ArrayPolyfills.map;
+
+Ember._suspendBeforeObservers = function(obj, paths, target, method, callback) {
+ var events = map.call(paths, beforeEvent);
+ return Ember._suspendListeners(obj, events, target, method, callback);
+};
+
+Ember._suspendObservers = function(obj, paths, target, method, callback) {
+ var events = map.call(paths, changeEvent);
+ return Ember._suspendListeners(obj, events, target, method, callback);
+};
+
+Ember.beforeObserversFor = function(obj, path) {
+ return Ember.listenersFor(obj, beforeEvent(path));
+};
+
+/**
+ @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; }
+
+ 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]);
+ }
+};
+
+Ember.notifyObservers = function(obj, keyName) {
+ if (obj.isDestroying) { return; }
+
+ 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]);
+ }
+};
+
+})();
+
+
+
+(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
+ notifyObservers = Ember.notifyObservers, // observer.js
+ forEach = Ember.ArrayPolyfills.forEach, // array.js
+ FIRST_KEY = /^([^\.\*]+)/,
+ IS_PATH = /[\.\*]/;
+
+var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER,
+o_defineProperty = Ember.platform.defineProperty;
+
+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;
+
+// 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; }
+
+ var seen = WILL_SEEN, top = !seen;
+ if (top) { seen = WILL_SEEN = {}; }
+ iterDeps(propertyWillChange, obj, depKey, seen, meta);
+ if (top) { WILL_SEEN = null; }
+}
+
+// called whenever a property has just changed to update dependent keys
+function dependentKeysDidChange(obj, depKey, meta) {
+ if (obj.isDestroying) { return; }
+
+ var seen = DID_SEEN, top = !seen;
+ if (top) { seen = DID_SEEN = {}; }
+ iterDeps(propertyDidChange, obj, depKey, seen, meta);
+ if (top) { DID_SEEN = null; }
+}
+
+// ..........................................................
+// CHAIN
+//
+
+function addChainWatcher(obj, keyName, node) {
+ if (!obj || ('object' !== typeof obj)) { return; } // nothing to do
+
+ var m = metaFor(obj), nodes = m.chainWatchers;
+
+ if (!m.hasOwnProperty('chainWatchers')) {
+ nodes = m.chainWatchers = {};
+ }
+
+ if (!nodes[keyName]) { nodes[keyName] = []; }
+ nodes[keyName].push(node);
+ Ember.watch(obj, keyName);
+}
+
+function removeChainWatcher(obj, keyName, node) {
+ if (!obj || 'object' !== typeof obj) { return; } // nothing to do
+
+ var m = metaFor(obj, false);
+ if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do
+
+ var nodes = m.chainWatchers;
+
+ if (nodes[keyName]) {
+ nodes = nodes[keyName];
+ for (var i = 0, l = nodes.length; i < l; i++) {
+ if (nodes[i] === node) { nodes.splice(i, 1); }
+ }
+ }
+ Ember.unwatch(obj, keyName);
+}
+
+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);
+}
+
+function isProto(pvalue) {
+ return metaFor(pvalue, false).proto === 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;
+ this._parent = parent;
+ this._key = key;
+
+ // _watching is true when calling get(this._parent, this._key) will
+ // return the value of this node.
+ //
+ // It is false for the root of a chain (because we have no parent)
+ // and for global paths (because the parent node is the object with
+ // the observer on it)
+ this._watching = value===undefined;
+
+ this._value = value;
+ this._paths = {};
+ if (this._watching) {
+ this._object = parent.value();
+ if (this._object) { addChainWatcher(this._object, this._key, this); }
+ }
+
+ // Special-case: the EachProxy relies on immediate evaluation to
+ // establish its observers.
+ //
+ // TODO: Replace this with an efficient callback that the EachProxy
+ // can implement.
+ if (this._parent && this._parent._key === '@each') {
+ this.value();
+ }
+};
+
+var ChainNodePrototype = ChainNode.prototype;
+
+ChainNodePrototype.value = function() {
+ if (this._value === undefined && this._watching) {
+ var obj = this._parent.value();
+ this._value = (obj && !isProto(obj)) ? get(obj, this._key) : undefined;
+ }
+ return this._value;
+};
+
+ChainNodePrototype.destroy = function() {
+ if (this._watching) {
+ var obj = this._object;
+ if (obj) { removeChainWatcher(obj, this._key, this); }
+ this._watching = false; // so future calls do nothing
+ }
+};
+
+// copies a top level object only
+ChainNodePrototype.copy = function(obj) {
+ var ret = new ChainNode(null, null, obj),
+ paths = this._paths, path;
+ for (path in paths) {
+ if (paths[path] <= 0) { continue; } // this check will also catch non-number vals.
+ ret.add(path);
+ }
+ return ret;
+};
+
+// called on the root node of a chain to setup watchers on the specified
+// path.
+ChainNodePrototype.add = function(path) {
+ var obj, tuple, key, src, paths;
+
+ paths = this._paths;
+ paths[path] = (paths[path] || 0) + 1;
+
+ obj = this.value();
+ tuple = normalizeTuple(obj, path);
+
+ // the path was a local path
+ if (tuple[0] && tuple[0] === obj) {
+ path = tuple[1];
+ key = firstKey(path);
+ path = path.slice(key.length+1);
+
+ // global path, but object does not exist yet.
+ // put into a queue and try to connect later.
+ } else if (!tuple[0]) {
+ pendingQueue.push([this, path]);
+ tuple.length = 0;
+ return;
+
+ // global path, and object already exists
+ } else {
+ src = tuple[0];
+ key = path.slice(0, 0-(tuple[1].length+1));
+ path = tuple[1];
+ }
+
+ tuple.length = 0;
+ this.chain(key, path, src);
+};
+
+// called on the root node of a chain to teardown watcher on the specified
+// path
+ChainNodePrototype.remove = function(path) {
+ var obj, tuple, key, src, paths;
+
+ paths = this._paths;
+ if (paths[path] > 0) { paths[path]--; }
+
+ obj = this.value();
+ tuple = normalizeTuple(obj, path);
+ if (tuple[0] === obj) {
+ path = tuple[1];
+ key = firstKey(path);
+ path = path.slice(key.length+1);
+ } else {
+ src = tuple[0];
+ key = path.slice(0, 0-(tuple[1].length+1));
+ path = tuple[1];
+ }
+
+ tuple.length = 0;
+ this.unchain(key, path);
+};
+
+ChainNodePrototype.count = 0;
+
+ChainNodePrototype.chain = function(key, path, src) {
+ var chains = this._chains, node;
+ if (!chains) { chains = this._chains = {}; }
+
+ node = chains[key];
+ if (!node) { node = chains[key] = new ChainNode(this, key, src); }
+ node.count++; // count chains...
+
+ // chain rest of path if there is one
+ if (path && path.length>0) {
+ key = firstKey(path);
+ path = path.slice(key.length+1);
+ node.chain(key, path); // NOTE: no src means it will observe changes...
+ }
+};
+
+ChainNodePrototype.unchain = function(key, path) {
+ var chains = this._chains, node = chains[key];
+
+ // unchain rest of path first...
+ if (path && path.length>1) {
+ key = firstKey(path);
+ path = path.slice(key.length+1);
+ node.unchain(key, path);
+ }
+
+ // delete node if needed.
+ node.count--;
+ if (node.count<=0) {
+ delete chains[node._key];
+ node.destroy();
+ }
+
+};
+
+ChainNodePrototype.willChange = function() {
+ var chains = this._chains;
+ if (chains) {
+ for(var key in chains) {
+ if (!chains.hasOwnProperty(key)) { continue; }
+ chains[key].willChange();
+ }
+ }
+
+ if (this._parent) { this._parent.chainWillChange(this, this._key, 1); }
+};
+
+ChainNodePrototype.chainWillChange = function(chain, path, depth) {
+ if (this._key) { path = this._key + '.' + path; }
+
+ if (this._parent) {
+ this._parent.chainWillChange(this, path, depth+1);
+ } else {
+ if (depth > 1) { Ember.propertyWillChange(this.value(), path); }
+ path = 'this.' + path;
+ if (this._paths[path] > 0) { Ember.propertyWillChange(this.value(), path); }
+ }
+};
+
+ChainNodePrototype.chainDidChange = function(chain, path, depth) {
+ if (this._key) { path = this._key + '.' + path; }
+ if (this._parent) {
+ this._parent.chainDidChange(this, path, depth+1);
+ } else {
+ if (depth > 1) { Ember.propertyDidChange(this.value(), path); }
+ path = 'this.' + path;
+ if (this._paths[path] > 0) { Ember.propertyDidChange(this.value(), path); }
+ }
+};
+
+ChainNodePrototype.didChange = function(suppressEvent) {
+ // invalidate my own value first.
+ if (this._watching) {
+ var obj = this._parent.value();
+ if (obj !== this._object) {
+ removeChainWatcher(this._object, this._key, this);
+ this._object = obj;
+ addChainWatcher(obj, this._key, this);
+ }
+ this._value = undefined;
+
+ // Special-case: the EachProxy relies on immediate evaluation to
+ // establish its observers.
+ if (this._parent && this._parent._key === '@each')
+ this.value();
+ }
+
+ // then notify chains...
+ var chains = this._chains;
+ if (chains) {
+ for(var key in chains) {
+ if (!chains.hasOwnProperty(key)) { continue; }
+ chains[key].didChange(suppressEvent);
+ }
+ }
+
+ if (suppressEvent) { return; }
+
+ // and finally tell parent about my path changing...
+ if (this._parent) { this._parent.chainDidChange(this, this._key, 1); }
+};
+
+// get the chains for the current object. If the current object has
+// chains inherited from the proto they will be cloned and reconfigured for
+// the current object.
+function chainsFor(obj) {
+ var m = metaFor(obj), ret = m.chains;
+ if (!ret) {
+ ret = m.chains = new ChainNode(null, null, obj);
+ } else if (ret.value() !== obj) {
+ ret = m.chains = ret.copy(obj);
+ }
+ return ret;
+}
+
+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) {
+ // 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 (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);
+ }
+
+ } else {
+ watching[keyName] = (watching[keyName] || 0) + 1;
+ }
+ return this;
+};
+
+Ember.isWatching = function isWatching(obj, key) {
+ var meta = obj[META_KEY];
+ return (meta && meta.watching[key]) > 0;
+};
+
+Ember.watch.flushPending = flushPendingChains;
+
+Ember.unwatch = function(obj, keyName) {
+ // can't watch length on Array - it is special...
+ if (keyName === 'length' && Ember.typeOf(obj) === 'array') { return this; }
+
+ var m = metaFor(obj), watching = m.watching, desc;
+
+ 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);
+ }
+
+ } else if (watching[keyName]>1) {
+ watching[keyName]--;
+ }
+
+ return this;
+};
+
+/**
+ @private
+
+ Call on an object when you first beget it from another object. This will
+ setup any chained watchers on the object instance as needed. This method is
+ safe to call multiple times.
+
+ @method rewatch
+ @for Ember
+ @param obj
+*/
+Ember.rewatch = function(obj) {
+ var m = metaFor(obj, false), chains = m.chains;
+
+ // make sure the object has its own guid.
+ if (GUID_KEY in obj && !obj.hasOwnProperty(GUID_KEY)) {
+ Ember.generateGuid(obj, 'ember');
+ }
+
+ // make sure any chained watchers update.
+ if (chains && chains.value() !== obj) {
+ m.chains = chains.copy(obj);
+ }
+
+ return this;
+};
+
+Ember.finishChains = function(obj) {
+ var m = metaFor(obj, false), chains = m.chains;
+ if (chains) {
+ if (chains.value() !== obj) {
+ m.chains = chains = chains.copy(obj);
+ }
+ chains.didChange(true);
+ }
+};
+
+// ..........................................................
+// PROPERTY CHANGES
+//
+
+/**
+ 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.
+
+ 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.
+
+ @method propertyWillChange
+ @for Ember
+ @param {Object} obj The object with the property that will change
+ @param {String} keyName The property key (or path) that will change.
+ @return {void}
+*/
+function propertyWillChange(obj, keyName, value) {
+ var m = metaFor(obj, false),
+ watching = m.watching[keyName] > 0 || keyName === 'length',
+ proto = m.proto,
+ desc = m.descs[keyName];
+
+ 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);
+}
+
+Ember.propertyWillChange = propertyWillChange;
+
+/**
+ This function is called just after an object property has changed.
+ It will notify any observers and clear caches among other things.
+
+ Normally you will not need to call this method directly but if for some
+ reason you can't directly watch a property you can invoke this method
+ manually along with `Ember.propertyWilLChange()` which you should call just
+ before the property value changes.
+
+ @method propertyDidChange
+ @for Ember
+ @param {Object} obj The object with the property that will change
+ @param {String} keyName The property key (or path) that will change.
+ @return {void}
+*/
+function propertyDidChange(obj, keyName) {
+ var m = metaFor(obj, false),
+ watching = m.watching[keyName] > 0 || keyName === 'length',
+ proto = m.proto,
+ desc = m.descs[keyName];
+
+ 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; }
+
+ dependentKeysDidChange(obj, keyName, m);
+ chainsDidChange(obj, keyName, m);
+ Ember.notifyObservers(obj, keyName);
+}
+
+Ember.propertyDidChange = propertyDidChange;
+
+var NODE_STACK = [];
+
+/**
+ Tears down the meta on an object so that it can be garbage collected.
+ Multiple calls will have no effect.
+
+ @method destroy
+ @for Ember
+ @param {Object} obj the object to destroy
+ @return {void}
+*/
+Ember.destroy = function (obj) {
+ var meta = obj[META_KEY], node, nodes, key, nodeObject;
+ if (meta) {
+ obj[META_KEY] = null;
+ // remove chainWatchers to remove circular references that would prevent GC
+ node = meta.chains;
+ if (node) {
+ NODE_STACK.push(node);
+ // process tree
+ while (NODE_STACK.length > 0) {
+ node = NODE_STACK.pop();
+ // push children
+ nodes = node._chains;
+ if (nodes) {
+ for (key in nodes) {
+ if (nodes.hasOwnProperty(key)) {
+ NODE_STACK.push(nodes[key]);
+ }
+ }
+ }
+ // remove chainWatcher in node object
+ if (node._watching) {
+ nodeObject = node._object;
+ if (nodeObject) {
+ removeChainWatcher(nodeObject, node._key, node);
+ }
+ }
+ }
+ }
+ }
+};
+
+})();
+
+
+
+(function() {
+/**
+@module ember-metal
+*/
+
+Ember.warn("The CP_DEFAULT_CACHEABLE flag has been removed and computed properties are always cached by default. Use `volatile` if you don't want caching.", Ember.ENV.CP_DEFAULT_CACHEABLE !== false);
+
+
+var get = Ember.get,
+ set = Ember.set,
+ metaFor = Ember.meta,
+ guidFor = Ember.guidFor,
+ a_slice = [].slice,
+ o_create = Ember.create,
+ META_KEY = Ember.META_KEY,
+ watch = Ember.watch,
+ unwatch = Ember.unwatch;
+
+// ..........................................................
+// DEPENDENT KEYS
+//
+
+// data structure:
+// meta.deps = {
+// 'depKey': {
+// 'keyName': count,
+// }
+// }
+
+/*
+ This function returns a map of unique dependencies for a
+ given object and key.
+*/
+function keysForDep(obj, depsMeta, depKey) {
+ var keys = depsMeta[depKey];
+ if (!keys) {
+ // if there are no dependencies yet for a the given key
+ // create a new empty list of dependencies for the key
+ keys = depsMeta[depKey] = {};
+ } else if (!depsMeta.hasOwnProperty(depKey)) {
+ // otherwise if the dependency list is inherited from
+ // a superclass, clone the hash
+ keys = depsMeta[depKey] = o_create(keys);
+ }
+ return keys;
+}
+
+/* return obj[META_KEY].deps */
+function metaForDeps(obj, meta) {
+ var deps = meta.deps;
+ // If the current object has no dependencies...
+ if (!deps) {
+ // initialize the dependencies with a pointer back to
+ // the current object
+ deps = meta.deps = {};
+ } else if (!meta.hasOwnProperty('deps')) {
+ // otherwise if the dependencies are inherited from the
+ // object's superclass, clone the deps
+ deps = meta.deps = o_create(deps);
+ }
+ return deps;
+}
+
+function addDependentKeys(desc, obj, keyName, meta) {
+ // the descriptor has a list of dependent keys, so
+ // add all of its dependent keys.
+ var depKeys = desc._dependentKeys, depsMeta, idx, len, depKey, keys;
+ if (!depKeys) return;
+
+ depsMeta = metaForDeps(obj, meta);
+
+ for(idx = 0, len = depKeys.length; idx < len; idx++) {
+ depKey = depKeys[idx];
+ // Lookup keys meta for depKey
+ keys = keysForDep(obj, depsMeta, depKey);
+ // Increment the number of times depKey depends on keyName.
+ keys[keyName] = (keys[keyName] || 0) + 1;
+ // Watch the depKey
+ watch(obj, depKey);
+ }
+}
+
+function removeDependentKeys(desc, obj, keyName, meta) {
+ // the descriptor has a list of dependent keys, so
+ // add all of its dependent keys.
+ var depKeys = desc._dependentKeys, depsMeta, idx, len, depKey, keys;
+ if (!depKeys) return;
+
+ depsMeta = metaForDeps(obj, meta);
+
+ for(idx = 0, len = depKeys.length; idx < len; idx++) {
+ depKey = depKeys[idx];
+ // Lookup keys meta for depKey
+ keys = keysForDep(obj, depsMeta, depKey);
+ // Increment the number of times depKey depends on keyName.
+ keys[keyName] = (keys[keyName] || 0) - 1;
+ // Watch the depKey
+ unwatch(obj, depKey);
+ }
+}
+
+// ..........................................................
+// COMPUTED PROPERTY
+//
+
+/**
+ @class ComputedProperty
+ @namespace Ember
+ @extends Ember.Descriptor
+ @constructor
+*/
+function ComputedProperty(func, opts) {
+ this.func = func;
+ this._cacheable = (opts && opts.cacheable !== undefined) ? opts.cacheable : true;
+ this._dependentKeys = opts && opts.dependentKeys;
+}
+
+Ember.ComputedProperty = ComputedProperty;
+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');
+
+ // 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')
+ });
+ ```
+
+ Properties are cacheable by default.
+
+ @method cacheable
+ @param {Boolean} aFlag optional set to `false` to disable caching
+ @chainable
+*/
+ComputedPropertyPrototype.cacheable = function(aFlag) {
+ this._cacheable = aFlag !== false;
+ return this;
+};
+
+/**
+ Call on a computed property to set it into non-cached mode. When in this
+ mode the computed property will not automatically cache the return value.
+
+ ```javascript
+ MyApp.outsideService = Ember.Object.create({
+ value: function() {
+ return OutsideService.getValue();
+ }.property().volatile()
+ });
+ ```
+
+ @method volatile
+ @chainable
+*/
+ComputedPropertyPrototype.volatile = function() {
+ return this.cacheable(false);
+};
+
+/**
+ Sets the dependent keys on this computed property. Pass any number of
+ arguments containing key paths that this computed property depends on.
+
+ ```javascript
+ MyApp.president = Ember.Object.create({
+ fullName: Ember.computed(function() {
+ return this.get('firstName') + ' ' + this.get('lastName');
+
+ // Tell Ember that this computed property depends on firstName
+ // and lastName
+ }).property('firstName', 'lastName')
+ });
+ ```
+
+ @method property
+ @param {String} path* zero or more property paths
+ @chainable
+*/
+ComputedPropertyPrototype.property = function() {
+ var args = [];
+ for (var i = 0, l = arguments.length; i < l; i++) {
+ args.push(arguments[i]);
+ }
+ this._dependentKeys = args;
+ return this;
+};
+
+/**
+ In some cases, you may want to annotate computed properties with additional
+ metadata about how they function or what values they operate on. For example,
+ computed property functions may close over variables that are then no longer
+ available for introspection.
+
+ You can pass a hash of these values to a computed property like this:
+
+ ```
+ person: function() {
+ var personId = this.get('personId');
+ return App.Person.create({ id: personId });
+ }.property().meta({ type: App.Person })
+ ```
+
+ The hash that you pass to the `meta()` function will be saved on the
+ computed property descriptor under the `_meta` key. Ember runtime
+ exposes a public API for retrieving these values from classes,
+ via the `metaForProperty()` function.
+
+ @method meta
+ @param {Hash} meta
+ @chainable
+*/
+
+ComputedPropertyPrototype.meta = function(meta) {
+ if (arguments.length === 0) {
+ return this._meta || {};
+ } else {
+ this._meta = meta;
+ return this;
+ }
+};
+
+/* impl descriptor API */
+ComputedPropertyPrototype.willWatch = function(obj, keyName) {
+ // watch already creates meta for this instance
+ var meta = obj[META_KEY];
+ Ember.assert('watch should have setup meta to be writable', meta.source === obj);
+ if (!(keyName in meta.cache)) {
+ addDependentKeys(this, obj, keyName, meta);
+ }
+};
+
+ComputedPropertyPrototype.didUnwatch = function(obj, keyName) {
+ var meta = obj[META_KEY];
+ Ember.assert('unwatch should have setup meta to be writable', meta.source === obj);
+ if (!(keyName in meta.cache)) {
+ // unwatch already creates meta for this instance
+ removeDependentKeys(this, obj, keyName, meta);
+ }
+};
+
+/* impl descriptor API */
+ComputedPropertyPrototype.didChange = function(obj, keyName) {
+ // _suspended is set via a CP.set to ensure we don't clear
+ // the cached value set by the setter
+ if (this._cacheable && this._suspended !== obj) {
+ var meta = metaFor(obj);
+ if (keyName in meta.cache) {
+ delete meta.cache[keyName];
+ if (!meta.watching[keyName]) {
+ removeDependentKeys(this, obj, keyName, meta);
+ }
+ }
+ }
+};
+
+/* impl descriptor API */
+ComputedPropertyPrototype.get = function(obj, keyName) {
+ var ret, cache, meta;
+ if (this._cacheable) {
+ meta = metaFor(obj);
+ cache = meta.cache;
+ if (keyName in cache) { return cache[keyName]; }
+ ret = cache[keyName] = this.func.call(obj, keyName);
+ if (!meta.watching[keyName]) {
+ addDependentKeys(this, obj, keyName, meta);
+ }
+ } else {
+ ret = this.func.call(obj, keyName);
+ }
+ return ret;
+};
+
+/* impl descriptor API */
+ComputedPropertyPrototype.set = function(obj, keyName, value) {
+ var cacheable = this._cacheable,
+ func = this.func,
+ meta = metaFor(obj, cacheable),
+ watched = meta.watching[keyName],
+ oldSuspended = this._suspended,
+ hadCachedValue = false,
+ cache = meta.cache,
+ cachedValue, ret;
+
+ this._suspended = obj;
+
+ try {
+ if (cacheable && cache.hasOwnProperty(keyName)) {
+ cachedValue = cache[keyName];
+ hadCachedValue = true;
+ }
+
+ // Check if the CP has been wrapped
+ if (func.wrappedFunction) { func = func.wrappedFunction; }
+
+ // For backwards-compatibility with computed properties
+ // that check for arguments.length === 2 to determine if
+ // they are being get or set, only pass the old cached
+ // value if the computed property opts into a third
+ // argument.
+ if (func.length === 3) {
+ ret = func.call(obj, keyName, value, cachedValue);
+ } else if (func.length === 2) {
+ ret = func.call(obj, keyName, value);
+ } else {
+ Ember.defineProperty(obj, keyName, null, cachedValue);
+ Ember.set(obj, keyName, value);
+ return;
+ }
+
+ if (hadCachedValue && cachedValue === ret) { return; }
+
+ if (watched) { Ember.propertyWillChange(obj, keyName); }
+
+ if (hadCachedValue) {
+ delete cache[keyName];
+ }
+
+ if (cacheable) {
+ if (!watched && !hadCachedValue) {
+ addDependentKeys(this, obj, keyName, meta);
+ }
+ cache[keyName] = ret;
+ }
+
+ if (watched) { Ember.propertyDidChange(obj, keyName); }
+ } finally {
+ this._suspended = oldSuspended;
+ }
+ return ret;
+};
+
+/* called when property is defined */
+ComputedPropertyPrototype.setup = function(obj, keyName) {
+ var meta = obj[META_KEY];
+ if (meta && meta.watching[keyName]) {
+ addDependentKeys(this, obj, keyName, metaFor(obj));
+ }
+};
+
+/* called before property is overridden */
+ComputedPropertyPrototype.teardown = function(obj, keyName) {
+ var meta = metaFor(obj);
+
+ if (meta.watching[keyName] || keyName in meta.cache) {
+ removeDependentKeys(this, obj, keyName, meta);
+ }
+
+ if (this._cacheable) { delete meta.cache[keyName]; }
+
+ return null; // no value to restore
+};
+
+
+/**
+ This helper returns a new property descriptor that wraps the passed
+ computed property function. You can use this helper to define properties
+ with mixins or via `Ember.defineProperty()`.
+
+ The function you pass will be used to both get and set property values.
+ The function should accept two parameters, key and value. If value is not
+ undefined you should set the value first. In either case return the
+ current value of the property.
+
+ @method computed
+ @for Ember
+ @param {Function} func The computed property function.
+ @return {Ember.ComputedProperty} property descriptor instance
+*/
+Ember.computed = function(func) {
+ var args;
+
+ if (arguments.length > 1) {
+ args = a_slice.call(arguments, 0, -1);
+ func = a_slice.call(arguments, -1)[0];
+ }
+
+ var cp = new ComputedProperty(func);
+
+ if (args) {
+ cp.property.apply(cp, args);
+ }
+
+ return cp;
+};
+
+/**
+ Returns the cached value for a property, if one exists.
+ This can be useful for peeking at the value of a computed
+ property that is generated lazily, without accidentally causing
+ it to be created.
+
+ @method cacheFor
+ @for Ember
+ @param {Object} obj the object whose property you want to check
+ @param {String} key the name of the property whose cached value you want
+ to return
+*/
+Ember.cacheFor = function cacheFor(obj, key) {
+ var cache = metaFor(obj, false).cache;
+
+ if (cache && key in cache) {
+ return cache[key];
+ }
+};
+
+/**
+ @method computed.not
+ @for Ember
+ @param {String} dependentKey
+*/
+Ember.computed.not = function(dependentKey) {
+ return Ember.computed(dependentKey, function(key) {
+ return !get(this, dependentKey);
+ });
+};
+
+/**
+ @method computed.empty
+ @for Ember
+ @param {String} dependentKey
+*/
+Ember.computed.empty = function(dependentKey) {
+ return Ember.computed(dependentKey, function(key) {
+ var val = get(this, dependentKey);
+ return val === undefined || val === null || val === '' || (Ember.isArray(val) && get(val, 'length') === 0);
+ });
+};
+
+/**
+ @method computed.bool
+ @for Ember
+ @param {String} dependentKey
+*/
+Ember.computed.bool = function(dependentKey) {
+ return Ember.computed(dependentKey, function(key) {
+ return !!get(this, dependentKey);
+ });
+};
+
+/**
+ @method computed.alias
+ @for Ember
+ @param {String} dependentKey
+*/
+Ember.computed.alias = function(dependentKey) {
+ return Ember.computed(dependentKey, function(key, value){
+ if (arguments.length === 1) {
+ return get(this, dependentKey);
+ } else {
+ set(this, dependentKey, value);
+ return value;
+ }
+ });
+};
+
+})();
+
+
+
+(function() {
+/**
+@module ember-metal
+*/
+
+var o_create = Ember.create,
+ metaFor = Ember.meta,
+ metaPath = Ember.metaPath,
+ 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);
+
+ if (actionIndex === -1) {
+ otherActions.push([target, method, once, suspended]);
+ }
+ }
+}
+
+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;
+}
+
+/**
+ Add an event listener
+
+ @method addListener
+ @for Ember
+ @param obj
+ @param {String} eventName
+ @param {Object|Function} targetOrMethod A target object or a function
+ @param {Function|String} method A function or the name of a function to be called on `target`
+*/
+function addListener(obj, eventName, target, method, once) {
+ Ember.assert("You must pass at least an object and event name to Ember.addListener", !!obj && !!eventName);
+
+ 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]);
+
+ if ('function' === typeof obj.didAddListener) {
+ obj.didAddListener(eventName, target, method);
+ }
+}
+
+/**
+ Remove an event listener
+
+ Arguments should match those passed to {{#crossLink "Ember/addListener"}}{{/crossLink}}
+
+ @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);
+
+ 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; }
+
+ actions.splice(actionIndex, 1);
+
+ if ('function' === typeof obj.didRemoveListener) {
+ obj.didRemoveListener(eventName, target, method);
+ }
+ }
+
+ if (method) {
+ _removeListener(target, method);
+ } else {
+ var meta = obj[META_KEY],
+ actions = meta && meta.listeners && meta.listeners[eventName];
+
+ if (!actions) { return; }
+ for (var i = actions.length - 1; i >= 0; i--) {
+ _removeListener(actions[i][0], actions[i][1]);
+ }
+ }
+}
+
+/**
+ @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.
+
+ @method suspendListener
+ @for Ember
+ @param obj
+ @param {String} eventName
+ @param {Object|Function} targetOrMethod A target object or a function
+ @param {Function|String} method A function or the name of a function to be called on `target`
+ @param {Function} callback
+*/
+function suspendListener(obj, eventName, target, method, callback) {
+ if (!method && 'function' === typeof target) {
+ method = target;
+ target = null;
+ }
+
+ 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
+ }
+
+ function tryable() { return callback.call(target); }
+ function finalizer() { if (action) { action[3] = undefined; } }
+
+ return Ember.tryFinally(tryable, finalizer);
+}
+
+/**
+ @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.
+
+ @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 suspendedActions = [],
+ eventName, actions, action, i, l;
+
+ for (i=0, l=eventNames.length; i<l; i++) {
+ eventName = eventNames[i];
+ actions = actionsFor(obj, eventName);
+ var actionIndex = indexOf(actions, target, method);
+
+ if (actionIndex !== -1) {
+ action = actions[actionIndex].slice();
+ action[3] = true;
+ actions[actionIndex] = action;
+ suspendedActions.push(action);
+ }
+ }
+
+ function tryable() { return callback.call(target); }
+
+ function finalizer() {
+ for (i = 0, l = suspendedActions.length; i < l; i++) {
+ suspendedActions[i][3] = undefined;
+ }
+ }
+
+ return Ember.tryFinally(tryable, finalizer);
+}
+
+/**
+ @private
+
+ Return a list of currently watched events
+
+ @method watchedEvents
+ @for Ember
+ @param obj
+*/
+function watchedEvents(obj) {
+ var listeners = obj[META_KEY].listeners, ret = [];
+
+ if (listeners) {
+ for(var eventName in listeners) {
+ if (listeners[eventName]) { ret.push(eventName); }
+ }
+ }
+ return ret;
+}
+
+/**
+ @method sendEvent
+ @for Ember
+ @param obj
+ @param {String} eventName
+ @param {Array} params
+ @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 (!actions) {
+ var meta = obj[META_KEY];
+ actions = meta && meta.listeners && meta.listeners[eventName];
+ }
+
+ if (!actions) { return; }
+
+ for (var i = actions.length - 1; i >= 0; i--) { // looping in reverse for once listeners
+ if (!actions[i] || actions[i][3] === true) { continue; }
+
+ var target = actions[i][0],
+ method = actions[i][1],
+ once = actions[i][2];
+
+ if (once) { removeListener(obj, eventName, target, method); }
+ if (!target) { target = obj; }
+ if ('string' === typeof method) { method = target[method]; }
+ if (params) {
+ method.apply(target, params);
+ } else {
+ method.apply(target);
+ }
+ }
+ return true;
+}
+
+/**
+ @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];
+
+ return !!(actions && actions.length);
+}
+
+/**
+ @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];
+
+ 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]);
+ }
+
+ return ret;
+}
+
+Ember.addListener = addListener;
+Ember.removeListener = removeListener;
+Ember._suspendListener = suspendListener;
+Ember._suspendListeners = suspendListeners;
+Ember.sendEvent = sendEvent;
+Ember.hasListeners = hasListeners;
+Ember.watchedEvents = watchedEvents;
+Ember.listenersFor = listenersFor;
+Ember.listenersDiff = actionsDiff;
+Ember.listenersUnion = actionsUnion;
+
+})();
+
+
+
+(function() {
+// Ember.Logger
+// Ember.watch.flushPending
+// Ember.beginPropertyChanges, Ember.endPropertyChanges
+// Ember.guidFor, Ember.tryFinally
+
+/**
+@module ember-metal
+*/
+
+// ..........................................................
+// HELPERS
+//
+
+var slice = [].slice,
+ forEach = Ember.ArrayPolyfills.forEach;
+
+// invokes passed params - normalizing so you can pass target/func,
+// target/string or just func
+function invoke(target, method, args, ignore) {
+
+ if (method === undefined) {
+ method = target;
+ target = undefined;
+ }
+
+ if ('string' === typeof method) { method = target[method]; }
+ if (args && ignore > 0) {
+ args = args.length > ignore ? slice.call(args, ignore) : null;
+ }
+
+ return Ember.handleErrors(function() {
+ // IE8's Function.prototype.apply doesn't accept undefined/null arguments.
+ return method.apply(target || this, args || []);
+ }, this);
+}
+
+
+// ..........................................................
+// RUNLOOP
+//
+
+var timerMark; // used by timers...
+
+/**
+Ember RunLoop (Private)
+
+@class RunLoop
+@namespace Ember
+@private
+@constructor
+*/
+var RunLoop = function(prev) {
+ this._prev = prev || null;
+ this.onceTimers = {};
+};
+
+RunLoop.prototype = {
+ /**
+ @method end
+ */
+ end: function() {
+ this.flush();
+ },
+
+ /**
+ @method prev
+ */
+ prev: function() {
+ return this._prev;
+ },
+
+ // ..........................................................
+ // Delayed Actions
+ //
+
+ /**
+ @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;
+ },
+
+ /**
+ @method flush
+ @param {String} queueName
+ */
+ flush: function(queueName) {
+ var queueNames, idx, len, queue, log;
+
+ if (!this._queues) { return this; } // nothing to do
+
+ function iter(item) {
+ invoke(item.target, item.method, item.args);
+ }
+
+ function tryable() {
+ forEach.call(queue, iter);
+ }
+
+ Ember.watch.flushPending(); // make sure all chained watchers are setup
+
+ if (queueName) {
+ while (this._queues && (queue = this._queues[queueName])) {
+ this._queues[queueName] = null;
+
+ // 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'); }
+
+ Ember.beginPropertyChanges();
+
+ Ember.tryFinally(tryable, Ember.endPropertyChanges);
+
+ if (log) { Ember.Logger.log('End: Flush Sync Queue'); }
+
+ } else {
+ forEach.call(queue, iter);
+ }
+ }
+
+ } else {
+ queueNames = Ember.run.queues;
+ len = queueNames.length;
+ idx = 0;
+
+ outerloop:
+ while (idx < len) {
+ queueName = queueNames[idx];
+ queue = this._queues && this._queues[queueName];
+ delete this._queues[queueName];
+
+ 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'); }
+
+ Ember.beginPropertyChanges();
+
+ Ember.tryFinally(tryable, Ember.endPropertyChanges);
+
+ if (log) { Ember.Logger.log('End: Flush Sync Queue'); }
+ } else {
+ forEach.call(queue, iter);
+ }
+ }
+
+ // 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;
+ }
+ }
+
+ idx++;
+ }
+ }
+
+ timerMark = null;
+
+ return this;
+ }
+
+};
+
+Ember.RunLoop = RunLoop;
+
+// ..........................................................
+// Ember.run - this is ideally the only public API the dev sees
+//
+
+/**
+ Runs the passed target and method inside of a RunLoop, ensuring any
+ deferred actions including bindings and views updates are flushed at the
+ end.
+
+ Normally you should not need to invoke this method yourself. However if
+ you are implementing raw event handlers when interfacing with other
+ libraries or plugins, you should probably wrap all of your code inside this
+ call.
+
+ ```javascript
+ Ember.run(function(){
+ // code to be execute within a RunLoop
+ });
+ ```
+
+ @class run
+ @namespace Ember
+ @static
+ @constructor
+ @param {Object} [target] target of method to call
+ @param {Function|String} method Method to invoke.
+ May be a function or a string. If you pass a string
+ then it will be looked up on the passed target.
+ @param {Object} [args*] Any additional arguments you wish to pass to the method.
+ @return {Object} return value from invoking the passed function.
+*/
+Ember.run = function(target, method) {
+ var loop,
+ args = arguments;
+ run.begin();
+
+ function tryable() {
+ if (target || method) {
+ return invoke(target, method, args, 2);
+ }
+ }
+
+ return Ember.tryFinally(tryable, run.end);
+};
+
+var run = Ember.run;
+
+
+/**
+ Begins a new RunLoop. Any deferred actions invoked after the begin will
+ be buffered until you invoke a matching call to `Ember.run.end()`. This is
+ an lower-level way to use a RunLoop instead of using `Ember.run()`.
+
+ ```javascript
+ Ember.run.begin();
+ // code to be execute within a RunLoop
+ Ember.run.end();
+ ```
+
+ @method begin
+ @return {void}
+*/
+Ember.run.begin = function() {
+ run.currentRunLoop = new RunLoop(run.currentRunLoop);
+};
+
+/**
+ Ends a RunLoop. This must be called sometime after you call
+ `Ember.run.begin()` to flush any deferred actions. This is a lower-level way
+ to use a RunLoop instead of using `Ember.run()`.
+
+ ```javascript
+ Ember.run.begin();
+ // code to be execute within a RunLoop
+ Ember.run.end();
+ ```
+
+ @method end
+ @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);
+};
+
+/**
+ Array of named queues. This array determines the order in which queues
+ are flushed at the end of the RunLoop. You can define your own queues by
+ simply adding the queue name to this array. Normally you should not need
+ to inspect or modify this property.
+
+ @property queues
+ @type Array
+ @default ['sync', 'actions', 'destroy', 'timers']
+*/
+Ember.run.queues = ['sync', 'actions', 'destroy', 'timers'];
+
+/**
+ Adds the passed target/method and any optional arguments to the named
+ queue to be executed at the end of the RunLoop. If you have not already
+ started a RunLoop when calling this method one will be started for you
+ automatically.
+
+ 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.
+
+ ```javascript
+ Ember.run.schedule('timers', this, function(){
+ // this will be executed at the end of the RunLoop, when timers are run
+ console.log("scheduled on timers queue");
+ });
+
+ Ember.run.schedule('sync', this, function(){
+ // this will be executed at the end of the RunLoop, when bindings are synced
+ console.log("scheduled on sync queue");
+ });
+
+ // Note the functions will be run in order based on the run queues order. Output would be:
+ // scheduled on sync queue
+ // scheduled on timers queue
+ ```
+
+ @method schedule
+ @param {String} queue The name of the queue to schedule against.
+ Default queues are 'sync' and 'actions'
+ @param {Object} [target] target object to use as the context when invoking a method.
+ @param {String|Function} method The method to invoke. If you pass a string it
+ will be resolved on the target object at the time the scheduled item is
+ invoked allowing you to change the target function.
+ @param {Object} [arguments*] Optional arguments to be passed to the queued method.
+ @return {void}
+*/
+Ember.run.schedule = function(queue, target, method) {
+ var loop = run.autorun();
+ loop.schedule.apply(loop, arguments);
+};
+
+var scheduledAutorun;
+function autorun() {
+ scheduledAutorun = null;
+ if (run.currentRunLoop) { run.end(); }
+}
+
+// Used by global test teardown
+Ember.run.hasScheduledTimers = function() {
+ return !!(scheduledAutorun || scheduledLater || scheduledNext);
+};
+
+// Used by global test teardown
+Ember.run.cancelTimers = function () {
+ if (scheduledAutorun) {
+ clearTimeout(scheduledAutorun);
+ scheduledAutorun = null;
+ }
+ if (scheduledLater) {
+ clearTimeout(scheduledLater);
+ scheduledLater = null;
+ }
+ if (scheduledNext) {
+ clearTimeout(scheduledNext);
+ scheduledNext = null;
+ }
+ timers = {};
+};
+
+/**
+ 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;
+};
+
+/**
+ Immediately flushes any events scheduled in the 'sync' queue. Bindings
+ use this queue so this method is a useful way to immediately force all
+ bindings in the application to sync.
+
+ You should call this method anytime you need any changed state to propagate
+ throughout the app immediately without repainting the UI.
+
+ ```javascript
+ Ember.run.sync();
+ ```
+
+ @method sync
+ @return {void}
+*/
+Ember.run.sync = function() {
+ run.autorun();
+ run.currentRunLoop.flush('sync');
+};
+
+// ..........................................................
+// TIMERS
+//
+
+var timers = {}; // active timers...
+
+var scheduledLater;
+function invokeLaterTimers() {
+ scheduledLater = null;
+ var now = (+ new Date()), earliest = -1;
+ for (var key in timers) {
+ if (!timers.hasOwnProperty(key)) { continue; }
+ var timer = timers[key];
+ if (timer && timer.expires) {
+ if (now >= timer.expires) {
+ delete timers[key];
+ invoke(timer.target, timer.method, timer.args, 2);
+ } else {
+ if (earliest<0 || (timer.expires < earliest)) earliest=timer.expires;
+ }
+ }
+ }
+
+ // schedule next timeout to fire...
+ if (earliest > 0) { scheduledLater = setTimeout(invokeLaterTimers, earliest-(+ new Date())); }
+}
+
+/**
+ Invokes the passed target/method and optional arguments after a specified
+ period if time. The last parameter of this method must always be a number
+ of milliseconds.
+
+ You should use this method whenever you need to run some action after a
+ period of time instead of using `setTimeout()`. This method will ensure that
+ items that expire during the same script execution cycle all execute
+ together, which is often more efficient than using a real setTimeout.
+
+ ```javascript
+ Ember.run.later(myContext, function(){
+ // code here will execute within a RunLoop in about 500ms with this == myContext
+ }, 500);
+ ```
+
+ @method later
+ @param {Object} [target] target of method to invoke
+ @param {Function|String} method The method to invoke.
+ If you pass a string it will be resolved on the
+ target at the time the method is invoked.
+ @param {Object} [args*] Optional arguments to pass to the timeout.
+ @param {Number} wait
+ Number of milliseconds to wait.
+ @return {String} a string you can use to cancel the timer in
+ {{#crossLink "Ember/run.cancel"}}{{/crossLink}} later.
+*/
+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;
+ run.once(timers, invokeLaterTimers);
+ return guid;
+};
+
+function invokeOnceTimer(guid, onceTimers) {
+ if (onceTimers[this.tguid]) { delete onceTimers[this.tguid][this.mguid]; }
+ if (timers[guid]) { invoke(this.target, this.method, this.args); }
+ delete timers[guid];
+}
+
+function scheduleOnce(queue, target, method, args) {
+ var tguid = Ember.guidFor(target),
+ mguid = Ember.guidFor(method),
+ onceTimers = run.autorun().onceTimers,
+ guid = onceTimers[tguid] && onceTimers[tguid][mguid],
+ timer;
+
+ 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);
+ }
+
+ return guid;
+}
+
+/**
+ Schedules an item to run one time during the current RunLoop. Calling
+ this method with the same target/method combination will have no effect.
+
+ Note that although you can pass optional arguments these will not be
+ considered when looking for duplicates. New arguments will replace previous
+ calls.
+
+ ```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
+ });
+ ```
+
+ @method once
+ @param {Object} [target] target of method to invoke
+ @param {Function|String} method The method to invoke.
+ If you pass a string it will be resolved on the
+ target at the time the method is invoked.
+ @param {Object} [args*] Optional arguments to pass to the timeout.
+ @return {Object} timer
+*/
+Ember.run.once = function(target, method) {
+ return scheduleOnce('actions', target, method, slice.call(arguments, 2));
+};
+
+Ember.run.scheduleOnce = function(queue, target, method, args) {
+ return scheduleOnce(queue, target, method, slice.call(arguments, 3));
+};
+
+var scheduledNext;
+function invokeNextTimers() {
+ scheduledNext = null;
+ for(var key in timers) {
+ if (!timers.hasOwnProperty(key)) { continue; }
+ var timer = timers[key];
+ if (timer.next) {
+ delete timers[key];
+ invoke(timer.target, timer.method, timer.args, 2);
+ }
+ }
+}
+
+/**
+ Schedules an item to run after control has been returned to the system.
+ This is often equivalent to calling `setTimeout(function() {}, 1)`.
+
+ ```javascript
+ Ember.run.next(myContext, function(){
+ // code to be executed in the next RunLoop, which will be scheduled after the current one
+ });
+ ```
+
+ @method next
+ @param {Object} [target] target of method to invoke
+ @param {Function|String} method The method to invoke.
+ If you pass a string it will be resolved on the
+ target at the time the method is invoked.
+ @param {Object} [args*] Optional arguments to pass to the timeout.
+ @return {Object} timer
+*/
+Ember.run.next = function(target, method) {
+ var guid,
+ timer = {
+ target: target,
+ method: method,
+ args: slice.call(arguments),
+ next: true
+ };
+
+ guid = Ember.guidFor(timer);
+ timers[guid] = timer;
+
+ if (!scheduledNext) { scheduledNext = setTimeout(invokeNextTimers, 1); }
+ return guid;
+};
+
+/**
+ Cancels a scheduled item. Must be a value returned by `Ember.run.later()`,
+ `Ember.run.once()`, or `Ember.run.next()`.
+
+ ```javascript
+ var runNext = Ember.run.next(myContext, function(){
+ // will not be executed
+ });
+ Ember.run.cancel(runNext);
+
+ var runLater = Ember.run.later(myContext, function(){
+ // will not be executed
+ }, 500);
+ Ember.run.cancel(runLater);
+
+ var runOnce = Ember.run.once(myContext, function(){
+ // will not be executed
+ });
+ Ember.run.cancel(runOnce);
+ ```
+
+ @method cancel
+ @param {Object} timer Timer object to cancel
+ @return {void}
+*/
+Ember.run.cancel = function(timer) {
+ delete timers[timer];
+};
+
+})();
+
+
+
+(function() {
+// Ember.Logger
+// get, set, trySet
+// guidFor, isArray, meta
+// addObserver, removeObserver
+// Ember.run.schedule
+/**
+@module ember-metal
+*/
+
+// ..........................................................
+// CONSTANTS
+//
+
+/**
+ Debug parameter you can turn on. This will log all bindings that fire to
+ the console. This should be disabled in production code. Note that you
+ can also enable this from the console or temporarily.
+
+ @property LOG_BINDINGS
+ @for Ember
+ @type Boolean
+ @default false
+*/
+Ember.LOG_BINDINGS = false || !!Ember.ENV.LOG_BINDINGS;
+
+var get = Ember.get,
+ set = Ember.set,
+ guidFor = Ember.guidFor,
+ isGlobalPath = Ember.isGlobalPath;
+
+
+function getWithGlobals(obj, path) {
+ return get(isGlobalPath(path) ? Ember.lookup : obj, path);
+}
+
+// ..........................................................
+// BINDING
+//
+
+var Binding = function(toPath, fromPath) {
+ this._direction = 'fwd';
+ this._from = fromPath;
+ this._to = toPath;
+ this._directionMap = Ember.Map.create();
+};
+
+/**
+@class Binding
+@namespace Ember
+*/
+
+Binding.prototype = {
+ /**
+ This copies the Binding so it can be connected to another object.
+
+ @method copy
+ @return {Ember.Binding}
+ */
+ copy: function () {
+ var copy = new Binding(this._to, this._from);
+ if (this._oneWay) { copy._oneWay = true; }
+ return copy;
+ },
+
+ // ..........................................................
+ // CONFIG
+ //
+
+ /**
+ This will set `from` property path to the specified value. It will not
+ attempt to resolve this property path to an actual object until you
+ connect the binding.
+
+ The binding will search for the property path starting at the root object
+ you pass when you `connect()` the binding. It follows the same rules as
+ `get()` - see that method for more information.
+
+ @method from
+ @param {String} propertyPath the property path to connect to
+ @return {Ember.Binding} `this`
+ */
+ from: function(path) {
+ this._from = path;
+ return this;
+ },
+
+ /**
+ This will set the `to` property path to the specified value. It will not
+ attempt to resolve this property path to an actual object until you
+ connect the binding.
+
+ The binding will search for the property path starting at the root object
+ you pass when you `connect()` the binding. It follows the same rules as
+ `get()` - see that method for more information.
+
+ @method to
+ @param {String|Tuple} propertyPath A property path or tuple
+ @return {Ember.Binding} `this`
+ */
+ to: function(path) {
+ this._to = path;
+ return this;
+ },
+
+ /**
+ Configures the binding as one way. A one-way binding will relay changes
+ on the `from` side to the `to` side, but not the other way around. This
+ means that if you change the `to` side directly, the `from` side may have
+ a different value.
+
+ @method oneWay
+ @return {Ember.Binding} `this`
+ */
+ oneWay: function() {
+ this._oneWay = true;
+ return this;
+ },
+
+ toString: function() {
+ var oneWay = this._oneWay ? '[oneWay]' : '';
+ return "Ember.Binding<" + guidFor(this) + ">(" + this._from + " -> " + this._to + ")" + oneWay;
+ },
+
+ // ..........................................................
+ // CONNECT AND SYNC
+ //
+
+ /**
+ Attempts to connect this binding instance so that it can receive and relay
+ changes. This method will raise an exception if you have not set the
+ from/to properties yet.
+
+ @method connect
+ @param {Object} obj The root object for this binding.
+ @return {Ember.Binding} `this`
+ */
+ connect: function(obj) {
+ Ember.assert('Must pass a valid object to Ember.Binding.connect()', !!obj);
+
+ var fromPath = this._from, toPath = this._to;
+ Ember.trySet(obj, toPath, getWithGlobals(obj, fromPath));
+
+ // add an observer on the object to be notified when the binding should be updated
+ Ember.addObserver(obj, fromPath, this, this.fromDidChange);
+
+ // if the binding is a two-way binding, also set up an observer on the target
+ if (!this._oneWay) { Ember.addObserver(obj, toPath, this, this.toDidChange); }
+
+ this._readyToSync = true;
+
+ return this;
+ },
+
+ /**
+ Disconnects the binding instance. Changes will no longer be relayed. You
+ will not usually need to call this method.
+
+ @method disconnect
+ @param {Object} obj The root object you passed when connecting the binding.
+ @return {Ember.Binding} `this`
+ */
+ disconnect: function(obj) {
+ Ember.assert('Must pass a valid object to Ember.Binding.disconnect()', !!obj);
+
+ var twoWay = !this._oneWay;
+
+ // remove an observer on the object so we're no longer notified of
+ // changes that should update bindings.
+ Ember.removeObserver(obj, this._from, this, this.fromDidChange);
+
+ // if the binding is two-way, remove the observer from the target as well
+ if (twoWay) { Ember.removeObserver(obj, this._to, this, this.toDidChange); }
+
+ this._readyToSync = false; // disable scheduled syncs...
+ return this;
+ },
+
+ // ..........................................................
+ // PRIVATE
+ //
+
+ /* called when the from side changes */
+ fromDidChange: function(target) {
+ this._scheduleSync(target, 'fwd');
+ },
+
+ /* called when the to side changes */
+ toDidChange: function(target) {
+ this._scheduleSync(target, 'back');
+ },
+
+ _scheduleSync: function(obj, dir) {
+ var directionMap = this._directionMap;
+ var existingDir = directionMap.get(obj);
+
+ // if we haven't scheduled the binding yet, schedule it
+ if (!existingDir) {
+ Ember.run.schedule('sync', this, this._sync, obj);
+ directionMap.set(obj, dir);
+ }
+
+ // If both a 'back' and 'fwd' sync have been scheduled on the same object,
+ // default to a 'fwd' sync so that it remains deterministic.
+ if (existingDir === 'back' && dir === 'fwd') {
+ directionMap.set(obj, 'fwd');
+ }
+ },
+
+ _sync: function(obj) {
+ var log = Ember.LOG_BINDINGS;
+
+ // don't synchronize destroyed objects or disconnected bindings
+ if (obj.isDestroyed || !this._readyToSync) { return; }
+
+ // get the direction of the binding for the object we are
+ // synchronizing from
+ var directionMap = this._directionMap;
+ var direction = directionMap.get(obj);
+
+ var fromPath = this._from, toPath = this._to;
+
+ directionMap.remove(obj);
+
+ // if we're synchronizing from the remote object...
+ if (direction === 'fwd') {
+ var fromValue = getWithGlobals(obj, this._from);
+ if (log) {
+ Ember.Logger.log(' ', this.toString(), '->', fromValue, obj);
+ }
+ if (this._oneWay) {
+ Ember.trySet(obj, toPath, fromValue);
+ } else {
+ Ember._suspendObserver(obj, toPath, this, this.toDidChange, function () {
+ Ember.trySet(obj, toPath, fromValue);
+ });
+ }
+ // if we're synchronizing *to* the remote object
+ } else if (direction === 'back') {
+ var toValue = get(obj, this._to);
+ if (log) {
+ Ember.Logger.log(' ', this.toString(), '<-', toValue, obj);
+ }
+ Ember._suspendObserver(obj, fromPath, this, this.fromDidChange, function () {
+ Ember.trySet(Ember.isGlobalPath(fromPath) ? Ember.lookup : obj, fromPath, toValue);
+ });
+ }
+ }
+
+};
+
+function mixinProperties(to, from) {
+ for (var key in from) {
+ if (from.hasOwnProperty(key)) {
+ to[key] = from[key];
+ }
+ }
+}
+
+mixinProperties(Binding, {
+
+ /**
+ See {{#crossLink "Ember.Binding/from"}}{{/crossLink}}
+
+ @method from
+ @static
+ */
+ from: function() {
+ var C = this, binding = new C();
+ return binding.from.apply(binding, arguments);
+ },
+
+ /**
+ See {{#crossLink "Ember.Binding/to"}}{{/crossLink}}
+
+ @method to
+ @static
+ */
+ to: function() {
+ var C = this, binding = new C();
+ return binding.to.apply(binding, arguments);
+ },
+
+ /**
+ Creates a new Binding instance and makes it apply in a single direction.
+ A one-way binding will relay changes on the `from` side object (supplied
+ as the `from` argument) the `to` side, but not the other way around.
+ This means that if you change the "to" side directly, the "from" side may have
+ a different value.
+
+ See {{#crossLink "Binding/oneWay"}}{{/crossLink}}
+
+ @method oneWay
+ @param {String} from from path.
+ @param {Boolean} [flag] (Optional) passing nothing here will make the
+ binding `oneWay`. You can instead pass `false` to disable `oneWay`, making the
+ binding two way again.
+ */
+ oneWay: function(from, flag) {
+ var C = this, binding = new C(null, from);
+ return binding.oneWay(flag);
+ }
+
+});
+
+/**
+ An `Ember.Binding` connects the properties of two objects so that whenever
+ the value of one property changes, the other property will be changed also.
+
+ ## Automatic Creation of Bindings with `/^*Binding/`-named Properties
+
+ You do not usually create Binding objects directly but instead describe
+ bindings in your class or object definition using automatic binding
+ detection.
+
+ 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"):
+
+ ```
+ valueBinding: "MyApp.someController.title"
+ ```
+
+ This will create a binding from `MyApp.someController.title` to the `value`
+ property of your object instance automatically. Now the two values will be
+ kept in sync.
+
+ ## One Way Bindings
+
+ One especially useful binding customization you can use is the `oneWay()`
+ helper. This helper tells Ember that you are only interested in
+ receiving changes on the object you are binding from. For example, if you
+ are binding to a preference and you want to be notified if the preference
+ has changed, but your object will not be changing the preference itself, you
+ could do:
+
+ ```
+ bigTitlesBinding: Ember.Binding.oneWay("MyApp.preferencesController.bigTitles")
+ ```
+
+ This way if the value of `MyApp.preferencesController.bigTitles` changes the
+ `bigTitles` property of your object will change also. However, if you
+ change the value of your `bigTitles` property, it will not update the
+ `preferencesController`.
+
+ One way bindings are almost twice as fast to setup and twice as fast to
+ execute because the binding only has to worry about changes to one side.
+
+ 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).
+
+ ## Adding Bindings Manually
+
+ All of the examples above show you how to configure a custom binding, but the
+ result of these customizations will be a binding template, not a fully active
+ Binding instance. The binding will actually become active only when you
+ instantiate the object the binding belongs to. It is useful however, to
+ understand what actually happens when the binding is activated.
+
+ For a binding to function it must have at least a `from` property and a `to`
+ property. The `from` property path points to the object/key that you want to
+ bind from while the `to` path points to the object/key you want to bind to.
+
+ When you define a custom binding, you are usually describing the property
+ you want to bind from (such as `MyApp.someController.value` in the examples
+ above). When your object is created, it will automatically assign the value
+ you want to bind `to` based on the name of your binding key. In the
+ examples above, during init, Ember objects will effectively call
+ something like this on your binding:
+
+ ```javascript
+ binding = Ember.Binding.from(this.valueBinding).to("value");
+ ```
+
+ This creates a new binding instance based on the template you provide, and
+ sets the to path to the `value` property of the new object. Now that the
+ binding is fully configured with a `from` and a `to`, it simply needs to be
+ connected to become active. This is done through the `connect()` method:
+
+ ```javascript
+ binding.connect(this);
+ ```
+
+ Note that when you connect a binding you pass the object you want it to be
+ connected to. This object will be used as the root for both the from and
+ to side of the binding when inspecting relative paths. This allows the
+ binding to be automatically inherited by subclassed objects as well.
+
+ Now that the binding is connected, it will observe both the from and to side
+ and relay changes.
+
+ If you ever needed to do so (you almost never will, but it is useful to
+ understand this anyway), you could manually create an active binding by
+ using the `Ember.bind()` helper method. (This is the same method used by
+ to setup your bindings on objects):
+
+ ```javascript
+ Ember.bind(MyApp.anotherObject, "value", "MyApp.someController.value");
+ ```
+
+ Both of these code fragments have the same effect as doing the most friendly
+ form of binding creation like so:
+
+ ```javascript
+ MyApp.anotherObject = Ember.Object.create({
+ valueBinding: "MyApp.someController.value",
+
+ // OTHER CODE FOR THIS OBJECT...
+ });
+ ```
+
+ Ember's built in binding creation method makes it easy to automatically
+ create bindings for you. You should always use the highest-level APIs
+ available, even if you understand how it works underneath.
+
+ @class Binding
+ @namespace Ember
+ @since Ember 0.9
+*/
+Ember.Binding = Binding;
+
+
+/**
+ Global helper method to create a new binding. Just pass the root object
+ along with a `to` and `from` path to create and connect the binding.
+
+ @method bind
+ @for Ember
+ @param {Object} obj The root object of the transform.
+ @param {String} to The path to the 'to' side of the binding.
+ Must be relative to obj.
+ @param {String} from The path to the 'from' side of the binding.
+ Must be relative to obj or a global path.
+ @return {Ember.Binding} binding instance
+*/
+Ember.bind = function(obj, to, from) {
+ return new Ember.Binding(to, from).connect(obj);
+};
+
+/**
+ @method oneWay
+ @for Ember
+ @param {Object} obj The root object of the transform.
+ @param {String} to The path to the 'to' side of the binding.
+ Must be relative to obj.
+ @param {String} from The path to the 'from' side of the binding.
+ Must be relative to obj or a global path.
+ @return {Ember.Binding} binding instance
+*/
+Ember.oneWay = function(obj, to, from) {
+ return new Ember.Binding(to, from).oneWay().connect(obj);
+};
+
+})();
+
+
+
+(function() {
+/**
+@module ember-metal
+*/
+
+var Mixin, REQUIRED, Alias,
+ a_map = Ember.ArrayPolyfills.map,
+ a_indexOf = Ember.ArrayPolyfills.indexOf,
+ a_forEach = Ember.ArrayPolyfills.forEach,
+ a_slice = [].slice,
+ EMPTY_META = {}, // dummy for non-writable meta
+ o_create = Ember.create,
+ defineProperty = Ember.defineProperty,
+ guidFor = Ember.guidFor;
+
+function mixinsMeta(obj) {
+ var m = Ember.meta(obj, true), ret = m.mixins;
+ if (!ret) {
+ ret = m.mixins = {};
+ } else if (!m.hasOwnProperty('mixins')) {
+ ret = m.mixins = o_create(ret);
+ }
+ return ret;
+}
+
+function initMixin(mixin, args) {
+ if (args && args.length > 0) {
+ mixin.mixins = a_map.call(args, function(x) {
+ if (x instanceof Mixin) { return x; }
+
+ // Note: Manually setup a primitive mixin here. This is the only
+ // way to actually get a primitive mixin. This way normal creation
+ // of mixins will give you combined mixins...
+ var mixin = new Mixin();
+ mixin.properties = x;
+ return mixin;
+ });
+ }
+ return mixin;
+}
+
+function isMethod(obj) {
+ return 'function' === typeof obj &&
+ obj.isMethod !== false &&
+ obj !== Boolean && obj !== Object && obj !== Number && obj !== Array && obj !== Date && obj !== String;
+}
+
+var CONTINUE = {};
+
+function mixinProperties(mixinsMeta, mixin) {
+ var guid;
+
+ if (mixin instanceof Mixin) {
+ guid = guidFor(mixin);
+ if (mixinsMeta[guid]) { return CONTINUE; }
+ mixinsMeta[guid] = mixin;
+ return mixin.properties;
+ } else {
+ return mixin; // apply anonymous mixin properties
+ }
+}
+
+function concatenatedProperties(props, values, base) {
+ var concats;
+
+ // reset before adding each new mixin to pickup concats from previous
+ concats = values.concatenatedProperties || base.concatenatedProperties;
+ if (props.concatenatedProperties) {
+ concats = concats ? concats.concat(props.concatenatedProperties) : props.concatenatedProperties;
+ }
+
+ return concats;
+}
+
+function giveDescriptorSuper(meta, key, property, values, descs) {
+ var superProperty;
+
+ // Computed properties override methods, and do not call super to them
+ if (values[key] === undefined) {
+ // Find the original descriptor in a parent mixin
+ superProperty = descs[key];
+ }
+
+ // If we didn't find the original descriptor in a parent mixin, find
+ // it on the original object.
+ superProperty = superProperty || meta.descs[key];
+
+ if (!superProperty || !(superProperty instanceof Ember.ComputedProperty)) {
+ return property;
+ }
+
+ // Since multiple mixins may inherit from the same parent, we need
+ // to clone the computed property so that other mixins do not receive
+ // the wrapped version.
+ property = o_create(property);
+ property.func = Ember.wrap(property.func, superProperty.func);
+
+ return property;
+}
+
+function giveMethodSuper(obj, key, method, values, descs) {
+ var superMethod;
+
+ // Methods overwrite computed properties, and do not call super to them.
+ if (descs[key] === undefined) {
+ // Find the original method in a parent mixin
+ superMethod = values[key];
+ }
+
+ // If we didn't find the original value in a parent mixin, find it in
+ // the original object
+ superMethod = superMethod || obj[key];
+
+ // Only wrap the new method if the original method was a function
+ if ('function' !== typeof superMethod) {
+ return method;
+ }
+
+ return Ember.wrap(method, superMethod);
+}
+
+function applyConcatenatedProperties(obj, key, value, values) {
+ var baseValue = values[key] || obj[key];
+
+ if (baseValue) {
+ if ('function' === typeof baseValue.concat) {
+ return baseValue.concat(value);
+ } else {
+ return Ember.makeArray(baseValue).concat(value);
+ }
+ } else {
+ return Ember.makeArray(value);
+ }
+}
+
+function addNormalizedProperty(base, key, value, meta, descs, values, concats) {
+ if (value instanceof Ember.Descriptor) {
+ if (value === REQUIRED && descs[key]) { return CONTINUE; }
+
+ // Wrap descriptor function to implement
+ // _super() if needed
+ if (value.func) {
+ value = giveDescriptorSuper(meta, key, value, values, descs);
+ }
+
+ descs[key] = value;
+ values[key] = undefined;
+ } else {
+ // impl super if needed...
+ if (isMethod(value)) {
+ value = giveMethodSuper(base, key, value, values, descs);
+ } else if ((concats && a_indexOf.call(concats, key) >= 0) || key === 'concatenatedProperties') {
+ value = applyConcatenatedProperties(base, key, value, values);
+ }
+
+ descs[key] = undefined;
+ values[key] = value;
+ }
+}
+
+function mergeMixins(mixins, m, descs, values, base) {
+ var mixin, props, key, concats, meta;
+
+ function removeKeys(keyName) {
+ delete descs[keyName];
+ delete values[keyName];
+ }
+
+ for(var i=0, l=mixins.length; i<l; i++) {
+ mixin = mixins[i];
+ Ember.assert('Expected hash or Mixin instance, got ' + Object.prototype.toString.call(mixin), typeof mixin === 'object' && mixin !== null && Object.prototype.toString.call(mixin) !== '[object Array]');
+
+ props = mixinProperties(m, mixin);
+ if (props === CONTINUE) { continue; }
+
+ if (props) {
+ meta = Ember.meta(base);
+ concats = concatenatedProperties(props, values, base);
+
+ for (key in props) {
+ if (!props.hasOwnProperty(key)) { continue; }
+ 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);
+ 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) {
+ if (IS_BINDING.test(key)) {
+ var bindings = m.bindings;
+ if (!bindings) {
+ bindings = m.bindings = {};
+ } else if (!m.hasOwnProperty('bindings')) {
+ bindings = m.bindings = o_create(m.bindings);
+ }
+ bindings[key] = value;
+ }
+}
+
+function connectBindings(obj, m) {
+ // TODO Mixin.apply(instance) should disconnect binding if exists
+ var bindings = m.bindings, key, binding, to;
+ if (bindings) {
+ for (key in bindings) {
+ binding = bindings[key];
+ if (binding) {
+ to = key.slice(0, -7); // strip Binding off end
+ if (binding instanceof Ember.Binding) {
+ binding = binding.copy(); // copy prototypes' instance
+ binding.to(to);
+ } else { // binding is string path
+ binding = new Ember.Binding(to, binding);
+ }
+ binding.connect(obj);
+ obj[key] = binding;
+ }
+ }
+ // mark as applied
+ m.bindings = {};
+ }
+}
+
+function finishPartial(obj, m) {
+ connectBindings(obj, m || Ember.meta(obj));
+ return obj;
+}
+
+function followAlias(obj, desc, m, descs, values) {
+ var altKey = desc.methodName, value;
+ if (descs[altKey] || values[altKey]) {
+ value = values[altKey];
+ desc = descs[altKey];
+ } else if (m.descs[altKey]) {
+ desc = m.descs[altKey];
+ value = undefined;
+ } else {
+ desc = undefined;
+ value = obj[altKey];
+ }
+
+ return { desc: desc, value: value };
+}
+
+function updateObservers(obj, key, observer, observerKey, method) {
+ if ('function' !== typeof observer) { return; }
+
+ var paths = observer[observerKey];
+
+ if (paths) {
+ for (var i=0, l=paths.length; i<l; i++) {
+ Ember[method](obj, paths[i], null, key);
+ }
+ }
+}
+
+function replaceObservers(obj, key, observer) {
+ var prevObserver = obj[key];
+
+ updateObservers(obj, key, prevObserver, '__ember_observesBefore__', 'removeBeforeObserver');
+ updateObservers(obj, key, prevObserver, '__ember_observes__', 'removeObserver');
+
+ updateObservers(obj, key, observer, '__ember_observesBefore__', 'addBeforeObserver');
+ updateObservers(obj, key, observer, '__ember_observes__', 'addObserver');
+}
+
+function applyMixin(obj, mixins, partial) {
+ var descs = {}, values = {}, m = Ember.meta(obj),
+ key, value, desc;
+
+ // Go through all mixins and hashes passed in, and:
+ //
+ // * Handle concatenated properties
+ // * Set up _super wrapping if necessary
+ // * Set up computed property descriptors
+ // * Copying `toString` in broken browsers
+ mergeMixins(mixins, mixinsMeta(obj), descs, values, obj);
+
+ for(key in values) {
+ if (key === 'contructor' || !values.hasOwnProperty(key)) { continue; }
+
+ desc = descs[key];
+ value = values[key];
+
+ if (desc === REQUIRED) { continue; }
+
+ while (desc && desc instanceof Alias) {
+ var followed = followAlias(obj, desc, m, descs, values);
+ desc = followed.desc;
+ value = followed.value;
+ }
+
+ if (desc === undefined && value === undefined) { continue; }
+
+ replaceObservers(obj, key, value);
+ detectBinding(obj, key, value, m);
+ defineProperty(obj, key, desc, value, m);
+ }
+
+ if (!partial) { // don't apply to prototype
+ finishPartial(obj, m);
+ }
+
+ return obj;
+}
+
+/**
+ @method mixin
+ @for Ember
+ @param obj
+ @param mixins*
+ @return obj
+*/
+Ember.mixin = function(obj) {
+ var args = a_slice.call(arguments, 1);
+ applyMixin(obj, args, false);
+ return obj;
+};
+
+/**
+ The `Ember.Mixin` class allows you to create mixins, whose properties can be
+ added to other classes. For instance,
+
+ ```javascript
+ App.Editable = Ember.Mixin.create({
+ edit: function() {
+ console.log('starting to edit');
+ this.set('isEditing', true);
+ },
+ isEditing: false
+ });
+
+ // Mix mixins into classes by passing them as the first arguments to
+ // .extend or .create.
+ App.CommentView = Ember.View.extend(App.Editable, {
+ template: Ember.Handlebars.compile('{{#if isEditing}}...{{else}}...{{/if}}')
+ });
+
+ commentView = App.CommentView.create();
+ commentView.edit(); // outputs 'starting to edit'
+ ```
+
+ Note that Mixins are created with `Ember.Mixin.create`, not
+ `Ember.Mixin.extend`.
+
+ @class Mixin
+ @namespace Ember
+*/
+Ember.Mixin = function() { return initMixin(this, arguments); };
+
+Mixin = Ember.Mixin;
+
+Mixin._apply = applyMixin;
+
+Mixin.applyPartial = function(obj) {
+ var args = a_slice.call(arguments, 1);
+ return applyMixin(obj, args, true);
+};
+
+Mixin.finishPartial = finishPartial;
+
+Ember.anyUnprocessedMixins = false;
+
+/**
+ @method create
+ @static
+ @param arguments*
+*/
+Mixin.create = function() {
+ Ember.anyUnprocessedMixins = true;
+ var M = this;
+ return initMixin(new M(), arguments);
+};
+
+var MixinPrototype = Mixin.prototype;
+
+/**
+ @method reopen
+ @param arguments*
+*/
+MixinPrototype.reopen = function() {
+ var mixin, tmp;
+
+ if (this.properties) {
+ mixin = Mixin.create();
+ mixin.properties = this.properties;
+ delete this.properties;
+ this.mixins = [mixin];
+ } else if (!this.mixins) {
+ this.mixins = [];
+ }
+
+ var len = arguments.length, mixins = this.mixins, idx;
+
+ for(idx=0; idx < len; idx++) {
+ mixin = arguments[idx];
+ Ember.assert('Expected hash or Mixin instance, got ' + Object.prototype.toString.call(mixin), typeof mixin === 'object' && mixin !== null && Object.prototype.toString.call(mixin) !== '[object Array]');
+
+ if (mixin instanceof Mixin) {
+ mixins.push(mixin);
+ } else {
+ tmp = Mixin.create();
+ tmp.properties = mixin;
+ mixins.push(tmp);
+ }
+ }
+
+ return this;
+};
+
+/**
+ @method apply
+ @param obj
+ @return applied object
+*/
+MixinPrototype.apply = function(obj) {
+ return applyMixin(obj, [this], false);
+};
+
+MixinPrototype.applyPartial = function(obj) {
+ return applyMixin(obj, [this], true);
+};
+
+function _detect(curMixin, targetMixin, seen) {
+ var guid = guidFor(curMixin);
+
+ if (seen[guid]) { return false; }
+ seen[guid] = true;
+
+ if (curMixin === targetMixin) { return true; }
+ var mixins = curMixin.mixins, loc = mixins ? mixins.length : 0;
+ while (--loc >= 0) {
+ if (_detect(mixins[loc], targetMixin, seen)) { return true; }
+ }
+ return false;
+}
+
+/**
+ @method detect
+ @param obj
+ @return {Boolean}
+*/
+MixinPrototype.detect = function(obj) {
+ if (!obj) { return false; }
+ if (obj instanceof Mixin) { return _detect(obj, this, {}); }
+ var mixins = Ember.meta(obj, false).mixins;
+ if (mixins) {
+ return !!mixins[guidFor(this)];
+ }
+ return false;
+};
+
+MixinPrototype.without = function() {
+ var ret = new Mixin(this);
+ ret._without = a_slice.call(arguments);
+ return ret;
+};
+
+function _keys(ret, mixin, seen) {
+ if (seen[guidFor(mixin)]) { return; }
+ seen[guidFor(mixin)] = true;
+
+ if (mixin.properties) {
+ var props = mixin.properties;
+ for (var key in props) {
+ if (props.hasOwnProperty(key)) { ret[key] = true; }
+ }
+ } else if (mixin.mixins) {
+ a_forEach.call(mixin.mixins, function(x) { _keys(ret, x, seen); });
+ }
+}
+
+MixinPrototype.keys = function() {
+ var keys = {}, seen = {}, ret = [];
+ _keys(keys, this, seen);
+ for(var key in keys) {
+ if (keys.hasOwnProperty(key)) { ret.push(key); }
+ }
+ return ret;
+};
+
+// returns the mixins currently applied to the specified object
+// TODO: Make Ember.mixin
+Mixin.mixins = function(obj) {
+ var mixins = Ember.meta(obj, false).mixins, ret = [];
+
+ if (!mixins) { return ret; }
+
+ for (var key in mixins) {
+ var mixin = mixins[key];
+
+ // skip primitive mixins since these are always anonymous
+ if (!mixin.properties) { ret.push(mixin); }
+ }
+
+ return ret;
+};
+
+REQUIRED = new Ember.Descriptor();
+REQUIRED.toString = function() { return '(Required Property)'; };
+
+/**
+ Denotes a required property for a mixin
+
+ @method required
+ @for Ember
+*/
+Ember.required = function() {
+ return REQUIRED;
+};
+
+Alias = function(methodName) {
+ this.methodName = methodName;
+};
+Alias.prototype = new Ember.Descriptor();
+
+/**
+ Makes a property or method available via an additional name.
+
+ ```javascript
+ App.PaintSample = Ember.Object.extend({
+ color: 'red',
+ colour: Ember.alias('color'),
+ name: function(){
+ return "Zed";
+ },
+ moniker: Ember.alias("name")
+ });
+
+ var paintSample = App.PaintSample.create()
+ paintSample.get('colour'); // 'red'
+ paintSample.moniker(); // 'Zed'
+ ```
+
+ @method alias
+ @for Ember
+ @param {String} methodName name of the method or property to alias
+ @return {Ember.Descriptor}
+ @deprecated Use `Ember.aliasMethod` or `Ember.computed.alias` instead
+*/
+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);
+
+/**
+ Makes a method available via an additional name.
+
+ ```javascript
+ App.Person = Ember.Object.extend({
+ name: function(){
+ return 'Tomhuda Katzdale';
+ },
+ moniker: Ember.aliasMethod('name')
+ });
+
+ var goodGuy = App.Person.create()
+ ```
+
+ @method aliasMethod
+ @for Ember
+ @param {String} methodName name of the method to alias
+ @return {Ember.Descriptor}
+*/
+Ember.aliasMethod = function(methodName) {
+ return new Alias(methodName);
+};
+
+// ..........................................................
+// OBSERVER HELPER
+//
+
+/**
+ @method observer
+ @for Ember
+ @param {Function} func
+ @param {String} propertyNames*
+ @return func
+*/
+Ember.observer = function(func) {
+ var paths = a_slice.call(arguments, 1);
+ func.__ember_observes__ = paths;
+ return func;
+};
+
+// If observers ever become asynchronous, Ember.immediateObserver
+// must remain synchronous.
+/**
+ @method immediateObserver
+ @for Ember
+ @param {Function} func
+ @param {String} propertyNames*
+ @return func
+*/
+Ember.immediateObserver = function() {
+ for (var i=0, l=arguments.length; i<l; i++) {
+ var arg = arguments[i];
+ Ember.assert("Immediate observers must observe internal properties only, not properties on other objects.", typeof arg !== "string" || arg.indexOf('.') === -1);
+ }
+
+ return Ember.observer.apply(this, arguments);
+};
+
+/**
+ @method beforeObserver
+ @for Ember
+ @param {Function} func
+ @param {String} propertyNames*
+ @return func
+*/
+Ember.beforeObserver = function(func) {
+ var paths = a_slice.call(arguments, 1);
+ func.__ember_observesBefore__ = paths;
+ return func;
+};
+
+})();
+
+
+
+(function() {
+/**
+Ember Metal
+
+@module ember
+@submodule ember-metal
+*/
+
+})();
+
+(function() {
+define("rsvp",
+ [],
+ function() {
+ "use strict";
+ var browserGlobal = (typeof window !== 'undefined') ? window : {};
+
+ var MutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver;
+ var RSVP, async;
+
+ if (typeof process !== 'undefined' &&
+ {}.toString.call(process) === '[object process]') {
+ async = function(callback, binding) {
+ process.nextTick(function() {
+ callback.call(binding);
+ });
+ };
+ } else if (MutationObserver) {
+ var queue = [];
+
+ var observer = new MutationObserver(function() {
+ var toProcess = queue.slice();
+ queue = [];
+
+ toProcess.forEach(function(tuple) {
+ var callback = tuple[0], binding = tuple[1];
+ callback.call(binding);
+ });
+ });
+
+ var element = document.createElement('div');
+ observer.observe(element, { attributes: true });
+
+ // Chrome Memory Leak: https://bugs.webkit.org/show_bug.cgi?id=93661
+ window.addEventListener('unload', function(){
+ observer.disconnect();
+ observer = null;
+ });
+
+ async = function(callback, binding) {
+ queue.push([callback, binding]);
+ element.setAttribute('drainQueue', 'drainQueue');
+ };
+ } else {
+ async = function(callback, binding) {
+ setTimeout(function() {
+ callback.call(binding);
+ }, 1);
+ };
+ }
+
+ var Event = function(type, options) {
+ this.type = type;
+
+ for (var option in options) {
+ if (!options.hasOwnProperty(option)) { continue; }
+
+ this[option] = options[option];
+ }
+ };
+
+ var indexOf = function(callbacks, callback) {
+ for (var i=0, l=callbacks.length; i<l; i++) {
+ if (callbacks[i][0] === callback) { return i; }
+ }
+
+ return -1;
+ };
+
+ var callbacksFor = function(object) {
+ var callbacks = object._promiseCallbacks;
+
+ if (!callbacks) {
+ callbacks = object._promiseCallbacks = {};
+ }
+
+ return callbacks;
+ };
+
+ var EventTarget = {
+ mixin: function(object) {
+ object.on = this.on;
+ object.off = this.off;
+ object.trigger = this.trigger;
+ return object;
+ },
+
+ on: function(eventNames, callback, binding) {
+ var allCallbacks = callbacksFor(this), callbacks, eventName;
+ eventNames = eventNames.split(/\s+/);
+ binding = binding || this;
+
+ while (eventName = eventNames.shift()) {
+ callbacks = allCallbacks[eventName];
+
+ if (!callbacks) {
+ callbacks = allCallbacks[eventName] = [];
+ }
+
+ if (indexOf(callbacks, callback) === -1) {
+ callbacks.push([callback, binding]);
+ }
+ }
+ },
+
+ off: function(eventNames, callback) {
+ var allCallbacks = callbacksFor(this), callbacks, eventName, index;
+ eventNames = eventNames.split(/\s+/);
+
+ while (eventName = eventNames.shift()) {
+ if (!callback) {
+ allCallbacks[eventName] = [];
+ continue;
+ }
+
+ callbacks = allCallbacks[eventName];
+
+ index = indexOf(callbacks, callback);
+
+ if (index !== -1) { callbacks.splice(index, 1); }
+ }
+ },
+
+ trigger: function(eventName, options) {
+ var allCallbacks = callbacksFor(this),
+ callbacks, callbackTuple, callback, binding, event;
+
+ if (callbacks = allCallbacks[eventName]) {
+ // Don't cache the callbacks.length since it may grow
+ for (var i=0; i<callbacks.length; i++) {
+ callbackTuple = callbacks[i];
+ callback = callbackTuple[0];
+ binding = callbackTuple[1];
+
+ if (typeof options !== 'object') {
+ options = { detail: options };
+ }
+
+ event = new Event(eventName, options);
+ callback.call(binding, event);
+ }
+ }
+ }
+ };
+
+ var Promise = function() {
+ this.on('promise:resolved', function(event) {
+ this.trigger('success', { detail: event.detail });
+ }, this);
+
+ this.on('promise:failed', function(event) {
+ this.trigger('error', { detail: event.detail });
+ }, this);
+ };
+
+ var noop = function() {};
+
+ var invokeCallback = function(type, promise, callback, event) {
+ var hasCallback = typeof callback === 'function',
+ value, error, succeeded, failed;
+
+ if (hasCallback) {
+ try {
+ value = callback(event.detail);
+ succeeded = true;
+ } catch(e) {
+ failed = true;
+ error = e;
+ }
+ } else {
+ value = event.detail;
+ succeeded = true;
+ }
+
+ if (value && typeof value.then === 'function') {
+ value.then(function(value) {
+ promise.resolve(value);
+ }, function(error) {
+ promise.reject(error);
+ });
+ } else if (hasCallback && succeeded) {
+ promise.resolve(value);
+ } else if (failed) {
+ promise.reject(error);
+ } else {
+ promise[type](value);
+ }
+ };
+
+ Promise.prototype = {
+ then: function(done, fail) {
+ var thenPromise = new Promise();
+
+ if (this.isResolved) {
+ RSVP.async(function() {
+ invokeCallback('resolve', thenPromise, done, { detail: this.resolvedValue });
+ }, this);
+ }
+
+ if (this.isRejected) {
+ RSVP.async(function() {
+ invokeCallback('reject', thenPromise, fail, { detail: this.rejectedValue });
+ }, this);
+ }
+
+ this.on('promise:resolved', function(event) {
+ invokeCallback('resolve', thenPromise, done, event);
+ });
+
+ this.on('promise:failed', function(event) {
+ invokeCallback('reject', thenPromise, fail, event);
+ });
+
+ return thenPromise;
+ },
+
+ resolve: function(value) {
+ resolve(this, value);
+
+ this.resolve = noop;
+ this.reject = noop;
+ },
+
+ reject: function(value) {
+ reject(this, value);
+
+ this.resolve = noop;
+ this.reject = noop;
+ }
+ };
+
+ function resolve(promise, value) {
+ RSVP.async(function() {
+ promise.trigger('promise:resolved', { detail: value });
+ promise.isResolved = true;
+ promise.resolvedValue = value;
+ });
+ }
+
+ function reject(promise, value) {
+ RSVP.async(function() {
+ promise.trigger('promise:failed', { detail: value });
+ promise.isRejected = true;
+ promise.rejectedValue = value;
+ });
+ }
+
+ function all(promises) {
+ var i, results = [];
+ var allPromise = new Promise();
+ var remaining = promises.length;
+
+ if (remaining === 0) {
+ allPromise.resolve([]);
+ }
+
+ var resolver = function(index) {
+ return function(value) {
+ resolve(index, value);
+ };
+ };
+
+ var resolve = function(index, value) {
+ results[index] = value;
+ if (--remaining === 0) {
+ allPromise.resolve(results);
+ }
+ };
+
+ var reject = function(error) {
+ allPromise.reject(error);
+ };
+
+ for (i = 0; i < remaining; i++) {
+ promises[i].then(resolver(i), reject);
+ }
+ return allPromise;
+ }
+
+ EventTarget.mixin(Promise.prototype);
+
+ RSVP = { async: async, Promise: Promise, Event: Event, EventTarget: EventTarget, all: all, raiseOnUncaughtExceptions: true };
+ return RSVP;
+ });
+
+})();
+
+(function() {
+define("container",
+ [],
+ function() {
+
+ var objectCreate = Object.create || function(parent) {
+ function F() {}
+ F.prototype = parent;
+ return new F();
+ };
+
+ function InheritingDict(parent) {
+ this.parent = parent;
+ this.dict = {};
+ }
+
+ InheritingDict.prototype = {
+ get: function(key) {
+ var dict = this.dict;
+
+ if (dict.hasOwnProperty(key)) {
+ return dict[key];
+ }
+
+ if (this.parent) {
+ return this.parent.get(key);
+ }
+ },
+
+ set: function(key, value) {
+ this.dict[key] = value;
+ },
+
+ has: function(key) {
+ var dict = this.dict;
+
+ if (dict.hasOwnProperty(key)) {
+ return true;
+ }
+
+ if (this.parent) {
+ return this.parent.has(key);
+ }
+
+ return false;
+ },
+
+ eachLocal: function(callback, binding) {
+ var dict = this.dict;
+
+ for (var prop in dict) {
+ if (dict.hasOwnProperty(prop)) {
+ callback.call(binding, prop, dict[prop]);
+ }
+ }
+ }
+ };
+
+ function Container(parent) {
+ this.parent = parent;
+ this.children = [];
+
+ this.resolver = parent && parent.resolver || function() {};
+ this.registry = new InheritingDict(parent && parent.registry);
+ this.cache = new InheritingDict(parent && parent.cache);
+ this.typeInjections = new InheritingDict(parent && parent.typeInjections);
+ this.injections = {};
+ this._options = new InheritingDict(parent && parent._options);
+ this._typeOptions = new InheritingDict(parent && parent._typeOptions);
+ }
+
+ Container.prototype = {
+ child: function() {
+ var container = new Container(this);
+ this.children.push(container);
+ return container;
+ },
+
+ set: function(object, key, value) {
+ object[key] = value;
+ },
+
+ register: function(type, name, factory, options) {
+ var fullName;
+
+
+ if (type.indexOf(':') !== -1){
+ options = factory;
+ factory = name;
+ fullName = type;
+ } else {
+ Ember.deprecate('register("'+type +'", "'+ name+'") is now deprecated in-favour of register("'+type+':'+name+'");', true);
+ fullName = type + ":" + name;
+ }
+
+ this.registry.set(fullName, factory);
+ this._options.set(fullName, options || {});
+ },
+
+ resolve: function(fullName) {
+ return this.resolver(fullName) || this.registry.get(fullName);
+ },
+
+ lookup: function(fullName) {
+ if (this.cache.has(fullName)) {
+ return this.cache.get(fullName);
+ }
+
+ var value = instantiate(this, fullName);
+
+ if (!value) { return; }
+
+ if (isSingleton(this, fullName)) {
+ this.cache.set(fullName, value);
+ }
+
+ return value;
+ },
+
+ has: function(fullName) {
+ if (this.cache.has(fullName)) {
+ return true;
+ }
+
+ return !!factoryFor(this, fullName);
+ },
+
+ optionsForType: function(type, options) {
+ if (this.parent) { illegalChildOperation('optionsForType'); }
+
+ this._typeOptions.set(type, options);
+ },
+
+ options: function(type, options) {
+ this.optionsForType(type, options);
+ },
+
+ typeInjection: function(type, property, fullName) {
+ if (this.parent) { illegalChildOperation('typeInjection'); }
+
+ var injections = this.typeInjections.get(type);
+ if (!injections) {
+ injections = [];
+ this.typeInjections.set(type, injections);
+ }
+ injections.push({ property: property, fullName: fullName });
+ },
+
+ injection: function(factoryName, property, injectionName) {
+ if (this.parent) { illegalChildOperation('injection'); }
+
+ if (factoryName.indexOf(':') === -1) {
+ return this.typeInjection(factoryName, property, injectionName);
+ }
+
+ var injections = this.injections[factoryName] = this.injections[factoryName] || [];
+ injections.push({ property: property, fullName: injectionName });
+ },
+
+ destroy: function() {
+ this.isDestroyed = true;
+
+ for (var i=0, l=this.children.length; i<l; i++) {
+ this.children[i].destroy();
+ }
+
+ this.children = [];
+
+ eachDestroyable(this, function(item) {
+ item.isDestroying = true;
+ });
+
+ eachDestroyable(this, function(item) {
+ item.destroy();
+ });
+
+ delete this.parent;
+ this.isDestroyed = true;
+ },
+
+ reset: function() {
+ for (var i=0, l=this.children.length; i<l; i++) {
+ resetCache(this.children[i]);
+ }
+ resetCache(this);
+ }
+ };
+
+ function illegalChildOperation(operation) {
+ throw new Error(operation + " is not currently supported on child containers");
+ }
+
+ function isSingleton(container, fullName) {
+ var singleton = option(container, fullName, 'singleton');
+
+ return singleton !== false;
+ }
+
+ function buildInjections(container, injections) {
+ var hash = {};
+
+ if (!injections) { return hash; }
+
+ var injection, lookup;
+
+ for (var i=0, l=injections.length; i<l; i++) {
+ injection = injections[i];
+ lookup = container.lookup(injection.fullName);
+ hash[injection.property] = lookup;
+ }
+
+ return hash;
+ }
+
+ function option(container, fullName, optionName) {
+ var options = container._options.get(fullName);
+
+ if (options && options[optionName] !== undefined) {
+ return options[optionName];
+ }
+
+ var type = fullName.split(":")[0];
+ options = container._typeOptions.get(type);
+
+ if (options) {
+ return options[optionName];
+ }
+ }
+
+ function factoryFor(container, fullName) {
+ return container.resolve(fullName);
+ }
+
+ function instantiate(container, fullName) {
+ var factory = factoryFor(container, fullName);
+
+ var splitName = fullName.split(":"),
+ type = splitName[0], name = splitName[1],
+ value;
+
+ if (option(container, fullName, 'instantiate') === false) {
+ return factory;
+ }
+
+ if (factory) {
+ var injections = [];
+ injections = injections.concat(container.typeInjections.get(type) || []);
+ injections = injections.concat(container.injections[fullName] || []);
+
+ var hash = buildInjections(container, injections);
+ hash.container = container;
+ hash._debugContainerKey = fullName;
+
+ value = factory.create(hash);
+
+ return value;
+ }
+ }
+
+ function eachDestroyable(container, callback) {
+ container.cache.eachLocal(function(key, value) {
+ if (option(container, key, 'instantiate') === false) { return; }
+ callback(value);
+ });
+ }
+
+ function resetCache(container) {
+ container.cache.eachLocal(function(key, value) {
+ if (option(container, key, 'instantiate') === false) { return; }
+ value.destroy();
+ });
+ container.cache.dict = {};
+ }
+
+ return Container;
+});
+
+})();
+
+(function() {
+/*globals ENV */
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+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' | A Ember class (created using Ember.Object.extend()) |
+ | 'instance' | A Ember object instance |
+ | 'error' | An instance of the Error object |
+ | 'object' | A JavaScript object not inheriting from Ember.Object |
+
+ 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 item {Object} 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;
+};
+
+/**
+ Returns true if the passed value is null or undefined. This avoids errors
+ from JSLint complaining about use of ==, which can be technically
+ confusing.
+
+ ```javascript
+ Ember.isNone(); // true
+ Ember.isNone(null); // true
+ Ember.isNone(undefined); // true
+ Ember.isNone(''); // false
+ Ember.isNone([]); // false
+ Ember.isNone(function(){}); // false
+ ```
+
+ @method isNone
+ @for Ember
+ @param {Object} obj Value to test
+ @return {Boolean}
+*/
+Ember.isNone = function(obj) {
+ return obj === null || obj === undefined;
+};
+Ember.none = Ember.deprecateFunc("Ember.none is deprecated. Please use Ember.isNone instead.", Ember.isNone);
+
+/**
+ Verifies that a value is `null` or an empty string, empty array,
+ or empty function.
+
+ Constrains the rules on `Ember.isNone` by returning false for empty
+ string and empty arrays.
+
+ ```javascript
+ Ember.isEmpty(); // true
+ Ember.isEmpty(null); // true
+ Ember.isEmpty(undefined); // true
+ Ember.isEmpty(''); // true
+ Ember.isEmpty([]); // true
+ Ember.isEmpty('Adam Hawkins'); // false
+ Ember.isEmpty([0,1,2]); // false
+ ```
+
+ @method isEmpty
+ @for Ember
+ @param {Object} obj Value to test
+ @return {Boolean}
+*/
+Ember.isEmpty = function(obj) {
+ return obj === null || obj === undefined || (obj.length === 0 && typeof obj !== 'function') || (typeof obj === 'object' && Ember.get(obj, 'length') === 0);
+};
+Ember.empty = Ember.deprecateFunc("Ember.empty is deprecated. Please use Ember.isEmpty instead.", Ember.isEmpty) ;
+
+/**
+ This will compare two javascript values of possibly different types.
+ It will tell you which one is greater than the other by returning:
+
+ - -1 if the first is smaller than the second,
+ - 0 if both are equal,
+ - 1 if the first is greater than the second.
+
+ The order is calculated based on `Ember.ORDER_DEFINITION`, if types are different.
+ In case they have the same type an appropriate comparison for this type is made.
+
+ ```javascript
+ Ember.compare('hello', 'hello'); // 0
+ Ember.compare('abc', 'dfg'); // -1
+ Ember.compare(2, 1); // 1
+ ```
+
+ @method compare
+ @for Ember
+ @param {Object} v First value to compare
+ @param {Object} w Second value to compare
+ @return {Number} -1 if v < w, 0 if v = w and 1 if v > w.
+*/
+Ember.compare = function compare(v, w) {
+ if (v === w) { return 0; }
+
+ var type1 = Ember.typeOf(v);
+ var type2 = Ember.typeOf(w);
+
+ var Comparable = Ember.Comparable;
+ if (Comparable) {
+ if (type1==='instance' && Comparable.detect(v.constructor)) {
+ return v.constructor.compare(v, w);
+ }
+
+ if (type2 === 'instance' && Comparable.detect(w.constructor)) {
+ return 1-w.constructor.compare(w, v);
+ }
+ }
+
+ // If we haven't yet generated a reverse-mapping of Ember.ORDER_DEFINITION,
+ // do so now.
+ var mapping = Ember.ORDER_DEFINITION_MAPPING;
+ if (!mapping) {
+ var order = Ember.ORDER_DEFINITION;
+ mapping = Ember.ORDER_DEFINITION_MAPPING = {};
+ var idx, len;
+ for (idx = 0, len = order.length; idx < len; ++idx) {
+ mapping[order[idx]] = idx;
+ }
+
+ // We no longer need Ember.ORDER_DEFINITION.
+ delete Ember.ORDER_DEFINITION;
+ }
+
+ var type1Index = mapping[type1];
+ var type2Index = mapping[type2];
+
+ if (type1Index < type2Index) { return -1; }
+ if (type1Index > type2Index) { return 1; }
+
+ // types are equal - so we have to check values now
+ switch (type1) {
+ case 'boolean':
+ case 'number':
+ if (v < w) { return -1; }
+ if (v > w) { return 1; }
+ return 0;
+
+ case 'string':
+ var comp = v.localeCompare(w);
+ if (comp < 0) { return -1; }
+ if (comp > 0) { return 1; }
+ return 0;
+
+ case 'array':
+ var vLen = v.length;
+ var wLen = w.length;
+ var l = Math.min(vLen, wLen);
+ var r = 0;
+ var i = 0;
+ while (r === 0 && i < l) {
+ r = compare(v[i],w[i]);
+ i++;
+ }
+ if (r !== 0) { return r; }
+
+ // all elements are equal now
+ // shorter array should be ordered first
+ if (vLen < wLen) { return -1; }
+ if (vLen > wLen) { return 1; }
+ // arrays are equal now
+ return 0;
+
+ case 'instance':
+ if (Ember.Comparable && Ember.Comparable.detect(v)) {
+ return v.compare(v, w);
+ }
+ return 0;
+
+ case 'date':
+ var vNum = v.getTime();
+ var wNum = w.getTime();
+ if (vNum < wNum) { return -1; }
+ if (vNum > wNum) { return 1; }
+ return 0;
+
+ default:
+ return 0;
+ }
+};
+
+function _copy(obj, deep, seen, copies) {
+ var ret, loc, key;
+
+ // primitive data types are immutable, just return them.
+ if ('object' !== typeof obj || obj===null) return obj;
+
+ // avoid cyclical loops
+ if (deep && (loc=indexOf(seen, obj))>=0) return copies[loc];
+
+ Ember.assert('Cannot clone an Ember.Object that does not implement Ember.Copyable', !(obj instanceof Ember.Object) || (Ember.Copyable && Ember.Copyable.detect(obj)));
+
+ // IMPORTANT: this specific test will detect a native array only. Any other
+ // object will need to implement Copyable.
+ if (Ember.typeOf(obj) === 'array') {
+ ret = obj.slice();
+ if (deep) {
+ loc = ret.length;
+ while(--loc>=0) ret[loc] = _copy(ret[loc], deep, seen, copies);
+ }
+ } else if (Ember.Copyable && Ember.Copyable.detect(obj)) {
+ ret = obj.copy(deep, seen, copies);
+ } else {
+ ret = {};
+ for(key in obj) {
+ if (!obj.hasOwnProperty(key)) continue;
+
+ // Prevents browsers that don't respect non-enumerability from
+ // copying internal Ember properties
+ if (key.substring(0,2) === '__') continue;
+
+ ret[key] = deep ? _copy(obj[key], deep, seen, copies) : obj[key];
+ }
+ }
+
+ if (deep) {
+ seen.push(obj);
+ copies.push(ret);
+ }
+
+ return ret;
+}
+
+/**
+ Creates a clone of the passed object. This function can take just about
+ any type of object and create a clone of it, including primitive values
+ (which are not actually cloned because they are immutable).
+
+ If the passed object implements the `clone()` method, then this function
+ will simply call that method and return the result.
+
+ @method copy
+ @for Ember
+ @param {Object} object The object to clone
+ @param {Boolean} deep If true, a deep copy of the object is made
+ @return {Object} The cloned object
+*/
+Ember.copy = function(obj, deep) {
+ // fast paths
+ if ('object' !== typeof obj || obj===null) return obj; // can't copy primitives
+ if (Ember.Copyable && Ember.Copyable.detect(obj)) return obj.copy(deep);
+ return _copy(obj, deep, deep ? [] : null, deep ? [] : null);
+};
+
+/**
+ Convenience method to inspect an object. This method will attempt to
+ convert the object into a useful string description.
+
+ It is a pretty simple implementation. If you want something more robust,
+ use something like JSDump: https://github.com/NV/jsDump
+
+ @method inspect
+ @for Ember
+ @param {Object} obj The object you want to inspect.
+ @return {String} A description of the object
+*/
+Ember.inspect = function(obj) {
+ if (typeof obj !== 'object' || obj === null) {
+ return obj + '';
+ }
+
+ var v, ret = [];
+ for(var key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ v = obj[key];
+ if (v === 'toString') { continue; } // ignore useless items
+ if (Ember.typeOf(v) === 'function') { v = "function() { ... }"; }
+ ret.push(key + ": " + v);
+ }
+ }
+ return "{" + ret.join(", ") + "}";
+};
+
+/**
+ Compares two objects, returning true if they are logically equal. This is
+ a deeper comparison than a simple triple equal. For sets it will compare the
+ internal objects. For any other object that implements `isEqual()` it will
+ respect that method.
+
+ ```javascript
+ Ember.isEqual('hello', 'hello'); // true
+ Ember.isEqual(1, 2); // false
+ Ember.isEqual([4,2], [4,2]); // false
+ ```
+
+ @method isEqual
+ @for Ember
+ @param {Object} a first object to compare
+ @param {Object} b second object to compare
+ @return {Boolean}
+*/
+Ember.isEqual = function(a, b) {
+ if (a && 'function'===typeof a.isEqual) return a.isEqual(b);
+ return a === b;
+};
+
+// Used by Ember.compare
+Ember.ORDER_DEFINITION = Ember.ENV.ORDER_DEFINITION || [
+ 'undefined',
+ 'null',
+ 'boolean',
+ 'number',
+ 'string',
+ 'array',
+ 'object',
+ 'instance',
+ 'function',
+ 'class',
+ 'date'
+];
+
+/**
+ Returns all of the keys defined on an object or hash. This is useful
+ when inspecting objects for debugging. On browsers that support it, this
+ uses the native `Object.keys` implementation.
+
+ @method keys
+ @for Ember
+ @param {Object} obj
+ @return {Array} Array containing keys of obj
+*/
+Ember.keys = Object.keys;
+
+if (!Ember.keys) {
+ Ember.keys = function(obj) {
+ var ret = [];
+ for(var key in obj) {
+ if (obj.hasOwnProperty(key)) { ret.push(key); }
+ }
+ return ret;
+ };
+}
+
+// ..........................................................
+// ERROR
+//
+
+var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack'];
+
+/**
+ A subclass of the JavaScript Error object for use in Ember.
+
+ @class Error
+ @namespace Ember
+ @extends Error
+ @constructor
+*/
+Ember.Error = function() {
+ var tmp = Error.prototype.constructor.apply(this, arguments);
+
+ // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work.
+ for (var idx = 0; idx < errorProps.length; idx++) {
+ this[errorProps[idx]] = tmp[errorProps[idx]];
+ }
+};
+
+Ember.Error.prototype = Ember.create(Error.prototype);
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+var STRING_DASHERIZE_REGEXP = (/[ _]/g);
+var STRING_DASHERIZE_CACHE = {};
+var STRING_DECAMELIZE_REGEXP = (/([a-z])([A-Z])/g);
+var STRING_CAMELIZE_REGEXP = (/(\-|_|\.|\s)+(.)?/g);
+var STRING_UNDERSCORE_REGEXP_1 = (/([a-z\d])([A-Z]+)/g);
+var STRING_UNDERSCORE_REGEXP_2 = (/\-|\s+/g);
+
+/**
+ Defines the hash of localized strings for the current language. Used by
+ the `Ember.String.loc()` helper. To localize, add string values to this
+ hash.
+
+ @property STRINGS
+ @for Ember
+ @type Hash
+*/
+Ember.STRINGS = {};
+
+/**
+ Defines string helper methods including string formatting and localization.
+ Unless `Ember.EXTEND_PROTOTYPES.String` is `false` these methods will also be
+ added to the `String.prototype` as well.
+
+ @class String
+ @namespace Ember
+ @static
+*/
+Ember.String = {
+
+ /**
+ Apply formatting options to the string. This will look for occurrences
+ of "%@" in your string and substitute them with the arguments you pass into
+ this method. If you want to control the specific order of replacement,
+ you can add a number after the key as well to indicate which argument
+ you want to insert.
+
+ Ordered insertions are most useful when building loc strings where values
+ you need to insert may appear in different orders.
+
+ ```javascript
+ "Hello %@ %@".fmt('John', 'Doe'); // "Hello John Doe"
+ "Hello %@2, %@1".fmt('John', 'Doe'); // "Hello Doe, John"
+ ```
+
+ @method fmt
+ @param {Object...} [args]
+ @return {String} formatted string
+ */
+ fmt: function(str, formats) {
+ // first, replace any ORDERED replacements.
+ var idx = 0; // the current index for non-numerical replacements
+ return str.replace(/%@([0-9]+)?/g, function(s, argIndex) {
+ argIndex = (argIndex) ? parseInt(argIndex,0) - 1 : idx++ ;
+ s = formats[argIndex];
+ return ((s === null) ? '(null)' : (s === undefined) ? '' : s).toString();
+ }) ;
+ },
+
+ /**
+ Formats the passed string, but first looks up the string in the localized
+ strings hash. This is a convenient way to localize text. See
+ `Ember.String.fmt()` for more information on formatting.
+
+ Note that it is traditional but not required to prefix localized string
+ keys with an underscore or other character so you can easily identify
+ localized strings.
+
+ ```javascript
+ Ember.STRINGS = {
+ '_Hello World': 'Bonjour le monde',
+ '_Hello %@ %@': 'Bonjour %@ %@'
+ };
+
+ Ember.String.loc("_Hello World"); // 'Bonjour le monde';
+ Ember.String.loc("_Hello %@ %@", ["John", "Smith"]); // "Bonjour John Smith";
+ ```
+
+ @method loc
+ @param {String} str The string to format
+ @param {Array} formats Optional array of parameters to interpolate into string.
+ @return {String} formatted string
+ */
+ loc: function(str, formats) {
+ str = Ember.STRINGS[str] || str;
+ return Ember.String.fmt(str, formats) ;
+ },
+
+ /**
+ Splits a string into separate units separated by spaces, eliminating any
+ empty strings in the process. This is a convenience method for split that
+ is mostly useful when applied to the `String.prototype`.
+
+ ```javascript
+ Ember.String.w("alpha beta gamma").forEach(function(key) {
+ console.log(key);
+ });
+
+ // > alpha
+ // > beta
+ // > gamma
+ ```
+
+ @method w
+ @param {String} str The string to split
+ @return {String} split string
+ */
+ w: function(str) { return str.split(/\s+/); },
+
+ /**
+ Converts a camelized string into all lower case separated by underscores.
+
+ ```javascript
+ 'innerHTML'.decamelize(); // 'inner_html'
+ 'action_name'.decamelize(); // 'action_name'
+ 'css-class-name'.decamelize(); // 'css-class-name'
+ 'my favorite items'.decamelize(); // 'my favorite items'
+ ```
+
+ @method decamelize
+ @param {String} str The string to decamelize.
+ @return {String} the decamelized string.
+ */
+ decamelize: function(str) {
+ return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase();
+ },
+
+ /**
+ Replaces underscores or spaces with dashes.
+
+ ```javascript
+ 'innerHTML'.dasherize(); // 'inner-html'
+ 'action_name'.dasherize(); // 'action-name'
+ 'css-class-name'.dasherize(); // 'css-class-name'
+ 'my favorite items'.dasherize(); // 'my-favorite-items'
+ ```
+
+ @method dasherize
+ @param {String} str The string to dasherize.
+ @return {String} the dasherized string.
+ */
+ dasherize: function(str) {
+ var cache = STRING_DASHERIZE_CACHE,
+ ret = cache[str];
+
+ if (ret) {
+ return ret;
+ } else {
+ ret = Ember.String.decamelize(str).replace(STRING_DASHERIZE_REGEXP,'-');
+ cache[str] = ret;
+ }
+
+ return ret;
+ },
+
+ /**
+ Returns the lowerCaseCamel form of a string.
+
+ ```javascript
+ 'innerHTML'.camelize(); // 'innerHTML'
+ 'action_name'.camelize(); // 'actionName'
+ 'css-class-name'.camelize(); // 'cssClassName'
+ 'my favorite items'.camelize(); // 'myFavoriteItems'
+ ```
+
+ @method camelize
+ @param {String} str The string to camelize.
+ @return {String} the camelized string.
+ */
+ camelize: function(str) {
+ return str.replace(STRING_CAMELIZE_REGEXP, function(match, separator, chr) {
+ return chr ? chr.toUpperCase() : '';
+ });
+ },
+
+ /**
+ Returns the UpperCamelCase form of a string.
+
+ ```javascript
+ 'innerHTML'.classify(); // 'InnerHTML'
+ 'action_name'.classify(); // 'ActionName'
+ 'css-class-name'.classify(); // 'CssClassName'
+ 'my favorite items'.classify(); // 'MyFavoriteItems'
+ ```
+
+ @method classify
+ @param {String} str the string to classify
+ @return {String} the classified string
+ */
+ classify: function(str) {
+ var parts = str.split("."),
+ out = [];
+
+ for (var i=0, l=parts.length; i<l; i++) {
+ var camelized = Ember.String.camelize(parts[i]);
+ out.push(camelized.charAt(0).toUpperCase() + camelized.substr(1));
+ }
+
+ return out.join(".");
+ },
+
+ /**
+ More general than decamelize. Returns the lower\_case\_and\_underscored
+ form of a string.
+
+ ```javascript
+ 'innerHTML'.underscore(); // 'inner_html'
+ 'action_name'.underscore(); // 'action_name'
+ 'css-class-name'.underscore(); // 'css_class_name'
+ 'my favorite items'.underscore(); // 'my_favorite_items'
+ ```
+
+ @method underscore
+ @param {String} str The string to underscore.
+ @return {String} the underscored string.
+ */
+ underscore: function(str) {
+ return str.replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2').
+ replace(STRING_UNDERSCORE_REGEXP_2, '_').toLowerCase();
+ },
+
+ /**
+ 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'
+
+ @method capitalize
+ @param {String} str
+ @return {String}
+ */
+ capitalize: function(str) {
+ return str.charAt(0).toUpperCase() + str.substr(1);
+ }
+
+};
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+
+
+var fmt = Ember.String.fmt,
+ w = Ember.String.w,
+ loc = Ember.String.loc,
+ camelize = Ember.String.camelize,
+ decamelize = Ember.String.decamelize,
+ dasherize = Ember.String.dasherize,
+ underscore = Ember.String.underscore,
+ capitalize = Ember.String.capitalize,
+ classify = Ember.String.classify;
+
+if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) {
+
+ /**
+ See {{#crossLink "Ember.String/fmt"}}{{/crossLink}}
+
+ @method fmt
+ @for String
+ */
+ String.prototype.fmt = function() {
+ return fmt(this, arguments);
+ };
+
+ /**
+ See {{#crossLink "Ember.String/w"}}{{/crossLink}}
+
+ @method w
+ @for String
+ */
+ String.prototype.w = function() {
+ return w(this);
+ };
+
+ /**
+ See {{#crossLink "Ember.String/loc"}}{{/crossLink}}
+
+ @method loc
+ @for String
+ */
+ String.prototype.loc = function() {
+ return loc(this, arguments);
+ };
+
+ /**
+ See {{#crossLink "Ember.String/camelize"}}{{/crossLink}}
+
+ @method camelize
+ @for String
+ */
+ String.prototype.camelize = function() {
+ return camelize(this);
+ };
+
+ /**
+ See {{#crossLink "Ember.String/decamelize"}}{{/crossLink}}
+
+ @method decamelize
+ @for String
+ */
+ String.prototype.decamelize = function() {
+ return decamelize(this);
+ };
+
+ /**
+ See {{#crossLink "Ember.String/dasherize"}}{{/crossLink}}
+
+ @method dasherize
+ @for String
+ */
+ String.prototype.dasherize = function() {
+ return dasherize(this);
+ };
+
+ /**
+ See {{#crossLink "Ember.String/underscore"}}{{/crossLink}}
+
+ @method underscore
+ @for String
+ */
+ String.prototype.underscore = function() {
+ return underscore(this);
+ };
+
+ /**
+ See {{#crossLink "Ember.String/classify"}}{{/crossLink}}
+
+ @method classify
+ @for String
+ */
+ String.prototype.classify = function() {
+ return classify(this);
+ };
+
+ /**
+ See {{#crossLink "Ember.String/capitalize"}}{{/crossLink}}
+
+ @method capitalize
+ @for String
+ */
+ String.prototype.capitalize = function() {
+ return capitalize(this);
+ };
+
+}
+
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+var a_slice = Array.prototype.slice;
+
+if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Function) {
+
+ /**
+ The `property` extension of Javascript's Function prototype is available
+ when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is
+ `true`, which is the default.
+
+ Computed properties allow you to treat a function like a property:
+
+ ```javascript
+ MyApp.president = Ember.Object.create({
+ firstName: "Barack",
+ lastName: "Obama",
+
+ fullName: function() {
+ return this.get('firstName') + ' ' + this.get('lastName');
+
+ // Call this flag to mark the function as a property
+ }.property()
+ });
+
+ MyApp.president.get('fullName'); // "Barack Obama"
+ ```
+
+ Treating a function like a property is useful because they can work with
+ bindings, just like any other property.
+
+ Many computed properties have dependencies on other properties. For
+ example, in the above example, the `fullName` property depends on
+ `firstName` and `lastName` to determine its value. You can tell Ember
+ about these dependencies like this:
+
+ ```javascript
+ MyApp.president = Ember.Object.create({
+ firstName: "Barack",
+ lastName: "Obama",
+
+ fullName: function() {
+ return this.get('firstName') + ' ' + this.get('lastName');
+
+ // Tell Ember.js that this computed property depends on firstName
+ // and lastName
+ }.property('firstName', 'lastName')
+ });
+ ```
+
+ Make sure you list these dependencies so Ember knows when to update
+ bindings that connect to a computed property. Changing a dependency
+ will not immediately trigger an update of the computed property, but
+ will instead clear the cache so that it is updated when the next `get`
+ is called on the property.
+
+ See {{#crossLink "Ember.ComputedProperty"}}{{/crossLink}},
+ {{#crossLink "Ember/computed"}}{{/crossLink}}
+
+ @method property
+ @for Function
+ */
+ Function.prototype.property = function() {
+ var ret = Ember.computed(this);
+ return ret.property.apply(ret, arguments);
+ };
+
+ /**
+ The `observes` extension of Javascript's Function prototype is available
+ when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is
+ true, which is the default.
+
+ You can observe property changes simply by adding the `observes`
+ call to the end of your method declarations in classes that you write.
+ For example:
+
+ ```javascript
+ Ember.Object.create({
+ valueObserver: function() {
+ // Executes whenever the "value" property changes
+ }.observes('value')
+ });
+ ```
+
+ See {{#crossLink "Ember.Observable/observes"}}{{/crossLink}}
+
+ @method observes
+ @for Function
+ */
+ Function.prototype.observes = function() {
+ this.__ember_observes__ = a_slice.call(arguments);
+ return this;
+ };
+
+ /**
+ The `observesBefore` extension of Javascript's Function prototype is
+ available when `Ember.EXTEND_PROTOTYPES` or
+ `Ember.EXTEND_PROTOTYPES.Function` is true, which is the default.
+
+ You can get notified when a property changes is about to happen by
+ by adding the `observesBefore` call to the end of your method
+ declarations in classes that you write. For example:
+
+ ```javascript
+ Ember.Object.create({
+ valueObserver: function() {
+ // Executes whenever the "value" property is about to change
+ }.observesBefore('value')
+ });
+ ```
+
+ See {{#crossLink "Ember.Observable/observesBefore"}}{{/crossLink}}
+
+ @method observesBefore
+ @for Function
+ */
+ Function.prototype.observesBefore = function() {
+ this.__ember_observesBefore__ = a_slice.call(arguments);
+ return this;
+ };
+
+}
+
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+// ..........................................................
+// HELPERS
+//
+
+var get = Ember.get, set = Ember.set;
+var a_slice = Array.prototype.slice;
+var a_indexOf = Ember.EnumerableUtils.indexOf;
+
+var contexts = [];
+
+function popCtx() {
+ return contexts.length===0 ? {} : contexts.pop();
+}
+
+function pushCtx(ctx) {
+ contexts.push(ctx);
+ return null;
+}
+
+function iter(key, value) {
+ var valueProvided = arguments.length === 2;
+
+ function i(item) {
+ var cur = get(item, key);
+ return valueProvided ? value===cur : !!cur;
+ }
+ return i ;
+}
+
+/**
+ This mixin defines the common interface implemented by enumerable objects
+ in Ember. Most of these methods follow the standard Array iteration
+ API defined up to JavaScript 1.8 (excluding language-specific features that
+ cannot be emulated in older versions of JavaScript).
+
+ This mixin is applied automatically to the Array class on page load, so you
+ can use any of these methods on simple arrays. If Array already implements
+ one of these methods, the mixin will not override them.
+
+ ## Writing Your Own Enumerable
+
+ To make your own custom class enumerable, you need two items:
+
+ 1. You must have a length property. This property should change whenever
+ the number of items in your enumerable object changes. If you using this
+ with an `Ember.Object` subclass, you should be sure to change the length
+ property using `set().`
+
+ 2. If you must implement `nextObject().` See documentation.
+
+ Once you have these two methods implement, apply the `Ember.Enumerable` mixin
+ to your class and you will be able to enumerate the contents of your object
+ like any other collection.
+
+ ## Using Ember Enumeration with Other Libraries
+
+ Many other libraries provide some kind of iterator or enumeration like
+ facility. This is often where the most common API conflicts occur.
+ Ember's API is designed to be as friendly as possible with other
+ libraries by implementing only methods that mostly correspond to the
+ JavaScript 1.8 API.
+
+ @class Enumerable
+ @namespace Ember
+ @extends Ember.Mixin
+ @since Ember 0.9
+*/
+Ember.Enumerable = Ember.Mixin.create(
+ /** @scope Ember.Enumerable.prototype */ {
+
+ // compatibility
+ isEnumerable: true,
+
+ /**
+ Implement this method to make your class enumerable.
+
+ This method will be call repeatedly during enumeration. The index value
+ will always begin with 0 and increment monotonically. You don't have to
+ rely on the index value to determine what object to return, but you should
+ always check the value and start from the beginning when you see the
+ requested index is 0.
+
+ The `previousObject` is the object that was returned from the last call
+ to `nextObject` for the current iteration. This is a useful way to
+ manage iteration if you are tracing a linked list, for example.
+
+ Finally the context parameter will always contain a hash you can use as
+ a "scratchpad" to maintain any other state you need in order to iterate
+ properly. The context object is reused and is not reset between
+ iterations so make sure you setup the context with a fresh state whenever
+ the index parameter is 0.
+
+ Generally iterators will continue to call `nextObject` until the index
+ reaches the your current length-1. If you run out of data before this
+ time for some reason, you should simply return undefined.
+
+ The default implementation of this method simply looks up the index.
+ This works great on any Array-like objects.
+
+ @method nextObject
+ @param {Number} index the current index of the iteration
+ @param {Object} previousObject the value returned by the last call to
+ `nextObject`.
+ @param {Object} context a context object you can use to maintain state.
+ @return {Object} the next object in the iteration or undefined
+ */
+ nextObject: Ember.required(Function),
+
+ /**
+ Helper method returns the first object from a collection. This is usually
+ used by bindings and other parts of the framework to extract a single
+ object if the enumerable contains only one item.
+
+ If you override this method, you should implement it so that it will
+ always return the same value each time it is called. If your enumerable
+ contains only one object, this method should always return that object.
+ If your enumerable is empty, this method should return `undefined`.
+
+ ```javascript
+ var arr = ["a", "b", "c"];
+ arr.firstObject(); // "a"
+
+ var arr = [];
+ arr.firstObject(); // undefined
+ ```
+
+ @property firstObject
+ @return {Object} the object or undefined
+ */
+ firstObject: Ember.computed(function() {
+ if (get(this, 'length')===0) return undefined ;
+
+ // handle generic enumerables
+ var context = popCtx(), ret;
+ ret = this.nextObject(0, null, context);
+ pushCtx(context);
+ return ret ;
+ }).property('[]'),
+
+ /**
+ Helper method returns the last object from a collection. If your enumerable
+ contains only one object, this method should always return that object.
+ If your enumerable is empty, this method should return `undefined`.
+
+ ```javascript
+ var arr = ["a", "b", "c"];
+ arr.lastObject(); // "c"
+
+ var arr = [];
+ arr.lastObject(); // undefined
+ ```
+
+ @property lastObject
+ @return {Object} the last object or undefined
+ */
+ lastObject: Ember.computed(function() {
+ var len = get(this, 'length');
+ if (len===0) return undefined ;
+ var context = popCtx(), idx=0, cur, last = null;
+ do {
+ last = cur;
+ cur = this.nextObject(idx++, last, context);
+ } while (cur !== undefined);
+ pushCtx(context);
+ return last;
+ }).property('[]'),
+
+ /**
+ Returns `true` if the passed object can be found in the receiver. The
+ default version will iterate through the enumerable until the object
+ is found. You may want to override this with a more efficient version.
+
+ ```javascript
+ var arr = ["a", "b", "c"];
+ arr.contains("a"); // true
+ arr.contains("z"); // false
+ ```
+
+ @method contains
+ @param {Object} obj The object to search for.
+ @return {Boolean} `true` if object is found in enumerable.
+ */
+ contains: function(obj) {
+ return this.find(function(item) { return item===obj; }) !== undefined;
+ },
+
+ /**
+ Iterates through the enumerable, calling the passed function on each
+ item. This method corresponds to the `forEach()` method defined in
+ JavaScript 1.6.
+
+ The callback method you provide should have the following signature (all
+ parameters are optional):
+
+ ```javascript
+ function(item, index, enumerable);
+ ```
+
+ - `item` is the current item in the iteration.
+ - `index` is the current index in the iteration.
+ - `enumerable` is the enumerable object itself.
+
+ Note that in addition to a callback, you can also pass an optional target
+ object that will be set as `this` on the context. This is a good way
+ to give your iterator function access to the current object.
+
+ @method forEach
+ @param {Function} callback The callback to execute
+ @param {Object} [target] The target object to use
+ @return {Object} receiver
+ */
+ forEach: function(callback, target) {
+ if (typeof callback !== "function") throw new TypeError() ;
+ var len = get(this, 'length'), last = null, context = popCtx();
+
+ if (target === undefined) target = null;
+
+ for(var idx=0;idx<len;idx++) {
+ var next = this.nextObject(idx, last, context) ;
+ callback.call(target, next, idx, this);
+ last = next ;
+ }
+ last = null ;
+ context = pushCtx(context);
+ return this ;
+ },
+
+ /**
+ Alias for `mapProperty`
+
+ @method getEach
+ @param {String} key name of the property
+ @return {Array} The mapped array.
+ */
+ getEach: function(key) {
+ return this.mapProperty(key);
+ },
+
+ /**
+ Sets the value on the named property for each member. This is more
+ efficient than using other methods defined on this helper. If the object
+ implements Ember.Observable, the value will be changed to `set(),` otherwise
+ it will be set directly. `null` objects are skipped.
+
+ @method setEach
+ @param {String} key The key to set
+ @param {Object} value The object to set
+ @return {Object} receiver
+ */
+ setEach: function(key, value) {
+ return this.forEach(function(item) {
+ set(item, key, value);
+ });
+ },
+
+ /**
+ Maps all of the items in the enumeration to another value, returning
+ a new array. This method corresponds to `map()` defined in JavaScript 1.6.
+
+ The callback method you provide should have the following signature (all
+ parameters are optional):
+
+ ```javascript
+ function(item, index, enumerable);
+ ```
+
+ - `item` is the current item in the iteration.
+ - `index` is the current index in the iteration.
+ - `enumerable` is the enumerable object itself.
+
+ It should return the mapped value.
+
+ Note that in addition to a callback, you can also pass an optional target
+ object that will be set as `this` on the context. This is a good way
+ to give your iterator function access to the current object.
+
+ @method map
+ @param {Function} callback The callback to execute
+ @param {Object} [target] The target object to use
+ @return {Array} The mapped array.
+ */
+ map: function(callback, target) {
+ var ret = [];
+ this.forEach(function(x, idx, i) {
+ ret[idx] = callback.call(target, x, idx,i);
+ });
+ return ret ;
+ },
+
+ /**
+ Similar to map, this specialized function returns the value of the named
+ property on all items in the enumeration.
+
+ @method mapProperty
+ @param {String} key name of the property
+ @return {Array} The mapped array.
+ */
+ mapProperty: function(key) {
+ return this.map(function(next) {
+ return get(next, key);
+ });
+ },
+
+ /**
+ Returns an array with all of the items in the enumeration that the passed
+ function returns true for. This method corresponds to `filter()` defined in
+ JavaScript 1.6.
+
+ The callback method you provide should have the following signature (all
+ parameters are optional):
+
+ ```javascript
+ function(item, index, enumerable);
+ ```
+
+ - `item` is the current item in the iteration.
+ - `index` is the current index in the iteration.
+ - `enumerable` is the enumerable object itself.
+
+ It should return the `true` to include the item in the results, `false`
+ otherwise.
+
+ Note that in addition to a callback, you can also pass an optional target
+ object that will be set as `this` on the context. This is a good way
+ to give your iterator function access to the current object.
+
+ @method filter
+ @param {Function} callback The callback to execute
+ @param {Object} [target] The target object to use
+ @return {Array} A filtered array.
+ */
+ filter: function(callback, target) {
+ var ret = [];
+ this.forEach(function(x, idx, i) {
+ if (callback.call(target, x, idx, i)) ret.push(x);
+ });
+ return ret ;
+ },
+
+ /**
+ Returns an array with all of the items in the enumeration where the passed
+ function returns false for. This method is the inverse of filter().
+
+ The callback method you provide should have the following signature (all
+ parameters are optional):
+
+ function(item, index, enumerable);
+
+ - *item* is the current item in the iteration.
+ - *index* is the current index in the iteration
+ - *enumerable* is the enumerable object itself.
+
+ It should return the a falsey value to include the item in the results.
+
+ Note that in addition to a callback, you can also pass an optional target
+ object that will be set as "this" on the context. This is a good way
+ to give your iterator function access to the current object.
+
+ @method reject
+ @param {Function} callback The callback to execute
+ @param {Object} [target] The target object to use
+ @return {Array} A rejected array.
+ */
+ reject: function(callback, target) {
+ return this.filter(function() {
+ return !(callback.apply(target, arguments));
+ });
+ },
+
+ /**
+ Returns an array with just the items with the matched property. You
+ can pass an optional second argument with the target value. Otherwise
+ this will match any property that evaluates to `true`.
+
+ @method filterProperty
+ @param {String} key the property to test
+ @param {String} [value] optional value to test against.
+ @return {Array} filtered array
+ */
+ filterProperty: function(key, value) {
+ return this.filter(iter.apply(this, arguments));
+ },
+
+ /**
+ Returns an array with the items that do not have truthy values for
+ key. You can pass an optional second argument with the target value. Otherwise
+ this will match any property that evaluates to false.
+
+ @method rejectProperty
+ @param {String} key the property to test
+ @param {String} [value] optional value to test against.
+ @return {Array} rejected array
+ */
+ rejectProperty: function(key, value) {
+ var exactValue = function(item) { return get(item, key) === value; },
+ hasValue = function(item) { return !!get(item, key); },
+ use = (arguments.length === 2 ? exactValue : hasValue);
+
+ return this.reject(use);
+ },
+
+ /**
+ Returns the first item in the array for which the callback returns true.
+ This method works similar to the `filter()` method defined in JavaScript 1.6
+ except that it will stop working on the array once a match is found.
+
+ The callback method you provide should have the following signature (all
+ parameters are optional):
+
+ ```javascript
+ function(item, index, enumerable);
+ ```
+
+ - `item` is the current item in the iteration.
+ - `index` is the current index in the iteration.
+ - `enumerable` is the enumerable object itself.
+
+ It should return the `true` to include the item in the results, `false`
+ otherwise.
+
+ Note that in addition to a callback, you can also pass an optional target
+ object that will be set as `this` on the context. This is a good way
+ to give your iterator function access to the current object.
+
+ @method find
+ @param {Function} callback The callback to execute
+ @param {Object} [target] The target object to use
+ @return {Object} Found item or `undefined`.
+ */
+ find: function(callback, target) {
+ var len = get(this, 'length') ;
+ if (target === undefined) target = null;
+
+ var last = null, next, found = false, ret ;
+ var context = popCtx();
+ for(var idx=0;idx<len && !found;idx++) {
+ next = this.nextObject(idx, last, context) ;
+ if (found = callback.call(target, next, idx, this)) ret = next ;
+ last = next ;
+ }
+ next = last = null ;
+ context = pushCtx(context);
+ return ret ;
+ },
+
+ /**
+ Returns the first item with a property matching the passed value. You
+ can pass an optional second argument with the target value. Otherwise
+ this will match any property that evaluates to `true`.
+
+ This method works much like the more generic `find()` method.
+
+ @method findProperty
+ @param {String} key the property to test
+ @param {String} [value] optional value to test against.
+ @return {Object} found item or `undefined`
+ */
+ findProperty: function(key, value) {
+ return this.find(iter.apply(this, arguments));
+ },
+
+ /**
+ Returns `true` if the passed function returns true for every item in the
+ enumeration. This corresponds with the `every()` method in JavaScript 1.6.
+
+ The callback method you provide should have the following signature (all
+ parameters are optional):
+
+ ```javascript
+ function(item, index, enumerable);
+ ```
+
+ - `item` is the current item in the iteration.
+ - `index` is the current index in the iteration.
+ - `enumerable` is the enumerable object itself.
+
+ It should return the `true` or `false`.
+
+ Note that in addition to a callback, you can also pass an optional target
+ object that will be set as `this` on the context. This is a good way
+ to give your iterator function access to the current object.
+
+ Example Usage:
+
+ ```javascript
+ if (people.every(isEngineer)) { Paychecks.addBigBonus(); }
+ ```
+
+ @method every
+ @param {Function} callback The callback to execute
+ @param {Object} [target] The target object to use
+ @return {Boolean}
+ */
+ every: function(callback, target) {
+ return !this.find(function(x, idx, i) {
+ return !callback.call(target, x, idx, i);
+ });
+ },
+
+ /**
+ Returns `true` if the passed property resolves to `true` for all items in
+ the enumerable. This method is often simpler/faster than using a callback.
+
+ @method everyProperty
+ @param {String} key the property to test
+ @param {String} [value] optional value to test against.
+ @return {Boolean}
+ */
+ everyProperty: function(key, value) {
+ return this.every(iter.apply(this, arguments));
+ },
+
+
+ /**
+ Returns `true` if the passed function returns true for any item in the
+ enumeration. This corresponds with the `some()` method in JavaScript 1.6.
+
+ The callback method you provide should have the following signature (all
+ parameters are optional):
+
+ ```javascript
+ function(item, index, enumerable);
+ ```
+
+ - `item` is the current item in the iteration.
+ - `index` is the current index in the iteration.
+ - `enumerable` is the enumerable object itself.
+
+ It should return the `true` to include the item in the results, `false`
+ otherwise.
+
+ Note that in addition to a callback, you can also pass an optional target
+ object that will be set as `this` on the context. This is a good way
+ to give your iterator function access to the current object.
+
+ Usage Example:
+
+ ```javascript
+ if (people.some(isManager)) { Paychecks.addBiggerBonus(); }
+ ```
+
+ @method some
+ @param {Function} callback The callback to execute
+ @param {Object} [target] The target object to use
+ @return {Array} A filtered array.
+ */
+ some: function(callback, target) {
+ return !!this.find(function(x, idx, i) {
+ return !!callback.call(target, x, idx, i);
+ });
+ },
+
+ /**
+ Returns `true` if the passed property resolves to `true` for any item in
+ the enumerable. This method is often simpler/faster than using a callback.
+
+ @method someProperty
+ @param {String} key the property to test
+ @param {String} [value] optional value to test against.
+ @return {Boolean} `true`
+ */
+ someProperty: function(key, value) {
+ return this.some(iter.apply(this, arguments));
+ },
+
+ /**
+ This will combine the values of the enumerator into a single value. It
+ is a useful way to collect a summary value from an enumeration. This
+ corresponds to the `reduce()` method defined in JavaScript 1.8.
+
+ The callback method you provide should have the following signature (all
+ parameters are optional):
+
+ ```javascript
+ function(previousValue, item, index, enumerable);
+ ```
+
+ - `previousValue` is the value returned by the last call to the iterator.
+ - `item` is the current item in the iteration.
+ - `index` is the current index in the iteration.
+ - `enumerable` is the enumerable object itself.
+
+ Return the new cumulative value.
+
+ In addition to the callback you can also pass an `initialValue`. An error
+ will be raised if you do not pass an initial value and the enumerator is
+ empty.
+
+ Note that unlike the other methods, this method does not allow you to
+ pass a target object to set as this for the callback. It's part of the
+ spec. Sorry.
+
+ @method reduce
+ @param {Function} callback The callback to execute
+ @param {Object} initialValue Initial value for the reduce
+ @param {String} reducerProperty internal use only.
+ @return {Object} The reduced value.
+ */
+ reduce: function(callback, initialValue, reducerProperty) {
+ if (typeof callback !== "function") { throw new TypeError(); }
+
+ var ret = initialValue;
+
+ this.forEach(function(item, i) {
+ ret = callback.call(null, ret, item, i, this, reducerProperty);
+ }, this);
+
+ return ret;
+ },
+
+ /**
+ Invokes the named method on every object in the receiver that
+ implements it. This method corresponds to the implementation in
+ Prototype 1.6.
+
+ @method invoke
+ @param {String} methodName the name of the method
+ @param {Object...} args optional arguments to pass as well.
+ @return {Array} return values from calling invoke.
+ */
+ invoke: function(methodName) {
+ var args, ret = [];
+ if (arguments.length>1) args = a_slice.call(arguments, 1);
+
+ this.forEach(function(x, idx) {
+ var method = x && x[methodName];
+ if ('function' === typeof method) {
+ ret[idx] = args ? method.apply(x, args) : method.call(x);
+ }
+ }, this);
+
+ return ret;
+ },
+
+ /**
+ Simply converts the enumerable into a genuine array. The order is not
+ guaranteed. Corresponds to the method implemented by Prototype.
+
+ @method toArray
+ @return {Array} the enumerable as an array.
+ */
+ toArray: function() {
+ var ret = [];
+ this.forEach(function(o, idx) { ret[idx] = o; });
+ return ret ;
+ },
+
+ /**
+ Returns a copy of the array with all null elements removed.
+
+ ```javascript
+ var arr = ["a", null, "c", null];
+ arr.compact(); // ["a", "c"]
+ ```
+
+ @method compact
+ @return {Array} the array without null elements.
+ */
+ compact: function() { return this.without(null); },
+
+ /**
+ Returns a new enumerable that excludes the passed value. The default
+ implementation returns an array regardless of the receiver type unless
+ the receiver does not contain the value.
+
+ ```javascript
+ var arr = ["a", "b", "a", "c"];
+ arr.without("a"); // ["b", "c"]
+ ```
+
+ @method without
+ @param {Object} value
+ @return {Ember.Enumerable}
+ */
+ without: function(value) {
+ if (!this.contains(value)) return this; // nothing to do
+ var ret = [] ;
+ this.forEach(function(k) {
+ if (k !== value) ret[ret.length] = k;
+ }) ;
+ return ret ;
+ },
+
+ /**
+ Returns a new enumerable that contains only unique values. The default
+ implementation returns an array regardless of the receiver type.
+
+ ```javascript
+ var arr = ["a", "a", "b", "b"];
+ arr.uniq(); // ["a", "b"]
+ ```
+
+ @method uniq
+ @return {Ember.Enumerable}
+ */
+ uniq: function() {
+ var ret = [];
+ this.forEach(function(k){
+ if (a_indexOf(ret, k)<0) ret.push(k);
+ });
+ return ret;
+ },
+
+ /**
+ This property will trigger anytime the enumerable's content changes.
+ You can observe this property to be notified of changes to the enumerables
+ content.
+
+ For plain enumerables, this property is read only. `Ember.Array` overrides
+ this method.
+
+ @property []
+ @type Ember.Array
+ */
+ '[]': Ember.computed(function(key, value) {
+ return this;
+ }),
+
+ // ..........................................................
+ // ENUMERABLE OBSERVERS
+ //
+
+ /**
+ Registers an enumerable observer. Must implement `Ember.EnumerableObserver`
+ mixin.
+
+ @method addEnumerableObserver
+ @param target {Object}
+ @param opts {Hash}
+ */
+ addEnumerableObserver: function(target, opts) {
+ var willChange = (opts && opts.willChange) || 'enumerableWillChange',
+ didChange = (opts && opts.didChange) || 'enumerableDidChange';
+
+ var hasObservers = get(this, 'hasEnumerableObservers');
+ if (!hasObservers) Ember.propertyWillChange(this, 'hasEnumerableObservers');
+ Ember.addListener(this, '@enumerable:before', target, willChange);
+ Ember.addListener(this, '@enumerable:change', target, didChange);
+ if (!hasObservers) Ember.propertyDidChange(this, 'hasEnumerableObservers');
+ return this;
+ },
+
+ /**
+ Removes a registered enumerable observer.
+
+ @method removeEnumerableObserver
+ @param target {Object}
+ @param [opts] {Hash}
+ */
+ removeEnumerableObserver: function(target, opts) {
+ var willChange = (opts && opts.willChange) || 'enumerableWillChange',
+ didChange = (opts && opts.didChange) || 'enumerableDidChange';
+
+ var hasObservers = get(this, 'hasEnumerableObservers');
+ if (hasObservers) Ember.propertyWillChange(this, 'hasEnumerableObservers');
+ Ember.removeListener(this, '@enumerable:before', target, willChange);
+ Ember.removeListener(this, '@enumerable:change', target, didChange);
+ if (hasObservers) Ember.propertyDidChange(this, 'hasEnumerableObservers');
+ return this;
+ },
+
+ /**
+ Becomes true whenever the array currently has observers watching changes
+ on the array.
+
+ @property hasEnumerableObservers
+ @type Boolean
+ */
+ hasEnumerableObservers: Ember.computed(function() {
+ return Ember.hasListeners(this, '@enumerable:change') || Ember.hasListeners(this, '@enumerable:before');
+ }),
+
+
+ /**
+ Invoke this method just before the contents of your enumerable will
+ change. You can either omit the parameters completely or pass the objects
+ to be removed or added if available or just a count.
+
+ @method enumerableContentWillChange
+ @param {Ember.Enumerable|Number} removing An enumerable of the objects to
+ be removed or the number of items to be removed.
+ @param {Ember.Enumerable|Number} adding An enumerable of the objects to be
+ added or the number of items to be added.
+ @chainable
+ */
+ enumerableContentWillChange: function(removing, adding) {
+
+ var removeCnt, addCnt, hasDelta;
+
+ if ('number' === typeof removing) removeCnt = removing;
+ else if (removing) removeCnt = get(removing, 'length');
+ else removeCnt = removing = -1;
+
+ if ('number' === typeof adding) addCnt = adding;
+ else if (adding) addCnt = get(adding,'length');
+ else addCnt = adding = -1;
+
+ hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0;
+
+ if (removing === -1) removing = null;
+ if (adding === -1) adding = null;
+
+ Ember.propertyWillChange(this, '[]');
+ if (hasDelta) Ember.propertyWillChange(this, 'length');
+ Ember.sendEvent(this, '@enumerable:before', [this, removing, adding]);
+
+ return this;
+ },
+
+ /**
+ Invoke this method when the contents of your enumerable has changed.
+ This will notify any observers watching for content changes. If your are
+ implementing an ordered enumerable (such as an array), also pass the
+ start and end values where the content changed so that it can be used to
+ notify range observers.
+
+ @method enumerableContentDidChange
+ @param {Number} [start] optional start offset for the content change.
+ For unordered enumerables, you should always pass -1.
+ @param {Ember.Enumerable|Number} removing An enumerable of the objects to
+ be removed or the number of items to be removed.
+ @param {Ember.Enumerable|Number} adding An enumerable of the objects to
+ be added or the number of items to be added.
+ @chainable
+ */
+ enumerableContentDidChange: function(removing, adding) {
+ var notify = this.propertyDidChange, removeCnt, addCnt, hasDelta;
+
+ if ('number' === typeof removing) removeCnt = removing;
+ else if (removing) removeCnt = get(removing, 'length');
+ else removeCnt = removing = -1;
+
+ if ('number' === typeof adding) addCnt = adding;
+ else if (adding) addCnt = get(adding, 'length');
+ else addCnt = adding = -1;
+
+ hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0;
+
+ if (removing === -1) removing = null;
+ if (adding === -1) adding = null;
+
+ Ember.sendEvent(this, '@enumerable:change', [this, removing, adding]);
+ if (hasDelta) Ember.propertyDidChange(this, 'length');
+ Ember.propertyDidChange(this, '[]');
+
+ return this ;
+ }
+
+}) ;
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+// ..........................................................
+// HELPERS
+//
+
+var get = Ember.get, set = Ember.set, meta = Ember.meta, map = Ember.EnumerableUtils.map, cacheFor = Ember.cacheFor;
+
+function none(obj) { return obj===null || obj===undefined; }
+
+// ..........................................................
+// ARRAY
+//
+/**
+ This module implements Observer-friendly Array-like behavior. This mixin is
+ picked up by the Array class as well as other controllers, etc. that want to
+ appear to be arrays.
+
+ Unlike `Ember.Enumerable,` this mixin defines methods specifically for
+ collections that provide index-ordered access to their contents. When you
+ are designing code that needs to accept any kind of Array-like object, you
+ should use these methods instead of Array primitives because these will
+ properly notify observers of changes to the array.
+
+ Although these methods are efficient, they do add a layer of indirection to
+ your application so it is a good idea to use them only when you need the
+ flexibility of using both true JavaScript arrays and "virtual" arrays such
+ as controllers and collections.
+
+ You can use the methods defined in this module to access and modify array
+ contents in a KVO-friendly way. You can also be notified whenever the
+ membership if an array changes by changing the syntax of the property to
+ `.observes('*myProperty.[]')`.
+
+ To support `Ember.Array` in your own class, you must override two
+ primitives to use it: `replace()` and `objectAt()`.
+
+ Note that the Ember.Array mixin also incorporates the `Ember.Enumerable`
+ mixin. All `Ember.Array`-like objects are also enumerable.
+
+ @class Array
+ @namespace Ember
+ @extends Ember.Mixin
+ @uses Ember.Enumerable
+ @since Ember 0.9.0
+*/
+Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.prototype */ {
+
+ // compatibility
+ isSCArray: true,
+
+ /**
+ Your array must support the `length` property. Your replace methods should
+ set this property whenever it changes.
+
+ @property {Number} length
+ */
+ length: Ember.required(),
+
+ /**
+ Returns the object at the given `index`. If the given `index` is negative
+ or is greater or equal than the array length, returns `undefined`.
+
+ This is one of the primitives you must implement to support `Ember.Array`.
+ If your object supports retrieving the value of an array item using `get()`
+ (i.e. `myArray.get(0)`), then you do not need to implement this method
+ yourself.
+
+ ```javascript
+ var arr = ['a', 'b', 'c', 'd'];
+ arr.objectAt(0); // "a"
+ arr.objectAt(3); // "d"
+ arr.objectAt(-1); // undefined
+ arr.objectAt(4); // undefined
+ arr.objectAt(5); // undefined
+ ```
+
+ @method objectAt
+ @param {Number} idx The index of the item to return.
+ */
+ objectAt: function(idx) {
+ if ((idx < 0) || (idx>=get(this, 'length'))) return undefined ;
+ return get(this, idx);
+ },
+
+ /**
+ This returns the objects at the specified indexes, using `objectAt`.
+
+ ```javascript
+ var arr = ['a', 'b', 'c', 'd'];
+ arr.objectsAt([0, 1, 2]); // ["a", "b", "c"]
+ arr.objectsAt([2, 3, 4]); // ["c", "d", undefined]
+ ```
+
+ @method objectsAt
+ @param {Array} indexes An array of indexes of items to return.
+ */
+ objectsAt: function(indexes) {
+ var self = this;
+ return map(indexes, function(idx){ return self.objectAt(idx); });
+ },
+
+ // overrides Ember.Enumerable version
+ nextObject: function(idx) {
+ return this.objectAt(idx);
+ },
+
+ /**
+ This is the handler for the special array content property. If you get
+ this property, it will return this. If you set this property it a new
+ array, it will replace the current content.
+
+ This property overrides the default property defined in `Ember.Enumerable`.
+
+ @property []
+ */
+ '[]': Ember.computed(function(key, value) {
+ if (value !== undefined) this.replace(0, get(this, 'length'), value) ;
+ return this ;
+ }),
+
+ firstObject: Ember.computed(function() {
+ return this.objectAt(0);
+ }),
+
+ lastObject: Ember.computed(function() {
+ return this.objectAt(get(this, 'length')-1);
+ }),
+
+ // optimized version from Enumerable
+ contains: function(obj){
+ return this.indexOf(obj) >= 0;
+ },
+
+ // Add any extra methods to Ember.Array that are native to the built-in Array.
+ /**
+ Returns a new array that is a slice of the receiver. This implementation
+ uses the observable array methods to retrieve the objects for the new
+ slice.
+
+ ```javascript
+ var arr = ['red', 'green', 'blue'];
+ arr.slice(0); // ['red', 'green', 'blue']
+ arr.slice(0, 2); // ['red', 'green']
+ arr.slice(1, 100); // ['green', 'blue']
+ ```
+
+ @method slice
+ @param beginIndex {Integer} (Optional) index to begin slicing from.
+ @param endIndex {Integer} (Optional) index to end the slice at.
+ @return {Array} New array with specified slice
+ */
+ slice: function(beginIndex, endIndex) {
+ var ret = [];
+ var length = get(this, 'length') ;
+ if (none(beginIndex)) beginIndex = 0 ;
+ if (none(endIndex) || (endIndex > length)) endIndex = length ;
+ while(beginIndex < endIndex) {
+ ret[ret.length] = this.objectAt(beginIndex++) ;
+ }
+ return ret ;
+ },
+
+ /**
+ Returns the index of the given object's first occurrence.
+ If no `startAt` argument is given, the starting location to
+ search is 0. If it's negative, will count backward from
+ the end of the array. Returns -1 if no match is found.
+
+ ```javascript
+ var arr = ["a", "b", "c", "d", "a"];
+ arr.indexOf("a"); // 0
+ arr.indexOf("z"); // -1
+ arr.indexOf("a", 2); // 4
+ arr.indexOf("a", -1); // 4
+ arr.indexOf("b", 3); // -1
+ arr.indexOf("a", 100); // -1
+ ```
+
+ @method indexOf
+ @param {Object} object the item to search for
+ @param {Number} startAt optional starting location to search, default 0
+ @return {Number} index or -1 if not found
+ */
+ indexOf: function(object, startAt) {
+ var idx, len = get(this, 'length');
+
+ if (startAt === undefined) startAt = 0;
+ if (startAt < 0) startAt += len;
+
+ for(idx=startAt;idx<len;idx++) {
+ if (this.objectAt(idx, true) === object) return idx ;
+ }
+ return -1;
+ },
+
+ /**
+ Returns the index of the given object's last occurrence.
+ If no `startAt` argument is given, the search starts from
+ the last position. If it's negative, will count backward
+ from the end of the array. Returns -1 if no match is found.
+
+ ```javascript
+ var arr = ["a", "b", "c", "d", "a"];
+ arr.lastIndexOf("a"); // 4
+ arr.lastIndexOf("z"); // -1
+ arr.lastIndexOf("a", 2); // 0
+ arr.lastIndexOf("a", -1); // 4
+ arr.lastIndexOf("b", 3); // 1
+ arr.lastIndexOf("a", 100); // 4
+ ```
+
+ @method lastIndexOf
+ @param {Object} object the item to search for
+ @param {Number} startAt optional starting location to search, default 0
+ @return {Number} index or -1 if not found
+ */
+ lastIndexOf: function(object, startAt) {
+ var idx, len = get(this, 'length');
+
+ if (startAt === undefined || startAt >= len) startAt = len-1;
+ if (startAt < 0) startAt += len;
+
+ for(idx=startAt;idx>=0;idx--) {
+ if (this.objectAt(idx) === object) return idx ;
+ }
+ return -1;
+ },
+
+ // ..........................................................
+ // ARRAY OBSERVERS
+ //
+
+ /**
+ Adds an array observer to the receiving array. The array observer object
+ normally must implement two methods:
+
+ * `arrayWillChange(start, removeCount, addCount)` - This method will be
+ called just before the array is modified.
+ * `arrayDidChange(start, removeCount, addCount)` - This method will be
+ called just after the array is modified.
+
+ Both callbacks will be passed the starting index of the change as well a
+ a count of the items to be removed and added. You can use these callbacks
+ to optionally inspect the array during the change, clear caches, or do
+ any other bookkeeping necessary.
+
+ In addition to passing a target, you can also include an options hash
+ which you can use to override the method names that will be invoked on the
+ target.
+
+ @method addArrayObserver
+ @param {Object} target The observer object.
+ @param {Hash} opts Optional hash of configuration options including
+ `willChange`, `didChange`, and a `context` option.
+ @return {Ember.Array} receiver
+ */
+ addArrayObserver: function(target, opts) {
+ var willChange = (opts && opts.willChange) || 'arrayWillChange',
+ didChange = (opts && opts.didChange) || 'arrayDidChange';
+
+ var hasObservers = get(this, 'hasArrayObservers');
+ if (!hasObservers) Ember.propertyWillChange(this, 'hasArrayObservers');
+ Ember.addListener(this, '@array:before', target, willChange);
+ Ember.addListener(this, '@array:change', target, didChange);
+ if (!hasObservers) Ember.propertyDidChange(this, 'hasArrayObservers');
+ return this;
+ },
+
+ /**
+ Removes an array observer from the object if the observer is current
+ registered. Calling this method multiple times with the same object will
+ have no effect.
+
+ @method removeArrayObserver
+ @param {Object} target The object observing the array.
+ @return {Ember.Array} receiver
+ */
+ removeArrayObserver: function(target, opts) {
+ var willChange = (opts && opts.willChange) || 'arrayWillChange',
+ didChange = (opts && opts.didChange) || 'arrayDidChange';
+
+ var hasObservers = get(this, 'hasArrayObservers');
+ if (hasObservers) Ember.propertyWillChange(this, 'hasArrayObservers');
+ Ember.removeListener(this, '@array:before', target, willChange);
+ Ember.removeListener(this, '@array:change', target, didChange);
+ if (hasObservers) Ember.propertyDidChange(this, 'hasArrayObservers');
+ return this;
+ },
+
+ /**
+ Becomes true whenever the array currently has observers watching changes
+ on the array.
+
+ @property Boolean
+ */
+ hasArrayObservers: Ember.computed(function() {
+ return Ember.hasListeners(this, '@array:change') || Ember.hasListeners(this, '@array:before');
+ }),
+
+ /**
+ If you are implementing an object that supports `Ember.Array`, call this
+ method just before the array content changes to notify any observers and
+ invalidate any related properties. Pass the starting index of the change
+ as well as a delta of the amounts to change.
+
+ @method arrayContentWillChange
+ @param {Number} startIdx The starting index in the array that will change.
+ @param {Number} removeAmt The number of items that will be removed. If you
+ pass `null` assumes 0
+ @param {Number} addAmt The number of items that will be added If you
+ pass `null` assumes 0.
+ @return {Ember.Array} receiver
+ */
+ arrayContentWillChange: function(startIdx, removeAmt, addAmt) {
+
+ // if no args are passed assume everything changes
+ if (startIdx===undefined) {
+ startIdx = 0;
+ removeAmt = addAmt = -1;
+ } else {
+ if (removeAmt === undefined) removeAmt=-1;
+ if (addAmt === undefined) addAmt=-1;
+ }
+
+ // Make sure the @each proxy is set up if anyone is observing @each
+ if (Ember.isWatching(this, '@each')) { get(this, '@each'); }
+
+ Ember.sendEvent(this, '@array:before', [this, startIdx, removeAmt, addAmt]);
+
+ var removing, lim;
+ if (startIdx>=0 && removeAmt>=0 && get(this, 'hasEnumerableObservers')) {
+ removing = [];
+ lim = startIdx+removeAmt;
+ for(var idx=startIdx;idx<lim;idx++) removing.push(this.objectAt(idx));
+ } else {
+ removing = removeAmt;
+ }
+
+ this.enumerableContentWillChange(removing, addAmt);
+
+ return this;
+ },
+
+ arrayContentDidChange: function(startIdx, removeAmt, addAmt) {
+
+ // if no args are passed assume everything changes
+ if (startIdx===undefined) {
+ startIdx = 0;
+ removeAmt = addAmt = -1;
+ } else {
+ if (removeAmt === undefined) removeAmt=-1;
+ if (addAmt === undefined) addAmt=-1;
+ }
+
+ var adding, lim;
+ if (startIdx>=0 && addAmt>=0 && get(this, 'hasEnumerableObservers')) {
+ adding = [];
+ lim = startIdx+addAmt;
+ for(var idx=startIdx;idx<lim;idx++) adding.push(this.objectAt(idx));
+ } else {
+ adding = addAmt;
+ }
+
+ this.enumerableContentDidChange(removeAmt, adding);
+ Ember.sendEvent(this, '@array:change', [this, startIdx, removeAmt, addAmt]);
+
+ var length = get(this, 'length'),
+ cachedFirst = cacheFor(this, 'firstObject'),
+ cachedLast = cacheFor(this, 'lastObject');
+ if (this.objectAt(0) !== cachedFirst) {
+ Ember.propertyWillChange(this, 'firstObject');
+ Ember.propertyDidChange(this, 'firstObject');
+ }
+ if (this.objectAt(length-1) !== cachedLast) {
+ Ember.propertyWillChange(this, 'lastObject');
+ Ember.propertyDidChange(this, 'lastObject');
+ }
+
+ return this;
+ },
+
+ // ..........................................................
+ // ENUMERATED PROPERTIES
+ //
+
+ /**
+ Returns a special object that can be used to observe individual properties
+ on the array. Just get an equivalent property on this object and it will
+ return an enumerable that maps automatically to the named key on the
+ member objects.
+
+ @property @each
+ */
+ '@each': Ember.computed(function() {
+ if (!this.__each) this.__each = new Ember.EachProxy(this);
+ return this.__each;
+ })
+
+}) ;
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+
+/**
+ Implements some standard methods for comparing objects. Add this mixin to
+ any class you create that can compare its instances.
+
+ You should implement the `compare()` method.
+
+ @class Comparable
+ @namespace Ember
+ @extends Ember.Mixin
+ @since Ember 0.9
+*/
+Ember.Comparable = Ember.Mixin.create( /** @scope Ember.Comparable.prototype */{
+
+ /**
+ walk like a duck. Indicates that the object can be compared.
+
+ @property isComparable
+ @type Boolean
+ @default true
+ */
+ isComparable: true,
+
+ /**
+ Override to return the result of the comparison of the two parameters. The
+ compare method should return:
+
+ - `-1` if `a < b`
+ - `0` if `a == b`
+ - `1` if `a > b`
+
+ Default implementation raises an exception.
+
+ @method compare
+ @param a {Object} the first object to compare
+ @param b {Object} the second object to compare
+ @return {Integer} the result of the comparison
+ */
+ compare: Ember.required(Function)
+
+});
+
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+
+
+var get = Ember.get, set = Ember.set;
+
+/**
+ Implements some standard methods for copying an object. Add this mixin to
+ any object you create that can create a copy of itself. This mixin is
+ added automatically to the built-in array.
+
+ You should generally implement the `copy()` method to return a copy of the
+ receiver.
+
+ Note that `frozenCopy()` will only work if you also implement
+ `Ember.Freezable`.
+
+ @class Copyable
+ @namespace Ember
+ @extends Ember.Mixin
+ @since Ember 0.9
+*/
+Ember.Copyable = Ember.Mixin.create(
+/** @scope Ember.Copyable.prototype */ {
+
+ /**
+ Override to return a copy of the receiver. Default implementation raises
+ an exception.
+
+ @method copy
+ @param deep {Boolean} if `true`, a deep copy of the object should be made
+ @return {Object} copy of receiver
+ */
+ copy: Ember.required(Function),
+
+ /**
+ If the object implements `Ember.Freezable`, then this will return a new
+ copy if the object is not frozen and the receiver if the object is frozen.
+
+ Raises an exception if you try to call this method on a object that does
+ not support freezing.
+
+ You should use this method whenever you want a copy of a freezable object
+ since a freezable object can simply return itself without actually
+ consuming more memory.
+
+ @method frozenCopy
+ @return {Object} copy of receiver or receiver
+ */
+ frozenCopy: function() {
+ if (Ember.Freezable && Ember.Freezable.detect(this)) {
+ return get(this, 'isFrozen') ? this : this.copy().freeze();
+ } else {
+ throw new Error(Ember.String.fmt("%@ does not support freezing", [this]));
+ }
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+
+var get = Ember.get, set = Ember.set;
+
+/**
+ The `Ember.Freezable` mixin implements some basic methods for marking an
+ object as frozen. Once an object is frozen it should be read only. No changes
+ may be made the internal state of the object.
+
+ ## Enforcement
+
+ To fully support freezing in your subclass, you must include this mixin and
+ override any method that might alter any property on the object to instead
+ raise an exception. You can check the state of an object by checking the
+ `isFrozen` property.
+
+ Although future versions of JavaScript may support language-level freezing
+ object objects, that is not the case today. Even if an object is freezable,
+ it is still technically possible to modify the object, even though it could
+ break other parts of your application that do not expect a frozen object to
+ change. It is, therefore, very important that you always respect the
+ `isFrozen` property on all freezable objects.
+
+ ## Example Usage
+
+ The example below shows a simple object that implement the `Ember.Freezable`
+ protocol.
+
+ ```javascript
+ Contact = Ember.Object.extend(Ember.Freezable, {
+ firstName: null,
+ lastName: null,
+
+ // swaps the names
+ swapNames: function() {
+ if (this.get('isFrozen')) throw Ember.FROZEN_ERROR;
+ var tmp = this.get('firstName');
+ this.set('firstName', this.get('lastName'));
+ this.set('lastName', tmp);
+ return this;
+ }
+
+ });
+
+ c = Context.create({ firstName: "John", lastName: "Doe" });
+ c.swapNames(); // returns c
+ c.freeze();
+ c.swapNames(); // EXCEPTION
+ ```
+
+ ## Copying
+
+ Usually the `Ember.Freezable` protocol is implemented in cooperation with the
+ `Ember.Copyable` protocol, which defines a `frozenCopy()` method that will
+ return a frozen object, if the object implements this method as well.
+
+ @class Freezable
+ @namespace Ember
+ @extends Ember.Mixin
+ @since Ember 0.9
+*/
+Ember.Freezable = Ember.Mixin.create(
+/** @scope Ember.Freezable.prototype */ {
+
+ /**
+ Set to `true` when the object is frozen. Use this property to detect
+ whether your object is frozen or not.
+
+ @property isFrozen
+ @type Boolean
+ */
+ isFrozen: false,
+
+ /**
+ Freezes the object. Once this method has been called the object should
+ no longer allow any properties to be edited.
+
+ @method freeze
+ @return {Object} receiver
+ */
+ freeze: function() {
+ if (get(this, 'isFrozen')) return this;
+ set(this, 'isFrozen', true);
+ return this;
+ }
+
+});
+
+Ember.FROZEN_ERROR = "Frozen object cannot be modified.";
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+var forEach = Ember.EnumerableUtils.forEach;
+
+/**
+ This mixin defines the API for modifying generic enumerables. These methods
+ can be applied to an object regardless of whether it is ordered or
+ unordered.
+
+ Note that an Enumerable can change even if it does not implement this mixin.
+ For example, a MappedEnumerable cannot be directly modified but if its
+ underlying enumerable changes, it will change also.
+
+ ## Adding Objects
+
+ 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.
+
+ ```javascript
+ set.addObject(contact);
+ ```
+
+ ## 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
+ this method has no effect.
+
+ ```javascript
+ set.removeObject(contact);
+ ```
+
+ ## Implementing In Your Own Code
+
+ If you are implementing an object and want to support this API, just include
+ this mixin in your class and implement the required methods. In your unit
+ tests, be sure to apply the Ember.MutableEnumerableTests to your object.
+
+ @class MutableEnumerable
+ @namespace Ember
+ @extends Ember.Mixin
+ @uses Ember.Enumerable
+*/
+Ember.MutableEnumerable = Ember.Mixin.create(Ember.Enumerable,
+ /** @scope Ember.MutableEnumerable.prototype */ {
+
+ /**
+ __Required.__ You must implement this method to apply this mixin.
+
+ Attempts to add the passed object to the receiver if the object is not
+ already present in the collection. If the object is present, this method
+ has no effect.
+
+ If the passed object is of a type not supported by the receiver
+ then this method should raise an exception.
+
+ @method addObject
+ @param {Object} object The object to add to the enumerable.
+ @return {Object} the passed object
+ */
+ addObject: Ember.required(Function),
+
+ /**
+ Adds each object in the passed enumerable to the receiver.
+
+ @method addObjects
+ @param {Ember.Enumerable} objects the objects to add.
+ @return {Object} receiver
+ */
+ addObjects: function(objects) {
+ Ember.beginPropertyChanges(this);
+ forEach(objects, function(obj) { this.addObject(obj); }, this);
+ Ember.endPropertyChanges(this);
+ return this;
+ },
+
+ /**
+ __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,
+ this method has no effect.
+
+ If the passed object is of a type not supported by the receiver
+ then this method should raise an exception.
+
+ @method removeObject
+ @param {Object} object The object to remove from the enumerable.
+ @return {Object} the passed object
+ */
+ removeObject: Ember.required(Function),
+
+
+ /**
+ Removes each objects in the passed enumerable from the receiver.
+
+ @method removeObjects
+ @param {Ember.Enumerable} objects the objects to remove
+ @return {Object} receiver
+ */
+ removeObjects: function(objects) {
+ Ember.beginPropertyChanges(this);
+ forEach(objects, function(obj) { this.removeObject(obj); }, this);
+ Ember.endPropertyChanges(this);
+ return this;
+ }
+
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+// ..........................................................
+// CONSTANTS
+//
+
+var OUT_OF_RANGE_EXCEPTION = "Index out of range" ;
+var EMPTY = [];
+
+// ..........................................................
+// HELPERS
+//
+
+var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach;
+
+/**
+ This mixin defines the API for modifying array-like objects. These methods
+ can be applied only to a collection that keeps its items in an ordered set.
+
+ Note that an Array can change even if it does not implement this mixin.
+ For example, one might implement a SparseArray that cannot be directly
+ modified, but if its underlying enumerable changes, it will change also.
+
+ @class MutableArray
+ @namespace Ember
+ @extends Ember.Mixin
+ @uses Ember.Array
+ @uses Ember.MutableEnumerable
+*/
+Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable,
+ /** @scope Ember.MutableArray.prototype */ {
+
+ /**
+ __Required.__ You must implement this method to apply this mixin.
+
+ This is one of the primitives you must implement to support `Ember.Array`.
+ You should replace amt objects started at idx with the objects in the
+ passed array. You should also call `this.enumerableContentDidChange()`
+
+ @method replace
+ @param {Number} idx Starting index in the array to replace. If
+ idx >= length, then append to the end of the array.
+ @param {Number} amt Number of elements that should be removed from
+ the array, starting at *idx*.
+ @param {Array} objects An array of zero or more objects that should be
+ inserted into the array at *idx*
+ */
+ replace: Ember.required(),
+
+ /**
+ Remove all elements from self. This is useful if you
+ want to reuse an existing array without having to recreate it.
+
+ ```javascript
+ var colors = ["red", "green", "blue"];
+ color.length(); // 3
+ colors.clear(); // []
+ colors.length(); // 0
+ ```
+
+ @method clear
+ @return {Ember.Array} An empty Array.
+ */
+ clear: function () {
+ var len = get(this, 'length');
+ if (len === 0) return this;
+ this.replace(0, len, EMPTY);
+ return this;
+ },
+
+ /**
+ This will use the primitive `replace()` method to insert an object at the
+ specified index.
+
+ ```javascript
+ var colors = ["red", "green", "blue"];
+ colors.insertAt(2, "yellow"); // ["red", "green", "yellow", "blue"]
+ colors.insertAt(5, "orange"); // Error: Index out of range
+ ```
+
+ @method insertAt
+ @param {Number} idx index of insert the object at.
+ @param {Object} object object to insert
+ */
+ insertAt: function(idx, object) {
+ if (idx > get(this, 'length')) throw new Error(OUT_OF_RANGE_EXCEPTION) ;
+ this.replace(idx, 0, [object]) ;
+ return this ;
+ },
+
+ /**
+ Remove an object at the specified index using the `replace()` primitive
+ method. You can pass either a single index, or a start and a length.
+
+ If you pass a start and length that is beyond the
+ length this method will throw an `Ember.OUT_OF_RANGE_EXCEPTION`
+
+ ```javascript
+ var colors = ["red", "green", "blue", "yellow", "orange"];
+ colors.removeAt(0); // ["green", "blue", "yellow", "orange"]
+ colors.removeAt(2, 2); // ["green", "blue"]
+ colors.removeAt(4, 2); // Error: Index out of range
+ ```
+
+ @method removeAt
+ @param {Number} start index, start of range
+ @param {Number} len length of passing range
+ @return {Object} receiver
+ */
+ removeAt: function(start, len) {
+ if ('number' === typeof start) {
+
+ if ((start < 0) || (start >= get(this, 'length'))) {
+ throw new Error(OUT_OF_RANGE_EXCEPTION);
+ }
+
+ // fast case
+ if (len === undefined) len = 1;
+ this.replace(start, len, EMPTY);
+ }
+
+ return this ;
+ },
+
+ /**
+ Push the object onto the end of the array. Works just like `push()` but it
+ is KVO-compliant.
+
+ ```javascript
+ var colors = ["red", "green", "blue"];
+ colors.pushObject("black"); // ["red", "green", "blue", "black"]
+ colors.pushObject(["yellow", "orange"]); // ["red", "green", "blue", "black", ["yellow", "orange"]]
+ ```
+
+ @method pushObject
+ @param {anything} obj object to push
+ */
+ pushObject: function(obj) {
+ this.insertAt(get(this, 'length'), obj) ;
+ return obj ;
+ },
+
+ /**
+ Add the objects in the passed numerable to the end of the array. Defers
+ notifying observers of the change until all objects are added.
+
+ ```javascript
+ var colors = ["red", "green", "blue"];
+ colors.pushObjects("black"); // ["red", "green", "blue", "black"]
+ colors.pushObjects(["yellow", "orange"]); // ["red", "green", "blue", "black", "yellow", "orange"]
+ ```
+
+ @method pushObjects
+ @param {Ember.Enumerable} objects the objects to add
+ @return {Ember.Array} receiver
+ */
+ pushObjects: function(objects) {
+ this.replace(get(this, 'length'), 0, objects);
+ return this;
+ },
+
+ /**
+ Pop object from array or nil if none are left. Works just like `pop()` but
+ it is KVO-compliant.
+
+ ```javascript
+ var colors = ["red", "green", "blue"];
+ colors.popObject(); // "blue"
+ console.log(colors); // ["red", "green"]
+ ```
+
+ @method popObject
+ @return object
+ */
+ popObject: function() {
+ var len = get(this, 'length') ;
+ if (len === 0) return null ;
+
+ var ret = this.objectAt(len-1) ;
+ this.removeAt(len-1, 1) ;
+ return ret ;
+ },
+
+ /**
+ Shift an object from start of array or nil if none are left. Works just
+ like `shift()` but it is KVO-compliant.
+
+ ```javascript
+ var colors = ["red", "green", "blue"];
+ colors.shiftObject(); // "red"
+ console.log(colors); // ["green", "blue"]
+ ```
+
+ @method shiftObject
+ @return object
+ */
+ shiftObject: function() {
+ if (get(this, 'length') === 0) return null ;
+ var ret = this.objectAt(0) ;
+ this.removeAt(0) ;
+ return ret ;
+ },
+
+ /**
+ Unshift an object to start of array. Works just like `unshift()` but it is
+ KVO-compliant.
+
+ ```javascript
+ var colors = ["red", "green", "blue"];
+ colors.unshiftObject("yellow"); // ["yellow", "red", "green", "blue"]
+ colors.unshiftObject(["black", "white"]); // [["black", "white"], "yellow", "red", "green", "blue"]
+ ```
+
+ @method unshiftObject
+ @param {anything} obj object to unshift
+ */
+ unshiftObject: function(obj) {
+ this.insertAt(0, obj) ;
+ return obj ;
+ },
+
+ /**
+ Adds the named objects to the beginning of the array. Defers notifying
+ observers until all objects have been added.
+
+ ```javascript
+ var colors = ["red", "green", "blue"];
+ colors.unshiftObjects(["black", "white"]); // ["black", "white", "red", "green", "blue"]
+ colors.unshiftObjects("yellow"); // Type Error: 'undefined' is not a function
+ ```
+
+ @method unshiftObjects
+ @param {Ember.Enumerable} objects the objects to add
+ @return {Ember.Array} receiver
+ */
+ unshiftObjects: function(objects) {
+ this.replace(0, 0, objects);
+ return this;
+ },
+
+ /**
+ Reverse objects in the array. Works just like `reverse()` but it is
+ KVO-compliant.
+
+ @method reverseObjects
+ @return {Ember.Array} receiver
+ */
+ reverseObjects: function() {
+ var len = get(this, 'length');
+ if (len === 0) return this;
+ var objects = this.toArray().reverse();
+ this.replace(0, len, objects);
+ return this;
+ },
+
+ /**
+ Replace all the the receiver's content with content of the argument.
+ If argument is an empty array receiver will be cleared.
+
+ ```javascript
+ var colors = ["red", "green", "blue"];
+ colors.setObjects(["black", "white"]); // ["black", "white"]
+ colors.setObjects([]); // []
+ ```
+
+ @method setObjects
+ @param {Ember.Array} objects array whose content will be used for replacing
+ the content of the receiver
+ @return {Ember.Array} receiver with the new content
+ */
+ setObjects: function(objects) {
+ if (objects.length === 0) return this.clear();
+
+ var len = get(this, 'length');
+ this.replace(0, len, objects);
+ return this;
+ },
+
+ // ..........................................................
+ // IMPLEMENT Ember.MutableEnumerable
+ //
+
+ removeObject: function(obj) {
+ var loc = get(this, 'length') || 0;
+ while(--loc >= 0) {
+ var curObject = this.objectAt(loc) ;
+ if (curObject === obj) this.removeAt(loc) ;
+ }
+ return this ;
+ },
+
+ addObject: function(obj) {
+ if (!this.contains(obj)) this.pushObject(obj);
+ return this ;
+ }
+
+});
+
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+var get = Ember.get, set = Ember.set, defineProperty = Ember.defineProperty;
+
+/**
+ ## Overview
+
+ This mixin provides properties and property observing functionality, core
+ features of the Ember object model.
+
+ Properties and observers allow one object to observe changes to a
+ property on another object. This is one of the fundamental ways that
+ models, controllers and views communicate with each other in an Ember
+ application.
+
+ Any object that has this mixin applied can be used in observer
+ operations. That includes `Ember.Object` and most objects you will
+ interact with as you write your Ember application.
+
+ Note that you will not generally apply this mixin to classes yourself,
+ but you will use the features provided by this module frequently, so it
+ is important to understand how to use it.
+
+ ## Using `get()` and `set()`
+
+ Because of Ember's support for bindings and observers, you will always
+ access properties using the get method, and set properties using the
+ set method. This allows the observing objects to be notified and
+ computed properties to be handled properly.
+
+ More documentation about `get` and `set` are below.
+
+ ## Observing Property Changes
+
+ You typically observe property changes simply by adding the `observes`
+ call to the end of your method declarations in classes that you write.
+ For example:
+
+ ```javascript
+ Ember.Object.create({
+ valueObserver: function() {
+ // Executes whenever the "value" property changes
+ }.observes('value')
+ });
+ ```
+
+ Although this is the most common way to add an observer, this capability
+ is actually built into the `Ember.Object` class on top of two methods
+ defined in this mixin: `addObserver` and `removeObserver`. You can use
+ these two methods to add and remove observers yourself if you need to
+ do so at runtime.
+
+ To add an observer for a property, call:
+
+ ```javascript
+ object.addObserver('propertyKey', targetObject, targetAction)
+ ```
+
+ This will call the `targetAction` method on the `targetObject` to be called
+ 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
+ resulting value of the computed property is unchanged. This is necessary
+ because computed properties are not computed until `get` is called.
+
+ @class Observable
+ @namespace Ember
+ @extends Ember.Mixin
+*/
+Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ {
+
+ /**
+ Retrieves the value of a property from the object.
+
+ This method is usually similar to using `object[keyName]` or `object.keyName`,
+ however it supports both computed properties and the unknownProperty
+ handler.
+
+ Because `get` unifies the syntax for accessing all these kinds
+ of properties, it can make many refactorings easier, such as replacing a
+ simple property with a computed property, or vice versa.
+
+ ### Computed Properties
+
+ Computed properties are methods defined with the `property` modifier
+ declared at the end, such as:
+
+ ```javascript
+ fullName: function() {
+ return this.getEach('firstName', 'lastName').compact().join(' ');
+ }.property('firstName', 'lastName')
+ ```
+
+ When you call `get` on a computed property, the function will be
+ called and the return value will be returned instead of the function
+ itself.
+
+ ### Unknown Properties
+
+ Likewise, if you try to call `get` on a property whose value is
+ `undefined`, the `unknownProperty()` method will be called on the object.
+ If this method returns any value other than `undefined`, it will be returned
+ instead. This allows you to implement "virtual" properties that are
+ not defined upfront.
+
+ @method get
+ @param {String} key The property to retrieve
+ @return {Object} The property value or undefined.
+ */
+ get: function(keyName) {
+ return get(this, keyName);
+ },
+
+ /**
+ To get multiple properties at once, call `getProperties`
+ with a list of strings or an array:
+
+ ```javascript
+ record.getProperties('firstName', 'lastName', 'zipCode'); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' }
+ ```
+
+ is equivalent to:
+
+ ```javascript
+ record.getProperties(['firstName', 'lastName', 'zipCode']); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' }
+ ```
+
+ @method getProperties
+ @param {String...|Array} list of keys to get
+ @return {Hash}
+ */
+ getProperties: function() {
+ var ret = {};
+ var propertyNames = arguments;
+ if (arguments.length === 1 && Ember.typeOf(arguments[0]) === 'array') {
+ propertyNames = arguments[0];
+ }
+ for(var i = 0; i < propertyNames.length; i++) {
+ ret[propertyNames[i]] = get(this, propertyNames[i]);
+ }
+ return ret;
+ },
+
+ /**
+ Sets the provided key or path to the value.
+
+ This method is generally very similar to calling `object[key] = value` or
+ `object.key = value`, except that it provides support for computed
+ properties, the `unknownProperty()` method and property observers.
+
+ ### Computed Properties
+
+ If you try to set a value on a key that has a computed property handler
+ defined (see the `get()` method for an example), then `set()` will call
+ that method, passing both the value and key instead of simply changing
+ the value itself. This is useful for those times when you need to
+ implement a property that is composed of one or more member
+ properties.
+
+ ### Unknown Properties
+
+ If you try to set a value on a key that is undefined in the target
+ object, then the `unknownProperty()` handler will be called instead. This
+ gives you an opportunity to implement complex "virtual" properties that
+ are not predefined on the object. If `unknownProperty()` returns
+ undefined, then `set()` will simply set the value on the object.
+
+ ### Property Observers
+
+ In addition to changing the property, `set()` will also register a property
+ change with the object. Unless you have placed this call inside of a
+ `beginPropertyChanges()` and `endPropertyChanges(),` any "local" observers
+ (i.e. observer methods declared on the same object), will be called
+ immediately. Any "remote" observers (i.e. observer methods declared on
+ another object) will be placed in a queue and called at a later time in a
+ coalesced manner.
+
+ ### Chaining
+
+ In addition to property changes, `set()` returns the value of the object
+ itself so you can do chaining like this:
+
+ ```javascript
+ record.set('firstName', 'Charles').set('lastName', 'Jolley');
+ ```
+
+ @method set
+ @param {String} key The property to set
+ @param {Object} value The value to set or `null`.
+ @return {Ember.Observable}
+ */
+ set: function(keyName, value) {
+ set(this, keyName, value);
+ return this;
+ },
+
+ /**
+ To set multiple properties at once, call `setProperties`
+ with a Hash:
+
+ ```javascript
+ record.setProperties({ firstName: 'Charles', lastName: 'Jolley' });
+ ```
+
+ @method setProperties
+ @param {Hash} hash the hash of keys and values to set
+ @return {Ember.Observable}
+ */
+ setProperties: function(hash) {
+ return Ember.setProperties(this, hash);
+ },
+
+ /**
+ Begins a grouping of property changes.
+
+ You can use this method to group property changes so that notifications
+ will not be sent until the changes are finished. If you plan to make a
+ large number of changes to an object at one time, you should call this
+ method at the beginning of the changes to begin deferring change
+ notifications. When you are done making changes, call
+ `endPropertyChanges()` to deliver the deferred change notifications and end
+ deferring.
+
+ @method beginPropertyChanges
+ @return {Ember.Observable}
+ */
+ beginPropertyChanges: function() {
+ Ember.beginPropertyChanges();
+ return this;
+ },
+
+ /**
+ Ends a grouping of property changes.
+
+ You can use this method to group property changes so that notifications
+ will not be sent until the changes are finished. If you plan to make a
+ large number of changes to an object at one time, you should call
+ `beginPropertyChanges()` at the beginning of the changes to defer change
+ notifications. When you are done making changes, call this method to
+ deliver the deferred change notifications and end deferring.
+
+ @method endPropertyChanges
+ @return {Ember.Observable}
+ */
+ endPropertyChanges: function() {
+ Ember.endPropertyChanges();
+ return this;
+ },
+
+ /**
+ Notify the observer system that a property is about to change.
+
+ Sometimes you need to change a value directly or indirectly without
+ actually calling `get()` or `set()` on it. In this case, you can use this
+ method and `propertyDidChange()` instead. Calling these two methods
+ together will notify all observers that the property has potentially
+ changed value.
+
+ Note that you must always call `propertyWillChange` and `propertyDidChange`
+ as a pair. If you do not, it may get the property change groups out of
+ order and cause notifications to be delivered more often than you would
+ like.
+
+ @method propertyWillChange
+ @param {String} key The property key that is about to change.
+ @return {Ember.Observable}
+ */
+ propertyWillChange: function(keyName){
+ Ember.propertyWillChange(this, keyName);
+ return this;
+ },
+
+ /**
+ Notify the observer system that a property has just changed.
+
+ Sometimes you need to change a value directly or indirectly without
+ actually calling `get()` or `set()` on it. In this case, you can use this
+ method and `propertyWillChange()` instead. Calling these two methods
+ together will notify all observers that the property has potentially
+ changed value.
+
+ Note that you must always call `propertyWillChange` and `propertyDidChange`
+ as a pair. If you do not, it may get the property change groups out of
+ order and cause notifications to be delivered more often than you would
+ like.
+
+ @method propertyDidChange
+ @param {String} keyName The property key that has just changed.
+ @return {Ember.Observable}
+ */
+ propertyDidChange: function(keyName) {
+ Ember.propertyDidChange(this, keyName);
+ return this;
+ },
+
+ /**
+ Convenience method to call `propertyWillChange` and `propertyDidChange` in
+ succession.
+
+ @method notifyPropertyChange
+ @param {String} keyName The property key to be notified about.
+ @return {Ember.Observable}
+ */
+ notifyPropertyChange: function(keyName) {
+ this.propertyWillChange(keyName);
+ this.propertyDidChange(keyName);
+ return this;
+ },
+
+ addBeforeObserver: function(key, target, method) {
+ Ember.addBeforeObserver(this, key, target, method);
+ },
+
+ /**
+ Adds an observer on a property.
+
+ 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
+ value is set, regardless of whether it has actually changed. Your
+ observer should be prepared to handle that.
+
+ You can also pass an optional context parameter to this method. The
+ context will be passed to your observer method whenever it is triggered.
+ Note that if you add the same target/method pair on a key multiple times
+ with different context parameters, your observer will only be called once
+ with the last context you passed.
+
+ ### Observer Methods
+
+ Observer methods you pass should generally have the following signature if
+ you do not pass a `context` parameter:
+
+ ```javascript
+ fooDidChange: function(sender, key, value, rev) { };
+ ```
+
+ The sender is the object that changed. The key is the property that
+ changes. The value property is currently reserved and unused. The rev
+ is the last property revision of the object when it changed, which you can
+ use to detect if the key value has really changed or not.
+
+ If you pass a `context` parameter, the context will be passed before the
+ revision like so:
+
+ ```javascript
+ fooDidChange: function(sender, key, value, context, rev) { };
+ ```
+
+ Usually you will not need the value, context or revision parameters at
+ the end. In this case, it is common to write observer methods that take
+ only a sender and key value as parameters or, if you aren't interested in
+ any of these values, to write an observer that has no parameters at all.
+
+ @method addObserver
+ @param {String} key The key to observer
+ @param {Object} target The target object to invoke
+ @param {String|Function} method The method to invoke.
+ @return {Ember.Object} self
+ */
+ addObserver: function(key, target, method) {
+ Ember.addObserver(this, key, target, method);
+ },
+
+ /**
+ Remove an observer you have previously registered on this object. Pass
+ the same key, target, and method you passed to `addObserver()` and your
+ target will no longer receive notifications.
+
+ @method removeObserver
+ @param {String} key The key to observer
+ @param {Object} target The target object to invoke
+ @param {String|Function} method The method to invoke.
+ @return {Ember.Observable} receiver
+ */
+ removeObserver: function(key, target, method) {
+ Ember.removeObserver(this, key, target, method);
+ },
+
+ /**
+ Returns `true` if the object currently has observers registered for a
+ particular key. You can use this method to potentially defer performing
+ an expensive action until someone begins observing a particular property
+ on the object.
+
+ @method hasObserverFor
+ @param {String} key Key to check
+ @return {Boolean}
+ */
+ hasObserverFor: function(key) {
+ return Ember.hasListeners(this, key+':change');
+ },
+
+ /**
+ @deprecated
+ @method getPath
+ @param {String} path The property path to retrieve
+ @return {Object} The property value or undefined.
+ */
+ getPath: function(path) {
+ Ember.deprecate("getPath is deprecated since get now supports paths");
+ return this.get(path);
+ },
+
+ /**
+ @deprecated
+ @method setPath
+ @param {String} path The path to the property that will be set
+ @param {Object} value The value to set or `null`.
+ @return {Ember.Observable}
+ */
+ setPath: function(path, value) {
+ Ember.deprecate("setPath is deprecated since set now supports paths");
+ return this.set(path, value);
+ },
+
+ /**
+ Retrieves the value of a property, or a default value in the case that the
+ property returns `undefined`.
+
+ ```javascript
+ person.getWithDefault('lastName', 'Doe');
+ ```
+
+ @method getWithDefault
+ @param {String} keyName The name of the property to retrieve
+ @param {Object} defaultValue The value to return if the property value is undefined
+ @return {Object} The property value or the defaultValue.
+ */
+ getWithDefault: function(keyName, defaultValue) {
+ return Ember.getWithDefault(this, keyName, defaultValue);
+ },
+
+ /**
+ Set the value of a property to the current value plus some amount.
+
+ ```javascript
+ person.incrementProperty('age');
+ team.incrementProperty('score', 2);
+ ```
+
+ @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
+ */
+ incrementProperty: function(keyName, increment) {
+ if (!increment) { increment = 1; }
+ set(this, keyName, (get(this, keyName) || 0)+increment);
+ return get(this, keyName);
+ },
+
+ /**
+ Set the value of a property to the current value minus some amount.
+
+ ```javascript
+ player.decrementProperty('lives');
+ orc.decrementProperty('health', 5);
+ ```
+
+ @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
+ */
+ decrementProperty: function(keyName, increment) {
+ if (!increment) { increment = 1; }
+ set(this, keyName, (get(this, keyName) || 0)-increment);
+ return get(this, keyName);
+ },
+
+ /**
+ Set the value of a boolean property to the opposite of it's
+ current value.
+
+ ```javascript
+ starship.toggleProperty('warpDriveEnaged');
+ ```
+
+ @method toggleProperty
+ @param {String} keyName The name of the property to toggle
+ @return {Object} The new property value
+ */
+ toggleProperty: function(keyName) {
+ set(this, keyName, !get(this, keyName));
+ return get(this, keyName);
+ },
+
+ /**
+ Returns the cached value of a computed property, if it exists.
+ This allows you to inspect the value of a computed property
+ without accidentally invoking it if it is intended to be
+ generated lazily.
+
+ @method cacheFor
+ @param {String} keyName
+ @return {Object} The cached value of the computed property, if any
+ */
+ cacheFor: function(keyName) {
+ return Ember.cacheFor(this, keyName);
+ },
+
+ // intended for debugging purposes
+ observersForKey: function(keyName) {
+ return Ember.observersFor(this, keyName);
+ }
+});
+
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+var get = Ember.get, set = Ember.set;
+
+/**
+@class TargetActionSupport
+@namespace Ember
+@extends Ember.Mixin
+*/
+Ember.TargetActionSupport = Ember.Mixin.create({
+ target: null,
+ action: null,
+
+ targetObject: Ember.computed(function() {
+ var target = get(this, 'target');
+
+ if (Ember.typeOf(target) === "string") {
+ var value = get(this, target);
+ if (value === undefined) { value = get(Ember.lookup, target); }
+ return value;
+ } else {
+ return target;
+ }
+ }).property('target'),
+
+ triggerAction: function() {
+ var action = get(this, 'action'),
+ target = get(this, 'targetObject');
+
+ if (target && action) {
+ var ret;
+
+ if (typeof target.send === 'function') {
+ ret = target.send(action, this);
+ } else {
+ if (typeof action === 'string') {
+ action = target[action];
+ }
+ ret = action.call(target, this);
+ }
+ if (ret !== false) ret = true;
+
+ return ret;
+ } else {
+ return false;
+ }
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+/**
+ This mixin allows for Ember objects to subscribe to and emit events.
+
+ ```javascript
+ App.Person = Ember.Object.extend(Ember.Evented, {
+ greet: function() {
+ // ...
+ this.trigger('greet');
+ }
+ });
+
+ var person = App.Person.create();
+
+ person.on('greet', function() {
+ console.log('Our person has greeted');
+ });
+
+ person.greet();
+
+ // outputs: 'Our person has greeted'
+ ```
+
+ @class Evented
+ @namespace Ember
+ @extends Ember.Mixin
+ */
+Ember.Evented = Ember.Mixin.create({
+
+ /**
+ Subscribes to a named event with given function.
+
+ ```javascript
+ person.on('didLoad', function() {
+ // fired once the person has loaded
+ });
+ ```
+
+ An optional target can be passed in as the 2nd argument that will
+ be set as the "this" for the callback. This is a good way to give your
+ function access to the object triggering the event. When the target
+ parameter is used the callback becomes the third argument.
+
+ @method on
+ @param {String} name The name of the event
+ @param {Object} [target] The "this" binding for the callback
+ @param {Function} method The callback to execute
+ */
+ on: function(name, target, method) {
+ Ember.addListener(this, name, target, method);
+ },
+
+ /**
+ Subscribes a function to a named event and then cancels the subscription
+ after the first time the event is triggered. It is good to use ``one`` when
+ you only care about the first time an event has taken place.
+
+ This function takes an optional 2nd argument that will become the "this"
+ value for the callback. If this argument is passed then the 3rd argument
+ becomes the function.
+
+ @method one
+ @param {String} name The name of the event
+ @param {Object} [target] The "this" binding for the callback
+ @param {Function} method The callback to execute
+ */
+ one: function(name, target, method) {
+ if (!method) {
+ method = target;
+ target = null;
+ }
+
+ Ember.addListener(this, name, target, method, true);
+ },
+
+ /**
+ Triggers a named event for the object. Any additional arguments
+ will be passed as parameters to the functions that are subscribed to the
+ event.
+
+ ```javascript
+ person.on('didEat', function(food) {
+ console.log('person ate some ' + food);
+ });
+
+ person.trigger('didEat', 'broccoli');
+
+ // outputs: person ate some broccoli
+ ```
+ @method trigger
+ @param {String} name The name of the event
+ @param {Object...} args Optional arguments to pass on
+ */
+ trigger: function(name) {
+ var args = [], i, l;
+ for (i = 1, l = arguments.length; i < l; i++) {
+ args.push(arguments[i]);
+ }
+ Ember.sendEvent(this, name, args);
+ },
+
+ fire: function(name) {
+ Ember.deprecate("Ember.Evented#fire() has been deprecated in favor of trigger() for compatibility with jQuery. It will be removed in 1.0. Please update your code to call trigger() instead.");
+ this.trigger.apply(this, arguments);
+ },
+
+ /**
+ Cancels subscription for give name, target, and method.
+
+ @method off
+ @param {String} name The name of the event
+ @param {Object} target The target of the subscription
+ @param {Function} method The function of the subscription
+ */
+ off: function(name, target, method) {
+ Ember.removeListener(this, name, target, method);
+ },
+
+ /**
+ Checks to see if object has any subscriptions for named event.
+
+ @method has
+ @param {String} name The name of the event
+ @return {Boolean} does the object have a subscription for event
+ */
+ has: function(name) {
+ return Ember.hasListeners(this, name);
+ }
+});
+
+})();
+
+
+
+(function() {
+var RSVP = requireModule("rsvp");
+
+RSVP.async = function(callback, binding) {
+ Ember.run.schedule('actions', binding, callback);
+};
+
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+var get = Ember.get,
+ slice = Array.prototype.slice;
+
+/**
+ @class Deferred
+ @namespace Ember
+ @extends Ember.Mixin
+ */
+Ember.DeferredMixin = Ember.Mixin.create({
+ /**
+ Add handlers to be called when the Deferred object is resolved or rejected.
+
+ @method then
+ @param {Function} doneCallback a callback function to be called when done
+ @param {Function} failCallback a callback function to be called when failed
+ */
+ then: function(doneCallback, failCallback) {
+ var promise = get(this, 'promise');
+ return promise.then.apply(promise, arguments);
+ },
+
+ /**
+ Resolve a Deferred object and call any `doneCallbacks` with the given args.
+
+ @method resolve
+ */
+ resolve: function(value) {
+ get(this, 'promise').resolve(value);
+ },
+
+ /**
+ Reject a Deferred object and call any `failCallbacks` with the given args.
+
+ @method reject
+ */
+ reject: function(value) {
+ get(this, 'promise').reject(value);
+ },
+
+ promise: Ember.computed(function() {
+ return new RSVP.Promise();
+ })
+});
+
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(function() {
+Ember.Container = requireModule('container');
+Ember.Container.set = Ember.set;
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+
+// NOTE: this object should never be included directly. Instead use Ember.
+// Ember.Object. We only define this separately so that Ember.Set can depend on it
+
+
+var set = Ember.set, get = Ember.get,
+ o_create = Ember.create,
+ o_defineProperty = Ember.platform.defineProperty,
+ a_slice = Array.prototype.slice,
+ GUID_KEY = Ember.GUID_KEY,
+ guidFor = Ember.guidFor,
+ generateGuid = Ember.generateGuid,
+ meta = Ember.meta,
+ rewatch = Ember.rewatch,
+ finishChains = Ember.finishChains,
+ destroy = Ember.destroy,
+ schedule = Ember.run.schedule,
+ Mixin = Ember.Mixin,
+ applyMixin = Mixin._apply,
+ finishPartial = Mixin.finishPartial,
+ reopen = Mixin.prototype.reopen,
+ MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER,
+ indexOf = Ember.EnumerableUtils.indexOf;
+
+var undefinedDescriptor = {
+ configurable: true,
+ writable: true,
+ enumerable: false,
+ value: undefined
+};
+
+function makeCtor() {
+
+ // Note: avoid accessing any properties on the object since it makes the
+ // method a lot faster. This is glue code so we want it to be as fast as
+ // possible.
+
+ var wasApplied = false, initMixins, initProperties;
+
+ var Class = function() {
+ if (!wasApplied) {
+ Class.proto(); // prepare prototype...
+ }
+ o_defineProperty(this, GUID_KEY, undefinedDescriptor);
+ o_defineProperty(this, '_super', undefinedDescriptor);
+ var m = meta(this);
+ m.proto = this;
+ if (initMixins) {
+ // capture locally so we can clear the closed over variable
+ var mixins = initMixins;
+ initMixins = null;
+ this.reopen.apply(this, mixins);
+ }
+ if (initProperties) {
+ // capture locally so we can clear the closed over variable
+ var props = initProperties;
+ initProperties = null;
+
+ var concatenatedProperties = this.concatenatedProperties;
+
+ for (var i = 0, l = props.length; i < l; i++) {
+ var properties = props[i];
+ for (var keyName in properties) {
+ if (!properties.hasOwnProperty(keyName)) { continue; }
+
+ var value = properties[keyName],
+ IS_BINDING = Ember.IS_BINDING;
+
+ if (IS_BINDING.test(keyName)) {
+ var bindings = m.bindings;
+ if (!bindings) {
+ bindings = m.bindings = {};
+ } else if (!m.hasOwnProperty('bindings')) {
+ bindings = m.bindings = o_create(m.bindings);
+ }
+ bindings[keyName] = value;
+ }
+
+ var desc = m.descs[keyName];
+
+ Ember.assert("Ember.Object.create no longer supports defining computed properties.", !(value instanceof Ember.ComputedProperty));
+ Ember.assert("Ember.Object.create no longer supports defining methods that call _super.", !(typeof value === 'function' && value.toString().indexOf('._super') !== -1));
+
+ if (concatenatedProperties && indexOf(concatenatedProperties, keyName) >= 0) {
+ var baseValue = this[keyName];
+
+ if (baseValue) {
+ if ('function' === typeof baseValue.concat) {
+ value = baseValue.concat(value);
+ } else {
+ value = Ember.makeArray(baseValue).concat(value);
+ }
+ } else {
+ value = Ember.makeArray(value);
+ }
+ }
+
+ if (desc) {
+ desc.set(this, keyName, value);
+ } else {
+ if (typeof this.setUnknownProperty === 'function' && !(keyName in this)) {
+ this.setUnknownProperty(keyName, value);
+ } else if (MANDATORY_SETTER) {
+ Ember.defineProperty(this, keyName, null, value); // setup mandatory setter
+ } else {
+ this[keyName] = value;
+ }
+ }
+ }
+ }
+ }
+ finishPartial(this, m);
+ delete m.proto;
+ finishChains(this);
+ this.init.apply(this, arguments);
+ };
+
+ Class.toString = Mixin.prototype.toString;
+ Class.willReopen = function() {
+ if (wasApplied) {
+ Class.PrototypeMixin = Mixin.create(Class.PrototypeMixin);
+ }
+
+ wasApplied = false;
+ };
+ Class._initMixins = function(args) { initMixins = args; };
+ Class._initProperties = function(args) { initProperties = args; };
+
+ Class.proto = function() {
+ var superclass = Class.superclass;
+ if (superclass) { superclass.proto(); }
+
+ if (!wasApplied) {
+ wasApplied = true;
+ Class.PrototypeMixin.applyPartial(Class.prototype);
+ rewatch(Class.prototype);
+ }
+
+ return this.prototype;
+ };
+
+ return Class;
+
+}
+
+var CoreObject = makeCtor();
+CoreObject.toString = function() { return "Ember.CoreObject"; };
+
+CoreObject.PrototypeMixin = Mixin.create({
+ reopen: function() {
+ applyMixin(this, arguments, true);
+ return this;
+ },
+
+ isInstance: true,
+
+ init: function() {},
+
+ /**
+ Defines the properties that will be concatenated from the superclass
+ (instead of overridden).
+
+ By default, when you extend an Ember class a property defined in
+ the subclass overrides a property with the same name that is defined
+ in the superclass. However, there are some cases where it is preferable
+ to build up a property's value by combining the superclass' property
+ value with the subclass' value. An example of this in use within Ember
+ is the `classNames` property of `Ember.View`.
+
+ Here is some sample code showing the difference between a concatenated
+ property and a normal one:
+
+ ```javascript
+ App.BarView = Ember.View.extend({
+ someNonConcatenatedProperty: ['bar'],
+ classNames: ['bar']
+ });
+
+ App.FooBarView = App.BarView.extend({
+ someNonConcatenatedProperty: ['foo'],
+ classNames: ['foo'],
+ });
+
+ var fooBarView = App.FooBarView.create();
+ fooBarView.get('someNonConcatenatedProperty'); // ['foo']
+ fooBarView.get('classNames'); // ['ember-view', 'bar', 'foo']
+ ```
+
+ This behavior extends to object creation as well. Continuing the
+ above example:
+
+ ```javascript
+ var view = App.FooBarView.create({
+ someNonConcatenatedProperty: ['baz'],
+ classNames: ['baz']
+ })
+ view.get('someNonConcatenatedProperty'); // ['baz']
+ view.get('classNames'); // ['ember-view', 'bar', 'foo', 'baz']
+ ```
+ Adding a single property that is not an array will just add it in the array:
+
+ ```javascript
+ var view = App.FooBarView.create({
+ classNames: 'baz'
+ })
+ view.get('classNames'); // ['ember-view', 'bar', 'foo', 'baz']
+ ```
+
+ Using the `concatenatedProperties` property, we can tell to Ember that mix
+ the content of the properties.
+
+ In `Ember.View` the `classNameBindings` and `attributeBindings` properties
+ are also concatenated, in addition to `classNames`.
+
+ This feature is available for you to use throughout the Ember object model,
+ although typical app developers are likely to use it infrequently.
+
+ @property concatenatedProperties
+ @type Array
+ @default null
+ */
+ concatenatedProperties: null,
+
+ /**
+ @property isDestroyed
+ @default false
+ */
+ isDestroyed: false,
+
+ /**
+ @property isDestroying
+ @default false
+ */
+ isDestroying: false,
+
+ /**
+ Destroys an object by setting the `isDestroyed` flag and removing its
+ metadata, which effectively destroys observers and bindings.
+
+ If you try to set a property on a destroyed object, an exception will be
+ raised.
+
+ Note that destruction is scheduled for the end of the run loop and does not
+ happen immediately.
+
+ @method destroy
+ @return {Ember.Object} receiver
+ */
+ destroy: function() {
+ if (this._didCallDestroy) { return; }
+
+ this.isDestroying = true;
+ this._didCallDestroy = true;
+
+ if (this.willDestroy) { this.willDestroy(); }
+
+ schedule('destroy', this, this._scheduledDestroy);
+ return this;
+ },
+
+ /**
+ @private
+
+ Invoked by the run loop to actually destroy the object. This is
+ scheduled for execution by the `destroy` method.
+
+ @method _scheduledDestroy
+ */
+ _scheduledDestroy: function() {
+ destroy(this);
+ set(this, 'isDestroyed', true);
+
+ if (this.didDestroy) { this.didDestroy(); }
+ },
+
+ bind: function(to, from) {
+ if (!(from instanceof Ember.Binding)) { from = Ember.Binding.from(from); }
+ from.to(to).connect(this);
+ return from;
+ },
+
+ /**
+ Returns a string representation which attempts to provide more information
+ than Javascript's `toString` typically does, in a generic way for all Ember
+ objects.
+
+ App.Person = Em.Object.extend()
+ person = App.Person.create()
+ person.toString() //=> "<App.Person:ember1024>"
+
+ If the object's class is not defined on an Ember namespace, it will
+ indicate it is a subclass of the registered superclass:
+
+ Student = App.Person.extend()
+ student = Student.create()
+ student.toString() //=> "<(subclass of App.Person):ember1025>"
+
+ If the method `toStringExtension` is defined, its return value will be
+ included in the output.
+
+ App.Teacher = App.Person.extend({
+ toStringExtension: function(){
+ return this.get('fullName');
+ }
+ });
+ teacher = App.Teacher.create()
+ teacher.toString(); // #=> "<App.Teacher:ember1026:Tom Dale>"
+
+ @method toString
+ @return {String} string representation
+ */
+ toString: function toString() {
+ var hasToStringExtension = typeof this.toStringExtension === 'function',
+ extension = hasToStringExtension ? ":" + this.toStringExtension() : '';
+ var ret = '<'+this.constructor.toString()+':'+guidFor(this)+extension+'>';
+ this.toString = makeToString(ret);
+ return ret;
+ }
+});
+
+CoreObject.PrototypeMixin.ownerConstructor = CoreObject;
+
+function makeToString(ret) {
+ return function() { return ret; };
+}
+
+if (Ember.config.overridePrototypeMixin) {
+ Ember.config.overridePrototypeMixin(CoreObject.PrototypeMixin);
+}
+
+CoreObject.__super__ = null;
+
+var ClassMixin = Mixin.create({
+
+ ClassMixin: Ember.required(),
+
+ PrototypeMixin: Ember.required(),
+
+ isClass: true,
+
+ isMethod: false,
+
+ extend: function() {
+ var Class = makeCtor(), proto;
+ Class.ClassMixin = Mixin.create(this.ClassMixin);
+ Class.PrototypeMixin = Mixin.create(this.PrototypeMixin);
+
+ Class.ClassMixin.ownerConstructor = Class;
+ Class.PrototypeMixin.ownerConstructor = Class;
+
+ reopen.apply(Class.PrototypeMixin, arguments);
+
+ Class.superclass = this;
+ Class.__super__ = this.prototype;
+
+ proto = Class.prototype = o_create(this.prototype);
+ proto.constructor = Class;
+ generateGuid(proto, 'ember');
+ meta(proto).proto = proto; // this will disable observers on prototype
+
+ Class.ClassMixin.apply(Class);
+ return Class;
+ },
+
+ createWithMixins: function() {
+ var C = this;
+ if (arguments.length>0) { this._initMixins(arguments); }
+ return new C();
+ },
+
+ create: function() {
+ var C = this;
+ if (arguments.length>0) { this._initProperties(arguments); }
+ return new C();
+ },
+
+ reopen: function() {
+ this.willReopen();
+ reopen.apply(this.PrototypeMixin, arguments);
+ return this;
+ },
+
+ reopenClass: function() {
+ reopen.apply(this.ClassMixin, arguments);
+ applyMixin(this, arguments, false);
+ return this;
+ },
+
+ detect: function(obj) {
+ if ('function' !== typeof obj) { return false; }
+ while(obj) {
+ if (obj===this) { return true; }
+ obj = obj.superclass;
+ }
+ return false;
+ },
+
+ detectInstance: function(obj) {
+ return obj instanceof this;
+ },
+
+ /**
+ In some cases, you may want to annotate computed properties with additional
+ metadata about how they function or what values they operate on. For
+ example, computed property functions may close over variables that are then
+ no longer available for introspection.
+
+ You can pass a hash of these values to a computed property like this:
+
+ ```javascript
+ person: function() {
+ var personId = this.get('personId');
+ return App.Person.create({ id: personId });
+ }.property().meta({ type: App.Person })
+ ```
+
+ Once you've done this, you can retrieve the values saved to the computed
+ property from your class like this:
+
+ ```javascript
+ MyClass.metaForProperty('person');
+ ```
+
+ This will return the original hash that was passed to `meta()`.
+
+ @method metaForProperty
+ @param key {String} property name
+ */
+ metaForProperty: function(key) {
+ var desc = meta(this.proto(), false).descs[key];
+
+ Ember.assert("metaForProperty() could not find a computed property with key '"+key+"'.", !!desc && desc instanceof Ember.ComputedProperty);
+ return desc._meta || {};
+ },
+
+ /**
+ Iterate over each computed property for the class, passing its name
+ and any associated metadata (see `metaForProperty`) to the callback.
+
+ @method eachComputedProperty
+ @param {Function} callback
+ @param {Object} binding
+ */
+ eachComputedProperty: function(callback, binding) {
+ var proto = this.proto(),
+ descs = meta(proto).descs,
+ empty = {},
+ property;
+
+ for (var name in descs) {
+ property = descs[name];
+
+ if (property instanceof Ember.ComputedProperty) {
+ callback.call(binding || this, name, property._meta || empty);
+ }
+ }
+ }
+
+});
+
+ClassMixin.ownerConstructor = CoreObject;
+
+if (Ember.config.overrideClassMixin) {
+ Ember.config.overrideClassMixin(ClassMixin);
+}
+
+CoreObject.ClassMixin = ClassMixin;
+ClassMixin.apply(CoreObject);
+
+/**
+ @class CoreObject
+ @namespace Ember
+*/
+Ember.CoreObject = CoreObject;
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, none = Ember.isNone;
+
+/**
+ An unordered collection of objects.
+
+ A Set works a bit like an array except that its items are not ordered. You
+ can create a set to efficiently test for membership for an object. You can
+ also iterate through a set just like an array, even accessing objects by
+ index, however there is no guarantee as to their order.
+
+ All Sets are observable via the Enumerable Observer API - which works
+ on any enumerable object including both Sets and Arrays.
+
+ ## Creating a Set
+
+ You can create a set like you would most objects using
+ `new Ember.Set()`. Most new sets you create will be empty, but you can
+ also initialize the set with some content by passing an array or other
+ enumerable of objects to the constructor.
+
+ Finally, you can pass in an existing set and the set will be copied. You
+ can also create a copy of a set by calling `Ember.Set#copy()`.
+
+ ```javascript
+ // creates a new empty set
+ var foundNames = new Ember.Set();
+
+ // creates a set with four names in it.
+ var names = new Ember.Set(["Charles", "Tom", "Juan", "Alex"]); // :P
+
+ // creates a copy of the names set.
+ var namesCopy = new Ember.Set(names);
+
+ // same as above.
+ var anotherNamesCopy = names.copy();
+ ```
+
+ ## Adding/Removing Objects
+
+ You generally add or remove objects from a set using `add()` or
+ `remove()`. You can add any type of object including primitives such as
+ numbers, strings, and booleans.
+
+ Unlike arrays, objects can only exist one time in a set. If you call `add()`
+ on a set with the same object multiple times, the object will only be added
+ once. Likewise, calling `remove()` with the same object multiple times will
+ remove the object the first time and have no effect on future calls until
+ you add the object to the set again.
+
+ NOTE: You cannot add/remove `null` or `undefined` to a set. Any attempt to do
+ so will be ignored.
+
+ In addition to add/remove you can also call `push()`/`pop()`. Push behaves
+ just like `add()` but `pop()`, unlike `remove()` will pick an arbitrary
+ object, remove it and return it. This is a good way to use a set as a job
+ queue when you don't care which order the jobs are executed in.
+
+ ## Testing for an Object
+
+ To test for an object's presence in a set you simply call
+ `Ember.Set#contains()`.
+
+ ## Observing changes
+
+ When using `Ember.Set`, you can observe the `"[]"` property to be
+ alerted whenever the content changes. You can also add an enumerable
+ observer to the set to be notified of specific objects that are added and
+ removed from the set. See `Ember.Enumerable` for more information on
+ enumerables.
+
+ This is often unhelpful. If you are filtering sets of objects, for instance,
+ it is very inefficient to re-filter all of the items each time the set
+ changes. It would be better if you could just adjust the filtered set based
+ on what was changed on the original set. The same issue applies to merging
+ sets, as well.
+
+ ## Other Methods
+
+ `Ember.Set` primary implements other mixin APIs. For a complete reference
+ on the methods you will use with `Ember.Set`, please consult these mixins.
+ The most useful ones will be `Ember.Enumerable` and
+ `Ember.MutableEnumerable` which implement most of the common iterator
+ methods you are used to on Array.
+
+ Note that you can also use the `Ember.Copyable` and `Ember.Freezable`
+ APIs on `Ember.Set` as well. Once a set is frozen it can no longer be
+ modified. The benefit of this is that when you call `frozenCopy()` on it,
+ Ember will avoid making copies of the set. This allows you to write
+ code that can know with certainty when the underlying set data will or
+ will not be modified.
+
+ @class Set
+ @namespace Ember
+ @extends Ember.CoreObject
+ @uses Ember.MutableEnumerable
+ @uses Ember.Copyable
+ @uses Ember.Freezable
+ @since Ember 0.9
+*/
+Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Ember.Freezable,
+ /** @scope Ember.Set.prototype */ {
+
+ // ..........................................................
+ // IMPLEMENT ENUMERABLE APIS
+ //
+
+ /**
+ This property will change as the number of objects in the set changes.
+
+ @property length
+ @type number
+ @default 0
+ */
+ length: 0,
+
+ /**
+ Clears the set. This is useful if you want to reuse an existing set
+ without having to recreate it.
+
+ ```javascript
+ var colors = new Ember.Set(["red", "green", "blue"]);
+ colors.length; // 3
+ colors.clear();
+ colors.length; // 0
+ ```
+
+ @method clear
+ @return {Ember.Set} An empty Set
+ */
+ clear: function() {
+ if (this.isFrozen) { throw new Error(Ember.FROZEN_ERROR); }
+
+ var len = get(this, 'length');
+ if (len === 0) { return this; }
+
+ var guid;
+
+ this.enumerableContentWillChange(len, 0);
+ Ember.propertyWillChange(this, 'firstObject');
+ Ember.propertyWillChange(this, 'lastObject');
+
+ for (var i=0; i < len; i++){
+ guid = guidFor(this[i]);
+ delete this[guid];
+ delete this[i];
+ }
+
+ set(this, 'length', 0);
+
+ Ember.propertyDidChange(this, 'firstObject');
+ Ember.propertyDidChange(this, 'lastObject');
+ this.enumerableContentDidChange(len, 0);
+
+ return this;
+ },
+
+ /**
+ Returns true if the passed object is also an enumerable that contains the
+ same objects as the receiver.
+
+ ```javascript
+ var colors = ["red", "green", "blue"],
+ same_colors = new Ember.Set(colors);
+
+ same_colors.isEqual(colors); // true
+ same_colors.isEqual(["purple", "brown"]); // false
+ ```
+
+ @method isEqual
+ @param {Ember.Set} obj the other object.
+ @return {Boolean}
+ */
+ isEqual: function(obj) {
+ // fail fast
+ if (!Ember.Enumerable.detect(obj)) return false;
+
+ var loc = get(this, 'length');
+ if (get(obj, 'length') !== loc) return false;
+
+ while(--loc >= 0) {
+ if (!obj.contains(this[loc])) return false;
+ }
+
+ return true;
+ },
+
+ /**
+ Adds an object to the set. Only non-`null` objects can be added to a set
+ and those can only be added once. If the object is already in the set or
+ the passed value is null this method will have no effect.
+
+ This is an alias for `Ember.MutableEnumerable.addObject()`.
+
+ ```javascript
+ var colors = new Ember.Set();
+ colors.add("blue"); // ["blue"]
+ colors.add("blue"); // ["blue"]
+ colors.add("red"); // ["blue", "red"]
+ colors.add(null); // ["blue", "red"]
+ colors.add(undefined); // ["blue", "red"]
+ ```
+
+ @method add
+ @param {Object} obj The object to add.
+ @return {Ember.Set} The set itself.
+ */
+ add: Ember.aliasMethod('addObject'),
+
+ /**
+ Removes the object from the set if it is found. If you pass a `null` value
+ or an object that is already not in the set, this method will have no
+ effect. This is an alias for `Ember.MutableEnumerable.removeObject()`.
+
+ ```javascript
+ var colors = new Ember.Set(["red", "green", "blue"]);
+ colors.remove("red"); // ["blue", "green"]
+ colors.remove("purple"); // ["blue", "green"]
+ colors.remove(null); // ["blue", "green"]
+ ```
+
+ @method remove
+ @param {Object} obj The object to remove
+ @return {Ember.Set} The set itself.
+ */
+ remove: Ember.aliasMethod('removeObject'),
+
+ /**
+ Removes the last element from the set and returns it, or `null` if it's empty.
+
+ ```javascript
+ var colors = new Ember.Set(["green", "blue"]);
+ colors.pop(); // "blue"
+ colors.pop(); // "green"
+ colors.pop(); // null
+ ```
+
+ @method pop
+ @return {Object} The removed object from the set or null.
+ */
+ pop: function() {
+ if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR);
+ var obj = this.length > 0 ? this[this.length-1] : null;
+ this.remove(obj);
+ return obj;
+ },
+
+ /**
+ Inserts the given object on to the end of the set. It returns
+ the set itself.
+
+ This is an alias for `Ember.MutableEnumerable.addObject()`.
+
+ ```javascript
+ var colors = new Ember.Set();
+ colors.push("red"); // ["red"]
+ colors.push("green"); // ["red", "green"]
+ colors.push("blue"); // ["red", "green", "blue"]
+ ```
+
+ @method push
+ @return {Ember.Set} The set itself.
+ */
+ push: Ember.aliasMethod('addObject'),
+
+ /**
+ Removes the last element from the set and returns it, or `null` if it's empty.
+
+ This is an alias for `Ember.Set.pop()`.
+
+ ```javascript
+ var colors = new Ember.Set(["green", "blue"]);
+ colors.shift(); // "blue"
+ colors.shift(); // "green"
+ colors.shift(); // null
+ ```
+
+ @method shift
+ @return {Object} The removed object from the set or null.
+ */
+ shift: Ember.aliasMethod('pop'),
+
+ /**
+ Inserts the given object on to the end of the set. It returns
+ the set itself.
+
+ This is an alias of `Ember.Set.push()`
+
+ ```javascript
+ var colors = new Ember.Set();
+ colors.unshift("red"); // ["red"]
+ colors.unshift("green"); // ["red", "green"]
+ colors.unshift("blue"); // ["red", "green", "blue"]
+ ```
+
+ @method unshift
+ @return {Ember.Set} The set itself.
+ */
+ unshift: Ember.aliasMethod('push'),
+
+ /**
+ Adds each object in the passed enumerable to the set.
+
+ This is an alias of `Ember.MutableEnumerable.addObjects()`
+
+ ```javascript
+ var colors = new Ember.Set();
+ colors.addEach(["red", "green", "blue"]); // ["red", "green", "blue"]
+ ```
+
+ @method addEach
+ @param {Ember.Enumerable} objects the objects to add.
+ @return {Ember.Set} The set itself.
+ */
+ addEach: Ember.aliasMethod('addObjects'),
+
+ /**
+ Removes each object in the passed enumerable to the set.
+
+ This is an alias of `Ember.MutableEnumerable.removeObjects()`
+
+ ```javascript
+ var colors = new Ember.Set(["red", "green", "blue"]);
+ colors.removeEach(["red", "blue"]); // ["green"]
+ ```
+
+ @method removeEach
+ @param {Ember.Enumerable} objects the objects to remove.
+ @return {Ember.Set} The set itself.
+ */
+ removeEach: Ember.aliasMethod('removeObjects'),
+
+ // ..........................................................
+ // PRIVATE ENUMERABLE SUPPORT
+ //
+
+ init: function(items) {
+ this._super();
+ if (items) this.addObjects(items);
+ },
+
+ // implement Ember.Enumerable
+ nextObject: function(idx) {
+ return this[idx];
+ },
+
+ // more optimized version
+ firstObject: Ember.computed(function() {
+ return this.length > 0 ? this[0] : undefined;
+ }),
+
+ // more optimized version
+ lastObject: Ember.computed(function() {
+ return this.length > 0 ? this[this.length-1] : undefined;
+ }),
+
+ // implements Ember.MutableEnumerable
+ addObject: function(obj) {
+ if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR);
+ if (none(obj)) return this; // nothing to do
+
+ var guid = guidFor(obj),
+ idx = this[guid],
+ len = get(this, 'length'),
+ added ;
+
+ if (idx>=0 && idx<len && (this[idx] === obj)) return this; // added
+
+ added = [obj];
+
+ this.enumerableContentWillChange(null, added);
+ Ember.propertyWillChange(this, 'lastObject');
+
+ len = get(this, 'length');
+ this[guid] = len;
+ this[len] = obj;
+ set(this, 'length', len+1);
+
+ Ember.propertyDidChange(this, 'lastObject');
+ this.enumerableContentDidChange(null, added);
+
+ return this;
+ },
+
+ // implements Ember.MutableEnumerable
+ removeObject: function(obj) {
+ if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR);
+ if (none(obj)) return this; // nothing to do
+
+ var guid = guidFor(obj),
+ idx = this[guid],
+ len = get(this, 'length'),
+ isFirst = idx === 0,
+ isLast = idx === len-1,
+ last, removed;
+
+
+ if (idx>=0 && idx<len && (this[idx] === obj)) {
+ removed = [obj];
+
+ this.enumerableContentWillChange(removed, null);
+ if (isFirst) { Ember.propertyWillChange(this, 'firstObject'); }
+ if (isLast) { Ember.propertyWillChange(this, 'lastObject'); }
+
+ // swap items - basically move the item to the end so it can be removed
+ if (idx < len-1) {
+ last = this[len-1];
+ this[idx] = last;
+ this[guidFor(last)] = idx;
+ }
+
+ delete this[guid];
+ delete this[len-1];
+ set(this, 'length', len-1);
+
+ if (isFirst) { Ember.propertyDidChange(this, 'firstObject'); }
+ if (isLast) { Ember.propertyDidChange(this, 'lastObject'); }
+ this.enumerableContentDidChange(removed, null);
+ }
+
+ return this;
+ },
+
+ // optimized version
+ contains: function(obj) {
+ return this[guidFor(obj)]>=0;
+ },
+
+ copy: function() {
+ var C = this.constructor, ret = new C(), loc = get(this, 'length');
+ set(ret, 'length', loc);
+ while(--loc>=0) {
+ ret[loc] = this[loc];
+ ret[guidFor(this[loc])] = loc;
+ }
+ return ret;
+ },
+
+ toString: function() {
+ var len = this.length, idx, array = [];
+ for(idx = 0; idx < len; idx++) {
+ array[idx] = this[idx];
+ }
+ return "Ember.Set<%@>".fmt(array.join(','));
+ }
+
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+/**
+ `Ember.Object` is the main base class for all Ember objects. It is a subclass
+ of `Ember.CoreObject` with the `Ember.Observable` mixin applied. For details,
+ see the documentation for each of these.
+
+ @class Object
+ @namespace Ember
+ @extends Ember.CoreObject
+ @uses Ember.Observable
+*/
+Ember.Object = Ember.CoreObject.extend(Ember.Observable);
+Ember.Object.toString = function() { return "Ember.Object"; };
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+var get = Ember.get, indexOf = Ember.ArrayPolyfills.indexOf;
+
+/**
+ A Namespace is an object usually used to contain other objects or methods
+ such as an application or framework. Create a namespace anytime you want
+ to define one of these new containers.
+
+ # Example Usage
+
+ ```javascript
+ MyFramework = Ember.Namespace.create({
+ VERSION: '1.0.0'
+ });
+ ```
+
+ @class Namespace
+ @namespace Ember
+ @extends Ember.Object
+*/
+var Namespace = Ember.Namespace = Ember.Object.extend({
+ isNamespace: true,
+
+ init: function() {
+ Ember.Namespace.NAMESPACES.push(this);
+ Ember.Namespace.PROCESSED = false;
+ },
+
+ toString: function() {
+ var name = get(this, 'name');
+ if (name) { return name; }
+
+ findNamespaces();
+ return this[Ember.GUID_KEY+'_name'];
+ },
+
+ nameClasses: function() {
+ processNamespace([this.toString()], this, {});
+ },
+
+ destroy: function() {
+ var namespaces = Ember.Namespace.NAMESPACES;
+ Ember.lookup[this.toString()] = undefined;
+ namespaces.splice(indexOf.call(namespaces, this), 1);
+ this._super();
+ }
+});
+
+Namespace.reopenClass({
+ NAMESPACES: [Ember],
+ NAMESPACES_BY_ID: {},
+ PROCESSED: false,
+ processAll: processAllNamespaces,
+ byName: function(name) {
+ if (!Ember.BOOTED) {
+ processAllNamespaces();
+ }
+
+ return NAMESPACES_BY_ID[name];
+ }
+});
+
+var NAMESPACES_BY_ID = Namespace.NAMESPACES_BY_ID;
+
+var hasOwnProp = ({}).hasOwnProperty,
+ guidFor = Ember.guidFor;
+
+function processNamespace(paths, root, seen) {
+ var idx = paths.length;
+
+ NAMESPACES_BY_ID[paths.join('.')] = root;
+
+ // Loop over all of the keys in the namespace, looking for classes
+ for(var key in root) {
+ if (!hasOwnProp.call(root, key)) { continue; }
+ var obj = root[key];
+
+ // If we are processing the `Ember` namespace, for example, the
+ // `paths` will start with `["Ember"]`. Every iteration through
+ // the loop will update the **second** element of this list with
+ // the key, so processing `Ember.View` will make the Array
+ // `['Ember', 'View']`.
+ paths[idx] = key;
+
+ // If we have found an unprocessed class
+ if (obj && obj.toString === classToString) {
+ // Replace the class' `toString` with the dot-separated path
+ // and set its `NAME_KEY`
+ obj.toString = makeToString(paths.join('.'));
+ obj[NAME_KEY] = paths.join('.');
+
+ // Support nested namespaces
+ } else if (obj && obj.isNamespace) {
+ // Skip aliased namespaces
+ if (seen[guidFor(obj)]) { continue; }
+ seen[guidFor(obj)] = true;
+
+ // Process the child namespace
+ processNamespace(paths, obj, seen);
+ }
+ }
+
+ paths.length = idx; // cut out last item
+}
+
+function findNamespaces() {
+ var Namespace = Ember.Namespace, lookup = Ember.lookup, obj, isNamespace;
+
+ if (Namespace.PROCESSED) { return; }
+
+ for (var prop in lookup) {
+ // These don't raise exceptions but can cause warnings
+ if (prop === "parent" || prop === "top" || prop === "frameElement") { 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
+ if (prop === "globalStorage" && lookup.StorageList && lookup.globalStorage instanceof lookup.StorageList) { continue; }
+ // Unfortunately, some versions of IE don't support window.hasOwnProperty
+ if (lookup.hasOwnProperty && !lookup.hasOwnProperty(prop)) { continue; }
+
+ // At times we are not allowed to access certain properties for security reasons.
+ // There are also times where even if we can access them, we are not allowed to access their properties.
+ try {
+ obj = Ember.lookup[prop];
+ isNamespace = obj && obj.isNamespace;
+ } catch (e) {
+ continue;
+ }
+
+ if (isNamespace) {
+ Ember.deprecate("Namespaces should not begin with lowercase.", /^[A-Z]/.test(prop));
+ obj[NAME_KEY] = prop;
+ }
+ }
+}
+
+var NAME_KEY = Ember.NAME_KEY = Ember.GUID_KEY + '_name';
+
+function superClassString(mixin) {
+ var superclass = mixin.superclass;
+ if (superclass) {
+ if (superclass[NAME_KEY]) { return superclass[NAME_KEY]; }
+ else { return superClassString(superclass); }
+ } else {
+ return;
+ }
+}
+
+function classToString() {
+ if (!Ember.BOOTED && !this[NAME_KEY]) {
+ processAllNamespaces();
+ }
+
+ var ret;
+
+ if (this[NAME_KEY]) {
+ ret = this[NAME_KEY];
+ } else {
+ var str = superClassString(this);
+ if (str) {
+ ret = "(subclass of " + str + ")";
+ } else {
+ ret = "(unknown mixin)";
+ }
+ this.toString = makeToString(ret);
+ }
+
+ return ret;
+}
+
+function processAllNamespaces() {
+ var unprocessedNamespaces = !Namespace.PROCESSED,
+ unprocessedMixins = Ember.anyUnprocessedMixins;
+
+ if (unprocessedNamespaces) {
+ findNamespaces();
+ Namespace.PROCESSED = true;
+ }
+
+ if (unprocessedNamespaces || unprocessedMixins) {
+ var namespaces = Namespace.NAMESPACES, namespace;
+ for (var i=0, l=namespaces.length; i<l; i++) {
+ namespace = namespaces[i];
+ processNamespace([namespace.toString()], namespace, {});
+ }
+
+ Ember.anyUnprocessedMixins = false;
+ }
+}
+
+function makeToString(ret) {
+ return function() { return ret; };
+}
+
+Ember.Mixin.prototype.toString = classToString;
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+/**
+ Defines a namespace that will contain an executable application. This is
+ very similar to a normal namespace except that it is expected to include at
+ least a 'ready' function which can be run to initialize the application.
+
+ Currently `Ember.Application` is very similar to `Ember.Namespace.` However,
+ this class may be augmented by additional frameworks so it is important to
+ use this instance when building new applications.
+
+ # Example Usage
+
+ ```javascript
+ MyApp = Ember.Application.create({
+ VERSION: '1.0.0',
+ store: Ember.Store.create().from(Ember.fixtures)
+ });
+
+ MyApp.ready = function() {
+ //..init code goes here...
+ }
+ ```
+
+ @class Application
+ @namespace Ember
+ @extends Ember.Namespace
+*/
+Ember.Application = Ember.Namespace.extend();
+
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+
+var get = Ember.get, set = Ember.set;
+
+/**
+ An ArrayProxy wraps any other object that implements `Ember.Array` and/or
+ `Ember.MutableArray,` forwarding all requests. This makes it very useful for
+ a number of binding use cases or other cases where being able to swap
+ out the underlying array is useful.
+
+ A simple example of usage:
+
+ ```javascript
+ var pets = ['dog', 'cat', 'fish'];
+ var ap = Ember.ArrayProxy.create({ content: Ember.A(pets) });
+
+ ap.get('firstObject'); // 'dog'
+ ap.set('content', ['amoeba', 'paramecium']);
+ ap.get('firstObject'); // 'amoeba'
+ ```
+
+ This class can also be useful as a layer to transform the contents of
+ an array, as they are accessed. This can be done by overriding
+ `objectAtContent`:
+
+ ```javascript
+ var pets = ['dog', 'cat', 'fish'];
+ var ap = Ember.ArrayProxy.create({
+ content: Ember.A(pets),
+ objectAtContent: function(idx) {
+ return this.get('content').objectAt(idx).toUpperCase();
+ }
+ });
+
+ ap.get('firstObject'); // . 'DOG'
+ ```
+
+ @class ArrayProxy
+ @namespace Ember
+ @extends Ember.Object
+ @uses Ember.MutableArray
+*/
+Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray,
+/** @scope Ember.ArrayProxy.prototype */ {
+
+ /**
+ The content array. Must be an object that implements `Ember.Array` and/or
+ `Ember.MutableArray.`
+
+ @property content
+ @type Ember.Array
+ */
+ content: null,
+
+ /**
+ The array that the proxy pretends to be. In the default `ArrayProxy`
+ implementation, this and `content` are the same. Subclasses of `ArrayProxy`
+ can override this property to provide things like sorting and filtering.
+
+ @property arrangedContent
+ */
+ arrangedContent: Ember.computed.alias('content'),
+
+ /**
+ Should actually retrieve the object at the specified index from the
+ content. You can override this method in subclasses to transform the
+ content item to something new.
+
+ This method will only be called if content is non-`null`.
+
+ @method objectAtContent
+ @param {Number} idx The index to retrieve.
+ @return {Object} the value or undefined if none found
+ */
+ objectAtContent: function(idx) {
+ return get(this, 'arrangedContent').objectAt(idx);
+ },
+
+ /**
+ Should actually replace the specified objects on the content array.
+ You can override this method in subclasses to transform the content item
+ into something new.
+
+ This method will only be called if content is non-`null`.
+
+ @method replaceContent
+ @param {Number} idx The starting index
+ @param {Number} amt The number of items to remove from the content.
+ @param {Array} objects Optional array of objects to insert or null if no
+ objects.
+ @return {void}
+ */
+ replaceContent: function(idx, amt, objects) {
+ get(this, 'content').replace(idx, amt, objects);
+ },
+
+ /**
+ @private
+
+ Invoked when the content property is about to change. Notifies observers that the
+ entire array content will change.
+
+ @method _contentWillChange
+ */
+ _contentWillChange: Ember.beforeObserver(function() {
+ this._teardownContent();
+ }, 'content'),
+
+ _teardownContent: function() {
+ var content = get(this, 'content');
+
+ if (content) {
+ content.removeArrayObserver(this, {
+ willChange: 'contentArrayWillChange',
+ didChange: 'contentArrayDidChange'
+ });
+ }
+ },
+
+ contentArrayWillChange: Ember.K,
+ contentArrayDidChange: Ember.K,
+
+ /**
+ @private
+
+ Invoked when the content property changes. Notifies observers that the
+ entire array content has changed.
+
+ @method _contentDidChange
+ */
+ _contentDidChange: Ember.observer(function() {
+ var content = get(this, 'content');
+
+ Ember.assert("Can't set ArrayProxy's content to itself", content !== this);
+
+ this._setupContent();
+ }, 'content'),
+
+ _setupContent: function() {
+ var content = get(this, 'content');
+
+ if (content) {
+ content.addArrayObserver(this, {
+ willChange: 'contentArrayWillChange',
+ didChange: 'contentArrayDidChange'
+ });
+ }
+ },
+
+ _arrangedContentWillChange: Ember.beforeObserver(function() {
+ var arrangedContent = get(this, 'arrangedContent'),
+ len = arrangedContent ? get(arrangedContent, 'length') : 0;
+
+ this.arrangedContentArrayWillChange(this, 0, len, undefined);
+ this.arrangedContentWillChange(this);
+
+ this._teardownArrangedContent(arrangedContent);
+ }, 'arrangedContent'),
+
+ _arrangedContentDidChange: Ember.observer(function() {
+ var arrangedContent = get(this, 'arrangedContent'),
+ len = arrangedContent ? get(arrangedContent, 'length') : 0;
+
+ Ember.assert("Can't set ArrayProxy's content to itself", arrangedContent !== this);
+
+ this._setupArrangedContent();
+
+ this.arrangedContentDidChange(this);
+ this.arrangedContentArrayDidChange(this, 0, undefined, len);
+ }, 'arrangedContent'),
+
+ _setupArrangedContent: function() {
+ var arrangedContent = get(this, 'arrangedContent');
+
+ if (arrangedContent) {
+ arrangedContent.addArrayObserver(this, {
+ willChange: 'arrangedContentArrayWillChange',
+ didChange: 'arrangedContentArrayDidChange'
+ });
+ }
+ },
+
+ _teardownArrangedContent: function() {
+ var arrangedContent = get(this, 'arrangedContent');
+
+ if (arrangedContent) {
+ arrangedContent.removeArrayObserver(this, {
+ willChange: 'arrangedContentArrayWillChange',
+ didChange: 'arrangedContentArrayDidChange'
+ });
+ }
+ },
+
+ arrangedContentWillChange: Ember.K,
+ arrangedContentDidChange: Ember.K,
+
+ objectAt: function(idx) {
+ return get(this, 'content') && this.objectAtContent(idx);
+ },
+
+ length: Ember.computed(function() {
+ var arrangedContent = get(this, 'arrangedContent');
+ return arrangedContent ? get(arrangedContent, 'length') : 0;
+ // No dependencies since Enumerable notifies length of change
+ }),
+
+ replace: function(idx, amt, objects) {
+ Ember.assert('The content property of '+ this.constructor + ' should be set before modifying it', this.get('content'));
+ if (get(this, 'content')) this.replaceContent(idx, amt, objects);
+ return this;
+ },
+
+ arrangedContentArrayWillChange: function(item, idx, removedCnt, addedCnt) {
+ this.arrayContentWillChange(idx, removedCnt, addedCnt);
+ },
+
+ arrangedContentArrayDidChange: function(item, idx, removedCnt, addedCnt) {
+ this.arrayContentDidChange(idx, removedCnt, addedCnt);
+ },
+
+ init: function() {
+ this._super();
+ this._setupContent();
+ this._setupArrangedContent();
+ },
+
+ willDestroy: function() {
+ this._teardownArrangedContent();
+ this._teardownContent();
+ }
+});
+
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+var get = Ember.get,
+ set = Ember.set,
+ fmt = Ember.String.fmt,
+ addBeforeObserver = Ember.addBeforeObserver,
+ addObserver = Ember.addObserver,
+ removeBeforeObserver = Ember.removeBeforeObserver,
+ removeObserver = Ember.removeObserver,
+ propertyWillChange = Ember.propertyWillChange,
+ propertyDidChange = Ember.propertyDidChange;
+
+function contentPropertyWillChange(content, contentKey) {
+ var key = contentKey.slice(8); // remove "content."
+ if (key in this) { return; } // if shadowed in proxy
+ propertyWillChange(this, key);
+}
+
+function contentPropertyDidChange(content, contentKey) {
+ var key = contentKey.slice(8); // remove "content."
+ if (key in this) { return; } // if shadowed in proxy
+ propertyDidChange(this, key);
+}
+
+/**
+ `Ember.ObjectProxy` forwards all properties not defined by the proxy itself
+ to a proxied `content` object.
+
+ ```javascript
+ object = Ember.Object.create({
+ name: 'Foo'
+ });
+
+ proxy = Ember.ObjectProxy.create({
+ content: object
+ });
+
+ // Access and change existing properties
+ proxy.get('name') // 'Foo'
+ proxy.set('name', 'Bar');
+ object.get('name') // 'Bar'
+
+ // Create new 'description' property on `object`
+ proxy.set('description', 'Foo is a whizboo baz');
+ object.get('description') // 'Foo is a whizboo baz'
+ ```
+
+ While `content` is unset, setting a property to be delegated will throw an
+ Error.
+
+ ```javascript
+ proxy = Ember.ObjectProxy.create({
+ content: null,
+ flag: null
+ });
+ proxy.set('flag', true);
+ proxy.get('flag'); // true
+ proxy.get('foo'); // undefined
+ proxy.set('foo', 'data'); // throws Error
+ ```
+
+ Delegated properties can be bound to and will change when content is updated.
+
+ Computed properties on the proxy itself can depend on delegated properties.
+
+ ```javascript
+ ProxyWithComputedProperty = Ember.ObjectProxy.extend({
+ fullName: function () {
+ var firstName = this.get('firstName'),
+ lastName = this.get('lastName');
+ if (firstName && lastName) {
+ return firstName + ' ' + lastName;
+ }
+ return firstName || lastName;
+ }.property('firstName', 'lastName')
+ });
+
+ proxy = ProxyWithComputedProperty.create();
+
+ proxy.get('fullName'); // undefined
+ proxy.set('content', {
+ firstName: 'Tom', lastName: 'Dale'
+ }); // triggers property change for fullName on proxy
+
+ proxy.get('fullName'); // 'Tom Dale'
+ ```
+
+ @class ObjectProxy
+ @namespace Ember
+ @extends Ember.Object
+*/
+Ember.ObjectProxy = Ember.Object.extend(
+/** @scope Ember.ObjectProxy.prototype */ {
+ /**
+ The object whose properties will be forwarded.
+
+ @property content
+ @type Ember.Object
+ @default null
+ */
+ content: null,
+ _contentDidChange: Ember.observer(function() {
+ Ember.assert("Can't set ObjectProxy's content to itself", this.get('content') !== this);
+ }, 'content'),
+
+ isTruthy: Ember.computed.bool('content'),
+
+ _debugContainerKey: null,
+
+ willWatchProperty: function (key) {
+ var contentKey = 'content.' + key;
+ addBeforeObserver(this, contentKey, null, contentPropertyWillChange);
+ addObserver(this, contentKey, null, contentPropertyDidChange);
+ },
+
+ didUnwatchProperty: function (key) {
+ var contentKey = 'content.' + key;
+ removeBeforeObserver(this, contentKey, null, contentPropertyWillChange);
+ removeObserver(this, contentKey, null, contentPropertyDidChange);
+ },
+
+ unknownProperty: function (key) {
+ var content = get(this, 'content');
+ if (content) {
+ return get(content, key);
+ }
+ },
+
+ setUnknownProperty: function (key, value) {
+ var content = get(this, 'content');
+ Ember.assert(fmt("Cannot delegate set('%@', %@) to the 'content' property of object proxy %@: its 'content' is undefined.", [key, value, this]), content);
+ return set(content, key, value);
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+
+var set = Ember.set, get = Ember.get, guidFor = Ember.guidFor;
+var forEach = Ember.EnumerableUtils.forEach;
+
+var EachArray = Ember.Object.extend(Ember.Array, {
+
+ init: function(content, keyName, owner) {
+ this._super();
+ this._keyName = keyName;
+ this._owner = owner;
+ this._content = content;
+ },
+
+ objectAt: function(idx) {
+ var item = this._content.objectAt(idx);
+ return item && get(item, this._keyName);
+ },
+
+ length: Ember.computed(function() {
+ var content = this._content;
+ return content ? get(content, 'length') : 0;
+ })
+
+});
+
+var IS_OBSERVER = /^.+:(before|change)$/;
+
+function addObserverForContentKey(content, keyName, proxy, idx, loc) {
+ var objects = proxy._objects, guid;
+ if (!objects) objects = proxy._objects = {};
+
+ while(--loc>=idx) {
+ var item = content.objectAt(loc);
+ if (item) {
+ Ember.addBeforeObserver(item, keyName, proxy, 'contentKeyWillChange');
+ Ember.addObserver(item, keyName, proxy, 'contentKeyDidChange');
+
+ // keep track of the indicies each item was found at so we can map
+ // it back when the obj changes.
+ guid = guidFor(item);
+ if (!objects[guid]) objects[guid] = [];
+ objects[guid].push(loc);
+ }
+ }
+}
+
+function removeObserverForContentKey(content, keyName, proxy, idx, loc) {
+ var objects = proxy._objects;
+ if (!objects) objects = proxy._objects = {};
+ var indicies, guid;
+
+ while(--loc>=idx) {
+ var item = content.objectAt(loc);
+ if (item) {
+ Ember.removeBeforeObserver(item, keyName, proxy, 'contentKeyWillChange');
+ Ember.removeObserver(item, keyName, proxy, 'contentKeyDidChange');
+
+ guid = guidFor(item);
+ indicies = objects[guid];
+ indicies[indicies.indexOf(loc)] = null;
+ }
+ }
+}
+
+/**
+ This is the object instance returned when you get the `@each` property on an
+ array. It uses the unknownProperty handler to automatically create
+ EachArray instances for property names.
+
+ @private
+ @class EachProxy
+ @namespace Ember
+ @extends Ember.Object
+*/
+Ember.EachProxy = Ember.Object.extend({
+
+ init: function(content) {
+ this._super();
+ this._content = content;
+ content.addArrayObserver(this);
+
+ // in case someone is already observing some keys make sure they are
+ // added
+ forEach(Ember.watchedEvents(this), function(eventName) {
+ this.didAddListener(eventName);
+ }, this);
+ },
+
+ /**
+ You can directly access mapped properties by simply requesting them.
+ The `unknownProperty` handler will generate an EachArray of each item.
+
+ @method unknownProperty
+ @param keyName {String}
+ @param value {anything}
+ */
+ unknownProperty: function(keyName, value) {
+ var ret;
+ ret = new EachArray(this._content, keyName, this);
+ Ember.defineProperty(this, keyName, null, ret);
+ this.beginObservingContentKey(keyName);
+ return ret;
+ },
+
+ // ..........................................................
+ // ARRAY CHANGES
+ // Invokes whenever the content array itself changes.
+
+ arrayWillChange: function(content, idx, removedCnt, addedCnt) {
+ var keys = this._keys, key, array, lim;
+
+ lim = removedCnt>0 ? idx+removedCnt : -1;
+ Ember.beginPropertyChanges(this);
+
+ for(key in keys) {
+ if (!keys.hasOwnProperty(key)) { continue; }
+
+ if (lim>0) removeObserverForContentKey(content, key, this, idx, lim);
+
+ Ember.propertyWillChange(this, key);
+ }
+
+ Ember.propertyWillChange(this._content, '@each');
+ Ember.endPropertyChanges(this);
+ },
+
+ arrayDidChange: function(content, idx, removedCnt, addedCnt) {
+ var keys = this._keys, key, array, lim;
+
+ lim = addedCnt>0 ? idx+addedCnt : -1;
+ Ember.beginPropertyChanges(this);
+
+ for(key in keys) {
+ if (!keys.hasOwnProperty(key)) { continue; }
+
+ if (lim>0) addObserverForContentKey(content, key, this, idx, lim);
+
+ Ember.propertyDidChange(this, key);
+ }
+
+ Ember.propertyDidChange(this._content, '@each');
+ Ember.endPropertyChanges(this);
+ },
+
+ // ..........................................................
+ // LISTEN FOR NEW OBSERVERS AND OTHER EVENT LISTENERS
+ // Start monitoring keys based on who is listening...
+
+ didAddListener: function(eventName) {
+ if (IS_OBSERVER.test(eventName)) {
+ this.beginObservingContentKey(eventName.slice(0, -7));
+ }
+ },
+
+ didRemoveListener: function(eventName) {
+ if (IS_OBSERVER.test(eventName)) {
+ this.stopObservingContentKey(eventName.slice(0, -7));
+ }
+ },
+
+ // ..........................................................
+ // CONTENT KEY OBSERVING
+ // Actual watch keys on the source content.
+
+ beginObservingContentKey: function(keyName) {
+ var keys = this._keys;
+ if (!keys) keys = this._keys = {};
+ if (!keys[keyName]) {
+ keys[keyName] = 1;
+ var content = this._content,
+ len = get(content, 'length');
+ addObserverForContentKey(content, keyName, this, 0, len);
+ } else {
+ keys[keyName]++;
+ }
+ },
+
+ stopObservingContentKey: function(keyName) {
+ var keys = this._keys;
+ if (keys && (keys[keyName]>0) && (--keys[keyName]<=0)) {
+ var content = this._content,
+ len = get(content, 'length');
+ removeObserverForContentKey(content, keyName, this, 0, len);
+ }
+ },
+
+ contentKeyWillChange: function(obj, keyName) {
+ Ember.propertyWillChange(this, keyName);
+ },
+
+ contentKeyDidChange: function(obj, keyName) {
+ Ember.propertyDidChange(this, keyName);
+ }
+
+});
+
+
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+
+var get = Ember.get, set = Ember.set;
+
+// Add Ember.Array to Array.prototype. Remove methods with native
+// implementations and supply some more optimized versions of generic methods
+// because they are so common.
+var NativeArray = Ember.Mixin.create(Ember.MutableArray, Ember.Observable, Ember.Copyable, {
+
+ // because length is a built-in property we need to know to just get the
+ // original property.
+ get: function(key) {
+ if (key==='length') return this.length;
+ else if ('number' === typeof key) return this[key];
+ else return this._super(key);
+ },
+
+ objectAt: function(idx) {
+ return this[idx];
+ },
+
+ // primitive for array support.
+ replace: function(idx, amt, objects) {
+
+ if (this.isFrozen) throw Ember.FROZEN_ERROR ;
+
+ // if we replaced exactly the same number of items, then pass only the
+ // replaced range. Otherwise, pass the full remaining array length
+ // since everything has shifted
+ var len = objects ? get(objects, 'length') : 0;
+ this.arrayContentWillChange(idx, amt, len);
+
+ if (!objects || objects.length === 0) {
+ this.splice(idx, amt) ;
+ } else {
+ var args = [idx, amt].concat(objects) ;
+ this.splice.apply(this,args) ;
+ }
+
+ this.arrayContentDidChange(idx, amt, len);
+ return this ;
+ },
+
+ // If you ask for an unknown property, then try to collect the value
+ // from member items.
+ unknownProperty: function(key, value) {
+ var ret;// = this.reducedProperty(key, value) ;
+ if ((value !== undefined) && ret === undefined) {
+ ret = this[key] = value;
+ }
+ return ret ;
+ },
+
+ // If browser did not implement indexOf natively, then override with
+ // specialized version
+ indexOf: function(object, startAt) {
+ var idx, len = this.length;
+
+ if (startAt === undefined) startAt = 0;
+ else startAt = (startAt < 0) ? Math.ceil(startAt) : Math.floor(startAt);
+ if (startAt < 0) startAt += len;
+
+ for(idx=startAt;idx<len;idx++) {
+ if (this[idx] === object) return idx ;
+ }
+ return -1;
+ },
+
+ lastIndexOf: function(object, startAt) {
+ var idx, len = this.length;
+
+ if (startAt === undefined) startAt = len-1;
+ else startAt = (startAt < 0) ? Math.ceil(startAt) : Math.floor(startAt);
+ if (startAt < 0) startAt += len;
+
+ for(idx=startAt;idx>=0;idx--) {
+ if (this[idx] === object) return idx ;
+ }
+ return -1;
+ },
+
+ copy: function(deep) {
+ if (deep) {
+ return this.map(function(item){ return Ember.copy(item, true); });
+ }
+
+ return this.slice();
+ }
+});
+
+// Remove any methods implemented natively so we don't override them
+var ignore = ['length'];
+Ember.EnumerableUtils.forEach(NativeArray.keys(), function(methodName) {
+ if (Array.prototype[methodName]) ignore.push(methodName);
+});
+
+if (ignore.length>0) {
+ NativeArray = NativeArray.without.apply(NativeArray, ignore);
+}
+
+/**
+ The NativeArray mixin contains the properties needed to to make the native
+ Array support Ember.MutableArray and all of its dependent APIs. Unless you
+ have `Ember.EXTEND_PROTOTYPES or `Ember.EXTEND_PROTOTYPES.Array` set to
+ false, this will be applied automatically. Otherwise you can apply the mixin
+ at anytime by calling `Ember.NativeArray.activate`.
+
+ @class NativeArray
+ @namespace Ember
+ @extends Ember.Mixin
+ @uses Ember.MutableArray
+ @uses Ember.MutableEnumerable
+ @uses Ember.Copyable
+ @uses Ember.Freezable
+*/
+Ember.NativeArray = NativeArray;
+
+/**
+ Creates an `Ember.NativeArray` from an Array like object.
+ Does not modify the original object.
+
+ @method A
+ @for Ember
+ @return {Ember.NativeArray}
+*/
+Ember.A = function(arr){
+ if (arr === undefined) { arr = []; }
+ return Ember.Array.detect(arr) ? arr : Ember.NativeArray.apply(arr);
+};
+
+/**
+ Activates the mixin on the Array.prototype if not already applied. Calling
+ this method more than once is safe.
+
+ @method activate
+ @for Ember.NativeArray
+ @static
+ @return {void}
+*/
+Ember.NativeArray.activate = function() {
+ NativeArray.apply(Array.prototype);
+
+ Ember.A = function(arr) { return arr || []; };
+};
+
+if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) {
+ Ember.NativeArray.activate();
+}
+
+
+})();
+
+
+
+(function() {
+var DeferredMixin = Ember.DeferredMixin, // mixins/deferred
+ EmberObject = Ember.Object, // system/object
+ get = Ember.get;
+
+var Deferred = Ember.Object.extend(DeferredMixin);
+
+Deferred.reopenClass({
+ promise: function(callback, binding) {
+ var deferred = Deferred.create();
+ callback.call(binding, deferred);
+ return get(deferred, 'promise');
+ }
+});
+
+Ember.Deferred = Deferred;
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+var loadHooks = Ember.ENV.EMBER_LOAD_HOOKS || {};
+var loaded = {};
+
+/**
+@method onLoad
+@for Ember
+@param name {String} name of hook
+@param callback {Function} callback to be called
+*/
+Ember.onLoad = function(name, callback) {
+ var object;
+
+ loadHooks[name] = loadHooks[name] || Ember.A();
+ loadHooks[name].pushObject(callback);
+
+ if (object = loaded[name]) {
+ callback(object);
+ }
+};
+
+/**
+@method runLoadHooks
+@for Ember
+@param name {String} name of hook
+@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) {
+ callback(object);
+ });
+ }
+};
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(function() {
+var get = Ember.get;
+
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+/**
+ `Ember.ControllerMixin` provides a standard interface for all classes that
+ compose Ember's controller layer: `Ember.Controller`,
+ `Ember.ArrayController`, and `Ember.ObjectController`.
+
+ Within an `Ember.Router`-managed application single shared instaces of every
+ Controller object in your application's namespace will be added to the
+ application's `Ember.Router` instance. See `Ember.Application#initialize`
+ for additional information.
+
+ ## Views
+
+ By default a controller instance will be the rendering context
+ for its associated `Ember.View.` This connection is made during calls to
+ `Ember.ControllerMixin#connectOutlet`.
+
+ Within the view's template, the `Ember.View` instance can be accessed
+ through the controller with `{{view}}`.
+
+ ## Target Forwarding
+
+ By default a controller will target your application's `Ember.Router`
+ instance. Calls to `{{action}}` within the template of a controller's view
+ are forwarded to the router. See `Ember.Handlebars.helpers.action` for
+ additional information.
+
+ @class ControllerMixin
+ @namespace Ember
+ @extends Ember.Mixin
+*/
+Ember.ControllerMixin = Ember.Mixin.create({
+ /* ducktype as a controller */
+ isController: true,
+
+ /**
+ The object to which events from the view should be sent.
+
+ For example, when a Handlebars template uses the `{{action}}` helper,
+ it will attempt to send the event to the view's controller's `target`.
+
+ By default, a controller's `target` is set to the router after it is
+ instantiated by `Ember.Application#initialize`.
+
+ @property target
+ @default null
+ */
+ target: null,
+
+ container: null,
+
+ store: null,
+
+ model: Ember.computed.alias('content'),
+
+ send: function(actionName) {
+ var args = [].slice.call(arguments, 1), target;
+
+ if (this[actionName]) {
+ Ember.assert("The controller " + this + " does not have the action " + actionName, typeof this[actionName] === 'function');
+ this[actionName].apply(this, args);
+ } else if(target = get(this, 'target')) {
+ Ember.assert("The target for controller " + this + " (" + target + ") did not define a `send` method", typeof target.send === 'function');
+ target.send.apply(target, arguments);
+ }
+ }
+});
+
+/**
+ @class Controller
+ @namespace Ember
+ @extends Ember.Object
+ @uses Ember.ControllerMixin
+*/
+Ember.Controller = Ember.Object.extend(Ember.ControllerMixin);
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach;
+
+/**
+ `Ember.SortableMixin` provides a standard interface for array proxies
+ to specify a sort order and maintain this sorting when objects are added,
+ removed, or updated without changing the implicit order of their underlying
+ content array:
+
+ ```javascript
+ songs = [
+ {trackNumber: 4, title: 'Ob-La-Di, Ob-La-Da'},
+ {trackNumber: 2, title: 'Back in the U.S.S.R.'},
+ {trackNumber: 3, title: 'Glass Onion'},
+ ];
+
+ songsController = Ember.ArrayController.create({
+ content: songs,
+ sortProperties: ['trackNumber'],
+ sortAscending: true
+ });
+
+ songsController.get('firstObject'); // {trackNumber: 2, title: 'Back in the U.S.S.R.'}
+
+ songsController.addObject({trackNumber: 1, title: 'Dear Prudence'});
+ songsController.get('firstObject'); // {trackNumber: 1, title: 'Dear Prudence'}
+ ```
+
+ @class SortableMixin
+ @namespace Ember
+ @extends Ember.Mixin
+ @uses Ember.MutableEnumerable
+*/
+Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, {
+
+ /**
+ Specifies which properties dictate the arrangedContent's sort order.
+
+ @property {Array} sortProperties
+ */
+ sortProperties: null,
+
+ /**
+ Specifies the arrangedContent's sort direction
+
+ @property {Boolean} sortAscending
+ */
+ sortAscending: true,
+
+ orderBy: function(item1, item2) {
+ var result = 0,
+ sortProperties = get(this, 'sortProperties'),
+ sortAscending = get(this, 'sortAscending');
+
+ Ember.assert("you need to define `sortProperties`", !!sortProperties);
+
+ forEach(sortProperties, function(propertyName) {
+ if (result === 0) {
+ result = Ember.compare(get(item1, propertyName), get(item2, propertyName));
+ if ((result !== 0) && !sortAscending) {
+ result = (-1) * result;
+ }
+ }
+ });
+
+ return result;
+ },
+
+ destroy: function() {
+ var content = get(this, 'content'),
+ sortProperties = get(this, 'sortProperties');
+
+ if (content && sortProperties) {
+ forEach(content, function(item) {
+ forEach(sortProperties, function(sortProperty) {
+ Ember.removeObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange');
+ }, this);
+ }, this);
+ }
+
+ return this._super();
+ },
+
+ isSorted: Ember.computed.bool('sortProperties'),
+
+ arrangedContent: Ember.computed('content', 'sortProperties.@each', function(key, value) {
+ var content = get(this, 'content'),
+ isSorted = get(this, 'isSorted'),
+ sortProperties = get(this, 'sortProperties'),
+ self = this;
+
+ if (content && isSorted) {
+ content = content.slice();
+ content.sort(function(item1, item2) {
+ return self.orderBy(item1, item2);
+ });
+ forEach(content, function(item) {
+ forEach(sortProperties, function(sortProperty) {
+ Ember.addObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange');
+ }, this);
+ }, this);
+ return Ember.A(content);
+ }
+
+ return content;
+ }),
+
+ _contentWillChange: Ember.beforeObserver(function() {
+ var content = get(this, 'content'),
+ sortProperties = get(this, 'sortProperties');
+
+ if (content && sortProperties) {
+ forEach(content, function(item) {
+ forEach(sortProperties, function(sortProperty) {
+ Ember.removeObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange');
+ }, this);
+ }, this);
+ }
+
+ this._super();
+ }, 'content'),
+
+ sortAscendingWillChange: Ember.beforeObserver(function() {
+ this._lastSortAscending = get(this, 'sortAscending');
+ }, 'sortAscending'),
+
+ sortAscendingDidChange: Ember.observer(function() {
+ if (get(this, 'sortAscending') !== this._lastSortAscending) {
+ var arrangedContent = get(this, 'arrangedContent');
+ arrangedContent.reverseObjects();
+ }
+ }, 'sortAscending'),
+
+ contentArrayWillChange: function(array, idx, removedCount, addedCount) {
+ var isSorted = get(this, 'isSorted');
+
+ if (isSorted) {
+ var arrangedContent = get(this, 'arrangedContent');
+ var removedObjects = array.slice(idx, idx+removedCount);
+ var sortProperties = get(this, 'sortProperties');
+
+ forEach(removedObjects, function(item) {
+ arrangedContent.removeObject(item);
+
+ forEach(sortProperties, function(sortProperty) {
+ Ember.removeObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange');
+ }, this);
+ }, this);
+ }
+
+ return this._super(array, idx, removedCount, addedCount);
+ },
+
+ contentArrayDidChange: function(array, idx, removedCount, addedCount) {
+ var isSorted = get(this, 'isSorted'),
+ sortProperties = get(this, 'sortProperties');
+
+ if (isSorted) {
+ var addedObjects = array.slice(idx, idx+addedCount);
+ var arrangedContent = get(this, 'arrangedContent');
+
+ forEach(addedObjects, function(item) {
+ this.insertItemSorted(item);
+
+ forEach(sortProperties, function(sortProperty) {
+ Ember.addObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange');
+ }, this);
+ }, this);
+ }
+
+ return this._super(array, idx, removedCount, addedCount);
+ },
+
+ insertItemSorted: function(item) {
+ var arrangedContent = get(this, 'arrangedContent');
+ var length = get(arrangedContent, 'length');
+
+ var idx = this._binarySearch(item, 0, length);
+ arrangedContent.insertAt(idx, item);
+ },
+
+ contentItemSortPropertyDidChange: function(item) {
+ var arrangedContent = get(this, 'arrangedContent'),
+ oldIndex = arrangedContent.indexOf(item),
+ leftItem = arrangedContent.objectAt(oldIndex - 1),
+ rightItem = arrangedContent.objectAt(oldIndex + 1),
+ leftResult = leftItem && this.orderBy(item, leftItem),
+ rightResult = rightItem && this.orderBy(item, rightItem);
+
+ if (leftResult < 0 || rightResult > 0) {
+ arrangedContent.removeObject(item);
+ this.insertItemSorted(item);
+ }
+ },
+
+ _binarySearch: function(item, low, high) {
+ var mid, midItem, res, arrangedContent;
+
+ if (low === high) {
+ return low;
+ }
+
+ arrangedContent = get(this, 'arrangedContent');
+
+ mid = low + Math.floor((high - low) / 2);
+ midItem = arrangedContent.objectAt(mid);
+
+ res = this.orderBy(midItem, item);
+
+ if (res < 0) {
+ return this._binarySearch(item, mid+1, high);
+ } else if (res > 0) {
+ return this._binarySearch(item, low, mid);
+ }
+
+ return mid;
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+var get = Ember.get, set = Ember.set, isGlobalPath = Ember.isGlobalPath,
+ forEach = Ember.EnumerableUtils.forEach, replace = Ember.EnumerableUtils.replace;
+
+/**
+ `Ember.ArrayController` provides a way for you to publish a collection of
+ objects so that you can easily bind to the collection from a Handlebars
+ `#each` helper, an `Ember.CollectionView`, or other controllers.
+
+ The advantage of using an `ArrayController` is that you only have to set up
+ your view bindings once; to change what's displayed, simply swap out the
+ `content` property on the controller.
+
+ For example, imagine you wanted to display a list of items fetched via an XHR
+ request. Create an `Ember.ArrayController` and set its `content` property:
+
+ ```javascript
+ MyApp.listController = Ember.ArrayController.create();
+
+ $.get('people.json', function(data) {
+ MyApp.listController.set('content', data);
+ });
+ ```
+
+ Then, create a view that binds to your new controller:
+
+ ```handlebars
+ {{#each MyApp.listController}}
+ {{firstName}} {{lastName}}
+ {{/each}}
+ ```
+
+ Although you are binding to the controller, the behavior of this controller
+ is to pass through any methods or properties to the underlying array. This
+ capability comes from `Ember.ArrayProxy`, which this class inherits from.
+
+ Sometimes you want to display computed properties within the body of an
+ `#each` helper that depend on the underlying items in `content`, but are not
+ present on those items. To do this, set `itemController` to the name of a
+ controller (probably an `ObjectController`) that will wrap each individual item.
+
+ For example:
+
+ ```handlebars
+ {{#each post in controller}}
+ <li>{{title}} ({{titleLength}} characters)</li>
+ {{/each}}
+ ```
+
+ ```javascript
+ App.PostsController = Ember.ArrayController.extend({
+ itemController: 'post'
+ });
+
+ App.PostController = Ember.ObjectController.extend({
+ // the `title` property will be proxied to the underlying post.
+
+ titleLength: function() {
+ return this.get('title').length;
+ }.property('title')
+ });
+ ```
+
+ In some cases it is helpful to return a different `itemController` depending
+ on the particular item. Subclasses can do this by overriding
+ `lookupItemController`.
+
+ For example:
+
+ ```javascript
+ App.MyArrayController = Ember.ArrayController.extend({
+ lookupItemController: function( object ) {
+ if (object.get('isSpecial')) {
+ return "special"; // use App.SpecialController
+ } else {
+ return "regular"; // use App.RegularController
+ }
+ }
+ });
+ ```
+
+ @class ArrayController
+ @namespace Ember
+ @extends Ember.ArrayProxy
+ @uses Ember.SortableMixin
+ @uses Ember.ControllerMixin
+*/
+
+Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin,
+ Ember.SortableMixin, {
+
+ /**
+ The controller used to wrap items, if any.
+
+ @property itemController
+ @type String
+ @default null
+ */
+ itemController: null,
+
+ /**
+ Return the name of the controller to wrap items, or `null` if items should
+ be returned directly. The default implementation simply returns the
+ `itemController` property, but subclasses can override this method to return
+ different controllers for different objects.
+
+ For example:
+
+ ```javascript
+ App.MyArrayController = Ember.ArrayController.extend({
+ lookupItemController: function( object ) {
+ if (object.get('isSpecial')) {
+ return "special"; // use App.SpecialController
+ } else {
+ return "regular"; // use App.RegularController
+ }
+ }
+ });
+ ```
+
+ @method
+ @type String
+ @default null
+ */
+ lookupItemController: function(object) {
+ return get(this, 'itemController');
+ },
+
+ objectAtContent: function(idx) {
+ var length = get(this, 'length'),
+ object = get(this,'arrangedContent').objectAt(idx);
+
+ if (idx >= 0 && idx < length) {
+ var controllerClass = this.lookupItemController(object);
+ if (controllerClass) {
+ return this.controllerAt(idx, object, controllerClass);
+ }
+ }
+
+ // When `controllerClass` is falsy, we have not opted in to using item
+ // controllers, so return the object directly.
+
+ // When the index is out of range, we want to return the "out of range"
+ // value, whatever that might be. Rather than make assumptions
+ // (e.g. guessing `null` or `undefined`) we defer this to `arrangedContent`.
+ return object;
+ },
+
+ arrangedContentDidChange: function() {
+ this._super();
+ this._resetSubContainers();
+ },
+
+ arrayContentDidChange: function(idx, removedCnt, addedCnt) {
+ var subContainers = get(this, 'subContainers'),
+ subContainersToRemove = subContainers.slice(idx, idx+removedCnt);
+
+ forEach(subContainersToRemove, function(subContainer) {
+ if (subContainer) { subContainer.destroy(); }
+ });
+
+ replace(subContainers, idx, removedCnt, new Array(addedCnt));
+
+ // The shadow array of subcontainers must be updated before we trigger
+ // observers, otherwise observers will get the wrong subcontainer when
+ // calling `objectAt`
+ this._super(idx, removedCnt, addedCnt);
+ },
+
+ init: function() {
+ this._super();
+ if (!this.get('content')) { this.set('content', Ember.A()); }
+ this._resetSubContainers();
+ },
+
+ controllerAt: function(idx, object, controllerClass) {
+ var container = get(this, 'container'),
+ subContainers = get(this, 'subContainers'),
+ subContainer = subContainers[idx],
+ controller;
+
+ if (!subContainer) {
+ subContainer = subContainers[idx] = container.child();
+ }
+
+ controller = subContainer.lookup("controller:" + controllerClass);
+ if (!controller) {
+ throw new Error('Could not resolve itemController: "' + controllerClass + '"');
+ }
+
+ controller.set('target', this);
+ controller.set('content', object);
+
+ return controller;
+ },
+
+ subContainers: null,
+
+ _resetSubContainers: function() {
+ var subContainers = get(this, 'subContainers');
+
+ if (subContainers) {
+ forEach(subContainers, function(subContainer) {
+ if (subContainer) { subContainer.destroy(); }
+ });
+ }
+
+ this.set('subContainers', Ember.A());
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-runtime
+*/
+
+/**
+ `Ember.ObjectController` is part of Ember's Controller layer. A single shared
+ instance of each `Ember.ObjectController` subclass in your application's
+ namespace will be created at application initialization and be stored on your
+ application's `Ember.Router` instance.
+
+ `Ember.ObjectController` derives its functionality from its superclass
+ `Ember.ObjectProxy` and the `Ember.ControllerMixin` mixin.
+
+ @class ObjectController
+ @namespace Ember
+ @extends Ember.ObjectProxy
+ @uses Ember.ControllerMixin
+**/
+Ember.ObjectController = Ember.ObjectProxy.extend(Ember.ControllerMixin);
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(function() {
+/**
+Ember Runtime
+
+@module ember
+@submodule ember-runtime
+@requires ember-metal
+*/
+
+})();
+
+(function() {
+/**
+@module ember
+@submodule ember-views
+*/
+
+var jQuery = Ember.imports.jQuery;
+Ember.assert("Ember Views require jQuery 1.8 or 1.9", jQuery && (jQuery().jquery.match(/^1\.(8|9)(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_JQUERY));
+
+/**
+ Alias for jQuery
+
+ @method $
+ @for Ember
+*/
+Ember.$ = jQuery;
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-views
+*/
+
+// http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#dndevents
+var dragEvents = Ember.String.w('dragstart drag dragenter dragleave dragover drop dragend');
+
+// Copies the `dataTransfer` property from a browser event object onto the
+// jQuery event object for the specified events
+Ember.EnumerableUtils.forEach(dragEvents, function(eventName) {
+ Ember.$.event.fixHooks[eventName] = { props: ['dataTransfer'] };
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-views
+*/
+
+/*** BEGIN METAMORPH HELPERS ***/
+
+// Internet Explorer prior to 9 does not allow setting innerHTML if the first element
+// is a "zero-scope" element. This problem can be worked around by making
+// the first node an invisible text node. We, like Modernizr, use &shy;
+var needsShy = (function(){
+ var testEl = document.createElement('div');
+ testEl.innerHTML = "<div></div>";
+ testEl.firstChild.innerHTML = "<script></script>";
+ return testEl.firstChild.innerHTML === '';
+})();
+
+// IE 8 (and likely earlier) likes to move whitespace preceeding
+// a script tag to appear after it. This means that we can
+// accidentally remove whitespace when updating a morph.
+var movesWhitespace = (function() {
+ var testEl = document.createElement('div');
+ testEl.innerHTML = "Test: <script type='text/x-placeholder'></script>Value";
+ return testEl.childNodes[0].nodeValue === 'Test:' &&
+ testEl.childNodes[2].nodeValue === ' Value';
+})();
+
+// Use this to find children by ID instead of using jQuery
+var findChildById = function(element, id) {
+ if (element.getAttribute('id') === id) { return element; }
+
+ var len = element.childNodes.length, idx, node, found;
+ for (idx=0; idx<len; idx++) {
+ node = element.childNodes[idx];
+ found = node.nodeType === 1 && findChildById(node, id);
+ if (found) { return found; }
+ }
+};
+
+var setInnerHTMLWithoutFix = function(element, html) {
+ if (needsShy) {
+ html = '&shy;' + html;
+ }
+
+ var matches = [];
+ if (movesWhitespace) {
+ // Right now we only check for script tags with ids with the
+ // goal of targeting morphs.
+ html = html.replace(/(\s+)(<script id='([^']+)')/g, function(match, spaces, tag, id) {
+ matches.push([id, spaces]);
+ return tag;
+ });
+ }
+
+ element.innerHTML = html;
+
+ // If we have to do any whitespace adjustments do them now
+ if (matches.length > 0) {
+ var len = matches.length, idx;
+ for (idx=0; idx<len; idx++) {
+ var script = findChildById(element, matches[idx][0]),
+ node = document.createTextNode(matches[idx][1]);
+ script.parentNode.insertBefore(node, script);
+ }
+ }
+
+ if (needsShy) {
+ var shyElement = element.firstChild;
+ while (shyElement.nodeType === 1 && !shyElement.nodeName) {
+ shyElement = shyElement.firstChild;
+ }
+ if (shyElement.nodeType === 3 && shyElement.nodeValue.charAt(0) === "\u00AD") {
+ shyElement.nodeValue = shyElement.nodeValue.slice(1);
+ }
+ }
+};
+
+/*** END METAMORPH HELPERS */
+
+
+var innerHTMLTags = {};
+var canSetInnerHTML = function(tagName) {
+ if (innerHTMLTags[tagName] !== undefined) {
+ return innerHTMLTags[tagName];
+ }
+
+ var canSet = true;
+
+ // IE 8 and earlier don't allow us to do innerHTML on select
+ if (tagName.toLowerCase() === 'select') {
+ var el = document.createElement('select');
+ setInnerHTMLWithoutFix(el, '<option value="test">Test</option>');
+ canSet = el.options.length === 1;
+ }
+
+ innerHTMLTags[tagName] = canSet;
+
+ return canSet;
+};
+
+var setInnerHTML = function(element, html) {
+ var tagName = element.tagName;
+
+ if (canSetInnerHTML(tagName)) {
+ setInnerHTMLWithoutFix(element, html);
+ } else {
+ Ember.assert("Can't set innerHTML on "+element.tagName+" in this browser", element.outerHTML);
+
+ var startTag = element.outerHTML.match(new RegExp("<"+tagName+"([^>]*)>", 'i'))[0],
+ endTag = '</'+tagName+'>';
+
+ var wrapper = document.createElement('div');
+ setInnerHTMLWithoutFix(wrapper, startTag + html + endTag);
+ element = wrapper.firstChild;
+ while (element.tagName !== tagName) {
+ element = element.nextSibling;
+ }
+ }
+
+ return element;
+};
+
+function isSimpleClick(event) {
+ var modifier = event.shiftKey || event.metaKey || event.altKey || event.ctrlKey,
+ secondaryClick = event.which > 1; // IE9 may return undefined
+
+ return !modifier && !secondaryClick;
+}
+
+Ember.ViewUtils = {
+ setInnerHTML: setInnerHTML,
+ isSimpleClick: isSimpleClick
+};
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-views
+*/
+
+var get = Ember.get, set = Ember.set;
+var indexOf = Ember.ArrayPolyfills.indexOf;
+
+
+
+
+
+var ClassSet = function() {
+ this.seen = {};
+ this.list = [];
+};
+
+ClassSet.prototype = {
+ add: function(string) {
+ if (string in this.seen) { return; }
+ this.seen[string] = true;
+
+ this.list.push(string);
+ },
+
+ toDOM: function() {
+ return this.list.join(" ");
+ }
+};
+
+/**
+ `Ember.RenderBuffer` gathers information regarding the a view and generates the
+ final representation. `Ember.RenderBuffer` will generate HTML which can be pushed
+ to the DOM.
+
+ @class RenderBuffer
+ @namespace Ember
+ @constructor
+*/
+Ember.RenderBuffer = function(tagName) {
+ return new Ember._RenderBuffer(tagName);
+};
+
+Ember._RenderBuffer = function(tagName) {
+ this.tagNames = [tagName || null];
+ this.buffer = [];
+};
+
+Ember._RenderBuffer.prototype =
+/** @scope Ember.RenderBuffer.prototype */ {
+
+ // The root view's element
+ _element: null,
+
+ /**
+ @private
+
+ An internal set used to de-dupe class names when `addClass()` is
+ used. After each call to `addClass()`, the `classes` property
+ will be updated.
+
+ @property elementClasses
+ @type Array
+ @default []
+ */
+ elementClasses: null,
+
+ /**
+ Array of class names which will be applied in the class attribute.
+
+ You can use `setClasses()` to set this property directly. If you
+ use `addClass()`, it will be maintained for you.
+
+ @property classes
+ @type Array
+ @default []
+ */
+ classes: null,
+
+ /**
+ The id in of the element, to be applied in the id attribute.
+
+ You should not set this property yourself, rather, you should use
+ the `id()` method of `Ember.RenderBuffer`.
+
+ @property elementId
+ @type String
+ @default null
+ */
+ elementId: null,
+
+ /**
+ A hash keyed on the name of the attribute and whose value will be
+ applied to that attribute. For example, if you wanted to apply a
+ `data-view="Foo.bar"` property to an element, you would set the
+ elementAttributes hash to `{'data-view':'Foo.bar'}`.
+
+ You should not maintain this hash yourself, rather, you should use
+ the `attr()` method of `Ember.RenderBuffer`.
+
+ @property elementAttributes
+ @type Hash
+ @default {}
+ */
+ elementAttributes: null,
+
+ /**
+ A hash keyed on the name of the properties and whose value will be
+ applied to that property. For example, if you wanted to apply a
+ `checked=true` property to an element, you would set the
+ elementProperties hash to `{'checked':true}`.
+
+ You should not maintain this hash yourself, rather, you should use
+ the `prop()` method of `Ember.RenderBuffer`.
+
+ @property elementProperties
+ @type Hash
+ @default {}
+ */
+ elementProperties: null,
+
+ /**
+ The tagname of the element an instance of `Ember.RenderBuffer` represents.
+
+ Usually, this gets set as the first parameter to `Ember.RenderBuffer`. For
+ example, if you wanted to create a `p` tag, then you would call
+
+ ```javascript
+ Ember.RenderBuffer('p')
+ ```
+
+ @property elementTag
+ @type String
+ @default null
+ */
+ elementTag: null,
+
+ /**
+ A hash keyed on the name of the style attribute and whose value will
+ be applied to that attribute. For example, if you wanted to apply a
+ `background-color:black;` style to an element, you would set the
+ elementStyle hash to `{'background-color':'black'}`.
+
+ You should not maintain this hash yourself, rather, you should use
+ the `style()` method of `Ember.RenderBuffer`.
+
+ @property elementStyle
+ @type Hash
+ @default {}
+ */
+ elementStyle: null,
+
+ /**
+ Nested `RenderBuffers` will set this to their parent `RenderBuffer`
+ instance.
+
+ @property parentBuffer
+ @type Ember._RenderBuffer
+ */
+ parentBuffer: null,
+
+ /**
+ Adds a string of HTML to the `RenderBuffer`.
+
+ @method push
+ @param {String} string HTML to push into the buffer
+ @chainable
+ */
+ push: function(string) {
+ this.buffer.push(string);
+ return this;
+ },
+
+ /**
+ Adds a class to the buffer, which will be rendered to the class attribute.
+
+ @method addClass
+ @param {String} className Class name to add to the buffer
+ @chainable
+ */
+ addClass: function(className) {
+ // lazily create elementClasses
+ var elementClasses = this.elementClasses = (this.elementClasses || new ClassSet());
+ this.elementClasses.add(className);
+ this.classes = this.elementClasses.list;
+
+ return this;
+ },
+
+ setClasses: function(classNames) {
+ this.classes = classNames;
+ },
+
+ /**
+ Sets the elementID to be used for the element.
+
+ @method id
+ @param {String} id
+ @chainable
+ */
+ id: function(id) {
+ this.elementId = id;
+ return this;
+ },
+
+ // duck type attribute functionality like jQuery so a render buffer
+ // can be used like a jQuery object in attribute binding scenarios.
+
+ /**
+ Adds an attribute which will be rendered to the element.
+
+ @method attr
+ @param {String} name The name of the attribute
+ @param {String} value The value to add to the attribute
+ @chainable
+ @return {Ember.RenderBuffer|String} this or the current attribute value
+ */
+ attr: function(name, value) {
+ var attributes = this.elementAttributes = (this.elementAttributes || {});
+
+ if (arguments.length === 1) {
+ return attributes[name];
+ } else {
+ attributes[name] = value;
+ }
+
+ return this;
+ },
+
+ /**
+ Remove an attribute from the list of attributes to render.
+
+ @method removeAttr
+ @param {String} name The name of the attribute
+ @chainable
+ */
+ removeAttr: function(name) {
+ var attributes = this.elementAttributes;
+ if (attributes) { delete attributes[name]; }
+
+ return this;
+ },
+
+ /**
+ Adds an property which will be rendered to the element.
+
+ @method prop
+ @param {String} name The name of the property
+ @param {String} value The value to add to the property
+ @chainable
+ @return {Ember.RenderBuffer|String} this or the current property value
+ */
+ prop: function(name, value) {
+ var properties = this.elementProperties = (this.elementProperties || {});
+
+ if (arguments.length === 1) {
+ return properties[name];
+ } else {
+ properties[name] = value;
+ }
+
+ return this;
+ },
+
+ /**
+ Remove an property from the list of properties to render.
+
+ @method removeProp
+ @param {String} name The name of the property
+ @chainable
+ */
+ removeProp: function(name) {
+ var properties = this.elementProperties;
+ if (properties) { delete properties[name]; }
+
+ return this;
+ },
+
+ /**
+ Adds a style to the style attribute which will be rendered to the element.
+
+ @method style
+ @param {String} name Name of the style
+ @param {String} value
+ @chainable
+ */
+ style: function(name, value) {
+ var style = this.elementStyle = (this.elementStyle || {});
+
+ this.elementStyle[name] = value;
+ return this;
+ },
+
+ begin: function(tagName) {
+ this.tagNames.push(tagName || null);
+ return this;
+ },
+
+ pushOpeningTag: function() {
+ var tagName = this.currentTagName();
+ if (!tagName) { return; }
+
+ if (!this._element && this.buffer.length === 0) {
+ this._element = this.generateElement();
+ return;
+ }
+
+ var buffer = this.buffer,
+ id = this.elementId,
+ classes = this.classes,
+ attrs = this.elementAttributes,
+ props = this.elementProperties,
+ style = this.elementStyle,
+ attr, prop;
+
+ buffer.push('<' + tagName);
+
+ if (id) {
+ buffer.push(' id="' + this._escapeAttribute(id) + '"');
+ this.elementId = null;
+ }
+ if (classes) {
+ buffer.push(' class="' + this._escapeAttribute(classes.join(' ')) + '"');
+ this.classes = null;
+ }
+
+ if (style) {
+ buffer.push(' style="');
+
+ for (prop in style) {
+ if (style.hasOwnProperty(prop)) {
+ buffer.push(prop + ':' + this._escapeAttribute(style[prop]) + ';');
+ }
+ }
+
+ buffer.push('"');
+
+ this.elementStyle = null;
+ }
+
+ if (attrs) {
+ for (attr in attrs) {
+ if (attrs.hasOwnProperty(attr)) {
+ buffer.push(' ' + attr + '="' + this._escapeAttribute(attrs[attr]) + '"');
+ }
+ }
+
+ this.elementAttributes = null;
+ }
+
+ if (props) {
+ for (prop in props) {
+ if (props.hasOwnProperty(prop)) {
+ var value = props[prop];
+ if (value || typeof(value) === 'number') {
+ if (value === true) {
+ buffer.push(' ' + prop + '="' + prop + '"');
+ } else {
+ buffer.push(' ' + prop + '="' + this._escapeAttribute(props[prop]) + '"');
+ }
+ }
+ }
+ }
+
+ this.elementProperties = null;
+ }
+
+ buffer.push('>');
+ },
+
+ pushClosingTag: function() {
+ var tagName = this.tagNames.pop();
+ if (tagName) { this.buffer.push('</' + tagName + '>'); }
+ },
+
+ currentTagName: function() {
+ return this.tagNames[this.tagNames.length-1];
+ },
+
+ generateElement: function() {
+ var tagName = this.tagNames.pop(), // pop since we don't need to close
+ element = document.createElement(tagName),
+ $element = Ember.$(element),
+ id = this.elementId,
+ classes = this.classes,
+ attrs = this.elementAttributes,
+ props = this.elementProperties,
+ style = this.elementStyle,
+ styleBuffer = '', attr, prop;
+
+ if (id) {
+ $element.attr('id', id);
+ this.elementId = null;
+ }
+ if (classes) {
+ $element.attr('class', classes.join(' '));
+ this.classes = null;
+ }
+
+ if (style) {
+ for (prop in style) {
+ if (style.hasOwnProperty(prop)) {
+ styleBuffer += (prop + ':' + style[prop] + ';');
+ }
+ }
+
+ $element.attr('style', styleBuffer);
+
+ this.elementStyle = null;
+ }
+
+ if (attrs) {
+ for (attr in attrs) {
+ if (attrs.hasOwnProperty(attr)) {
+ $element.attr(attr, attrs[attr]);
+ }
+ }
+
+ this.elementAttributes = null;
+ }
+
+ if (props) {
+ for (prop in props) {
+ if (props.hasOwnProperty(prop)) {
+ $element.prop(prop, props[prop]);
+ }
+ }
+
+ this.elementProperties = null;
+ }
+
+ return element;
+ },
+
+ /**
+ @method element
+ @return {DOMElement} The element corresponding to the generated HTML
+ of this buffer
+ */
+ element: function() {
+ var html = this.innerString();
+
+ if (html) {
+ this._element = Ember.ViewUtils.setInnerHTML(this._element, html);
+ }
+
+ return this._element;
+ },
+
+ /**
+ Generates the HTML content for this buffer.
+
+ @method string
+ @return {String} The generated HTML
+ */
+ string: function() {
+ if (this._element) {
+ return this.element().outerHTML;
+ } else {
+ return this.innerString();
+ }
+ },
+
+ innerString: function() {
+ return this.buffer.join('');
+ },
+
+ _escapeAttribute: function(value) {
+ // Stolen shamelessly from Handlebars
+
+ var escape = {
+ "<": "&lt;",
+ ">": "&gt;",
+ '"': "&quot;",
+ "'": "&#x27;",
+ "`": "&#x60;"
+ };
+
+ var badChars = /&(?!\w+;)|[<>"'`]/g;
+ var possible = /[&<>"'`]/;
+
+ var escapeChar = function(chr) {
+ return escape[chr] || "&amp;";
+ };
+
+ var string = value.toString();
+
+ if(!possible.test(string)) { return string; }
+ return string.replace(badChars, escapeChar);
+ }
+
+};
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-views
+*/
+
+var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt;
+
+/**
+ `Ember.EventDispatcher` handles delegating browser events to their
+ corresponding `Ember.Views.` For example, when you click on a view,
+ `Ember.EventDispatcher` ensures that that view's `mouseDown` method gets
+ called.
+
+ @class EventDispatcher
+ @namespace Ember
+ @private
+ @extends Ember.Object
+*/
+Ember.EventDispatcher = Ember.Object.extend(
+/** @scope Ember.EventDispatcher.prototype */{
+
+ /**
+ @private
+
+ The root DOM element to which event listeners should be attached. Event
+ listeners will be attached to the document unless this is overridden.
+
+ Can be specified as a DOMElement or a selector string.
+
+ The default body is a string since this may be evaluated before document.body
+ exists in the DOM.
+
+ @property rootElement
+ @type DOMElement
+ @default 'body'
+ */
+ rootElement: 'body',
+
+ /**
+ @private
+
+ Sets up event listeners for standard browser events.
+
+ This will be called after the browser sends a `DOMContentReady` event. By
+ default, it will set up all of the listeners on the document body. If you
+ would like to register the listeners on a different element, set the event
+ dispatcher's `root` property.
+
+ @method setup
+ @param addedEvents {Hash}
+ */
+ setup: function(addedEvents) {
+ var event, events = {
+ touchstart : 'touchStart',
+ touchmove : 'touchMove',
+ touchend : 'touchEnd',
+ touchcancel : 'touchCancel',
+ keydown : 'keyDown',
+ keyup : 'keyUp',
+ keypress : 'keyPress',
+ mousedown : 'mouseDown',
+ mouseup : 'mouseUp',
+ contextmenu : 'contextMenu',
+ click : 'click',
+ dblclick : 'doubleClick',
+ mousemove : 'mouseMove',
+ focusin : 'focusIn',
+ focusout : 'focusOut',
+ mouseenter : 'mouseEnter',
+ mouseleave : 'mouseLeave',
+ submit : 'submit',
+ input : 'input',
+ change : 'change',
+ dragstart : 'dragStart',
+ drag : 'drag',
+ dragenter : 'dragEnter',
+ dragleave : 'dragLeave',
+ dragover : 'dragOver',
+ drop : 'drop',
+ dragend : 'dragEnd'
+ };
+
+ Ember.$.extend(events, addedEvents || {});
+
+ var rootElement = Ember.$(get(this, 'rootElement'));
+
+ Ember.assert(fmt('You cannot use the same root element (%@) multiple times in an Ember.Application', [rootElement.selector || rootElement[0].tagName]), !rootElement.is('.ember-application'));
+ Ember.assert('You cannot make a new Ember.Application using a root element that is a descendent of an existing Ember.Application', !rootElement.closest('.ember-application').length);
+ Ember.assert('You cannot make a new Ember.Application using a root element that is an ancestor of an existing Ember.Application', !rootElement.find('.ember-application').length);
+
+ rootElement.addClass('ember-application');
+
+ Ember.assert('Unable to add "ember-application" class to rootElement. Make sure you set rootElement to the body or an element in the body.', rootElement.is('.ember-application'));
+
+ for (event in events) {
+ if (events.hasOwnProperty(event)) {
+ this.setupHandler(rootElement, event, events[event]);
+ }
+ }
+ },
+
+ /**
+ @private
+
+ Registers an event listener on the document. If the given event is
+ triggered, the provided event handler will be triggered on the target view.
+
+ If the target view does not implement the event handler, or if the handler
+ returns `false`, the parent view will be called. The event will continue to
+ bubble to each successive parent view until it reaches the top.
+
+ For example, to have the `mouseDown` method called on the target view when
+ a `mousedown` event is received from the browser, do the following:
+
+ ```javascript
+ setupHandler('mousedown', 'mouseDown');
+ ```
+
+ @method setupHandler
+ @param {Element} rootElement
+ @param {String} event the browser-originated event to listen to
+ @param {String} eventName the name of the method to call on the view
+ */
+ setupHandler: function(rootElement, event, eventName) {
+ var self = this;
+
+ rootElement.delegate('.ember-view', event + '.ember', function(evt, triggeringManager) {
+ return Ember.handleErrors(function() {
+ var view = Ember.View.views[this.id],
+ result = true, manager = null;
+
+ manager = self._findNearestEventManager(view,eventName);
+
+ if (manager && manager !== triggeringManager) {
+ result = self._dispatchEvent(manager, evt, eventName, view);
+ } else if (view) {
+ result = self._bubbleEvent(view,evt,eventName);
+ } else {
+ evt.stopPropagation();
+ }
+
+ return result;
+ }, this);
+ });
+
+ rootElement.delegate('[data-ember-action]', event + '.ember', function(evt) {
+ return Ember.handleErrors(function() {
+ var actionId = Ember.$(evt.currentTarget).attr('data-ember-action'),
+ action = Ember.Handlebars.ActionHelper.registeredActions[actionId];
+
+ // We have to check for action here since in some cases, jQuery will trigger
+ // an event on `removeChild` (i.e. focusout) after we've already torn down the
+ // action handlers for the view.
+ if (action && action.eventName === eventName) {
+ return action.handler(evt);
+ }
+ }, this);
+ });
+ },
+
+ _findNearestEventManager: function(view, eventName) {
+ var manager = null;
+
+ while (view) {
+ manager = get(view, 'eventManager');
+ if (manager && manager[eventName]) { break; }
+
+ view = get(view, 'parentView');
+ }
+
+ return manager;
+ },
+
+ _dispatchEvent: function(object, evt, eventName, view) {
+ var result = true;
+
+ var handler = object[eventName];
+ if (Ember.typeOf(handler) === 'function') {
+ result = handler.call(object, evt, view);
+ // Do not preventDefault in eventManagers.
+ evt.stopPropagation();
+ }
+ else {
+ result = this._bubbleEvent(view, evt, eventName);
+ }
+
+ return result;
+ },
+
+ _bubbleEvent: function(view, evt, eventName) {
+ return Ember.run(function() {
+ return view.handleEvent(eventName, evt);
+ });
+ },
+
+ destroy: function() {
+ var rootElement = get(this, 'rootElement');
+ Ember.$(rootElement).undelegate('.ember').removeClass('ember-application');
+ return this._super();
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-views
+*/
+
+// Add a new named queue for rendering views that happens
+// after bindings have synced, and a queue for scheduling actions
+// that that should occur after view rendering.
+var queues = Ember.run.queues;
+queues.splice(Ember.$.inArray('actions', queues)+1, 0, 'render', 'afterRender');
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-views
+*/
+
+var get = Ember.get, set = Ember.set;
+
+// Original class declaration and documentation in runtime/lib/controllers/controller.js
+// NOTE: It may be possible with YUIDoc to combine docs in two locations
+
+/**
+Additional methods for the ControllerMixin
+
+@class ControllerMixin
+@namespace Ember
+*/
+Ember.ControllerMixin.reopen({
+ target: null,
+ namespace: null,
+ view: null,
+ container: null,
+ _childContainers: null,
+
+ init: function() {
+ this._super();
+ set(this, '_childContainers', {});
+ },
+
+ _modelDidChange: Ember.observer(function() {
+ var containers = get(this, '_childContainers'),
+ container;
+
+ for (var prop in containers) {
+ if (!containers.hasOwnProperty(prop)) { continue; }
+ containers[prop].destroy();
+ }
+
+ set(this, '_childContainers', {});
+ }, 'model')
+});
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(function() {
+var states = {};
+
+/**
+@module ember
+@submodule ember-views
+*/
+
+var get = Ember.get, set = Ember.set, addObserver = Ember.addObserver, removeObserver = Ember.removeObserver;
+var meta = Ember.meta, guidFor = Ember.guidFor, fmt = Ember.String.fmt;
+var a_slice = [].slice;
+var a_forEach = Ember.EnumerableUtils.forEach;
+var a_addObject = Ember.EnumerableUtils.addObject;
+
+var childViewsProperty = Ember.computed(function() {
+ var childViews = this._childViews, ret = Ember.A(), view = this;
+
+ a_forEach(childViews, function(view) {
+ if (view.isVirtual) {
+ ret.pushObjects(get(view, 'childViews'));
+ } else {
+ ret.push(view);
+ }
+ });
+
+ ret.replace = function (idx, removedCount, addedViews) {
+ if (view instanceof Ember.ContainerView) {
+ Ember.deprecate("Manipulating a Ember.ContainerView through its childViews property is deprecated. Please use the ContainerView instance itself as an Ember.MutableArray.");
+ return view.replace(idx, removedCount, addedViews);
+ }
+ throw new Error("childViews is immutable");
+ };
+
+ return ret;
+});
+
+Ember.warn("The VIEW_PRESERVES_CONTEXT flag has been removed and the functionality can no longer be disabled.", Ember.ENV.VIEW_PRESERVES_CONTEXT !== false);
+
+/**
+ Global hash of shared templates. This will automatically be populated
+ by the build tools so that you can store your Handlebars templates in
+ separate files that get loaded into JavaScript at buildtime.
+
+ @property TEMPLATES
+ @for Ember
+ @type Hash
+*/
+Ember.TEMPLATES = {};
+
+Ember.CoreView = Ember.Object.extend(Ember.Evented, {
+ isView: true,
+
+ states: states,
+
+ 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');
+ },
+
+ /**
+ If the view is currently inserted into the DOM of a parent view, this
+ property will point to the parent of the view.
+
+ @property parentView
+ @type Ember.View
+ @default null
+ */
+ parentView: Ember.computed(function() {
+ var parent = this._parentView;
+
+ if (parent && parent.isVirtual) {
+ return get(parent, 'parentView');
+ } else {
+ return parent;
+ }
+ }).property('_parentView'),
+
+ state: null,
+
+ _parentView: null,
+
+ // return the current view, not including virtual views
+ concreteView: Ember.computed(function() {
+ if (!this.isVirtual) { return this; }
+ else { return get(this, 'parentView'); }
+ }).property('parentView').volatile(),
+
+ instrumentName: 'core_view',
+
+ instrumentDetails: function(hash) {
+ hash.object = this.toString();
+ },
+
+ /**
+ @private
+
+ Invoked by the view system when this view needs to produce an HTML
+ representation. This method will create a new render buffer, if needed,
+ then apply any default attributes, such as class names and visibility.
+ Finally, the `render()` method is invoked, which is responsible for
+ doing the bulk of the rendering.
+
+ You should not need to override this method; instead, implement the
+ `template` property, or if you need more control, override the `render`
+ method.
+
+ @method renderToBuffer
+ @param {Ember.RenderBuffer} buffer the render buffer. If no buffer is
+ passed, a default buffer, using the current view's `tagName`, will
+ be used.
+ */
+ renderToBuffer: function(parentBuffer, bufferOperation) {
+ var name = 'render.' + this.instrumentName,
+ details = {};
+
+ this.instrumentDetails(details);
+
+ return Ember.instrument(name, details, function() {
+ return this._renderToBuffer(parentBuffer, bufferOperation);
+ }, this);
+ },
+
+ _renderToBuffer: function(parentBuffer, bufferOperation) {
+ 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
+ // insert a new buffer after the "parent buffer").
+ var tagName = this.tagName;
+
+ if (tagName === null || tagName === undefined) {
+ tagName = 'div';
+ }
+
+ var buffer = this.buffer = parentBuffer && parentBuffer.begin(tagName) || Ember.RenderBuffer(tagName);
+ this.transitionTo('inBuffer', false);
+
+ this.beforeRender(buffer);
+ this.render(buffer);
+ this.afterRender(buffer);
+
+ return buffer;
+ },
+
+ /**
+ @private
+
+ Override the default event firing from `Ember.Evented` to
+ also call methods with the given name.
+
+ @method trigger
+ @param name {String}
+ */
+ trigger: function(name) {
+ this._super.apply(this, arguments);
+ var method = this[name];
+ if (method) {
+ var args = [], i, l;
+ for (i = 1, l = arguments.length; i < l; i++) {
+ args.push(arguments[i]);
+ }
+ return method.apply(this, args);
+ }
+ },
+
+ has: function(name) {
+ return Ember.typeOf(this[name]) === 'function' || this._super(name);
+ },
+
+ willDestroy: function() {
+ var parent = this._parentView;
+
+ // destroy the element -- this will avoid each child view destroying
+ // the element over and over again...
+ if (!this.removedFromDOM) { this.destroyElement(); }
+
+ // 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');
+
+ // next remove view from global hash
+ if (!this.isVirtual) delete Ember.View.views[this.elementId];
+ },
+
+ clearRenderedChildren: Ember.K,
+ triggerRecursively: Ember.K,
+ invokeRecursively: Ember.K,
+ transitionTo: Ember.K,
+ destroyElement: Ember.K
+});
+
+/**
+ `Ember.View` is the class in Ember responsible for encapsulating templates of
+ HTML content, combining templates with data to render as sections of a page's
+ DOM, and registering and responding to user-initiated events.
+
+ ## HTML Tag
+
+ The default HTML tag name used for a view's DOM representation is `div`. This
+ can be customized by setting the `tagName` property. The following view
+class:
+
+ ```javascript
+ ParagraphView = Ember.View.extend({
+ tagName: 'em'
+ });
+ ```
+
+ Would result in instances with the following HTML:
+
+ ```html
+ <em id="ember1" class="ember-view"></em>
+ ```
+
+ ## HTML `class` Attribute
+
+ The HTML `class` attribute of a view's tag can be set by providing a
+ `classNames` property that is set to an array of strings:
+
+ ```javascript
+ MyView = Ember.View.extend({
+ classNames: ['my-class', 'my-other-class']
+ });
+ ```
+
+ Will result in view instances with an HTML representation of:
+
+ ```html
+ <div id="ember1" class="ember-view my-class my-other-class"></div>
+ ```
+
+ `class` attribute values can also be set by providing a `classNameBindings`
+ property set to an array of properties names for the view. The return value
+ of these properties will be added as part of the value for the view's `class`
+ attribute. These properties can be computed properties:
+
+ ```javascript
+ MyView = Ember.View.extend({
+ classNameBindings: ['propertyA', 'propertyB'],
+ propertyA: 'from-a',
+ propertyB: function(){
+ if(someLogic){ return 'from-b'; }
+ }.property()
+ });
+ ```
+
+ Will result in view instances with an HTML representation of:
+
+ ```html
+ <div id="ember1" class="ember-view from-a from-b"></div>
+ ```
+
+ If the value of a class name binding returns a boolean the property name
+ itself will be used as the class name if the property is true. The class name
+ will not be added if the value is `false` or `undefined`.
+
+ ```javascript
+ MyView = Ember.View.extend({
+ classNameBindings: ['hovered'],
+ hovered: true
+ });
+ ```
+
+ Will result in view instances with an HTML representation of:
+
+ ```html
+ <div id="ember1" class="ember-view hovered"></div>
+ ```
+
+ When using boolean class name bindings you can supply a string value other
+ than the property name for use as the `class` HTML attribute by appending the
+ preferred value after a ":" character when defining the binding:
+
+ ```javascript
+ MyView = Ember.View.extend({
+ classNameBindings: ['awesome:so-very-cool'],
+ awesome: true
+ });
+ ```
+
+ Will result in view instances with an HTML representation of:
+
+ ```html
+ <div id="ember1" class="ember-view so-very-cool"></div>
+ ```
+
+ Boolean value class name bindings whose property names are in a
+ camelCase-style format will be converted to a dasherized format:
+
+ ```javascript
+ MyView = Ember.View.extend({
+ classNameBindings: ['isUrgent'],
+ isUrgent: true
+ });
+ ```
+
+ Will result in view instances with an HTML representation of:
+
+ ```html
+ <div id="ember1" class="ember-view is-urgent"></div>
+ ```
+
+ Class name bindings can also refer to object values that are found by
+ traversing a path relative to the view itself:
+
+ ```javascript
+ MyView = Ember.View.extend({
+ classNameBindings: ['messages.empty']
+ messages: Ember.Object.create({
+ empty: true
+ })
+ });
+ ```
+
+ Will result in view instances with an HTML representation of:
+
+ ```html
+ <div id="ember1" class="ember-view empty"></div>
+ ```
+
+ If you want to add a class name for a property which evaluates to true and
+ and a different class name if it evaluates to false, you can pass a binding
+ like this:
+
+ ```javascript
+ // Applies 'enabled' class when isEnabled is true and 'disabled' when isEnabled is false
+ Ember.View.create({
+ classNameBindings: ['isEnabled:enabled:disabled']
+ isEnabled: true
+ });
+ ```
+
+ Will result in view instances with an HTML representation of:
+
+ ```html
+ <div id="ember1" class="ember-view enabled"></div>
+ ```
+
+ When isEnabled is `false`, the resulting HTML reprensentation looks like
+ this:
+
+ ```html
+ <div id="ember1" class="ember-view disabled"></div>
+ ```
+
+ This syntax offers the convenience to add a class if a property is `false`:
+
+ ```javascript
+ // Applies no class when isEnabled is true and class 'disabled' when isEnabled is false
+ Ember.View.create({
+ classNameBindings: ['isEnabled::disabled']
+ isEnabled: true
+ });
+ ```
+
+ Will result in view instances with an HTML representation of:
+
+ ```html
+ <div id="ember1" class="ember-view"></div>
+ ```
+
+ When the `isEnabled` property on the view is set to `false`, it will result
+ in view instances with an HTML representation of:
+
+ ```html
+ <div id="ember1" class="ember-view disabled"></div>
+ ```
+
+ Updates to the the value of a class name binding will result in automatic
+ update of the HTML `class` attribute in the view's rendered HTML
+ representation. If the value becomes `false` or `undefined` the class name
+ will be removed.
+
+ Both `classNames` and `classNameBindings` are concatenated properties. See
+ `Ember.Object` documentation for more information about concatenated
+ properties.
+
+ ## HTML Attributes
+
+ The HTML attribute section of a view's tag can be set by providing an
+ `attributeBindings` property set to an array of property names on the view.
+ The return value of these properties will be used as the value of the view's
+ HTML associated attribute:
+
+ ```javascript
+ AnchorView = Ember.View.extend({
+ tagName: 'a',
+ attributeBindings: ['href'],
+ href: 'http://google.com'
+ });
+ ```
+
+ Will result in view instances with an HTML representation of:
+
+ ```html
+ <a id="ember1" class="ember-view" href="http://google.com"></a>
+ ```
+
+ If the return value of an `attributeBindings` monitored property is a boolean
+ the property will follow HTML's pattern of repeating the attribute's name as
+ its value:
+
+ ```javascript
+ MyTextInput = Ember.View.extend({
+ tagName: 'input',
+ attributeBindings: ['disabled'],
+ disabled: true
+ });
+ ```
+
+ Will result in view instances with an HTML representation of:
+
+ ```html
+ <input id="ember1" class="ember-view" disabled="disabled" />
+ ```
+
+ `attributeBindings` can refer to computed properties:
+
+ ```javascript
+ MyTextInput = Ember.View.extend({
+ tagName: 'input',
+ attributeBindings: ['disabled'],
+ disabled: function(){
+ if (someLogic) {
+ return true;
+ } else {
+ return false;
+ }
+ }.property()
+ });
+ ```
+
+ Updates to the the property of an attribute binding will result in automatic
+ update of the HTML attribute in the view's rendered HTML representation.
+
+ `attributeBindings` is a concatenated property. See `Ember.Object`
+ documentation for more information about concatenated properties.
+
+ ## Templates
+
+ The HTML contents of a view's rendered representation are determined by its
+ template. Templates can be any function that accepts an optional context
+ parameter and returns a string of HTML that will be inserted within the
+ view's tag. Most typically in Ember this function will be a compiled
+ `Ember.Handlebars` template.
+
+ ```javascript
+ AView = Ember.View.extend({
+ template: Ember.Handlebars.compile('I am the template')
+ });
+ ```
+
+ Will result in view instances with an HTML representation of:
+
+ ```html
+ <div id="ember1" class="ember-view">I am the template</div>
+ ```
+
+ Within an Ember application is more common to define a Handlebars templates as
+ part of a page:
+
+ ```html
+ <script type='text/x-handlebars' data-template-name='some-template'>
+ Hello
+ </script>
+ ```
+
+ And associate it by name using a view's `templateName` property:
+
+ ```javascript
+ AView = Ember.View.extend({
+ templateName: 'some-template'
+ });
+ ```
+
+ Using a value for `templateName` that does not have a Handlebars template
+ with a matching `data-template-name` attribute will throw an error.
+
+ Assigning a value to both `template` and `templateName` properties will throw
+ an error.
+
+ For views classes that may have a template later defined (e.g. as the block
+ portion of a `{{view}}` Handlebars helper call in another template or in
+ a subclass), you can provide a `defaultTemplate` property set to compiled
+ template function. If a template is not later provided for the view instance
+ the `defaultTemplate` value will be used:
+
+ ```javascript
+ AView = Ember.View.extend({
+ defaultTemplate: Ember.Handlebars.compile('I was the default'),
+ template: null,
+ templateName: null
+ });
+ ```
+
+ Will result in instances with an HTML representation of:
+
+ ```html
+ <div id="ember1" class="ember-view">I was the default</div>
+ ```
+
+ If a `template` or `templateName` is provided it will take precedence over
+ `defaultTemplate`:
+
+ ```javascript
+ AView = Ember.View.extend({
+ defaultTemplate: Ember.Handlebars.compile('I was the default')
+ });
+
+ aView = AView.create({
+ template: Ember.Handlebars.compile('I was the template, not default')
+ });
+ ```
+
+ Will result in the following HTML representation when rendered:
+
+ ```html
+ <div id="ember1" class="ember-view">I was the template, not default</div>
+ ```
+
+ ## View Context
+
+ The default context of the compiled template is the view's controller:
+
+ ```javascript
+ AView = Ember.View.extend({
+ template: Ember.Handlebars.compile('Hello {{excitedGreeting}}')
+ });
+
+ aController = Ember.Object.create({
+ firstName: 'Barry',
+ excitedGreeting: function(){
+ return this.get("content.firstName") + "!!!"
+ }.property()
+ });
+
+ aView = AView.create({
+ controller: aController,
+ });
+ ```
+
+ Will result in an HTML representation of:
+
+ ```html
+ <div id="ember1" class="ember-view">Hello Barry!!!</div>
+ ```
+
+ A context can also be explicitly supplied through the view's `context`
+ property. If the view has neither `context` nor `controller` properties, the
+ `parentView`'s context will be used.
+
+ ## Layouts
+
+ Views can have a secondary template that wraps their main template. Like
+ primary templates, layouts can be any function that accepts an optional
+ context parameter and returns a string of HTML that will be inserted inside
+ view's tag. Views whose HTML element is self closing (e.g. `<input />`)
+ cannot have a layout and this property will be ignored.
+
+ Most typically in Ember a layout will be a compiled `Ember.Handlebars`
+ template.
+
+ A view's layout can be set directly with the `layout` property or reference
+ an existing Handlebars template by name with the `layoutName` property.
+
+ A template used as a layout must contain a single use of the Handlebars
+ `{{yield}}` helper. The HTML contents of a view's rendered `template` will be
+ inserted at this location:
+
+ ```javascript
+ AViewWithLayout = Ember.View.extend({
+ layout: Ember.Handlebars.compile("<div class='my-decorative-class'>{{yield}}</div>")
+ template: Ember.Handlebars.compile("I got wrapped"),
+ });
+ ```
+
+ Will result in view instances with an HTML representation of:
+
+ ```html
+ <div id="ember1" class="ember-view">
+ <div class="my-decorative-class">
+ I got wrapped
+ </div>
+ </div>
+ ```
+
+ See `Handlebars.helpers.yield` for more information.
+
+ ## Responding to Browser Events
+
+ Views can respond to user-initiated events in one of three ways: method
+ implementation, through an event manager, and through `{{action}}` helper use
+ in their template or layout.
+
+ ### Method Implementation
+
+ Views can respond to user-initiated events by implementing a method that
+ matches the event name. A `jQuery.Event` object will be passed as the
+ argument to this method.
+
+ ```javascript
+ AView = Ember.View.extend({
+ click: function(event){
+ // will be called when when an instance's
+ // rendered element is clicked
+ }
+ });
+ ```
+
+ ### Event Managers
+
+ Views can define an object as their `eventManager` property. This object can
+ then implement methods that match the desired event names. Matching events
+ that occur on the view's rendered HTML or the rendered HTML of any of its DOM
+ descendants will trigger this method. A `jQuery.Event` object will be passed
+ as the first argument to the method and an `Ember.View` object as the
+ second. The `Ember.View` will be the view whose rendered HTML was interacted
+ with. This may be the view with the `eventManager` property or one of its
+ descendent views.
+
+ ```javascript
+ AView = Ember.View.extend({
+ eventManager: Ember.Object.create({
+ doubleClick: function(event, view){
+ // will be called when when an instance's
+ // rendered element or any rendering
+ // of this views's descendent
+ // elements is clicked
+ }
+ })
+ });
+ ```
+
+ An event defined for an event manager takes precedence over events of the
+ same name handled through methods on the view.
+
+ ```javascript
+ AView = Ember.View.extend({
+ mouseEnter: function(event){
+ // will never trigger.
+ },
+ eventManager: Ember.Object.create({
+ mouseEnter: function(event, view){
+ // takes presedence over AView#mouseEnter
+ }
+ })
+ });
+ ```
+
+ Similarly a view's event manager will take precedence for events of any views
+ rendered as a descendent. A method name that matches an event name will not
+ be called if the view instance was rendered inside the HTML representation of
+ a view that has an `eventManager` property defined that handles events of the
+ name. Events not handled by the event manager will still trigger method calls
+ on the descendent.
+
+ ```javascript
+ OuterView = Ember.View.extend({
+ template: Ember.Handlebars.compile("outer {{#view InnerView}}inner{{/view}} outer"),
+ eventManager: Ember.Object.create({
+ mouseEnter: function(event, view){
+ // view might be instance of either
+ // OutsideView or InnerView depending on
+ // where on the page the user interaction occured
+ }
+ })
+ });
+
+ InnerView = Ember.View.extend({
+ click: function(event){
+ // will be called if rendered inside
+ // an OuterView because OuterView's
+ // eventManager doesn't handle click events
+ },
+ mouseEnter: function(event){
+ // will never be called if rendered inside
+ // an OuterView.
+ }
+ });
+ ```
+
+ ### Handlebars `{{action}}` Helper
+
+ See `Handlebars.helpers.action`.
+
+ ### Event Names
+
+ Possible events names for any of the responding approaches described above
+ are:
+
+ Touch events:
+
+ * `touchStart`
+ * `touchMove`
+ * `touchEnd`
+ * `touchCancel`
+
+ Keyboard events
+
+ * `keyDown`
+ * `keyUp`
+ * `keyPress`
+
+ Mouse events
+
+ * `mouseDown`
+ * `mouseUp`
+ * `contextMenu`
+ * `click`
+ * `doubleClick`
+ * `mouseMove`
+ * `focusIn`
+ * `focusOut`
+ * `mouseEnter`
+ * `mouseLeave`
+
+ Form events:
+
+ * `submit`
+ * `change`
+ * `focusIn`
+ * `focusOut`
+ * `input`
+
+ HTML5 drag and drop events:
+
+ * `dragStart`
+ * `drag`
+ * `dragEnter`
+ * `dragLeave`
+ * `drop`
+ * `dragEnd`
+
+ ## Handlebars `{{view}}` Helper
+
+ Other `Ember.View` instances can be included as part of a view's template by
+ using the `{{view}}` Handlebars helper. See `Handlebars.helpers.view` for
+ additional information.
+
+ @class View
+ @namespace Ember
+ @extends Ember.Object
+ @uses Ember.Evented
+*/
+Ember.View = Ember.CoreView.extend(
+/** @scope Ember.View.prototype */ {
+
+ concatenatedProperties: ['classNames', 'classNameBindings', 'attributeBindings'],
+
+ /**
+ @property isView
+ @type Boolean
+ @default true
+ @final
+ */
+ isView: true,
+
+ // ..........................................................
+ // TEMPLATE SUPPORT
+ //
+
+ /**
+ The name of the template to lookup if no template is provided.
+
+ `Ember.View` will look for a template with this name in this view's
+ `templates` object. By default, this will be a global object
+ shared in `Ember.TEMPLATES`.
+
+ @property templateName
+ @type String
+ @default null
+ */
+ templateName: null,
+
+ /**
+ The name of the layout to lookup if no layout is provided.
+
+ `Ember.View` will look for a template with this name in this view's
+ `templates` object. By default, this will be a global object
+ shared in `Ember.TEMPLATES`.
+
+ @property layoutName
+ @type String
+ @default null
+ */
+ layoutName: null,
+
+ /**
+ The hash in which to look for `templateName`.
+
+ @property templates
+ @type Ember.Object
+ @default Ember.TEMPLATES
+ */
+ templates: Ember.TEMPLATES,
+
+ /**
+ The template used to render the view. This should be a function that
+ accepts an optional context parameter and returns a string of HTML that
+ will be inserted into the DOM relative to its parent view.
+
+ In general, you should set the `templateName` property instead of setting
+ the template yourself.
+
+ @property template
+ @type Function
+ */
+ template: Ember.computed(function(key, value) {
+ if (value !== undefined) { return value; }
+
+ var templateName = get(this, 'templateName'),
+ template = this.templateForName(templateName, 'template');
+
+ Ember.assert("You specified the templateName " + templateName + " for " + this + ", but it did not exist.", !templateName || template);
+
+ return template || get(this, 'defaultTemplate');
+ }).property('templateName'),
+
+ 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.
+
+ @property controller
+ @type Object
+ */
+ controller: Ember.computed(function(key) {
+ var parentView = get(this, '_parentView');
+ return parentView ? get(parentView, 'controller') : null;
+ }).property('_parentView'),
+
+ /**
+ A view may contain a layout. A layout is a regular template but
+ supersedes the `template` property during rendering. It is the
+ responsibility of the layout template to retrieve the `template`
+ property from the view (or alternatively, call `Handlebars.helpers.yield`,
+ `{{yield}}`) to render it in the correct location.
+
+ This is useful for a view that has a shared wrapper, but which delegates
+ the rendering of the contents of the wrapper to the `template` property
+ on a subclass.
+
+ @property layout
+ @type Function
+ */
+ layout: Ember.computed(function(key) {
+ var layoutName = get(this, 'layoutName'),
+ layout = this.templateForName(layoutName, 'layout');
+
+ Ember.assert("You specified the layoutName " + layoutName + " for " + this + ", but it did not exist.", !layoutName || layout);
+
+ return layout || get(this, 'defaultLayout');
+ }).property('layoutName'),
+
+ 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 object from which templates should access properties.
+
+ This object will be passed to the template function each time the render
+ method is called, but it is up to the individual function to decide what
+ to do with it.
+
+ By default, this will be the view's controller.
+
+ @property context
+ @type Object
+ */
+ context: Ember.computed(function(key, value) {
+ if (arguments.length === 2) {
+ set(this, '_context', value);
+ return value;
+ } else {
+ return get(this, '_context');
+ }
+ }).volatile(),
+
+ /**
+ @private
+
+ Private copy of the view's template context. This can be set directly
+ by Handlebars without triggering the observer that causes the view
+ to be re-rendered.
+
+ The context of a view is looked up as follows:
+
+ 1. Supplied context (usually by Handlebars)
+ 2. Specified controller
+ 3. `parentView`'s context (for a child of a ContainerView)
+
+ The code in Handlebars that overrides the `_context` property first
+ checks to see whether the view has a specified controller. This is
+ something of a hack and should be revisited.
+
+ @property _context
+ */
+ _context: Ember.computed(function(key) {
+ var parentView, controller;
+
+ if (controller = get(this, 'controller')) {
+ return controller;
+ }
+
+ parentView = this._parentView;
+ if (parentView) {
+ return get(parentView, '_context');
+ }
+
+ return null;
+ }),
+
+ /**
+ @private
+
+ If a value that affects template rendering changes, the view should be
+ re-rendered to reflect the new value.
+
+ @method _displayPropertyDidChange
+ */
+ _contextDidChange: Ember.observer(function() {
+ this.rerender();
+ }, 'context'),
+
+ /**
+ If `false`, the view will appear hidden in DOM.
+
+ @property isVisible
+ @type Boolean
+ @default null
+ */
+ isVisible: true,
+
+ /**
+ @private
+
+ Array of child views. You should never edit this array directly.
+ Instead, use `appendChild` and `removeFromParent`.
+
+ @property childViews
+ @type Array
+ @default []
+ */
+ childViews: childViewsProperty,
+
+ _childViews: [],
+
+ // When it's a virtual view, we need to notify the parent that their
+ // childViews will change.
+ _childViewsWillChange: Ember.beforeObserver(function() {
+ if (this.isVirtual) {
+ var parentView = get(this, 'parentView');
+ if (parentView) { Ember.propertyWillChange(parentView, 'childViews'); }
+ }
+ }, 'childViews'),
+
+ // When it's a virtual view, we need to notify the parent that their
+ // childViews did change.
+ _childViewsDidChange: Ember.observer(function() {
+ if (this.isVirtual) {
+ var parentView = get(this, 'parentView');
+ if (parentView) { Ember.propertyDidChange(parentView, 'childViews'); }
+ }
+ }, 'childViews'),
+
+ /**
+ Return the nearest ancestor that is an instance of the provided
+ class.
+
+ @property nearestInstanceOf
+ @param {Class} klass Subclass of Ember.View (or Ember.View itself)
+ @return Ember.View
+ @deprecated
+ */
+ nearestInstanceOf: function(klass) {
+ Ember.deprecate("nearestInstanceOf is deprecated and will be removed from future releases. Use nearestOfType.");
+ var view = get(this, 'parentView');
+
+ while (view) {
+ if(view instanceof klass) { return view; }
+ view = get(view, 'parentView');
+ }
+ },
+
+ /**
+ Return the nearest ancestor that is an instance of the provided
+ class or mixin.
+
+ @property nearestOfType
+ @param {Class,Mixin} klass Subclass of Ember.View (or Ember.View itself),
+ or an instance of Ember.Mixin.
+ @return Ember.View
+ */
+ nearestOfType: function(klass) {
+ var view = get(this, 'parentView'),
+ isOfType = klass instanceof Ember.Mixin ?
+ function(view) { return klass.detect(view); } :
+ function(view) { return klass.detect(view.constructor); };
+
+ while (view) {
+ if( isOfType(view) ) { return view; }
+ view = get(view, 'parentView');
+ }
+ },
+
+ /**
+ Return the nearest ancestor that has a given property.
+
+ @property nearestWithProperty
+ @param {String} property A property name
+ @return Ember.View
+ */
+ nearestWithProperty: function(property) {
+ var view = get(this, 'parentView');
+
+ while (view) {
+ if (property in view) { return view; }
+ view = get(view, 'parentView');
+ }
+ },
+
+ /**
+ Return the nearest ancestor whose parent is an instance of
+ `klass`.
+
+ @property nearestChildOf
+ @param {Class} klass Subclass of Ember.View (or Ember.View itself)
+ @return Ember.View
+ */
+ nearestChildOf: function(klass) {
+ var view = get(this, 'parentView');
+
+ while (view) {
+ if(get(view, 'parentView') instanceof klass) { return view; }
+ view = get(view, 'parentView');
+ }
+ },
+
+ /**
+ @private
+
+ When the parent view changes, recursively invalidate `controller`
+
+ @method _parentViewDidChange
+ */
+ _parentViewDidChange: Ember.observer(function() {
+ if (this.isDestroying) { return; }
+
+ if (get(this, 'parentView.controller') && !get(this, 'controller')) {
+ this.notifyPropertyChange('controller');
+ }
+ }, '_parentView'),
+
+ _controllerDidChange: Ember.observer(function() {
+ if (this.isDestroying) { return; }
+
+ this.rerender();
+
+ this.forEachChildView(function(view) {
+ view.propertyDidChange('controller');
+ });
+ }, 'controller'),
+
+ cloneKeywords: function() {
+ var templateData = get(this, 'templateData');
+
+ var keywords = templateData ? Ember.copy(templateData.keywords) : {};
+ set(keywords, 'view', get(this, 'concreteView'));
+ set(keywords, '_view', this);
+ set(keywords, 'controller', get(this, 'controller'));
+
+ return keywords;
+ },
+
+ /**
+ Called on your view when it should push strings of HTML into a
+ `Ember.RenderBuffer`. Most users will want to override the `template`
+ or `templateName` properties instead of this method.
+
+ By default, `Ember.View` will look for a function in the `template`
+ property and invoke it with the value of `context`. The value of
+ `context` will be the view's controller unless you override it.
+
+ @method render
+ @param {Ember.RenderBuffer} buffer The render buffer
+ */
+ render: function(buffer) {
+ // If this view has a layout, it is the responsibility of the
+ // the layout to render the view's template. Otherwise, render the template
+ // directly.
+ var template = get(this, 'layout') || get(this, 'template');
+
+ if (template) {
+ var context = get(this, 'context');
+ var keywords = this.cloneKeywords();
+ var output;
+
+ var data = {
+ view: this,
+ buffer: buffer,
+ isRenderData: true,
+ keywords: keywords,
+ insideGroup: get(this, 'templateData.insideGroup')
+ };
+
+ // Invoke the template with the provided template context, which
+ // is the view's controller by default. A hash of data is also passed that provides
+ // the template with access to the view and render buffer.
+
+ Ember.assert('template must be a function. Did you mean to call Ember.Handlebars.compile("...") or specify templateName instead?', typeof template === 'function');
+ // The template should write directly to the render buffer instead
+ // of returning a string.
+ output = template(context, { data: data });
+
+ // If the template returned a string instead of writing to the buffer,
+ // push the string onto the buffer.
+ if (output !== undefined) { buffer.push(output); }
+ }
+ },
+
+ /**
+ Renders the view again. This will work regardless of whether the
+ view is already in the DOM or not. If the view is in the DOM, the
+ rendering process will be deferred to give bindings a chance
+ to synchronize.
+
+ If children were added during the rendering process using `appendChild`,
+ `rerender` will remove them, because they will be added again
+ if needed by the next `render`.
+
+ In general, if the display of your view changes, you should modify
+ the DOM element directly instead of manually calling `rerender`, which can
+ be slow.
+
+ @method rerender
+ */
+ rerender: function() {
+ return this.currentState.rerender(this);
+ },
+
+ clearRenderedChildren: function() {
+ var lengthBefore = this.lengthBeforeRender,
+ lengthAfter = this.lengthAfterRender;
+
+ // If there were child views created during the last call to render(),
+ // remove them under the assumption that they will be re-created when
+ // we re-render.
+
+ // VIEW-TODO: Unit test this path.
+ var childViews = this._childViews;
+ for (var i=lengthAfter-1; i>=lengthBefore; i--) {
+ if (childViews[i]) { childViews[i].destroy(); }
+ }
+ },
+
+ /**
+ @private
+
+ Iterates over the view's `classNameBindings` array, inserts the value
+ of the specified property into the `classNames` array, then creates an
+ observer to update the view's element if the bound property ever changes
+ in the future.
+
+ @method _applyClassNameBindings
+ */
+ _applyClassNameBindings: function(classBindings) {
+ var classNames = this.classNames,
+ elem, newClass, dasherizedClass;
+
+ // Loop through all of the configured bindings. These will be either
+ // property names ('isUrgent') or property paths relative to the view
+ // ('content.isUrgent')
+ a_forEach(classBindings, function(binding) {
+
+ // Variable in which the old class value is saved. The observer function
+ // closes over this variable, so it knows which string to remove when
+ // the property changes.
+ var oldClass;
+ // Extract just the property name from bindings like 'foo:bar'
+ var parsedPath = Ember.View._parsePropertyPath(binding);
+
+ // Set up an observer on the context. If the property changes, toggle the
+ // class name.
+ var observer = function() {
+ // Get the current value of the property
+ newClass = this._classStringForProperty(binding);
+ elem = this.$();
+
+ // If we had previously added a class to the element, remove it.
+ if (oldClass) {
+ elem.removeClass(oldClass);
+ // Also remove from classNames so that if the view gets rerendered,
+ // the class doesn't get added back to the DOM.
+ classNames.removeObject(oldClass);
+ }
+
+ // If necessary, add a new class. Make sure we keep track of it so
+ // it can be removed in the future.
+ if (newClass) {
+ elem.addClass(newClass);
+ oldClass = newClass;
+ } else {
+ oldClass = null;
+ }
+ };
+
+ // Get the class name for the property at its current value
+ dasherizedClass = this._classStringForProperty(binding);
+
+ if (dasherizedClass) {
+ // Ensure that it gets into the classNames array
+ // so it is displayed when we render.
+ a_addObject(classNames, dasherizedClass);
+
+ // Save a reference to the class name so we can remove it
+ // if the observer fires. Remember that this variable has
+ // been closed over by the observer.
+ oldClass = dasherizedClass;
+ }
+
+ this.registerObserver(this, parsedPath.path, observer);
+ // Remove className so when the view is rerendered,
+ // the className is added based on binding reevaluation
+ this.one('willClearRender', function() {
+ if (oldClass) {
+ classNames.removeObject(oldClass);
+ oldClass = null;
+ }
+ });
+
+ }, this);
+ },
+
+ /**
+ @private
+
+ Iterates through the view's attribute bindings, sets up observers for each,
+ then applies the current value of the attributes to the passed render buffer.
+
+ @method _applyAttributeBindings
+ @param {Ember.RenderBuffer} buffer
+ */
+ _applyAttributeBindings: function(buffer, attributeBindings) {
+ var attributeValue, elem, type;
+
+ a_forEach(attributeBindings, function(binding) {
+ var split = binding.split(':'),
+ property = split[0],
+ attributeName = split[1] || property;
+
+ // Create an observer to add/remove/change the attribute if the
+ // JavaScript property changes.
+ var observer = function() {
+ elem = this.$();
+ if (!elem) { return; }
+
+ attributeValue = get(this, property);
+
+ Ember.View.applyAttributeBindings(elem, attributeName, attributeValue);
+ };
+
+ this.registerObserver(this, property, observer);
+
+ // Determine the current value and add it to the render buffer
+ // if necessary.
+ attributeValue = get(this, property);
+ Ember.View.applyAttributeBindings(buffer, attributeName, attributeValue);
+ }, this);
+ },
+
+ /**
+ @private
+
+ Given a property name, returns a dasherized version of that
+ property name if the property evaluates to a non-falsy value.
+
+ For example, if the view has property `isUrgent` that evaluates to true,
+ passing `isUrgent` to this method will return `"is-urgent"`.
+
+ @method _classStringForProperty
+ @param property
+ */
+ _classStringForProperty: function(property) {
+ var parsedPath = Ember.View._parsePropertyPath(property);
+ var path = parsedPath.path;
+
+ var val = get(this, path);
+ if (val === undefined && Ember.isGlobalPath(path)) {
+ val = get(Ember.lookup, path);
+ }
+
+ return Ember.View._classStringForValue(path, val, parsedPath.className, parsedPath.falsyClassName);
+ },
+
+ // ..........................................................
+ // ELEMENT SUPPORT
+ //
+
+ /**
+ Returns the current DOM element for the view.
+
+ @property element
+ @type DOMElement
+ */
+ element: Ember.computed(function(key, value) {
+ if (value !== undefined) {
+ return this.currentState.setElement(this, value);
+ } else {
+ return this.currentState.getElement(this);
+ }
+ }).property('_parentView'),
+
+ /**
+ Returns a jQuery object for this view's element. If you pass in a selector
+ string, this method will return a jQuery object, using the current element
+ as its buffer.
+
+ For example, calling `view.$('li')` will return a jQuery object containing
+ all of the `li` elements inside the DOM element of this view.
+
+ @property $
+ @param {String} [selector] a jQuery-compatible selector string
+ @return {jQuery} the CoreQuery object for the DOM node
+ */
+ $: function(sel) {
+ return this.currentState.$(this, sel);
+ },
+
+ mutateChildViews: function(callback) {
+ var childViews = this._childViews,
+ idx = childViews.length,
+ view;
+
+ while(--idx >= 0) {
+ view = childViews[idx];
+ callback.call(this, view, idx);
+ }
+
+ return this;
+ },
+
+ forEachChildView: function(callback) {
+ var childViews = this._childViews;
+
+ if (!childViews) { return this; }
+
+ var len = childViews.length,
+ view, idx;
+
+ for(idx = 0; idx < len; idx++) {
+ view = childViews[idx];
+ callback.call(this, view);
+ }
+
+ return this;
+ },
+
+ /**
+ Appends the view's element to the specified parent element.
+
+ If the view does not have an HTML representation yet, `createElement()`
+ will be called automatically.
+
+ Note that this method just schedules the view to be appended; the DOM
+ element will not be appended to the given element until all bindings have
+ finished synchronizing.
+
+ This is not typically a function that you will need to call directly when
+ building your application. You might consider using `Ember.ContainerView`
+ instead. If you do need to use `appendTo`, be sure that the target element
+ you are providing is associated with an `Ember.Application` and does not
+ have an ancestor element that is associated with an Ember view.
+
+ @method appendTo
+ @param {String|DOMElement|jQuery} A selector, element, HTML string, or jQuery object
+ @return {Ember.View} receiver
+ */
+ appendTo: function(target) {
+ // Schedule the DOM element to be created and appended to the given
+ // element after bindings have synchronized.
+ this._insertElementLater(function() {
+ Ember.assert("You cannot append to an existing Ember.View. Consider using Ember.ContainerView instead.", !Ember.$(target).is('.ember-view') && !Ember.$(target).parents().is('.ember-view'));
+ this.$().appendTo(target);
+ });
+
+ return this;
+ },
+
+ /**
+ Replaces the content of the specified parent element with this view's
+ element. If the view does not have an HTML representation yet,
+ `createElement()` will be called automatically.
+
+ Note that this method just schedules the view to be appended; the DOM
+ element will not be appended to the given element until all bindings have
+ finished synchronizing
+
+ @method replaceIn
+ @param {String|DOMElement|jQuery} A selector, element, HTML string, or jQuery object
+ @return {Ember.View} received
+ */
+ replaceIn: function(target) {
+ Ember.assert("You cannot replace an existing Ember.View. Consider using Ember.ContainerView instead.", !Ember.$(target).is('.ember-view') && !Ember.$(target).parents().is('.ember-view'));
+
+ this._insertElementLater(function() {
+ Ember.$(target).empty();
+ this.$().appendTo(target);
+ });
+
+ return this;
+ },
+
+ /**
+ @private
+
+ Schedules a DOM operation to occur during the next render phase. This
+ ensures that all bindings have finished synchronizing before the view is
+ rendered.
+
+ To use, pass a function that performs a DOM operation.
+
+ Before your function is called, this view and all child views will receive
+ the `willInsertElement` event. After your function is invoked, this view
+ and all of its child views will receive the `didInsertElement` event.
+
+ ```javascript
+ view._insertElementLater(function() {
+ this.createElement();
+ this.$().appendTo('body');
+ });
+ ```
+
+ @method _insertElementLater
+ @param {Function} fn the function that inserts the element into the DOM
+ */
+ _insertElementLater: function(fn) {
+ this._scheduledInsert = Ember.run.scheduleOnce('render', this, '_insertElement', fn);
+ },
+
+ _insertElement: function (fn) {
+ this._scheduledInsert = null;
+ this.currentState.insertElement(this, fn);
+ },
+
+ /**
+ Appends the view's element to the document body. If the view does
+ not have an HTML representation yet, `createElement()` will be called
+ automatically.
+
+ Note that this method just schedules the view to be appended; the DOM
+ element will not be appended to the document body until all bindings have
+ finished synchronizing.
+
+ @method append
+ @return {Ember.View} receiver
+ */
+ append: function() {
+ return this.appendTo(document.body);
+ },
+
+ /**
+ Removes the view's element from the element to which it is attached.
+
+ @method remove
+ @return {Ember.View} receiver
+ */
+ remove: function() {
+ // What we should really do here is wait until the end of the run loop
+ // to determine if the element has been re-appended to a different
+ // element.
+ // In the interim, we will just re-render if that happens. It is more
+ // important than elements get garbage collected.
+ if (!this.removedFromDOM) { this.destroyElement(); }
+ this.invokeRecursively(function(view) {
+ if (view.clearRenderedChildren) { view.clearRenderedChildren(); }
+ });
+ },
+
+ elementId: null,
+
+ /**
+ Attempts to discover the element in the parent element. The default
+ implementation looks for an element with an ID of `elementId` (or the
+ view's guid if `elementId` is null). You can override this method to
+ provide your own form of lookup. For example, if you want to discover your
+ element using a CSS class name instead of an ID.
+
+ @method findElementInParentElement
+ @param {DOMElement} parentElement The parent's DOM element
+ @return {DOMElement} The discovered element
+ */
+ findElementInParentElement: function(parentElem) {
+ var id = "#" + this.elementId;
+ return Ember.$(id)[0] || Ember.$(id, parentElem)[0];
+ },
+
+ /**
+ Creates a DOM representation of the view and all of its
+ child views by recursively calling the `render()` method.
+
+ After the element has been created, `didInsertElement` will
+ be called on this view and all of its child views.
+
+ @method createElement
+ @return {Ember.View} receiver
+ */
+ createElement: function() {
+ if (get(this, 'element')) { return this; }
+
+ var buffer = this.renderToBuffer();
+ set(this, 'element', buffer.element());
+
+ return this;
+ },
+
+ /**
+ Called when a view is going to insert an element into the DOM.
+
+ @event willInsertElement
+ */
+ willInsertElement: Ember.K,
+
+ /**
+ Called when the element of the view has been inserted into the DOM.
+ Override this function to do any set up that requires an element in the
+ document body.
+
+ @event didInsertElement
+ */
+ didInsertElement: Ember.K,
+
+ /**
+ Called when the view is about to rerender, but before anything has
+ been torn down. This is a good opportunity to tear down any manual
+ observers you have installed based on the DOM state
+
+ @event willClearRender
+ */
+ willClearRender: Ember.K,
+
+ /**
+ @private
+
+ Run this callback on the current view and recursively on child views.
+
+ @method invokeRecursively
+ @param fn {Function}
+ */
+ invokeRecursively: function(fn) {
+ var childViews = [this], currentViews, view;
+
+ while (childViews.length) {
+ currentViews = childViews.slice();
+ childViews = [];
+
+ for (var i=0, l=currentViews.length; i<l; i++) {
+ view = currentViews[i];
+ fn.call(view, view);
+ if (view._childViews) {
+ childViews.push.apply(childViews, view._childViews);
+ }
+ }
+ }
+ },
+
+ triggerRecursively: function(eventName) {
+ var childViews = [this], currentViews, view;
+
+ while (childViews.length) {
+ currentViews = childViews.slice();
+ childViews = [];
+
+ for (var i=0, l=currentViews.length; i<l; i++) {
+ view = currentViews[i];
+ if (view.trigger) { view.trigger(eventName); }
+ if (view._childViews) {
+ childViews.push.apply(childViews, view._childViews);
+ }
+ }
+ }
+ },
+
+ /**
+ Destroys any existing element along with the element for any child views
+ as well. If the view does not currently have a element, then this method
+ will do nothing.
+
+ If you implement `willDestroyElement()` on your view, then this method will
+ be invoked on your view before your element is destroyed to give you a
+ chance to clean up any event handlers, etc.
+
+ If you write a `willDestroyElement()` handler, you can assume that your
+ `didInsertElement()` handler was called earlier for the same element.
+
+ Normally you will not call or override this method yourself, but you may
+ want to implement the above callbacks when it is run.
+
+ @method destroyElement
+ @return {Ember.View} receiver
+ */
+ destroyElement: function() {
+ return this.currentState.destroyElement(this);
+ },
+
+ /**
+ Called when the element of the view is going to be destroyed. Override
+ this function to do any teardown that requires an element, like removing
+ event listeners.
+
+ @event willDestroyElement
+ */
+ willDestroyElement: function() {},
+
+ /**
+ @private
+
+ Triggers the `willDestroyElement` event (which invokes the
+ `willDestroyElement()` method if it exists) on this view and all child
+ views.
+
+ Before triggering `willDestroyElement`, it first triggers the
+ `willClearRender` event recursively.
+
+ @method _notifyWillDestroyElement
+ */
+ _notifyWillDestroyElement: function() {
+ this.triggerRecursively('willClearRender');
+ this.triggerRecursively('willDestroyElement');
+ },
+
+ _elementWillChange: Ember.beforeObserver(function() {
+ this.forEachChildView(function(view) {
+ Ember.propertyWillChange(view, 'element');
+ });
+ }, 'element'),
+
+ /**
+ @private
+
+ If this view's element changes, we need to invalidate the caches of our
+ child views so that we do not retain references to DOM elements that are
+ no longer needed.
+
+ @method _elementDidChange
+ */
+ _elementDidChange: Ember.observer(function() {
+ this.forEachChildView(function(view) {
+ Ember.propertyDidChange(view, 'element');
+ });
+ }, 'element'),
+
+ /**
+ Called when the parentView property has changed.
+
+ @event parentViewDidChange
+ */
+ parentViewDidChange: Ember.K,
+
+ instrumentName: 'view',
+
+ instrumentDetails: function(hash) {
+ hash.template = get(this, 'templateName');
+ this._super(hash);
+ },
+
+ _renderToBuffer: function(parentBuffer, bufferOperation) {
+ this.lengthBeforeRender = this._childViews.length;
+ var buffer = this._super(parentBuffer, bufferOperation);
+ this.lengthAfterRender = this._childViews.length;
+
+ return buffer;
+ },
+
+ renderToBufferIfNeeded: function () {
+ return this.currentState.renderToBufferIfNeeded(this, this);
+ },
+
+ beforeRender: function(buffer) {
+ this.applyAttributesToBuffer(buffer);
+ buffer.pushOpeningTag();
+ },
+
+ afterRender: function(buffer) {
+ buffer.pushClosingTag();
+ },
+
+ applyAttributesToBuffer: function(buffer) {
+ // Creates observers for all registered class name and attribute bindings,
+ // then adds them to the element.
+ var classNameBindings = get(this, 'classNameBindings');
+ if (classNameBindings.length) {
+ this._applyClassNameBindings(classNameBindings);
+ }
+
+ // Pass the render buffer so the method can apply attributes directly.
+ // This isn't needed for class name bindings because they use the
+ // existing classNames infrastructure.
+ var attributeBindings = get(this, 'attributeBindings');
+ if (attributeBindings.length) {
+ this._applyAttributeBindings(buffer, attributeBindings);
+ }
+
+ buffer.setClasses(this.classNames);
+ buffer.id(this.elementId);
+
+ var role = get(this, 'ariaRole');
+ if (role) {
+ buffer.attr('role', role);
+ }
+
+ if (get(this, 'isVisible') === false) {
+ buffer.style('display', 'none');
+ }
+ },
+
+ // ..........................................................
+ // STANDARD RENDER PROPERTIES
+ //
+
+ /**
+ Tag name for the view's outer element. The tag name is only used when an
+ element is first created. If you change the `tagName` for an element, you
+ must destroy and recreate the view element.
+
+ By default, the render buffer will use a `<div>` tag for views.
+
+ @property tagName
+ @type String
+ @default null
+ */
+
+ // We leave this null by default so we can tell the difference between
+ // the default case and a user-specified tag.
+ tagName: null,
+
+ /**
+ The WAI-ARIA role of the control represented by this view. For example, a
+ button may have a role of type 'button', or a pane may have a role of
+ type 'alertdialog'. This property is used by assistive software to help
+ visually challenged users navigate rich web applications.
+
+ The full list of valid WAI-ARIA roles is available at:
+ http://www.w3.org/TR/wai-aria/roles#roles_categorization
+
+ @property ariaRole
+ @type String
+ @default null
+ */
+ ariaRole: null,
+
+ /**
+ Standard CSS class names to apply to the view's outer element. This
+ property automatically inherits any class names defined by the view's
+ superclasses as well.
+
+ @property classNames
+ @type Array
+ @default ['ember-view']
+ */
+ classNames: ['ember-view'],
+
+ /**
+ A list of properties of the view to apply as class names. If the property
+ is a string value, the value of that string will be applied as a class
+ name.
+
+ ```javascript
+ // Applies the 'high' class to the view element
+ Ember.View.create({
+ classNameBindings: ['priority']
+ priority: 'high'
+ });
+ ```
+
+ If the value of the property is a Boolean, the name of that property is
+ added as a dasherized class name.
+
+ ```javascript
+ // Applies the 'is-urgent' class to the view element
+ Ember.View.create({
+ classNameBindings: ['isUrgent']
+ isUrgent: true
+ });
+ ```
+
+ If you would prefer to use a custom value instead of the dasherized
+ property name, you can pass a binding like this:
+
+ ```javascript
+ // Applies the 'urgent' class to the view element
+ Ember.View.create({
+ classNameBindings: ['isUrgent:urgent']
+ isUrgent: true
+ });
+ ```
+
+ This list of properties is inherited from the view's superclasses as well.
+
+ @property classNameBindings
+ @type Array
+ @default []
+ */
+ classNameBindings: [],
+
+ /**
+ A list of properties of the view to apply as attributes. If the property is
+ a string value, the value of that string will be applied as the attribute.
+
+ ```javascript
+ // Applies the type attribute to the element
+ // with the value "button", like <div type="button">
+ Ember.View.create({
+ attributeBindings: ['type'],
+ type: 'button'
+ });
+ ```
+
+ If the value of the property is a Boolean, the name of that property is
+ added as an attribute.
+
+ ```javascript
+ // Renders something like <div enabled="enabled">
+ Ember.View.create({
+ attributeBindings: ['enabled'],
+ enabled: true
+ });
+ ```
+
+ @property attributeBindings
+ */
+ attributeBindings: [],
+
+ // .......................................................
+ // CORE DISPLAY METHODS
+ //
+
+ /**
+ @private
+
+ Setup a view, but do not finish waking it up.
+ - configure `childViews`
+ - register the view with the global views hash, which is used for event
+ dispatch
+
+ @method init
+ */
+ init: function() {
+ this.elementId = this.elementId || guidFor(this);
+
+ this._super();
+
+ // setup child views. be sure to clone the child views array first
+ this._childViews = this._childViews.slice();
+
+ Ember.assert("Only arrays are allowed for 'classNameBindings'", Ember.typeOf(this.classNameBindings) === 'array');
+ this.classNameBindings = Ember.A(this.classNameBindings.slice());
+
+ Ember.assert("Only arrays are allowed for 'classNames'", Ember.typeOf(this.classNames) === 'array');
+ this.classNames = Ember.A(this.classNames.slice());
+
+ var viewController = get(this, 'viewController');
+ if (viewController) {
+ viewController = get(viewController);
+ if (viewController) {
+ set(viewController, 'view', this);
+ }
+ }
+ },
+
+ appendChild: function(view, options) {
+ return this.currentState.appendChild(this, view, options);
+ },
+
+ /**
+ Removes the child view from the parent view.
+
+ @method removeChild
+ @param {Ember.View} view
+ @return {Ember.View} receiver
+ */
+ removeChild: function(view) {
+ // If we're destroying, the entire subtree will be
+ // freed, and the DOM will be handled separately,
+ // so no need to mess with childViews.
+ if (this.isDestroying) { return; }
+
+ // update parent node
+ set(view, '_parentView', null);
+
+ // remove view from childViews array.
+ var childViews = this._childViews;
+
+ Ember.EnumerableUtils.removeObject(childViews, view);
+
+ this.propertyDidChange('childViews'); // HUH?! what happened to will change?
+
+ return this;
+ },
+
+ /**
+ Removes all children from the `parentView`.
+
+ @method removeAllChildren
+ @return {Ember.View} receiver
+ */
+ removeAllChildren: function() {
+ return this.mutateChildViews(function(view) {
+ this.removeChild(view);
+ });
+ },
+
+ destroyAllChildren: function() {
+ return this.mutateChildViews(function(view) {
+ view.destroy();
+ });
+ },
+
+ /**
+ Removes the view from its `parentView`, if one is found. Otherwise
+ does nothing.
+
+ @method removeFromParent
+ @return {Ember.View} receiver
+ */
+ removeFromParent: function() {
+ var parent = this._parentView;
+
+ // Remove DOM element from parent
+ this.remove();
+
+ if (parent) { parent.removeChild(this); }
+ return this;
+ },
+
+ /**
+ You must call `destroy` on a view to destroy the view (and all of its
+ child views). This will remove the view from any parent node, then make
+ sure that the DOM element managed by the view can be released by the
+ memory manager.
+
+ @method willDestroy
+ */
+ willDestroy: function() {
+ // calling this._super() will nuke computed properties and observers,
+ // so collect any information we need before calling super.
+ var childViews = this._childViews,
+ parent = this._parentView,
+ childLen, i;
+
+ // destroy the element -- this will avoid each child view destroying
+ // the element over and over again...
+ if (!this.removedFromDOM) { this.destroyElement(); }
+
+ childLen = childViews.length;
+ for (i=childLen-1; i>=0; i--) {
+ childViews[i].removedFromDOM = true;
+ }
+
+ // remove from non-virtual parent view if viewName was specified
+ if (this.viewName) {
+ var nonVirtualParentView = get(this, 'parentView');
+ if (nonVirtualParentView) {
+ set(nonVirtualParentView, this.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')];
+ },
+
+ /**
+ Instantiates a view to be added to the childViews array during view
+ initialization. You generally will not call this method directly unless
+ you are overriding `createChildViews()`. Note that this method will
+ automatically configure the correct settings on the new view instance to
+ act as a child of the parent.
+
+ @method createChildView
+ @param {Class} viewClass
+ @param {Hash} [attrs] Attributes to add
+ @return {Ember.View} new instance
+ */
+ createChildView: function(view, attrs) {
+ if (view.isView && view._parentView === this) { return view; }
+
+ if (Ember.CoreView.detect(view)) {
+ attrs = attrs || {};
+ attrs._parentView = this;
+ attrs.templateData = attrs.templateData || get(this, 'templateData');
+
+ view = view.create(attrs);
+
+ // don't set the property on a virtual view, as they are invisible to
+ // consumers of the view API
+ if (view.viewName) { set(get(this, 'concreteView'), view.viewName, view); }
+ } else {
+ Ember.assert('You must pass instance or subclass of View', view.isView);
+
+ if (attrs) {
+ view.setProperties(attrs);
+ }
+
+ if (!get(view, 'templateData')) {
+ set(view, 'templateData', get(this, 'templateData'));
+ }
+
+ set(view, '_parentView', this);
+ }
+
+ return view;
+ },
+
+ becameVisible: Ember.K,
+ becameHidden: Ember.K,
+
+ /**
+ @private
+
+ When the view's `isVisible` property changes, toggle the visibility
+ element of the actual DOM element.
+
+ @method _isVisibleDidChange
+ */
+ _isVisibleDidChange: Ember.observer(function() {
+ var $el = this.$();
+ if (!$el) { return; }
+
+ var isVisible = get(this, 'isVisible');
+
+ $el.toggle(isVisible);
+
+ if (this._isAncestorHidden()) { return; }
+
+ if (isVisible) {
+ this._notifyBecameVisible();
+ } else {
+ this._notifyBecameHidden();
+ }
+ }, 'isVisible'),
+
+ _notifyBecameVisible: function() {
+ this.trigger('becameVisible');
+
+ this.forEachChildView(function(view) {
+ var isVisible = get(view, 'isVisible');
+
+ if (isVisible || isVisible === null) {
+ view._notifyBecameVisible();
+ }
+ });
+ },
+
+ _notifyBecameHidden: function() {
+ this.trigger('becameHidden');
+ this.forEachChildView(function(view) {
+ var isVisible = get(view, 'isVisible');
+
+ if (isVisible || isVisible === null) {
+ view._notifyBecameHidden();
+ }
+ });
+ },
+
+ _isAncestorHidden: function() {
+ var parent = get(this, 'parentView');
+
+ while (parent) {
+ if (get(parent, 'isVisible') === false) { return true; }
+
+ parent = get(parent, 'parentView');
+ }
+
+ return false;
+ },
+
+ clearBuffer: function() {
+ this.invokeRecursively(function(view) {
+ view.buffer = null;
+ });
+ },
+
+ transitionTo: function(state, children) {
+ this.currentState = this.states[state];
+ this.state = state;
+
+ if (children !== false) {
+ this.forEachChildView(function(view) {
+ view.transitionTo(state);
+ });
+ }
+ },
+
+ // .......................................................
+ // EVENT HANDLING
+ //
+
+ /**
+ @private
+
+ Handle events from `Ember.EventDispatcher`
+
+ @method handleEvent
+ @param eventName {String}
+ @param evt {Event}
+ */
+ handleEvent: function(eventName, evt) {
+ return this.currentState.handleEvent(this, eventName, evt);
+ },
+
+ registerObserver: function(root, path, target, observer) {
+ Ember.addObserver(root, path, target, observer);
+
+ this.one('willClearRender', function() {
+ Ember.removeObserver(root, path, target, observer);
+ });
+ }
+
+});
+
+/*
+ Describe how the specified actions should behave in the various
+ states that a view can exist in. Possible states:
+
+ * preRender: when a view is first instantiated, and after its
+ element was destroyed, it is in the preRender state
+ * inBuffer: once a view has been rendered, but before it has
+ been inserted into the DOM, it is in the inBuffer state
+ * inDOM: once a view has been inserted into the DOM it is in
+ the inDOM state. A view spends the vast majority of its
+ existence in this state.
+ * destroyed: once a view has been destroyed (using the destroy
+ method), it is in this state. No further actions can be invoked
+ on a destroyed view.
+*/
+
+ // in the destroyed state, everything is illegal
+
+ // before rendering has begun, all legal manipulations are noops.
+
+ // inside the buffer, legal manipulations are done on the buffer
+
+ // once the view has been inserted into the DOM, legal manipulations
+ // are done on the DOM element.
+
+var DOMManager = {
+ prepend: function(view, html) {
+ view.$().prepend(html);
+ },
+
+ after: function(view, html) {
+ view.$().after(html);
+ },
+
+ html: function(view, html) {
+ view.$().html(html);
+ },
+
+ replace: function(view) {
+ var element = get(view, 'element');
+
+ set(view, 'element', null);
+
+ view._insertElementLater(function() {
+ Ember.$(element).replaceWith(get(view, 'element'));
+ });
+ },
+
+ remove: function(view) {
+ view.$().remove();
+ },
+
+ empty: function(view) {
+ view.$().empty();
+ }
+};
+
+Ember.View.reopen({
+ domManager: DOMManager
+});
+
+Ember.View.reopenClass({
+
+ /**
+ @private
+
+ Parse a path and return an object which holds the parsed properties.
+
+ For example a path like "content.isEnabled:enabled:disabled" wil return the
+ following object:
+
+ ```javascript
+ {
+ path: "content.isEnabled",
+ className: "enabled",
+ falsyClassName: "disabled",
+ classNames: ":enabled:disabled"
+ }
+ ```
+
+ @method _parsePropertyPath
+ @static
+ */
+ _parsePropertyPath: function(path) {
+ var split = path.split(':'),
+ propertyPath = split[0],
+ classNames = "",
+ className,
+ falsyClassName;
+
+ // check if the property is defined as prop:class or prop:trueClass:falseClass
+ if (split.length > 1) {
+ className = split[1];
+ if (split.length === 3) { falsyClassName = split[2]; }
+
+ classNames = ':' + className;
+ if (falsyClassName) { classNames += ":" + falsyClassName; }
+ }
+
+ return {
+ path: propertyPath,
+ classNames: classNames,
+ className: (className === '') ? undefined : className,
+ falsyClassName: falsyClassName
+ };
+ },
+
+ /**
+ @private
+
+ Get the class name for a given value, based on the path, optional
+ `className` and optional `falsyClassName`.
+
+ - if a `className` or `falsyClassName` has been specified:
+ - if the value is truthy and `className` has been specified,
+ `className` is returned
+ - if the value is falsy and `falsyClassName` has been specified,
+ `falsyClassName` is returned
+ - otherwise `null` is returned
+ - if the value is `true`, the dasherized last part of the supplied path
+ is returned
+ - if the value is not `false`, `undefined` or `null`, the `value`
+ is returned
+ - if none of the above rules apply, `null` is returned
+
+ @method _classStringForValue
+ @param path
+ @param val
+ @param className
+ @param falsyClassName
+ @static
+ */
+ _classStringForValue: function(path, val, className, falsyClassName) {
+ // When using the colon syntax, evaluate the truthiness or falsiness
+ // of the value to determine which className to return
+ if (className || falsyClassName) {
+ if (className && !!val) {
+ return className;
+
+ } else if (falsyClassName && !val) {
+ return falsyClassName;
+
+ } else {
+ return null;
+ }
+
+ // If value is a Boolean and true, return the dasherized property
+ // name.
+ } else if (val === true) {
+ // Normalize property path to be suitable for use
+ // as a class name. For exaple, content.foo.barBaz
+ // becomes bar-baz.
+ var parts = path.split('.');
+ return Ember.String.dasherize(parts[parts.length-1]);
+
+ // If the value is not false, undefined, or null, return the current
+ // value of the property.
+ } else if (val !== false && val !== undefined && val !== null) {
+ return val;
+
+ // Nothing to display. Return null so that the old class is removed
+ // but no new class is added.
+ } else {
+ return null;
+ }
+ }
+});
+
+/**
+ Global views hash
+
+ @property views
+ @static
+ @type Hash
+*/
+Ember.View.views = {};
+
+// If someone overrides the child views computed property when
+// defining their class, we want to be able to process the user's
+// supplied childViews and then restore the original computed property
+// at view initialization time. This happens in Ember.ContainerView's init
+// method.
+Ember.View.childViewsProperty = childViewsProperty;
+
+Ember.View.applyAttributeBindings = function(elem, name, value) {
+ var type = Ember.typeOf(value);
+
+ // if this changes, also change the logic in ember-handlebars/lib/helpers/binding.js
+ if (name !== 'value' && (type === 'string' || (type === 'number' && !isNaN(value)))) {
+ if (value !== elem.attr(name)) {
+ elem.attr(name, value);
+ }
+ } else if (name === 'value' || type === 'boolean') {
+ if (value !== elem.prop(name)) {
+ // value and booleans should always be properties
+ elem.prop(name, value);
+ }
+ } else if (!value) {
+ elem.removeAttr(name);
+ }
+};
+
+Ember.View.states = states;
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-views
+*/
+
+var get = Ember.get, set = Ember.set;
+
+Ember.View.states._default = {
+ // appendChild is only legal while rendering the buffer.
+ appendChild: function() {
+ throw "You can't use appendChild outside of the rendering process";
+ },
+
+ $: function() {
+ return undefined;
+ },
+
+ getElement: function() {
+ return null;
+ },
+
+ // Handle events from `Ember.EventDispatcher`
+ handleEvent: function() {
+ return true; // continue event propagation
+ },
+
+ destroyElement: function(view) {
+ set(view, 'element', null);
+ if (view._scheduledInsert) {
+ Ember.run.cancel(view._scheduledInsert);
+ view._scheduledInsert = null;
+ }
+ return view;
+ },
+
+ renderToBufferIfNeeded: function () {
+ return false;
+ },
+
+ rerender: Ember.K
+};
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-views
+*/
+
+var preRender = Ember.View.states.preRender = Ember.create(Ember.View.states._default);
+
+Ember.merge(preRender, {
+ // a view leaves the preRender state once its element has been
+ // created (createElement).
+ insertElement: function(view, fn) {
+ view.createElement();
+ view.triggerRecursively('willInsertElement');
+ // after createElement, the view will be in the hasElement state.
+ fn.call(view);
+ view.transitionTo('inDOM');
+ view.triggerRecursively('didInsertElement');
+ },
+
+ renderToBufferIfNeeded: function(view) {
+ return view.renderToBuffer();
+ },
+
+ empty: Ember.K,
+
+ setElement: function(view, value) {
+ if (value !== null) {
+ view.transitionTo('hasElement');
+ }
+ return value;
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-views
+*/
+
+var get = Ember.get, set = Ember.set, meta = Ember.meta;
+
+var inBuffer = Ember.View.states.inBuffer = Ember.create(Ember.View.states._default);
+
+Ember.merge(inBuffer, {
+ $: function(view, sel) {
+ // if we don't have an element yet, someone calling this.$() is
+ // trying to update an element that isn't in the DOM. Instead,
+ // rerender the view to allow the render method to reflect the
+ // changes.
+ view.rerender();
+ return Ember.$();
+ },
+
+ // when a view is rendered in a buffer, rerendering it simply
+ // replaces the existing buffer with a new one
+ rerender: function(view) {
+ throw new Ember.Error("Something you did caused a view to re-render after it rendered but before it was inserted into the DOM.");
+ },
+
+ // when a view is rendered in a buffer, appending a child
+ // view will render that view and append the resulting
+ // buffer into its buffer.
+ appendChild: function(view, childView, options) {
+ var buffer = view.buffer;
+
+ childView = view.createChildView(childView, options);
+ view._childViews.push(childView);
+
+ childView.renderToBuffer(buffer);
+
+ view.propertyDidChange('childViews');
+
+ return childView;
+ },
+
+ // when a view is rendered in a buffer, destroying the
+ // element will simply destroy the buffer and put the
+ // state back into the preRender state.
+ destroyElement: function(view) {
+ view.clearBuffer();
+ view._notifyWillDestroyElement();
+ view.transitionTo('preRender');
+
+ return view;
+ },
+
+ empty: function() {
+ Ember.assert("Emptying a view in the inBuffer state is not allowed and should not happen under normal circumstances. Most likely there is a bug in your application. This may be due to excessive property change notifications.");
+ },
+
+ renderToBufferIfNeeded: function (view) {
+ return view.buffer;
+ },
+
+ // It should be impossible for a rendered view to be scheduled for
+ // insertion.
+ insertElement: function() {
+ throw "You can't insert an element that has already been rendered";
+ },
+
+ setElement: function(view, value) {
+ if (value === null) {
+ view.transitionTo('preRender');
+ } else {
+ view.clearBuffer();
+ view.transitionTo('hasElement');
+ }
+
+ return value;
+ }
+});
+
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-views
+*/
+
+var get = Ember.get, set = Ember.set, meta = Ember.meta;
+
+var hasElement = Ember.View.states.hasElement = Ember.create(Ember.View.states._default);
+
+Ember.merge(hasElement, {
+ $: function(view, sel) {
+ var elem = get(view, 'element');
+ return sel ? Ember.$(sel, elem) : Ember.$(elem);
+ },
+
+ getElement: function(view) {
+ var parent = get(view, 'parentView');
+ if (parent) { parent = get(parent, 'element'); }
+ if (parent) { return view.findElementInParentElement(parent); }
+ return Ember.$("#" + get(view, 'elementId'))[0];
+ },
+
+ setElement: function(view, value) {
+ if (value === null) {
+ view.transitionTo('preRender');
+ } else {
+ throw "You cannot set an element to a non-null value when the element is already in the DOM.";
+ }
+
+ return value;
+ },
+
+ // once the view has been inserted into the DOM, rerendering is
+ // deferred to allow bindings to synchronize.
+ rerender: function(view) {
+ view.triggerRecursively('willClearRender');
+
+ view.clearRenderedChildren();
+
+ view.domManager.replace(view);
+ return view;
+ },
+
+ // once the view is already in the DOM, destroying it removes it
+ // from the DOM, nukes its element, and puts it back into the
+ // preRender state if inDOM.
+
+ destroyElement: function(view) {
+ view._notifyWillDestroyElement();
+ view.domManager.remove(view);
+ set(view, 'element', null);
+ if (view._scheduledInsert) {
+ Ember.run.cancel(view._scheduledInsert);
+ view._scheduledInsert = null;
+ }
+ return view;
+ },
+
+ empty: function(view) {
+ var _childViews = view._childViews, len, idx;
+ if (_childViews) {
+ len = _childViews.length;
+ for (idx = 0; idx < len; idx++) {
+ _childViews[idx]._notifyWillDestroyElement();
+ }
+ }
+ view.domManager.empty(view);
+ },
+
+ // Handle events from `Ember.EventDispatcher`
+ handleEvent: function(view, eventName, evt) {
+ if (view.has(eventName)) {
+ // Handler should be able to re-dispatch events, so we don't
+ // preventDefault or stopPropagation.
+ return view.trigger(eventName, evt);
+ } else {
+ return true; // continue event propagation
+ }
+ }
+});
+
+var inDOM = Ember.View.states.inDOM = Ember.create(hasElement);
+
+Ember.merge(inDOM, {
+ insertElement: function(view, fn) {
+ throw "You can't insert an element into the DOM that has already been inserted";
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-views
+*/
+
+var destroyedError = "You can't call %@ on a destroyed view", fmt = Ember.String.fmt;
+
+var destroyed = Ember.View.states.destroyed = Ember.create(Ember.View.states._default);
+
+Ember.merge(destroyed, {
+ appendChild: function() {
+ throw fmt(destroyedError, ['appendChild']);
+ },
+ rerender: function() {
+ throw fmt(destroyedError, ['rerender']);
+ },
+ destroyElement: function() {
+ throw fmt(destroyedError, ['destroyElement']);
+ },
+ empty: function() {
+ throw fmt(destroyedError, ['empty']);
+ },
+
+ setElement: function() {
+ throw fmt(destroyedError, ["set('element', ...)"]);
+ },
+
+ renderToBufferIfNeeded: function() {
+ throw fmt(destroyedError, ["renderToBufferIfNeeded"]);
+ },
+
+ // Since element insertion is scheduled, don't do anything if
+ // the view has been destroyed between scheduling and execution
+ insertElement: Ember.K
+});
+
+
+})();
+
+
+
+(function() {
+Ember.View.cloneStates = function(from) {
+ var into = {};
+
+ into._default = {};
+ into.preRender = Ember.create(into._default);
+ into.destroyed = Ember.create(into._default);
+ into.inBuffer = Ember.create(into._default);
+ into.hasElement = Ember.create(into._default);
+ into.inDOM = Ember.create(into.hasElement);
+
+ var viewState;
+
+ for (var stateName in from) {
+ if (!from.hasOwnProperty(stateName)) { continue; }
+ Ember.merge(into[stateName], from[stateName]);
+ }
+
+ return into;
+};
+
+})();
+
+
+
+(function() {
+var states = Ember.View.cloneStates(Ember.View.states);
+
+/**
+@module ember
+@submodule ember-views
+*/
+
+var get = Ember.get, set = Ember.set, meta = Ember.meta;
+var forEach = Ember.EnumerableUtils.forEach;
+
+/**
+ A `ContainerView` is an `Ember.View` subclass that implements `Ember.MutableArray`
+ allowing programatic management of its child views.
+
+ ## Setting Initial Child Views
+
+ The initial array of child views can be set in one of two ways. You can
+ provide a `childViews` property at creation time that contains instance of
+ `Ember.View`:
+
+ ```javascript
+ aContainer = Ember.ContainerView.create({
+ childViews: [Ember.View.create(), Ember.View.create()]
+ });
+ ```
+
+ You can also provide a list of property names whose values are instances of
+ `Ember.View`:
+
+ ```javascript
+ aContainer = Ember.ContainerView.create({
+ childViews: ['aView', 'bView', 'cView'],
+ aView: Ember.View.create(),
+ bView: Ember.View.create(),
+ cView: Ember.View.create()
+ });
+ ```
+
+ The two strategies can be combined:
+
+ ```javascript
+ aContainer = Ember.ContainerView.create({
+ childViews: ['aView', Ember.View.create()],
+ aView: Ember.View.create()
+ });
+ ```
+
+ Each child view's rendering will be inserted into the container's rendered
+ HTML in the same order as its position in the `childViews` property.
+
+ ## Adding and Removing Child Views
+
+ The container view implements `Ember.MutableArray` allowing programatic management of its child views.
+
+ To remove a view, pass that view into a `removeObject` call on the container view.
+
+ Given an empty `<body>` the following code
+
+ ```javascript
+ aContainer = Ember.ContainerView.create({
+ classNames: ['the-container'],
+ childViews: ['aView', 'bView'],
+ aView: Ember.View.create({
+ template: Ember.Handlebars.compile("A")
+ }),
+ bView: Ember.View.create({
+ template: Ember.Handlebars.compile("B")
+ })
+ });
+
+ aContainer.appendTo('body');
+ ```
+
+ Results in the HTML
+
+ ```html
+ <div class="ember-view the-container">
+ <div class="ember-view">A</div>
+ <div class="ember-view">B</div>
+ </div>
+ ```
+
+ Removing a view
+
+ ```javascript
+ aContainer.toArray(); // [aContainer.aView, aContainer.bView]
+ aContainer.removeObject(aContainer.get('bView'));
+ aContainer.toArray(); // [aContainer.aView]
+ ```
+
+ Will result in the following HTML
+
+ ```html
+ <div class="ember-view the-container">
+ <div class="ember-view">A</div>
+ </div>
+ ```
+
+ Similarly, adding a child view is accomplished by adding `Ember.View` instances to the
+ container view.
+
+ Given an empty `<body>` the following code
+
+ ```javascript
+ aContainer = Ember.ContainerView.create({
+ classNames: ['the-container'],
+ childViews: ['aView', 'bView'],
+ aView: Ember.View.create({
+ template: Ember.Handlebars.compile("A")
+ }),
+ bView: Ember.View.create({
+ template: Ember.Handlebars.compile("B")
+ })
+ });
+
+ aContainer.appendTo('body');
+ ```
+
+ Results in the HTML
+
+ ```html
+ <div class="ember-view the-container">
+ <div class="ember-view">A</div>
+ <div class="ember-view">B</div>
+ </div>
+ ```
+
+ Adding a view
+
+ ```javascript
+ AnotherViewClass = Ember.View.extend({
+ template: Ember.Handlebars.compile("Another view")
+ });
+
+ aContainer.toArray(); // [aContainer.aView, aContainer.bView]
+ aContainer.pushObject(AnotherViewClass.create());
+ aContainer.toArray(); // [aContainer.aView, aContainer.bView, <AnotherViewClass instance>]
+ ```
+
+ Will result in the following HTML
+
+ ```html
+ <div class="ember-view the-container">
+ <div class="ember-view">A</div>
+ <div class="ember-view">B</div>
+ <div class="ember-view">Another view</div>
+ </div>
+ ```
+
+ ## Templates and Layout
+
+ A `template`, `templateName`, `defaultTemplate`, `layout`, `layoutName` or
+ `defaultLayout` property on a container view will not result in the template
+ or layout being rendered. The HTML contents of a `Ember.ContainerView`'s DOM
+ representation will only be the rendered HTML of its child views.
+
+ ## Binding a View to Display
+
+ If you would like to display a single view in your ContainerView, you can set
+ its `currentView` property. When the `currentView` property is set to a view
+ instance, it will be added to the ContainerView. If the `currentView` property
+ is later changed to a different view, the new view will replace the old view.
+ If `currentView` is set to `null`, the last `currentView` will be removed.
+
+ This functionality is useful for cases where you want to bind the display of
+ a ContainerView to a controller or state manager. For example, you can bind
+ the `currentView` of a container to a controller like this:
+
+ ```javascript
+ App.appController = Ember.Object.create({
+ view: Ember.View.create({
+ templateName: 'person_template'
+ })
+ });
+ ```
+
+ ```handlebars
+ {{view Ember.ContainerView currentViewBinding="App.appController.view"}}
+ ```
+
+ @class ContainerView
+ @namespace Ember
+ @extends Ember.View
+*/
+Ember.ContainerView = Ember.View.extend(Ember.MutableArray, {
+ states: states,
+
+ init: function() {
+ this._super();
+
+ var childViews = get(this, 'childViews');
+
+ // redefine view's childViews property that was obliterated
+ Ember.defineProperty(this, 'childViews', Ember.View.childViewsProperty);
+
+ var _childViews = this._childViews;
+
+ forEach(childViews, function(viewName, idx) {
+ var view;
+
+ if ('string' === typeof viewName) {
+ view = get(this, viewName);
+ view = this.createChildView(view);
+ set(this, viewName, view);
+ } else {
+ view = this.createChildView(viewName);
+ }
+
+ _childViews[idx] = view;
+ }, this);
+
+ var currentView = get(this, 'currentView');
+ if (currentView) {
+ _childViews.push(this.createChildView(currentView));
+ }
+ },
+
+ replace: function(idx, removedCount, addedViews) {
+ var addedCount = addedViews ? get(addedViews, 'length') : 0;
+
+ this.arrayContentWillChange(idx, removedCount, addedCount);
+ this.childViewsWillChange(this._childViews, idx, removedCount);
+
+ if (addedCount === 0) {
+ this._childViews.splice(idx, removedCount) ;
+ } else {
+ var args = [idx, removedCount].concat(addedViews);
+ this._childViews.splice.apply(this._childViews, args);
+ }
+
+ this.arrayContentDidChange(idx, removedCount, addedCount);
+ this.childViewsDidChange(this._childViews, idx, removedCount, addedCount);
+
+ return this;
+ },
+
+ objectAt: function(idx) {
+ return this._childViews[idx];
+ },
+
+ length: Ember.computed(function () {
+ return this._childViews.length;
+ }),
+
+ /**
+ @private
+
+ Instructs each child view to render to the passed render buffer.
+
+ @method render
+ @param {Ember.RenderBuffer} buffer the buffer to render to
+ */
+ render: function(buffer) {
+ this.forEachChildView(function(view) {
+ view.renderToBuffer(buffer);
+ });
+ },
+
+ instrumentName: 'render.container',
+
+ /**
+ @private
+
+ When a child view is removed, destroy its element so that
+ it is removed from the DOM.
+
+ The array observer that triggers this action is set up in the
+ `renderToBuffer` method.
+
+ @method childViewsWillChange
+ @param {Ember.Array} views the child views array before mutation
+ @param {Number} start the start position of the mutation
+ @param {Number} removed the number of child views removed
+ **/
+ childViewsWillChange: function(views, start, removed) {
+ this.propertyWillChange('childViews');
+
+ if (removed > 0) {
+ var changedViews = views.slice(start, start+removed);
+ // transition to preRender before clearing parentView
+ this.currentState.childViewsWillChange(this, views, start, removed);
+ this.initializeViews(changedViews, null, null);
+ }
+ },
+
+ removeChild: function(child) {
+ this.removeObject(child);
+ return this;
+ },
+
+ /**
+ @private
+
+ When a child view is added, make sure the DOM gets updated appropriately.
+
+ If the view has already rendered an element, we tell the child view to
+ create an element and insert it into the DOM. If the enclosing container
+ view has already written to a buffer, but not yet converted that buffer
+ into an element, we insert the string representation of the child into the
+ appropriate place in the buffer.
+
+ @method childViewsDidChange
+ @param {Ember.Array} views the array of child views afte the mutation has occurred
+ @param {Number} start the start position of the mutation
+ @param {Number} removed the number of child views removed
+ @param {Number} the number of child views added
+ */
+ childViewsDidChange: function(views, start, removed, added) {
+ if (added > 0) {
+ var changedViews = views.slice(start, start+added);
+ this.initializeViews(changedViews, this, get(this, 'templateData'));
+ this.currentState.childViewsDidChange(this, views, start, added);
+ }
+ this.propertyDidChange('childViews');
+ },
+
+ initializeViews: function(views, parentView, templateData) {
+ forEach(views, function(view) {
+ set(view, '_parentView', parentView);
+
+ if (!get(view, 'templateData')) {
+ set(view, 'templateData', templateData);
+ }
+ });
+ },
+
+ currentView: null,
+
+ _currentViewWillChange: Ember.beforeObserver(function() {
+ var currentView = get(this, 'currentView');
+ if (currentView) {
+ currentView.destroy();
+ }
+ }, 'currentView'),
+
+ _currentViewDidChange: Ember.observer(function() {
+ var currentView = get(this, 'currentView');
+ if (currentView) {
+ this.pushObject(currentView);
+ }
+ }, 'currentView'),
+
+ _ensureChildrenAreInDOM: function () {
+ this.currentState.ensureChildrenAreInDOM(this);
+ }
+});
+
+Ember.merge(states._default, {
+ childViewsWillChange: Ember.K,
+ childViewsDidChange: Ember.K,
+ ensureChildrenAreInDOM: Ember.K
+});
+
+Ember.merge(states.inBuffer, {
+ childViewsDidChange: function(parentView, views, start, added) {
+ throw new Error('You cannot modify child views while in the inBuffer state');
+ }
+});
+
+Ember.merge(states.hasElement, {
+ childViewsWillChange: function(view, views, start, removed) {
+ for (var i=start; i<start+removed; i++) {
+ views[i].remove();
+ }
+ },
+
+ childViewsDidChange: function(view, views, start, added) {
+ Ember.run.scheduleOnce('render', view, '_ensureChildrenAreInDOM');
+ },
+
+ ensureChildrenAreInDOM: function(view) {
+ var childViews = view._childViews, i, len, childView, previous, buffer;
+ for (i = 0, len = childViews.length; i < len; i++) {
+ childView = childViews[i];
+ buffer = childView.renderToBufferIfNeeded();
+ if (buffer) {
+ childView.triggerRecursively('willInsertElement');
+ if (previous) {
+ previous.domManager.after(previous, buffer.string());
+ } else {
+ view.domManager.prepend(view, buffer.string());
+ }
+ childView.transitionTo('inDOM');
+ childView.propertyDidChange('element');
+ childView.triggerRecursively('didInsertElement');
+ }
+ previous = childView;
+ }
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-views
+*/
+
+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
+ 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.
+
+ ## Setting content
+
+ The managed collection of objects is referenced as the `Ember.CollectionView`
+ instance's `content` property.
+
+ ```javascript
+ someItemsView = Ember.CollectionView.create({
+ content: ['A', 'B','C']
+ })
+ ```
+
+ The view for each item in the collection will have its `content` property set
+ to the item.
+
+ ## Specifying itemViewClass
+
+ By default the view class for each item in the managed collection will be an
+ instance of `Ember.View`. You can supply a different class by setting the
+ `CollectionView`'s `itemViewClass` property.
+
+ Given an empty `<body>` and the following code:
+
+ ```javascript
+ someItemsView = Ember.CollectionView.create({
+ classNames: ['a-collection'],
+ content: ['A','B','C'],
+ itemViewClass: Ember.View.extend({
+ template: Ember.Handlebars.compile("the letter: {{view.content}}")
+ })
+ });
+
+ someItemsView.appendTo('body');
+ ```
+
+ Will result in the following HTML structure
+
+ ```html
+ <div class="ember-view a-collection">
+ <div class="ember-view">the letter: A</div>
+ <div class="ember-view">the letter: B</div>
+ <div class="ember-view">the letter: C</div>
+ </div>
+ ```
+
+ ## Automatic matching of parent/child tagNames
+
+ Setting the `tagName` property of a `CollectionView` to any of
+ "ul", "ol", "table", "thead", "tbody", "tfoot", "tr", or "select" will result
+ in the item views receiving an appropriately matched `tagName` property.
+
+ Given an empty `<body>` and the following code:
+
+ ```javascript
+ anUndorderedListView = Ember.CollectionView.create({
+ tagName: 'ul',
+ content: ['A','B','C'],
+ itemViewClass: Ember.View.extend({
+ template: Ember.Handlebars.compile("the letter: {{view.content}}")
+ })
+ });
+
+ anUndorderedListView.appendTo('body');
+ ```
+
+ Will result in the following HTML structure
+
+ ```html
+ <ul class="ember-view a-collection">
+ <li class="ember-view">the letter: A</li>
+ <li class="ember-view">the letter: B</li>
+ <li class="ember-view">the letter: C</li>
+ </ul>
+ ```
+
+ Additional `tagName` pairs can be provided by adding to
+ `Ember.CollectionView.CONTAINER_MAP `
+
+ ```javascript
+ Ember.CollectionView.CONTAINER_MAP['article'] = 'section'
+ ```
+
+ ## Programatic creation of child views
+
+ For cases where additional customization beyond the use of a single
+ `itemViewClass` or `tagName` matching is required CollectionView's
+ `createChildView` method can be overidden:
+
+ ```javascript
+ CustomCollectionView = Ember.CollectionView.extend({
+ createChildView: function(viewClass, attrs) {
+ if (attrs.content.kind == 'album') {
+ viewClass = App.AlbumView;
+ } else {
+ viewClass = App.SongView;
+ }
+ this._super(viewClass, attrs);
+ }
+ });
+ ```
+
+ ## Empty View
+
+ You can provide an `Ember.View` subclass to the `Ember.CollectionView`
+ instance as its `emptyView` property. If the `content` property of a
+ `CollectionView` is set to `null` or an empty array, an instance of this view
+ will be the `CollectionView`s only child.
+
+ ```javascript
+ aListWithNothing = Ember.CollectionView.create({
+ classNames: ['nothing']
+ content: null,
+ emptyView: Ember.View.extend({
+ template: Ember.Handlebars.compile("The collection is empty")
+ })
+ });
+
+ aListWithNothing.appendTo('body');
+ ```
+
+ Will result in the following HTML structure
+
+ ```html
+ <div class="ember-view nothing">
+ <div class="ember-view">
+ The collection is empty
+ </div>
+ </div>
+ ```
+
+ ## Adding and Removing items
+
+ The `childViews` property of a `CollectionView` should not be directly
+ manipulated. Instead, add, remove, replace items from its `content` property.
+ This will trigger appropriate changes to its rendered HTML.
+
+ ## Use in templates via the `{{collection}}` `Ember.Handlebars` helper
+
+ `Ember.Handlebars` provides a helper specifically for adding
+ `CollectionView`s to templates. See `Ember.Handlebars.collection` for more
+ details
+
+ @class CollectionView
+ @namespace Ember
+ @extends Ember.ContainerView
+ @since Ember 0.9
+*/
+Ember.CollectionView = Ember.ContainerView.extend(
+/** @scope Ember.CollectionView.prototype */ {
+
+ /**
+ A list of items to be displayed by the `Ember.CollectionView`.
+
+ @property content
+ @type Ember.Array
+ @default null
+ */
+ content: null,
+
+ /**
+ @private
+
+ This provides metadata about what kind of empty view class this
+ collection would like if it is being instantiated from another
+ system (like Handlebars)
+
+ @property emptyViewClass
+ */
+ emptyViewClass: Ember.View,
+
+ /**
+ An optional view to display if content is set to an empty array.
+
+ @property emptyView
+ @type Ember.View
+ @default null
+ */
+ emptyView: null,
+
+ /**
+ @property itemViewClass
+ @type Ember.View
+ @default Ember.View
+ */
+ itemViewClass: Ember.View,
+
+ init: function() {
+ var ret = this._super();
+ this._contentDidChange();
+ return ret;
+ },
+
+ _contentWillChange: Ember.beforeObserver(function() {
+ var content = this.get('content');
+
+ if (content) { content.removeArrayObserver(this); }
+ var len = content ? get(content, 'length') : 0;
+ this.arrayWillChange(content, 0, len);
+ }, 'content'),
+
+ /**
+ @private
+
+ Check to make sure that the content has changed, and if so,
+ update the children directly. This is always scheduled
+ asynchronously, to allow the element to be created before
+ bindings have synchronized and vice versa.
+
+ @method _contentDidChange
+ */
+ _contentDidChange: Ember.observer(function() {
+ var content = get(this, 'content');
+
+ if (content) {
+ Ember.assert(fmt("an Ember.CollectionView's content must implement Ember.Array. You passed %@", [content]), Ember.Array.detect(content));
+ content.addArrayObserver(this);
+ }
+
+ var len = content ? get(content, 'length') : 0;
+ this.arrayDidChange(content, 0, null, len);
+ }, 'content'),
+
+ willDestroy: function() {
+ var content = get(this, 'content');
+ if (content) { content.removeArrayObserver(this); }
+
+ this._super();
+
+ if (this._createdEmptyView) {
+ this._createdEmptyView.destroy();
+ }
+ },
+
+ arrayWillChange: function(content, start, removedCount) {
+ // If the contents were empty before and this template collection has an
+ // empty view remove it now.
+ var emptyView = get(this, 'emptyView');
+ if (emptyView && emptyView instanceof Ember.View) {
+ emptyView.removeFromParent();
+ }
+
+ // Loop through child views that correspond with the removed items.
+ // Note that we loop from the end of the array to the beginning because
+ // we are mutating it as we go.
+ var childViews = this._childViews, childView, idx, len;
+
+ len = this._childViews.length;
+
+ var removingAll = removedCount === len;
+
+ if (removingAll) {
+ this.currentState.empty(this);
+ }
+
+ for (idx = start + removedCount - 1; idx >= start; idx--) {
+ childView = childViews[idx];
+ if (removingAll) { childView.removedFromDOM = true; }
+ childView.destroy();
+ }
+ },
+
+ /**
+ Called when a mutation to the underlying content array occurs.
+
+ This method will replay that mutation against the views that compose the
+ `Ember.CollectionView`, ensuring that the view reflects the model.
+
+ This array observer is added in `contentDidChange`.
+
+ @method arrayDidChange
+ @param {Array} addedObjects the objects that were added to the content
+ @param {Array} removedObjects the objects that were removed from the content
+ @param {Number} changeIndex the index at which the changes occurred
+ */
+ arrayDidChange: function(content, start, removed, added) {
+ var itemViewClass = get(this, 'itemViewClass'),
+ addedViews = [], view, item, idx, len, itemTagName;
+
+ if ('string' === typeof itemViewClass) {
+ itemViewClass = get(itemViewClass);
+ }
+
+ Ember.assert(fmt("itemViewClass must be a subclass of Ember.View, not %@", [itemViewClass]), Ember.View.detect(itemViewClass));
+
+ len = content ? get(content, 'length') : 0;
+ if (len) {
+ for (idx = start; idx < start+added; idx++) {
+ item = content.objectAt(idx);
+
+ view = this.createChildView(itemViewClass, {
+ content: item,
+ contentIndex: idx
+ });
+
+ addedViews.push(view);
+ }
+ } else {
+ var emptyView = get(this, 'emptyView');
+ if (!emptyView) { return; }
+
+ var isClass = Ember.CoreView.detect(emptyView);
+
+ emptyView = this.createChildView(emptyView);
+ addedViews.push(emptyView);
+ set(this, 'emptyView', emptyView);
+
+ if (isClass) { this._createdEmptyView = emptyView; }
+ }
+ this.replace(start, 0, addedViews);
+ },
+
+ createChildView: function(view, attrs) {
+ view = this._super(view, attrs);
+
+ var itemTagName = get(view, 'tagName');
+ var tagName = (itemTagName === null || itemTagName === undefined) ? Ember.CollectionView.CONTAINER_MAP[get(this, 'tagName')] : itemTagName;
+
+ set(view, 'tagName', tagName);
+
+ return view;
+ }
+});
+
+/**
+ 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() {
+/*globals jQuery*/
+/**
+Ember Views
+
+@module ember
+@submodule ember-views
+@requires ember-runtime
+@main ember-views
+*/
+
+})();
+
+(function() {
+define("metamorph",
+ [],
+ function() {
+ "use strict";
+ // ==========================================================================
+ // Project: metamorph
+ // Copyright: ©2011 My Company Inc. All rights reserved.
+ // ==========================================================================
+
+ var K = function(){},
+ guid = 0,
+ document = window.document,
+
+ // Feature-detect the W3C range API, the extended check is for IE9 which only partially supports ranges
+ supportsRange = ('createRange' in document) && (typeof Range !== 'undefined') && Range.prototype.createContextualFragment,
+
+ // Internet Explorer prior to 9 does not allow setting innerHTML if the first element
+ // is a "zero-scope" element. This problem can be worked around by making
+ // the first node an invisible text node. We, like Modernizr, use &shy;
+ needsShy = (function(){
+ var testEl = document.createElement('div');
+ testEl.innerHTML = "<div></div>";
+ testEl.firstChild.innerHTML = "<script></script>";
+ return testEl.firstChild.innerHTML === '';
+ })(),
+
+
+ // IE 8 (and likely earlier) likes to move whitespace preceeding
+ // a script tag to appear after it. This means that we can
+ // accidentally remove whitespace when updating a morph.
+ movesWhitespace = (function() {
+ var testEl = document.createElement('div');
+ testEl.innerHTML = "Test: <script type='text/x-placeholder'></script>Value";
+ return testEl.childNodes[0].nodeValue === 'Test:' &&
+ testEl.childNodes[2].nodeValue === ' Value';
+ })();
+
+ // Constructor that supports either Metamorph('foo') or new
+ // Metamorph('foo');
+ //
+ // Takes a string of HTML as the argument.
+
+ var Metamorph = function(html) {
+ var self;
+
+ if (this instanceof Metamorph) {
+ self = this;
+ } else {
+ self = new K();
+ }
+
+ self.innerHTML = html;
+ var myGuid = 'metamorph-'+(guid++);
+ self.start = myGuid + '-start';
+ self.end = myGuid + '-end';
+
+ return self;
+ };
+
+ K.prototype = Metamorph.prototype;
+
+ var rangeFor, htmlFunc, removeFunc, outerHTMLFunc, appendToFunc, afterFunc, prependFunc, startTagFunc, endTagFunc;
+
+ outerHTMLFunc = function() {
+ return this.startTag() + this.innerHTML + this.endTag();
+ };
+
+ startTagFunc = function() {
+ /*
+ * We replace chevron by its hex code in order to prevent escaping problems.
+ * Check this thread for more explaination:
+ * http://stackoverflow.com/questions/8231048/why-use-x3c-instead-of-when-generating-html-from-javascript
+ */
+ return "<script id='" + this.start + "' type='text/x-placeholder'>\x3C/script>";
+ };
+
+ endTagFunc = function() {
+ /*
+ * We replace chevron by its hex code in order to prevent escaping problems.
+ * Check this thread for more explaination:
+ * http://stackoverflow.com/questions/8231048/why-use-x3c-instead-of-when-generating-html-from-javascript
+ */
+ return "<script id='" + this.end + "' type='text/x-placeholder'>\x3C/script>";
+ };
+
+ // If we have the W3C range API, this process is relatively straight forward.
+ if (supportsRange) {
+
+ // Get a range for the current morph. Optionally include the starting and
+ // ending placeholders.
+ rangeFor = function(morph, outerToo) {
+ var range = document.createRange();
+ var before = document.getElementById(morph.start);
+ var after = document.getElementById(morph.end);
+
+ if (outerToo) {
+ range.setStartBefore(before);
+ range.setEndAfter(after);
+ } else {
+ range.setStartAfter(before);
+ range.setEndBefore(after);
+ }
+
+ return range;
+ };
+
+ htmlFunc = function(html, outerToo) {
+ // get a range for the current metamorph object
+ var range = rangeFor(this, outerToo);
+
+ // delete the contents of the range, which will be the
+ // nodes between the starting and ending placeholder.
+ range.deleteContents();
+
+ // create a new document fragment for the HTML
+ var fragment = range.createContextualFragment(html);
+
+ // insert the fragment into the range
+ range.insertNode(fragment);
+ };
+
+ removeFunc = function() {
+ // get a range for the current metamorph object including
+ // the starting and ending placeholders.
+ var range = rangeFor(this, true);
+
+ // delete the entire range.
+ range.deleteContents();
+ };
+
+ appendToFunc = function(node) {
+ var range = document.createRange();
+ range.setStart(node);
+ range.collapse(false);
+ var frag = range.createContextualFragment(this.outerHTML());
+ node.appendChild(frag);
+ };
+
+ afterFunc = function(html) {
+ var range = document.createRange();
+ var after = document.getElementById(this.end);
+
+ range.setStartAfter(after);
+ range.setEndAfter(after);
+
+ var fragment = range.createContextualFragment(html);
+ range.insertNode(fragment);
+ };
+
+ prependFunc = function(html) {
+ var range = document.createRange();
+ var start = document.getElementById(this.start);
+
+ range.setStartAfter(start);
+ range.setEndAfter(start);
+
+ var fragment = range.createContextualFragment(html);
+ range.insertNode(fragment);
+ };
+
+ } else {
+ /**
+ * This code is mostly taken from jQuery, with one exception. In jQuery's case, we
+ * have some HTML and we need to figure out how to convert it into some nodes.
+ *
+ * In this case, jQuery needs to scan the HTML looking for an opening tag and use
+ * that as the key for the wrap map. In our case, we know the parent node, and
+ * can use its type as the key for the wrap map.
+ **/
+ var wrapMap = {
+ select: [ 1, "<select multiple='multiple'>", "</select>" ],
+ fieldset: [ 1, "<fieldset>", "</fieldset>" ],
+ table: [ 1, "<table>", "</table>" ],
+ tbody: [ 2, "<table><tbody>", "</tbody></table>" ],
+ tr: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
+ colgroup: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
+ map: [ 1, "<map>", "</map>" ],
+ _default: [ 0, "", "" ]
+ };
+
+ var findChildById = function(element, id) {
+ if (element.getAttribute('id') === id) { return element; }
+
+ var len = element.childNodes.length, idx, node, found;
+ for (idx=0; idx<len; idx++) {
+ node = element.childNodes[idx];
+ found = node.nodeType === 1 && findChildById(node, id);
+ if (found) { return found; }
+ }
+ };
+
+ var setInnerHTML = function(element, html) {
+ var matches = [];
+ if (movesWhitespace) {
+ // Right now we only check for script tags with ids with the
+ // goal of targeting morphs.
+ html = html.replace(/(\s+)(<script id='([^']+)')/g, function(match, spaces, tag, id) {
+ matches.push([id, spaces]);
+ return tag;
+ });
+ }
+
+ element.innerHTML = html;
+
+ // If we have to do any whitespace adjustments do them now
+ if (matches.length > 0) {
+ var len = matches.length, idx;
+ for (idx=0; idx<len; idx++) {
+ var script = findChildById(element, matches[idx][0]),
+ node = document.createTextNode(matches[idx][1]);
+ script.parentNode.insertBefore(node, script);
+ }
+ }
+ };
+
+ /**
+ * Given a parent node and some HTML, generate a set of nodes. Return the first
+ * node, which will allow us to traverse the rest using nextSibling.
+ *
+ * We need to do this because innerHTML in IE does not really parse the nodes.
+ **/
+ var firstNodeFor = function(parentNode, html) {
+ var arr = wrapMap[parentNode.tagName.toLowerCase()] || wrapMap._default;
+ var depth = arr[0], start = arr[1], end = arr[2];
+
+ if (needsShy) { html = '&shy;'+html; }
+
+ var element = document.createElement('div');
+
+ setInnerHTML(element, start + html + end);
+
+ for (var i=0; i<=depth; i++) {
+ element = element.firstChild;
+ }
+
+ // Look for &shy; to remove it.
+ if (needsShy) {
+ var shyElement = element;
+
+ // Sometimes we get nameless elements with the shy inside
+ while (shyElement.nodeType === 1 && !shyElement.nodeName) {
+ shyElement = shyElement.firstChild;
+ }
+
+ // At this point it's the actual unicode character.
+ if (shyElement.nodeType === 3 && shyElement.nodeValue.charAt(0) === "\u00AD") {
+ shyElement.nodeValue = shyElement.nodeValue.slice(1);
+ }
+ }
+
+ return element;
+ };
+
+ /**
+ * In some cases, Internet Explorer can create an anonymous node in
+ * the hierarchy with no tagName. You can create this scenario via:
+ *
+ * div = document.createElement("div");
+ * div.innerHTML = "<table>&shy<script></script><tr><td>hi</td></tr></table>";
+ * div.firstChild.firstChild.tagName //=> ""
+ *
+ * If our script markers are inside such a node, we need to find that
+ * node and use *it* as the marker.
+ **/
+ var realNode = function(start) {
+ while (start.parentNode.tagName === "") {
+ start = start.parentNode;
+ }
+
+ return start;
+ };
+
+ /**
+ * When automatically adding a tbody, Internet Explorer inserts the
+ * tbody immediately before the first <tr>. Other browsers create it
+ * before the first node, no matter what.
+ *
+ * This means the the following code:
+ *
+ * div = document.createElement("div");
+ * div.innerHTML = "<table><script id='first'></script><tr><td>hi</td></tr><script id='last'></script></table>
+ *
+ * Generates the following DOM in IE:
+ *
+ * + div
+ * + table
+ * - script id='first'
+ * + tbody
+ * + tr
+ * + td
+ * - "hi"
+ * - script id='last'
+ *
+ * Which means that the two script tags, even though they were
+ * inserted at the same point in the hierarchy in the original
+ * HTML, now have different parents.
+ *
+ * This code reparents the first script tag by making it the tbody's
+ * first child.
+ **/
+ var fixParentage = function(start, end) {
+ if (start.parentNode !== end.parentNode) {
+ end.parentNode.insertBefore(start, end.parentNode.firstChild);
+ }
+ };
+
+ htmlFunc = function(html, outerToo) {
+ // get the real starting node. see realNode for details.
+ var start = realNode(document.getElementById(this.start));
+ var end = document.getElementById(this.end);
+ var parentNode = end.parentNode;
+ var node, nextSibling, last;
+
+ // make sure that the start and end nodes share the same
+ // parent. If not, fix it.
+ fixParentage(start, end);
+
+ // remove all of the nodes after the starting placeholder and
+ // before the ending placeholder.
+ node = start.nextSibling;
+ while (node) {
+ nextSibling = node.nextSibling;
+ last = node === end;
+
+ // if this is the last node, and we want to remove it as well,
+ // set the `end` node to the next sibling. This is because
+ // for the rest of the function, we insert the new nodes
+ // before the end (note that insertBefore(node, null) is
+ // the same as appendChild(node)).
+ //
+ // if we do not want to remove it, just break.
+ if (last) {
+ if (outerToo) { end = node.nextSibling; } else { break; }
+ }
+
+ node.parentNode.removeChild(node);
+
+ // if this is the last node and we didn't break before
+ // (because we wanted to remove the outer nodes), break
+ // now.
+ if (last) { break; }
+
+ node = nextSibling;
+ }
+
+ // get the first node for the HTML string, even in cases like
+ // tables and lists where a simple innerHTML on a div would
+ // swallow some of the content.
+ node = firstNodeFor(start.parentNode, html);
+
+ // copy the nodes for the HTML between the starting and ending
+ // placeholder.
+ while (node) {
+ nextSibling = node.nextSibling;
+ parentNode.insertBefore(node, end);
+ node = nextSibling;
+ }
+ };
+
+ // remove the nodes in the DOM representing this metamorph.
+ //
+ // this includes the starting and ending placeholders.
+ removeFunc = function() {
+ var start = realNode(document.getElementById(this.start));
+ var end = document.getElementById(this.end);
+
+ this.html('');
+ start.parentNode.removeChild(start);
+ end.parentNode.removeChild(end);
+ };
+
+ appendToFunc = function(parentNode) {
+ var node = firstNodeFor(parentNode, this.outerHTML());
+ var nextSibling;
+
+ while (node) {
+ nextSibling = node.nextSibling;
+ parentNode.appendChild(node);
+ node = nextSibling;
+ }
+ };
+
+ afterFunc = function(html) {
+ // get the real starting node. see realNode for details.
+ var end = document.getElementById(this.end);
+ var insertBefore = end.nextSibling;
+ var parentNode = end.parentNode;
+ var nextSibling;
+ var node;
+
+ // get the first node for the HTML string, even in cases like
+ // tables and lists where a simple innerHTML on a div would
+ // swallow some of the content.
+ node = firstNodeFor(parentNode, html);
+
+ // copy the nodes for the HTML between the starting and ending
+ // placeholder.
+ while (node) {
+ nextSibling = node.nextSibling;
+ parentNode.insertBefore(node, insertBefore);
+ node = nextSibling;
+ }
+ };
+
+ prependFunc = function(html) {
+ var start = document.getElementById(this.start);
+ var parentNode = start.parentNode;
+ var nextSibling;
+ var node;
+
+ node = firstNodeFor(parentNode, html);
+ var insertBefore = start.nextSibling;
+
+ while (node) {
+ nextSibling = node.nextSibling;
+ parentNode.insertBefore(node, insertBefore);
+ node = nextSibling;
+ }
+ };
+ }
+
+ Metamorph.prototype.html = function(html) {
+ this.checkRemoved();
+ if (html === undefined) { return this.innerHTML; }
+
+ htmlFunc.call(this, html);
+
+ this.innerHTML = html;
+ };
+
+ Metamorph.prototype.replaceWith = function(html) {
+ this.checkRemoved();
+ htmlFunc.call(this, html, true);
+ };
+
+ Metamorph.prototype.remove = removeFunc;
+ Metamorph.prototype.outerHTML = outerHTMLFunc;
+ Metamorph.prototype.appendTo = appendToFunc;
+ Metamorph.prototype.after = afterFunc;
+ Metamorph.prototype.prepend = prependFunc;
+ Metamorph.prototype.startTag = startTagFunc;
+ Metamorph.prototype.endTag = endTagFunc;
+
+ Metamorph.prototype.isRemoved = function() {
+ var before = document.getElementById(this.start);
+ var after = document.getElementById(this.end);
+
+ return !before || !after;
+ };
+
+ Metamorph.prototype.checkRemoved = function() {
+ if (this.isRemoved()) {
+ throw new Error("Cannot perform operations on a Metamorph that is not in the DOM.");
+ }
+ };
+
+ return Metamorph;
+ });
+
+})();
+
+(function() {
+/**
+@module ember
+@submodule ember-handlebars
+*/
+
+// Eliminate dependency on any Ember to simplify precompilation workflow
+var objectCreate = Object.create || function(parent) {
+ function F() {}
+ F.prototype = parent;
+ return new F();
+};
+
+var Handlebars = this.Handlebars || Ember.imports.Handlebars;
+Ember.assert("Ember Handlebars requires Handlebars 1.0.0-rc.3 or greater", Handlebars && Handlebars.VERSION.match(/^1\.0\.[0-9](\.rc\.[23456789]+)?/));
+
+/**
+ Prepares the Handlebars templating library for use inside Ember's view
+ system.
+
+ The `Ember.Handlebars` object is the standard Handlebars library, extended to
+ use Ember's `get()` method instead of direct property access, which allows
+ computed properties to be used inside templates.
+
+ To create an `Ember.Handlebars` template, call `Ember.Handlebars.compile()`.
+ This will return a function that can be used by `Ember.View` for rendering.
+
+ @class Handlebars
+ @namespace Ember
+*/
+Ember.Handlebars = objectCreate(Handlebars);
+
+/**
+@class helpers
+@namespace Ember.Handlebars
+*/
+Ember.Handlebars.helpers = objectCreate(Handlebars.helpers);
+
+/**
+ Override the the opcode compiler and JavaScript compiler for Handlebars.
+
+ @class Compiler
+ @namespace Ember.Handlebars
+ @private
+ @constructor
+*/
+Ember.Handlebars.Compiler = function() {};
+
+// Handlebars.Compiler doesn't exist in runtime-only
+if (Handlebars.Compiler) {
+ Ember.Handlebars.Compiler.prototype = objectCreate(Handlebars.Compiler.prototype);
+}
+
+Ember.Handlebars.Compiler.prototype.compiler = Ember.Handlebars.Compiler;
+
+/**
+ @class JavaScriptCompiler
+ @namespace Ember.Handlebars
+ @private
+ @constructor
+*/
+Ember.Handlebars.JavaScriptCompiler = function() {};
+
+// Handlebars.JavaScriptCompiler doesn't exist in runtime-only
+if (Handlebars.JavaScriptCompiler) {
+ Ember.Handlebars.JavaScriptCompiler.prototype = objectCreate(Handlebars.JavaScriptCompiler.prototype);
+ Ember.Handlebars.JavaScriptCompiler.prototype.compiler = Ember.Handlebars.JavaScriptCompiler;
+}
+
+
+Ember.Handlebars.JavaScriptCompiler.prototype.namespace = "Ember.Handlebars";
+
+
+Ember.Handlebars.JavaScriptCompiler.prototype.initializeBuffer = function() {
+ return "''";
+};
+
+/**
+ @private
+
+ Override the default buffer for Ember Handlebars. By default, Handlebars
+ creates an empty String at the beginning of each invocation and appends to
+ it. Ember's Handlebars overrides this to append to a single shared buffer.
+
+ @method appendToBuffer
+ @param string {String}
+*/
+Ember.Handlebars.JavaScriptCompiler.prototype.appendToBuffer = function(string) {
+ return "data.buffer.push("+string+");";
+};
+
+var prefix = "ember" + (+new Date()), incr = 1;
+
+/**
+ @private
+
+ Rewrite simple mustaches from `{{foo}}` to `{{bind "foo"}}`. This means that
+ all simple mustaches in Ember's Handlebars will also set up an observer to
+ keep the DOM up to date when the underlying property changes.
+
+ @method mustache
+ @for Ember.Handlebars.Compiler
+ @param mustache
+*/
+Ember.Handlebars.Compiler.prototype.mustache = function(mustache) {
+ if (mustache.isHelper && mustache.id.string === 'control') {
+ mustache.hash = mustache.hash || new Handlebars.AST.HashNode([]);
+ mustache.hash.pairs.push(["controlID", new Handlebars.AST.StringNode(prefix + incr++)]);
+ } else if (mustache.params.length || mustache.hash) {
+ // no changes required
+ } else {
+ var id = new Handlebars.AST.IdNode(['_triageMustache']);
+
+ // Update the mustache node to include a hash value indicating whether the original node
+ // was escaped. This will allow us to properly escape values when the underlying value
+ // changes and we need to re-render the value.
+ if(!mustache.escaped) {
+ mustache.hash = mustache.hash || new Handlebars.AST.HashNode([]);
+ mustache.hash.pairs.push(["unescaped", new Handlebars.AST.StringNode("true")]);
+ }
+ mustache = new Handlebars.AST.MustacheNode([id].concat([mustache.id]), mustache.hash, !mustache.escaped);
+ }
+
+ return Handlebars.Compiler.prototype.mustache.call(this, mustache);
+};
+
+/**
+ Used for precompilation of Ember Handlebars templates. This will not be used
+ during normal app execution.
+
+ @method precompile
+ @for Ember.Handlebars
+ @static
+ @param {String} string The template to precompile
+*/
+Ember.Handlebars.precompile = function(string) {
+ var ast = Handlebars.parse(string);
+
+ var options = {
+ knownHelpers: {
+ action: true,
+ unbound: true,
+ bindAttr: true,
+ template: true,
+ view: true,
+ _triageMustache: true
+ },
+ data: true,
+ stringParams: true
+ };
+
+ var environment = new Ember.Handlebars.Compiler().compile(ast, options);
+ return new Ember.Handlebars.JavaScriptCompiler().compile(environment, options, undefined, true);
+};
+
+// We don't support this for Handlebars runtime-only
+if (Handlebars.compile) {
+ /**
+ The entry point for Ember Handlebars. This replaces the default
+ `Handlebars.compile` and turns on template-local data and String
+ parameters.
+
+ @method compile
+ @for Ember.Handlebars
+ @static
+ @param {String} string The template to compile
+ @return {Function}
+ */
+ Ember.Handlebars.compile = function(string) {
+ var ast = Handlebars.parse(string);
+ var options = { data: true, stringParams: true };
+ var environment = new Ember.Handlebars.Compiler().compile(ast, options);
+ var templateSpec = new Ember.Handlebars.JavaScriptCompiler().compile(environment, options, undefined, true);
+
+ return Ember.Handlebars.template(templateSpec);
+ };
+}
+
+
+})();
+
+(function() {
+var slice = Array.prototype.slice;
+
+/**
+ @private
+
+ If a path starts with a reserved keyword, returns the root
+ that should be used.
+
+ @method normalizePath
+ @for Ember
+ @param root {Object}
+ @param path {String}
+ @param data {Hash}
+*/
+var normalizePath = Ember.Handlebars.normalizePath = function(root, path, data) {
+ var keywords = (data && data.keywords) || {},
+ keyword, isKeyword;
+
+ // Get the first segment of the path. For example, if the
+ // path is "foo.bar.baz", returns "foo".
+ keyword = path.split('.', 1)[0];
+
+ // Test to see if the first path is a keyword that has been
+ // passed along in the view's data hash. If so, we will treat
+ // that object as the new root.
+ if (keywords.hasOwnProperty(keyword)) {
+ // Look up the value in the template's data hash.
+ root = keywords[keyword];
+ isKeyword = true;
+
+ // Handle cases where the entire path is the reserved
+ // word. In that case, return the object itself.
+ if (path === keyword) {
+ path = '';
+ } else {
+ // Strip the keyword from the path and look up
+ // the remainder from the newly found root.
+ path = path.substr(keyword.length+1);
+ }
+ }
+
+ return { root: root, path: path, isKeyword: isKeyword };
+};
+
+
+/**
+ Lookup both on root and on window. If the path starts with
+ a keyword, the corresponding object will be looked up in the
+ template's data hash and used to resolve the path.
+
+ @method get
+ @for Ember.Handlebars
+ @param {Object} root The object to look up the property on
+ @param {String} path The path to be lookedup
+ @param {Object} options The template's option hash
+*/
+var handlebarsGet = Ember.Handlebars.get = function(root, path, options) {
+ var data = options && options.data,
+ normalizedPath = normalizePath(root, path, data),
+ value;
+
+ // In cases where the path begins with a keyword, change the
+ // root to the value represented by that keyword, and ensure
+ // the path is relative to it.
+ root = normalizedPath.root;
+ path = normalizedPath.path;
+
+ value = Ember.get(root, path);
+
+ // If the path starts with a capital letter, look it up on Ember.lookup,
+ // which defaults to the `window` object in browsers.
+ if (value === undefined && root !== Ember.lookup && Ember.isGlobalPath(path)) {
+ value = Ember.get(Ember.lookup, path);
+ }
+ return value;
+};
+Ember.Handlebars.getPath = Ember.deprecateFunc('`Ember.Handlebars.getPath` has been changed to `Ember.Handlebars.get` for consistency.', Ember.Handlebars.get);
+
+Ember.Handlebars.resolveParams = function(context, params, options) {
+ var resolvedParams = [], types = options.types, param, type;
+
+ for (var i=0, l=params.length; i<l; i++) {
+ param = params[i];
+ type = types[i];
+
+ if (type === 'ID') {
+ resolvedParams.push(handlebarsGet(context, param, options));
+ } else {
+ resolvedParams.push(param);
+ }
+ }
+
+ return resolvedParams;
+};
+
+Ember.Handlebars.resolveHash = function(context, hash, options) {
+ var resolvedHash = {}, types = options.hashTypes, type;
+
+ for (var key in hash) {
+ if (!hash.hasOwnProperty(key)) { continue; }
+
+ type = types[key];
+
+ if (type === 'ID') {
+ resolvedHash[key] = handlebarsGet(context, hash[key], options);
+ } else {
+ resolvedHash[key] = hash[key];
+ }
+ }
+
+ return resolvedHash;
+};
+
+/**
+ @private
+
+ Registers a helper in Handlebars that will be called if no property with the
+ given name can be found on the current context object, and no helper with
+ that name is registered.
+
+ This throws an exception with a more helpful error message so the user can
+ track down where the problem is happening.
+
+ @method helperMissing
+ @for Ember.Handlebars.helpers
+ @param {String} path
+ @param {Hash} options
+*/
+Ember.Handlebars.registerHelper('helperMissing', function(path, options) {
+ var error, view = "";
+
+ error = "%@ Handlebars error: Could not find property '%@' on object %@.";
+ if (options.data){
+ view = options.data.view;
+ }
+ throw new Ember.Error(Ember.String.fmt(error, [view, path, this]));
+});
+
+/**
+ Register a bound handlebars helper. Bound helpers behave similarly to regular
+ handlebars helpers, with the added ability to re-render when the underlying data
+ changes.
+
+ ## Simple example
+
+ ```javascript
+ Ember.Handlebars.registerBoundHelper('capitalize', function(value) {
+ return value.toUpperCase();
+ });
+ ```
+
+ The above bound helper can be used inside of templates as follows:
+
+ ```handlebars
+ {{capitalize name}}
+ ```
+
+ In this case, when the `name` property of the template's context changes,
+ the rendered value of the helper will update to reflect this change.
+
+ ## Example with options
+
+ Like normal handlebars helpers, bound helpers have access to the options
+ passed into the helper call.
+
+ ```javascript
+ Ember.Handlebars.registerBoundHelper('repeat', function(value, options) {
+ var count = options.hash.count;
+ var a = [];
+ while(a.length < count){
+ a.push(value);
+ }
+ return a.join('');
+ });
+ ```
+
+ This helper could be used in a template as follows:
+
+ ```handlebars
+ {{repeat text count=3}}
+ ```
+
+ ## Example with bound options
+
+ Bound hash options are also supported. Example:
+
+ ```handlebars
+ {{repeat text countBinding="numRepeats"}}
+ ```
+
+ In this example, count will be bound to the value of
+ the `numRepeats` property on the context. If that property
+ changes, the helper will be re-rendered.
+
+ ## Example with extra dependencies
+
+ The `Ember.Handlebars.registerBoundHelper` method takes a variable length
+ third parameter which indicates extra dependencies on the passed in value.
+ This allows the handlebars helper to update when these dependencies change.
+
+ ```javascript
+ Ember.Handlebars.registerBoundHelper('capitalizeName', function(value) {
+ return value.get('name').toUpperCase();
+ }, 'name');
+ ```
+
+ ## Example with multiple bound properties
+
+ `Ember.Handlebars.registerBoundHelper` supports binding to
+ multiple properties, e.g.:
+
+ ```javascript
+ Ember.Handlebars.registerBoundHelper('concatenate', function() {
+ var values = arguments[arguments.length - 1];
+ return values.join('||');
+ });
+ ```
+
+ Which allows for template syntax such as {{concatenate prop1 prop2}} or
+ {{concatenate prop1 prop2 prop3}}. If any of the properties change,
+ the helpr will re-render. Note that dependency keys cannot be
+ using in conjunction with multi-property helpers, since it is ambiguous
+ which property the dependent keys would belong to.
+
+ ## Use with unbound helper
+
+ The {{unbound}} helper can be used with bound helper invocations
+ to render them in their unbound form, e.g.
+
+ ```handlebars
+ {{unbound capitalize name}}
+ ```
+
+ In this example, if the name property changes, the helper
+ will not re-render.
+
+
+ @method registerBoundHelper
+ @for Ember.Handlebars
+ @param {String} name
+ @param {Function} function
+ @param {String} dependentKeys*
+*/
+Ember.Handlebars.registerBoundHelper = function(name, fn) {
+ var dependentKeys = slice.call(arguments, 2);
+
+ function helper() {
+ var properties = slice.call(arguments, 0, -1),
+ numProperties = properties.length,
+ options = arguments[arguments.length - 1],
+ normalizedProperties = [],
+ data = options.data,
+ hash = options.hash,
+ view = data.view,
+ currentContext = (options.contexts && options.contexts[0]) || this,
+ normalized,
+ pathRoot, path,
+ loc, hashOption;
+
+ // Detect bound options (e.g. countBinding="otherCount")
+ hash.boundOptions = {};
+ for (hashOption in hash) {
+ if (!hash.hasOwnProperty(hashOption)) { continue; }
+
+ if (Ember.IS_BINDING.test(hashOption) && typeof hash[hashOption] === 'string') {
+ // Lop off 'Binding' suffix.
+ hash.boundOptions[hashOption.slice(0, -7)] = hash[hashOption];
+ }
+ }
+
+ // Expose property names on data.properties object.
+ data.properties = [];
+ for (loc = 0; loc < numProperties; ++loc) {
+ data.properties.push(properties[loc]);
+ normalizedProperties.push(normalizePath(currentContext, properties[loc], data));
+ }
+
+ if (data.isUnbound) {
+ return evaluateUnboundHelper(this, fn, normalizedProperties, options);
+ }
+
+ if (dependentKeys.length === 0) {
+ return evaluateMultiPropertyBoundHelper(currentContext, fn, normalizedProperties, options);
+ }
+
+ Ember.assert("Dependent keys can only be used with single-property helpers.", properties.length === 1);
+
+ normalized = normalizedProperties[0];
+
+ pathRoot = normalized.root;
+ path = normalized.path;
+
+ var bindView = new Ember._SimpleHandlebarsView(
+ path, pathRoot, !options.hash.unescaped, options.data
+ );
+
+ bindView.normalizedValue = function() {
+ var value = Ember._SimpleHandlebarsView.prototype.normalizedValue.call(bindView);
+ return fn.call(view, value, options);
+ };
+
+ view.appendChild(bindView);
+
+ view.registerObserver(pathRoot, path, bindView, rerenderBoundHelperView);
+
+ for (var i=0, l=dependentKeys.length; i<l; i++) {
+ view.registerObserver(pathRoot, path + '.' + dependentKeys[i], bindView, rerenderBoundHelperView);
+ }
+ }
+
+ helper._rawFunction = fn;
+ Ember.Handlebars.registerHelper(name, helper);
+};
+
+/**
+ @private
+
+ Renders the unbound form of an otherwise bound helper function.
+
+ @param {Function} fn
+ @param {Object} context
+ @param {Array} normalizedProperties
+ @param {String} options
+*/
+function evaluateMultiPropertyBoundHelper(context, fn, normalizedProperties, options) {
+ var numProperties = normalizedProperties.length,
+ self = this,
+ data = options.data,
+ view = data.view,
+ hash = options.hash,
+ boundOptions = hash.boundOptions,
+ watchedProperties,
+ boundOption, bindView, loc, property, len;
+
+ bindView = new Ember._SimpleHandlebarsView(null, null, !hash.unescaped, data);
+ bindView.normalizedValue = function() {
+ var args = [], value, boundOption;
+
+ // Copy over bound options.
+ for (boundOption in boundOptions) {
+ if (!boundOptions.hasOwnProperty(boundOption)) { continue; }
+ property = normalizePath(context, boundOptions[boundOption], data);
+ bindView.path = property.path;
+ bindView.pathRoot = property.root;
+ hash[boundOption] = Ember._SimpleHandlebarsView.prototype.normalizedValue.call(bindView);
+ }
+
+ for (loc = 0; loc < numProperties; ++loc) {
+ property = normalizedProperties[loc];
+ bindView.path = property.path;
+ bindView.pathRoot = property.root;
+ args.push(Ember._SimpleHandlebarsView.prototype.normalizedValue.call(bindView));
+ }
+ args.push(options);
+ return fn.apply(context, args);
+ };
+
+ view.appendChild(bindView);
+
+ // Assemble liast of watched properties that'll re-render this helper.
+ watchedProperties = [];
+ for (boundOption in boundOptions) {
+ if (boundOptions.hasOwnProperty(boundOption)) {
+ watchedProperties.push(normalizePath(context, boundOptions[boundOption], data));
+ }
+ }
+ watchedProperties = watchedProperties.concat(normalizedProperties);
+
+ // Observe each property.
+ for (loc = 0, len = watchedProperties.length; loc < len; ++loc) {
+ property = watchedProperties[loc];
+ view.registerObserver(property.root, property.path, bindView, rerenderBoundHelperView);
+ }
+
+}
+
+/**
+ @private
+
+ An observer function used with bound helpers which
+ will schedule a re-render of the _SimpleHandlebarsView
+ connected with the helper.
+*/
+function rerenderBoundHelperView() {
+ Ember.run.scheduleOnce('render', this, 'rerender');
+}
+
+/**
+ @private
+
+ Renders the unbound form of an otherwise bound helper function.
+
+ @param {Function} fn
+ @param {Object} context
+ @param {Array} normalizedProperties
+ @param {String} options
+*/
+function evaluateUnboundHelper(context, fn, normalizedProperties, options) {
+ var args = [], hash = options.hash, boundOptions = hash.boundOptions, loc, len, property, boundOption;
+
+ for (boundOption in boundOptions) {
+ if (!boundOptions.hasOwnProperty(boundOption)) { continue; }
+ hash[boundOption] = Ember.Handlebars.get(context, boundOptions[boundOption], options);
+ }
+
+ for(loc = 0, len = normalizedProperties.length; loc < len; ++loc) {
+ property = normalizedProperties[loc];
+ args.push(Ember.Handlebars.get(context, property.path, options));
+ }
+ args.push(options);
+ return fn.apply(context, args);
+}
+
+/**
+ @private
+
+ Overrides Handlebars.template so that we can distinguish
+ user-created, top-level templates from inner contexts.
+
+ @method template
+ @for Ember.Handlebars
+ @param {String} template spec
+*/
+Ember.Handlebars.template = function(spec){
+ var t = Handlebars.template(spec);
+ t.isTop = true;
+ return t;
+};
+
+
+})();
+
+
+
+(function() {
+/**
+ @method htmlSafe
+ @for Ember.String
+ @static
+*/
+Ember.String.htmlSafe = function(str) {
+ return new Handlebars.SafeString(str);
+};
+
+var htmlSafe = Ember.String.htmlSafe;
+
+if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) {
+
+ /**
+ See {{#crossLink "Ember.String/htmlSafe"}}{{/crossLink}}
+
+ @method htmlSafe
+ @for String
+ */
+ String.prototype.htmlSafe = function() {
+ return htmlSafe(this);
+ };
+}
+
+})();
+
+
+
+(function() {
+Ember.Handlebars.resolvePaths = function(options) {
+ var ret = [],
+ contexts = options.contexts,
+ roots = options.roots,
+ data = options.data;
+
+ for (var i=0, l=contexts.length; i<l; i++) {
+ ret.push( Ember.Handlebars.get(roots[i], contexts[i], { data: data }) );
+ }
+
+ return ret;
+};
+
+})();
+
+
+
+(function() {
+/*jshint newcap:false*/
+/**
+@module ember
+@submodule ember-handlebars
+*/
+
+var set = Ember.set, get = Ember.get;
+var Metamorph = requireModule('metamorph');
+
+// DOMManager should just abstract dom manipulation between jquery and metamorph
+var DOMManager = {
+ remove: function(view) {
+ view.morph.remove();
+ },
+
+ prepend: function(view, html) {
+ view.morph.prepend(html);
+ },
+
+ after: function(view, html) {
+ view.morph.after(html);
+ },
+
+ html: function(view, html) {
+ view.morph.html(html);
+ },
+
+ // This is messed up.
+ replace: function(view) {
+ var morph = view.morph;
+
+ view.transitionTo('preRender');
+
+ Ember.run.schedule('render', this, function() {
+ if (view.isDestroying) { return; }
+
+ view.clearRenderedChildren();
+ var buffer = view.renderToBuffer();
+
+ view.invokeRecursively(function(view) {
+ view.propertyDidChange('element');
+ });
+
+ view.triggerRecursively('willInsertElement');
+ morph.replaceWith(buffer.string());
+ view.transitionTo('inDOM');
+ view.triggerRecursively('didInsertElement');
+ });
+ },
+
+ empty: function(view) {
+ view.morph.html("");
+ }
+};
+
+// The `morph` and `outerHTML` properties are internal only
+// and not observable.
+
+/**
+ @class _Metamorph
+ @namespace Ember
+ @extends Ember.Mixin
+ @private
+*/
+Ember._Metamorph = Ember.Mixin.create({
+ isVirtual: true,
+ tagName: '',
+
+ instrumentName: 'render.metamorph',
+
+ init: function() {
+ this._super();
+ this.morph = Metamorph();
+ },
+
+ beforeRender: function(buffer) {
+ buffer.push(this.morph.startTag());
+ buffer.pushOpeningTag();
+ },
+
+ afterRender: function(buffer) {
+ buffer.pushClosingTag();
+ buffer.push(this.morph.endTag());
+ },
+
+ createElement: function() {
+ var buffer = this.renderToBuffer();
+ this.outerHTML = buffer.string();
+ this.clearBuffer();
+ },
+
+ domManager: DOMManager
+});
+
+/**
+ @class _MetamorphView
+ @namespace Ember
+ @extends Ember.View
+ @uses Ember._Metamorph
+ @private
+*/
+Ember._MetamorphView = Ember.View.extend(Ember._Metamorph);
+
+/**
+ @class _SimpleMetamorphView
+ @namespace Ember
+ @extends Ember.View
+ @uses Ember._Metamorph
+ @private
+*/
+Ember._SimpleMetamorphView = Ember.CoreView.extend(Ember._Metamorph);
+
+
+})();
+
+
+
+(function() {
+/*globals Handlebars */
+/*jshint newcap:false*/
+/**
+@module ember
+@submodule ember-handlebars
+*/
+
+var get = Ember.get, set = Ember.set, handlebarsGet = Ember.Handlebars.get;
+var Metamorph = requireModule('metamorph');
+function SimpleHandlebarsView(path, pathRoot, isEscaped, templateData) {
+ this.path = path;
+ this.pathRoot = pathRoot;
+ this.isEscaped = isEscaped;
+ this.templateData = templateData;
+
+ this.morph = Metamorph();
+ this.state = 'preRender';
+ this.updateId = null;
+}
+
+Ember._SimpleHandlebarsView = SimpleHandlebarsView;
+
+SimpleHandlebarsView.prototype = {
+ isVirtual: true,
+ isView: true,
+
+ destroy: function () {
+ if (this.updateId) {
+ Ember.run.cancel(this.updateId);
+ this.updateId = null;
+ }
+ this.morph = null;
+ },
+
+ propertyDidChange: Ember.K,
+
+ normalizedValue: function() {
+ var path = this.path,
+ pathRoot = this.pathRoot,
+ result, templateData;
+
+ // Use the pathRoot as the result if no path is provided. This
+ // happens if the path is `this`, which gets normalized into
+ // a `pathRoot` of the current Handlebars context and a path
+ // of `''`.
+ if (path === '') {
+ result = pathRoot;
+ } else {
+ templateData = this.templateData;
+ result = handlebarsGet(pathRoot, path, { data: templateData });
+ }
+
+ return result;
+ },
+
+ renderToBuffer: function(buffer) {
+ var string = '';
+
+ string += this.morph.startTag();
+ string += this.render();
+ string += this.morph.endTag();
+
+ buffer.push(string);
+ },
+
+ render: function() {
+ // If not invoked via a triple-mustache ({{{foo}}}), escape
+ // the content of the template.
+ var escape = this.isEscaped;
+ var result = this.normalizedValue();
+
+ if (result === null || result === undefined) {
+ result = "";
+ } else if (!(result instanceof Handlebars.SafeString)) {
+ result = String(result);
+ }
+
+ if (escape) { result = Handlebars.Utils.escapeExpression(result); }
+ return result;
+ },
+
+ rerender: function() {
+ switch(this.state) {
+ case 'preRender':
+ case 'destroyed':
+ break;
+ case 'inBuffer':
+ throw new Ember.Error("Something you did tried to replace an {{expression}} before it was inserted into the DOM.");
+ case 'hasElement':
+ case 'inDOM':
+ this.updateId = Ember.run.scheduleOnce('render', this, 'update');
+ break;
+ }
+
+ return this;
+ },
+
+ update: function () {
+ this.updateId = null;
+ this.morph.html(this.render());
+ },
+
+ transitionTo: function(state) {
+ this.state = state;
+ }
+};
+
+var states = Ember.View.cloneStates(Ember.View.states), merge = Ember.merge;
+
+merge(states._default, {
+ rerenderIfNeeded: Ember.K
+});
+
+merge(states.inDOM, {
+ rerenderIfNeeded: function(view) {
+ if (get(view, 'normalizedValue') !== view._lastNormalizedValue) {
+ view.rerender();
+ }
+ }
+});
+
+/**
+ `Ember._HandlebarsBoundView` is a private view created by the Handlebars
+ `{{bind}}` helpers that is used to keep track of bound properties.
+
+ Every time a property is bound using a `{{mustache}}`, an anonymous subclass
+ of `Ember._HandlebarsBoundView` is created with the appropriate sub-template
+ and context set up. When the associated property changes, just the template
+ for this view will re-render.
+
+ @class _HandlebarsBoundView
+ @namespace Ember
+ @extends Ember._MetamorphView
+ @private
+*/
+Ember._HandlebarsBoundView = Ember._MetamorphView.extend({
+ instrumentName: 'render.boundHandlebars',
+ states: states,
+
+ /**
+ The function used to determine if the `displayTemplate` or
+ `inverseTemplate` should be rendered. This should be a function that takes
+ a value and returns a Boolean.
+
+ @property shouldDisplayFunc
+ @type Function
+ @default null
+ */
+ shouldDisplayFunc: null,
+
+ /**
+ Whether the template rendered by this view gets passed the context object
+ of its parent template, or gets passed the value of retrieving `path`
+ from the `pathRoot`.
+
+ For example, this is true when using the `{{#if}}` helper, because the
+ template inside the helper should look up properties relative to the same
+ object as outside the block. This would be `false` when used with `{{#with
+ foo}}` because the template should receive the object found by evaluating
+ `foo`.
+
+ @property preserveContext
+ @type Boolean
+ @default false
+ */
+ preserveContext: false,
+
+ /**
+ If `preserveContext` is true, this is the object that will be used
+ to render the template.
+
+ @property previousContext
+ @type Object
+ */
+ previousContext: null,
+
+ /**
+ The template to render when `shouldDisplayFunc` evaluates to `true`.
+
+ @property displayTemplate
+ @type Function
+ @default null
+ */
+ displayTemplate: null,
+
+ /**
+ The template to render when `shouldDisplayFunc` evaluates to `false`.
+
+ @property inverseTemplate
+ @type Function
+ @default null
+ */
+ inverseTemplate: null,
+
+
+ /**
+ The path to look up on `pathRoot` that is passed to
+ `shouldDisplayFunc` to determine which template to render.
+
+ In addition, if `preserveContext` is `false,` the object at this path will
+ be passed to the template when rendering.
+
+ @property path
+ @type String
+ @default null
+ */
+ path: null,
+
+ /**
+ The object from which the `path` will be looked up. Sometimes this is the
+ same as the `previousContext`, but in cases where this view has been
+ generated for paths that start with a keyword such as `view` or
+ `controller`, the path root will be that resolved object.
+
+ @property pathRoot
+ @type Object
+ */
+ pathRoot: null,
+
+ normalizedValue: Ember.computed(function() {
+ var path = get(this, 'path'),
+ pathRoot = get(this, 'pathRoot'),
+ valueNormalizer = get(this, 'valueNormalizerFunc'),
+ result, templateData;
+
+ // Use the pathRoot as the result if no path is provided. This
+ // happens if the path is `this`, which gets normalized into
+ // a `pathRoot` of the current Handlebars context and a path
+ // of `''`.
+ if (path === '') {
+ result = pathRoot;
+ } else {
+ templateData = get(this, 'templateData');
+ result = handlebarsGet(pathRoot, path, { data: templateData });
+ }
+
+ return valueNormalizer ? valueNormalizer(result) : result;
+ }).property('path', 'pathRoot', 'valueNormalizerFunc').volatile(),
+
+ rerenderIfNeeded: function() {
+ this.currentState.rerenderIfNeeded(this);
+ },
+
+ /**
+ Determines which template to invoke, sets up the correct state based on
+ that logic, then invokes the default `Ember.View` `render` implementation.
+
+ This method will first look up the `path` key on `pathRoot`,
+ then pass that value to the `shouldDisplayFunc` function. If that returns
+ `true,` the `displayTemplate` function will be rendered to DOM. Otherwise,
+ `inverseTemplate`, if specified, will be rendered.
+
+ For example, if this `Ember._HandlebarsBoundView` represented the `{{#with
+ foo}}` helper, it would look up the `foo` property of its context, and
+ `shouldDisplayFunc` would always return true. The object found by looking
+ up `foo` would be passed to `displayTemplate`.
+
+ @method render
+ @param {Ember.RenderBuffer} buffer
+ */
+ render: function(buffer) {
+ // If not invoked via a triple-mustache ({{{foo}}}), escape
+ // the content of the template.
+ var escape = get(this, 'isEscaped');
+
+ var shouldDisplay = get(this, 'shouldDisplayFunc'),
+ preserveContext = get(this, 'preserveContext'),
+ context = get(this, 'previousContext');
+
+ var inverseTemplate = get(this, 'inverseTemplate'),
+ displayTemplate = get(this, 'displayTemplate');
+
+ var result = get(this, 'normalizedValue');
+ this._lastNormalizedValue = result;
+
+ // First, test the conditional to see if we should
+ // render the template or not.
+ if (shouldDisplay(result)) {
+ set(this, 'template', displayTemplate);
+
+ // If we are preserving the context (for example, if this
+ // is an #if block, call the template with the same object.
+ if (preserveContext) {
+ set(this, '_context', context);
+ } else {
+ // Otherwise, determine if this is a block bind or not.
+ // If so, pass the specified object to the template
+ if (displayTemplate) {
+ set(this, '_context', result);
+ } else {
+ // This is not a bind block, just push the result of the
+ // expression to the render context and return.
+ if (result === null || result === undefined) {
+ result = "";
+ } else if (!(result instanceof Handlebars.SafeString)) {
+ result = String(result);
+ }
+
+ if (escape) { result = Handlebars.Utils.escapeExpression(result); }
+ buffer.push(result);
+ return;
+ }
+ }
+ } else if (inverseTemplate) {
+ set(this, 'template', inverseTemplate);
+
+ if (preserveContext) {
+ set(this, '_context', context);
+ } else {
+ set(this, '_context', result);
+ }
+ } else {
+ set(this, 'template', function() { return ''; });
+ }
+
+ return this._super(buffer);
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-handlebars
+*/
+
+var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt;
+var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.normalizePath;
+var forEach = Ember.ArrayPolyfills.forEach;
+
+var EmberHandlebars = Ember.Handlebars, helpers = EmberHandlebars.helpers;
+
+// Binds a property into the DOM. This will create a hook in DOM that the
+// KVO system will look for and update if the property changes.
+function bind(property, options, preserveContext, shouldDisplay, valueNormalizer, childProperties) {
+ var data = options.data,
+ fn = options.fn,
+ inverse = options.inverse,
+ view = data.view,
+ currentContext = this,
+ normalized, observer, i;
+
+ normalized = normalizePath(currentContext, property, data);
+
+ // Set up observers for observable objects
+ if ('object' === typeof this) {
+ if (data.insideGroup) {
+ observer = function() {
+ Ember.run.once(view, 'rerender');
+ };
+
+ var template, context, result = handlebarsGet(currentContext, property, options);
+
+ result = valueNormalizer(result);
+
+ context = preserveContext ? currentContext : result;
+ if (shouldDisplay(result)) {
+ template = fn;
+ } else if (inverse) {
+ template = inverse;
+ }
+
+ template(context, { data: options.data });
+ } else {
+ // Create the view that will wrap the output of this template/property
+ // and add it to the nearest view's childViews array.
+ // See the documentation of Ember._HandlebarsBoundView for more.
+ var bindView = view.createChildView(Ember._HandlebarsBoundView, {
+ preserveContext: preserveContext,
+ shouldDisplayFunc: shouldDisplay,
+ valueNormalizerFunc: valueNormalizer,
+ displayTemplate: fn,
+ inverseTemplate: inverse,
+ path: property,
+ pathRoot: currentContext,
+ previousContext: currentContext,
+ isEscaped: !options.hash.unescaped,
+ templateData: options.data
+ });
+
+ view.appendChild(bindView);
+
+ observer = function() {
+ Ember.run.scheduleOnce('render', bindView, 'rerenderIfNeeded');
+ };
+ }
+
+ // Observes the given property on the context and
+ // tells the Ember._HandlebarsBoundView to re-render. If property
+ // is an empty string, we are printing the current context
+ // object ({{this}}) so updating it is not our responsibility.
+ if (normalized.path !== '') {
+ view.registerObserver(normalized.root, normalized.path, observer);
+ if (childProperties) {
+ for (i=0; i<childProperties.length; i++) {
+ view.registerObserver(normalized.root, normalized.path+'.'+childProperties[i], observer);
+ }
+ }
+ }
+ } else {
+ // The object is not observable, so just render it out and
+ // be done with it.
+ data.buffer.push(handlebarsGet(currentContext, property, options));
+ }
+}
+
+function simpleBind(property, options) {
+ var data = options.data,
+ view = data.view,
+ currentContext = this,
+ normalized, observer;
+
+ normalized = normalizePath(currentContext, property, data);
+
+ // Set up observers for observable objects
+ if ('object' === typeof this) {
+ if (data.insideGroup) {
+ observer = function() {
+ Ember.run.once(view, 'rerender');
+ };
+
+ var result = handlebarsGet(currentContext, property, options);
+ if (result === null || result === undefined) { result = ""; }
+ data.buffer.push(result);
+ } else {
+ var bindView = new Ember._SimpleHandlebarsView(
+ property, currentContext, !options.hash.unescaped, options.data
+ );
+
+ bindView._parentView = view;
+ view.appendChild(bindView);
+
+ observer = function() {
+ Ember.run.scheduleOnce('render', bindView, 'rerender');
+ };
+ }
+
+ // Observes the given property on the context and
+ // tells the Ember._HandlebarsBoundView to re-render. If property
+ // is an empty string, we are printing the current context
+ // object ({{this}}) so updating it is not our responsibility.
+ if (normalized.path !== '') {
+ view.registerObserver(normalized.root, normalized.path, observer);
+ }
+ } else {
+ // The object is not observable, so just render it out and
+ // be done with it.
+ data.buffer.push(handlebarsGet(currentContext, property, options));
+ }
+}
+
+/**
+ @private
+
+ '_triageMustache' is used internally select between a binding and helper for
+ the given context. Until this point, it would be hard to determine if the
+ mustache is a property reference or a regular helper reference. This triage
+ helper resolves that.
+
+ This would not be typically invoked by directly.
+
+ @method _triageMustache
+ @for Ember.Handlebars.helpers
+ @param {String} property Property/helperID to triage
+ @param {Function} fn Context to provide for rendering
+ @return {String} HTML string
+*/
+EmberHandlebars.registerHelper('_triageMustache', function(property, fn) {
+ Ember.assert("You cannot pass more than one argument to the _triageMustache helper", arguments.length <= 2);
+ if (helpers[property]) {
+ return helpers[property].call(this, fn);
+ }
+ else {
+ return helpers.bind.apply(this, arguments);
+ }
+});
+
+/**
+ @private
+
+ `bind` can be used to display a value, then update that value if it
+ changes. For example, if you wanted to print the `title` property of
+ `content`:
+
+ ```handlebars
+ {{bind "content.title"}}
+ ```
+
+ This will return the `title` property as a string, then create a new observer
+ at the specified path. If it changes, it will update the value in DOM. Note
+ that if you need to support IE7 and IE8 you must modify the model objects
+ properties using `Ember.get()` and `Ember.set()` for this to work as it
+ relies on Ember's KVO system. For all other browsers this will be handled for
+ you automatically.
+
+ @method bind
+ @for Ember.Handlebars.helpers
+ @param {String} property Property to bind
+ @param {Function} fn Context to provide for rendering
+ @return {String} HTML string
+*/
+EmberHandlebars.registerHelper('bind', function(property, options) {
+ Ember.assert("You cannot pass more than one argument to the bind helper", arguments.length <= 2);
+
+ var context = (options.contexts && options.contexts[0]) || this;
+
+ if (!options.fn) {
+ return simpleBind.call(context, property, options);
+ }
+
+ return bind.call(context, property, options, false, function(result) {
+ return !Ember.isNone(result);
+ });
+});
+
+/**
+ @private
+
+ Use the `boundIf` helper to create a conditional that re-evaluates
+ whenever the truthiness of the bound value changes.
+
+ ```handlebars
+ {{#boundIf "content.shouldDisplayTitle"}}
+ {{content.title}}
+ {{/boundIf}}
+ ```
+
+ @method boundIf
+ @for Ember.Handlebars.helpers
+ @param {String} property Property to bind
+ @param {Function} fn Context to provide for rendering
+ @return {String} HTML string
+*/
+EmberHandlebars.registerHelper('boundIf', function(property, fn) {
+ var context = (fn.contexts && fn.contexts[0]) || this;
+ var func = function(result) {
+ var truthy = result && get(result, 'isTruthy');
+ if (typeof truthy === 'boolean') { return truthy; }
+
+ if (Ember.isArray(result)) {
+ return get(result, 'length') !== 0;
+ } else {
+ return !!result;
+ }
+ };
+
+ return bind.call(context, property, fn, true, func, func, ['isTruthy', 'length']);
+});
+
+/**
+ @method with
+ @for Ember.Handlebars.helpers
+ @param {Function} context
+ @param {Hash} options
+ @return {String} HTML string
+*/
+EmberHandlebars.registerHelper('with', function(context, options) {
+ if (arguments.length === 4) {
+ var keywordName, path, rootPath, normalized;
+
+ Ember.assert("If you pass more than one argument to the with helper, it must be in the form #with foo as bar", arguments[1] === "as");
+ options = arguments[3];
+ keywordName = arguments[2];
+ path = arguments[0];
+
+ Ember.assert("You must pass a block to the with helper", options.fn && options.fn !== Handlebars.VM.noop);
+
+ if (Ember.isGlobalPath(path)) {
+ Ember.bind(options.data.keywords, keywordName, path);
+ } else {
+ normalized = normalizePath(this, path, options.data);
+ path = normalized.path;
+ rootPath = normalized.root;
+
+ // This is a workaround for the fact that you cannot bind separate objects
+ // together. When we implement that functionality, we should use it here.
+ var contextKey = Ember.$.expando + Ember.guidFor(rootPath);
+ options.data.keywords[contextKey] = rootPath;
+
+ // if the path is '' ("this"), just bind directly to the current context
+ var contextPath = path ? contextKey + '.' + path : contextKey;
+ Ember.bind(options.data.keywords, keywordName, contextPath);
+ }
+
+ return bind.call(this, path, options, true, function(result) {
+ return !Ember.isNone(result);
+ });
+ } else {
+ Ember.assert("You must pass exactly one argument to the with helper", arguments.length === 2);
+ Ember.assert("You must pass a block to the with helper", options.fn && options.fn !== Handlebars.VM.noop);
+ return helpers.bind.call(options.contexts[0], context, options);
+ }
+});
+
+
+/**
+ See `boundIf`
+
+ @method if
+ @for Ember.Handlebars.helpers
+ @param {Function} context
+ @param {Hash} options
+ @return {String} HTML string
+*/
+EmberHandlebars.registerHelper('if', function(context, options) {
+ Ember.assert("You must pass exactly one argument to the if helper", arguments.length === 2);
+ Ember.assert("You must pass a block to the if helper", options.fn && options.fn !== Handlebars.VM.noop);
+
+ return helpers.boundIf.call(options.contexts[0], context, options);
+});
+
+/**
+ @method unless
+ @for Ember.Handlebars.helpers
+ @param {Function} context
+ @param {Hash} options
+ @return {String} HTML string
+*/
+EmberHandlebars.registerHelper('unless', function(context, options) {
+ Ember.assert("You must pass exactly one argument to the unless helper", arguments.length === 2);
+ Ember.assert("You must pass a block to the unless helper", options.fn && options.fn !== Handlebars.VM.noop);
+
+ var fn = options.fn, inverse = options.inverse;
+
+ options.fn = inverse;
+ options.inverse = fn;
+
+ return helpers.boundIf.call(options.contexts[0], context, options);
+});
+
+/**
+ `bindAttr` allows you to create a binding between DOM element attributes and
+ Ember objects. For example:
+
+ ```handlebars
+ <img {{bindAttr src="imageUrl" alt="imageTitle"}}>
+ ```
+
+ The above handlebars template will fill the `<img>`'s `src` attribute will
+ the value of the property referenced with `"imageUrl"` and its `alt`
+ attribute with the value of the property referenced with `"imageTitle"`.
+
+ If the rendering context of this template is the following object:
+
+ ```javascript
+ {
+ imageUrl: 'http://lolcats.info/haz-a-funny',
+ imageTitle: 'A humorous image of a cat'
+ }
+ ```
+
+ The resulting HTML output will be:
+
+ ```html
+ <img src="http://lolcats.info/haz-a-funny" alt="A humorous image of a cat">
+ ```
+
+ `bindAttr` cannot redeclare existing DOM element attributes. The use of `src`
+ in the following `bindAttr` example will be ignored and the hard coded value
+ of `src="/failwhale.gif"` will take precedence:
+
+ ```handlebars
+ <img src="/failwhale.gif" {{bindAttr src="imageUrl" alt="imageTitle"}}>
+ ```
+
+ ### `bindAttr` and the `class` attribute
+
+ `bindAttr` supports a special syntax for handling a number of cases unique
+ to the `class` DOM element attribute. The `class` attribute combines
+ multiple discreet values into a single attribute as a space-delimited
+ list of strings. Each string can be:
+
+ * a string return value of an object's property.
+ * a boolean return value of an object's property
+ * a hard-coded value
+
+ A string return value works identically to other uses of `bindAttr`. The
+ return value of the property will become the value of the attribute. For
+ example, the following view and template:
+
+ ```javascript
+ AView = Ember.View.extend({
+ someProperty: function(){
+ return "aValue";
+ }.property()
+ })
+ ```
+
+ ```handlebars
+ <img {{bindAttr class="view.someProperty}}>
+ ```
+
+ Result in the following rendered output:
+
+ ```html
+ <img class="aValue">
+ ```
+
+ A boolean return value will insert a specified class name if the property
+ returns `true` and remove the class name if the property returns `false`.
+
+ A class name is provided via the syntax
+ `somePropertyName:class-name-if-true`.
+
+ ```javascript
+ AView = Ember.View.extend({
+ someBool: true
+ })
+ ```
+
+ ```handlebars
+ <img {{bindAttr class="view.someBool:class-name-if-true"}}>
+ ```
+
+ Result in the following rendered output:
+
+ ```html
+ <img class="class-name-if-true">
+ ```
+
+ An additional section of the binding can be provided if you want to
+ replace the existing class instead of removing it when the boolean
+ value changes:
+
+ ```handlebars
+ <img {{bindAttr class="view.someBool:class-name-if-true:class-name-if-false"}}>
+ ```
+
+ A hard-coded value can be used by prepending `:` to the desired
+ class name: `:class-name-to-always-apply`.
+
+ ```handlebars
+ <img {{bindAttr class=":class-name-to-always-apply"}}>
+ ```
+
+ Results in the following rendered output:
+
+ ```html
+ <img class=":class-name-to-always-apply">
+ ```
+
+ All three strategies - string return value, boolean return value, and
+ hard-coded value – can be combined in a single declaration:
+
+ ```handlebars
+ <img {{bindAttr class=":class-name-to-always-apply view.someBool:class-name-if-true view.someProperty"}}>
+ ```
+
+ @method bindAttr
+ @for Ember.Handlebars.helpers
+ @param {Hash} options
+ @return {String} HTML string
+*/
+EmberHandlebars.registerHelper('bindAttr', function(options) {
+
+ var attrs = options.hash;
+
+ Ember.assert("You must specify at least one hash argument to bindAttr", !!Ember.keys(attrs).length);
+
+ var view = options.data.view;
+ var ret = [];
+ var ctx = this;
+
+ // Generate a unique id for this element. This will be added as a
+ // data attribute to the element so it can be looked up when
+ // the bound property changes.
+ var dataId = ++Ember.uuid;
+
+ // Handle classes differently, as we can bind multiple classes
+ var classBindings = attrs['class'];
+ if (classBindings !== null && classBindings !== undefined) {
+ var classResults = EmberHandlebars.bindClasses(this, classBindings, view, dataId, options);
+
+ ret.push('class="' + Handlebars.Utils.escapeExpression(classResults.join(' ')) + '"');
+ delete attrs['class'];
+ }
+
+ var attrKeys = Ember.keys(attrs);
+
+ // For each attribute passed, create an observer and emit the
+ // current value of the property as an attribute.
+ forEach.call(attrKeys, function(attr) {
+ var path = attrs[attr],
+ normalized;
+
+ Ember.assert(fmt("You must provide a String for a bound attribute, not %@", [path]), typeof path === 'string');
+
+ normalized = normalizePath(ctx, path, options.data);
+
+ var value = (path === 'this') ? normalized.root : handlebarsGet(ctx, path, options),
+ type = Ember.typeOf(value);
+
+ Ember.assert(fmt("Attributes must be numbers, strings or booleans, not %@", [value]), value === null || value === undefined || type === 'number' || type === 'string' || type === 'boolean');
+
+ var observer, invoker;
+
+ observer = function observer() {
+ var result = handlebarsGet(ctx, path, options);
+
+ Ember.assert(fmt("Attributes must be numbers, strings or booleans, not %@", [result]), result === null || result === undefined || typeof result === 'number' || typeof result === 'string' || typeof result === 'boolean');
+
+ var elem = view.$("[data-bindattr-" + dataId + "='" + dataId + "']");
+
+ // If we aren't able to find the element, it means the element
+ // to which we were bound has been removed from the view.
+ // In that case, we can assume the template has been re-rendered
+ // and we need to clean up the observer.
+ if (!elem || elem.length === 0) {
+ Ember.removeObserver(normalized.root, normalized.path, invoker);
+ return;
+ }
+
+ Ember.View.applyAttributeBindings(elem, attr, result);
+ };
+
+ invoker = function() {
+ Ember.run.scheduleOnce('render', observer);
+ };
+
+ // Add an observer to the view for when the property changes.
+ // When the observer fires, find the element using the
+ // unique data id and update the attribute to the new value.
+ if (path !== 'this') {
+ view.registerObserver(normalized.root, normalized.path, invoker);
+ }
+
+ // if this changes, also change the logic in ember-views/lib/views/view.js
+ if ((type === 'string' || (type === 'number' && !isNaN(value)))) {
+ ret.push(attr + '="' + Handlebars.Utils.escapeExpression(value) + '"');
+ } else if (value && type === 'boolean') {
+ // The developer controls the attr name, so it should always be safe
+ ret.push(attr + '="' + attr + '"');
+ }
+ }, this);
+
+ // Add the unique identifier
+ // NOTE: We use all lower-case since Firefox has problems with mixed case in SVG
+ ret.push('data-bindattr-' + dataId + '="' + dataId + '"');
+ return new EmberHandlebars.SafeString(ret.join(' '));
+});
+
+/**
+ @private
+
+ Helper that, given a space-separated string of property paths and a context,
+ returns an array of class names. Calling this method also has the side
+ effect of setting up observers at those property paths, such that if they
+ change, the correct class name will be reapplied to the DOM element.
+
+ For example, if you pass the string "fooBar", it will first look up the
+ "fooBar" value of the context. If that value is true, it will add the
+ "foo-bar" class to the current element (i.e., the dasherized form of
+ "fooBar"). If the value is a string, it will add that string as the class.
+ Otherwise, it will not add any new class name.
+
+ @method bindClasses
+ @for Ember.Handlebars
+ @param {Ember.Object} context The context from which to lookup properties
+ @param {String} classBindings A string, space-separated, of class bindings
+ to use
+ @param {Ember.View} view The view in which observers should look for the
+ element to update
+ @param {Srting} bindAttrId Optional bindAttr id used to lookup elements
+ @return {Array} An array of class names to add
+*/
+EmberHandlebars.bindClasses = function(context, classBindings, view, bindAttrId, options) {
+ var ret = [], newClass, value, elem;
+
+ // Helper method to retrieve the property from the context and
+ // determine which class string to return, based on whether it is
+ // a Boolean or not.
+ var classStringForPath = function(root, parsedPath, options) {
+ var val,
+ path = parsedPath.path;
+
+ if (path === 'this') {
+ val = root;
+ } else if (path === '') {
+ val = true;
+ } else {
+ val = handlebarsGet(root, path, options);
+ }
+
+ return Ember.View._classStringForValue(path, val, parsedPath.className, parsedPath.falsyClassName);
+ };
+
+ // For each property passed, loop through and setup
+ // an observer.
+ forEach.call(classBindings.split(' '), function(binding) {
+
+ // Variable in which the old class value is saved. The observer function
+ // closes over this variable, so it knows which string to remove when
+ // the property changes.
+ var oldClass;
+
+ var observer, invoker;
+
+ var parsedPath = Ember.View._parsePropertyPath(binding),
+ path = parsedPath.path,
+ pathRoot = context,
+ normalized;
+
+ if (path !== '' && path !== 'this') {
+ normalized = normalizePath(context, path, options.data);
+
+ pathRoot = normalized.root;
+ path = normalized.path;
+ }
+
+ // Set up an observer on the context. If the property changes, toggle the
+ // class name.
+ observer = function() {
+ // Get the current value of the property
+ newClass = classStringForPath(context, parsedPath, options);
+ elem = bindAttrId ? view.$("[data-bindattr-" + bindAttrId + "='" + bindAttrId + "']") : view.$();
+
+ // If we can't find the element anymore, a parent template has been
+ // re-rendered and we've been nuked. Remove the observer.
+ if (!elem || elem.length === 0) {
+ Ember.removeObserver(pathRoot, path, invoker);
+ } else {
+ // If we had previously added a class to the element, remove it.
+ if (oldClass) {
+ elem.removeClass(oldClass);
+ }
+
+ // If necessary, add a new class. Make sure we keep track of it so
+ // it can be removed in the future.
+ if (newClass) {
+ elem.addClass(newClass);
+ oldClass = newClass;
+ } else {
+ oldClass = null;
+ }
+ }
+ };
+
+ invoker = function() {
+ Ember.run.scheduleOnce('render', observer);
+ };
+
+ if (path !== '' && path !== 'this') {
+ view.registerObserver(pathRoot, path, invoker);
+ }
+
+ // We've already setup the observer; now we just need to figure out the
+ // correct behavior right now on the first pass through.
+ value = classStringForPath(context, parsedPath, options);
+
+ if (value) {
+ ret.push(value);
+
+ // Make sure we save the current value so that it can be removed if the
+ // observer fires.
+ oldClass = value;
+ }
+ });
+
+ return ret;
+};
+
+
+})();
+
+
+
+(function() {
+/*globals Handlebars */
+
+// TODO: Don't require the entire module
+/**
+@module ember
+@submodule ember-handlebars
+*/
+
+var get = Ember.get, set = Ember.set;
+var PARENT_VIEW_PATH = /^parentView\./;
+var EmberHandlebars = Ember.Handlebars;
+
+EmberHandlebars.ViewHelper = Ember.Object.create({
+
+ propertiesFromHTMLOptions: function(options, thisContext) {
+ var hash = options.hash, data = options.data;
+ var extensions = {},
+ classes = hash['class'],
+ dup = false;
+
+ if (hash.id) {
+ extensions.elementId = hash.id;
+ dup = true;
+ }
+
+ if (classes) {
+ classes = classes.split(' ');
+ extensions.classNames = classes;
+ dup = true;
+ }
+
+ if (hash.classBinding) {
+ extensions.classNameBindings = hash.classBinding.split(' ');
+ dup = true;
+ }
+
+ if (hash.classNameBindings) {
+ if (extensions.classNameBindings === undefined) extensions.classNameBindings = [];
+ extensions.classNameBindings = extensions.classNameBindings.concat(hash.classNameBindings.split(' '));
+ dup = true;
+ }
+
+ if (hash.attributeBindings) {
+ Ember.assert("Setting 'attributeBindings' via Handlebars is not allowed. Please subclass Ember.View and set it there instead.");
+ extensions.attributeBindings = null;
+ dup = true;
+ }
+
+ if (dup) {
+ hash = Ember.$.extend({}, hash);
+ delete hash.id;
+ delete hash['class'];
+ delete hash.classBinding;
+ }
+
+ // Set the proper context for all bindings passed to the helper. This applies to regular attribute bindings
+ // as well as class name bindings. If the bindings are local, make them relative to the current context
+ // instead of the view.
+ var path;
+
+ // Evaluate the context of regular attribute bindings:
+ for (var prop in hash) {
+ if (!hash.hasOwnProperty(prop)) { continue; }
+
+ // Test if the property ends in "Binding"
+ if (Ember.IS_BINDING.test(prop) && typeof hash[prop] === 'string') {
+ path = this.contextualizeBindingPath(hash[prop], data);
+ if (path) { hash[prop] = path; }
+ }
+ }
+
+ // Evaluate the context of class name bindings:
+ if (extensions.classNameBindings) {
+ for (var b in extensions.classNameBindings) {
+ var full = extensions.classNameBindings[b];
+ if (typeof full === 'string') {
+ // Contextualize the path of classNameBinding so this:
+ //
+ // classNameBinding="isGreen:green"
+ //
+ // is converted to this:
+ //
+ // classNameBinding="_parentView.context.isGreen:green"
+ var parsedPath = Ember.View._parsePropertyPath(full);
+ path = this.contextualizeBindingPath(parsedPath.path, data);
+ if (path) { extensions.classNameBindings[b] = path + parsedPath.classNames; }
+ }
+ }
+ }
+
+ return Ember.$.extend(hash, extensions);
+ },
+
+ // Transform bindings from the current context to a context that can be evaluated within the view.
+ // Returns null if the path shouldn't be changed.
+ //
+ // TODO: consider the addition of a prefix that would allow this method to return `path`.
+ contextualizeBindingPath: function(path, data) {
+ var normalized = Ember.Handlebars.normalizePath(null, path, data);
+ if (normalized.isKeyword) {
+ return 'templateData.keywords.' + path;
+ } else if (Ember.isGlobalPath(path)) {
+ return null;
+ } else if (path === 'this') {
+ return '_parentView.context';
+ } else {
+ return '_parentView.context.' + path;
+ }
+ },
+
+ helper: function(thisContext, path, options) {
+ var inverse = options.inverse,
+ data = options.data,
+ view = data.view,
+ fn = options.fn,
+ hash = options.hash,
+ newView;
+
+ if ('string' === typeof path) {
+ newView = EmberHandlebars.get(thisContext, path, options);
+ Ember.assert("Unable to find view at path '" + path + "'", !!newView);
+ } else {
+ newView = path;
+ }
+
+ Ember.assert(Ember.String.fmt('You must pass a view to the #view helper, not %@ (%@)', [path, newView]), Ember.View.detect(newView) || Ember.View.detectInstance(newView));
+
+ var viewOptions = this.propertiesFromHTMLOptions(options, thisContext);
+ var currentView = data.view;
+ viewOptions.templateData = options.data;
+ var newViewProto = newView.proto ? newView.proto() : newView;
+
+ if (fn) {
+ Ember.assert("You cannot provide a template block if you also specified a templateName", !get(viewOptions, 'templateName') && !get(newViewProto, 'templateName'));
+ viewOptions.template = fn;
+ }
+
+ // We only want to override the `_context` computed property if there is
+ // no specified controller. See View#_context for more information.
+ if (!newViewProto.controller && !newViewProto.controllerBinding && !viewOptions.controller && !viewOptions.controllerBinding) {
+ viewOptions._context = thisContext;
+ }
+
+ currentView.appendChild(newView, viewOptions);
+ }
+});
+
+/**
+ `{{view}}` inserts a new instance of `Ember.View` into a template passing its
+ options to the `Ember.View`'s `create` method and using the supplied block as
+ the view's own template.
+
+ An empty `<body>` and the following template:
+
+ ```handlebars
+ A span:
+ {{#view tagName="span"}}
+ hello.
+ {{/view}}
+ ```
+
+ Will result in HTML structure:
+
+ ```html
+ <body>
+ <!-- Note: the handlebars template script
+ also results in a rendered Ember.View
+ which is the outer <div> here -->
+
+ <div class="ember-view">
+ A span:
+ <span id="ember1" class="ember-view">
+ Hello.
+ </span>
+ </div>
+ </body>
+ ```
+
+ ### `parentView` setting
+
+ The `parentView` property of the new `Ember.View` instance created through
+ `{{view}}` will be set to the `Ember.View` instance of the template where
+ `{{view}}` was called.
+
+ ```javascript
+ aView = Ember.View.create({
+ template: Ember.Handlebars.compile("{{#view}} my parent: {{parentView.elementId}} {{/view}}")
+ });
+
+ aView.appendTo('body');
+ ```
+
+ Will result in HTML structure:
+
+ ```html
+ <div id="ember1" class="ember-view">
+ <div id="ember2" class="ember-view">
+ my parent: ember1
+ </div>
+ </div>
+ ```
+
+ ### Setting CSS id and class attributes
+
+ The HTML `id` attribute can be set on the `{{view}}`'s resulting element with
+ the `id` option. This option will _not_ be passed to `Ember.View.create`.
+
+ ```handlebars
+ {{#view tagName="span" id="a-custom-id"}}
+ hello.
+ {{/view}}
+ ```
+
+ Results in the following HTML structure:
+
+ ```html
+ <div class="ember-view">
+ <span id="a-custom-id" class="ember-view">
+ hello.
+ </span>
+ </div>
+ ```
+
+ The HTML `class` attribute can be set on the `{{view}}`'s resulting element
+ with the `class` or `classNameBindings` options. The `class` option will
+ directly set the CSS `class` attribute and will not be passed to
+ `Ember.View.create`. `classNameBindings` will be passed to `create` and use
+ `Ember.View`'s class name binding functionality:
+
+ ```handlebars
+ {{#view tagName="span" class="a-custom-class"}}
+ hello.
+ {{/view}}
+ ```
+
+ Results in the following HTML structure:
+
+ ```html
+ <div class="ember-view">
+ <span id="ember2" class="ember-view a-custom-class">
+ hello.
+ </span>
+ </div>
+ ```
+
+ ### Supplying a different view class
+
+ `{{view}}` can take an optional first argument before its supplied options to
+ specify a path to a custom view class.
+
+ ```handlebars
+ {{#view "MyApp.CustomView"}}
+ hello.
+ {{/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:
+
+ ```javascript
+ MyApp = Ember.Application.create({});
+ MyApp.OuterView = Ember.View.extend({
+ innerViewClass: Ember.View.extend({
+ classNames: ['a-custom-view-class-as-property']
+ }),
+ template: Ember.Handlebars.compile('{{#view "innerViewClass"}} hi {{/view}}')
+ });
+
+ MyApp.OuterView.create().appendTo('body');
+ ```
+
+ Will result in the following HTML:
+
+ ```html
+ <div id="ember1" class="ember-view">
+ <div id="ember2" class="ember-view a-custom-view-class-as-property">
+ hi
+ </div>
+ </div>
+ ```
+
+ ### Blockless use
+
+ If you supply a custom `Ember.View` subclass that specifies its own template
+ or provide a `templateName` option to `{{view}}` it can be used without
+ supplying a block. Attempts to use both a `templateName` option and supply a
+ block will throw an error.
+
+ ```handlebars
+ {{view "MyApp.ViewWithATemplateDefined"}}
+ ```
+
+ ### `viewName` property
+
+ You can supply a `viewName` option to `{{view}}`. The `Ember.View` instance
+ will be referenced as a property of its parent view by this name.
+
+ ```javascript
+ aView = Ember.View.create({
+ template: Ember.Handlebars.compile('{{#view viewName="aChildByName"}} hi {{/view}}')
+ });
+
+ aView.appendTo('body');
+ aView.get('aChildByName') // the instance of Ember.View created by {{view}} helper
+ ```
+
+ @method view
+ @for Ember.Handlebars.helpers
+ @param {String} path
+ @param {Hash} options
+ @return {String} HTML string
+*/
+EmberHandlebars.registerHelper('view', function(path, options) {
+ Ember.assert("The view helper only takes a single argument", arguments.length <= 2);
+
+ // If no path is provided, treat path param as options.
+ if (path && path.data && path.data.isRenderData) {
+ options = path;
+ path = "Ember.View";
+ }
+
+ return EmberHandlebars.ViewHelper.helper(this, path, options);
+});
+
+
+})();
+
+
+
+(function() {
+/*globals Handlebars */
+
+// TODO: Don't require all of this module
+/**
+@module ember
+@submodule ember-handlebars
+*/
+
+var get = Ember.get, handlebarsGet = Ember.Handlebars.get, fmt = Ember.String.fmt;
+
+/**
+ `{{collection}}` is a `Ember.Handlebars` helper for adding instances of
+ `Ember.CollectionView` to a template. See `Ember.CollectionView` for
+ additional information on how a `CollectionView` functions.
+
+ `{{collection}}`'s primary use is as a block helper with a `contentBinding`
+ option pointing towards an `Ember.Array`-compatible object. An `Ember.View`
+ instance will be created for each item in its `content` property. Each view
+ will have its own `content` property set to the appropriate item in the
+ collection.
+
+ The provided block will be applied as the template for each item's view.
+
+ Given an empty `<body>` the following template:
+
+ ```handlebars
+ {{#collection contentBinding="App.items"}}
+ Hi {{view.content.name}}
+ {{/collection}}
+ ```
+
+ And the following application code
+
+ ```javascript
+ App = Ember.Application.create()
+ App.items = [
+ Ember.Object.create({name: 'Dave'}),
+ Ember.Object.create({name: 'Mary'}),
+ Ember.Object.create({name: 'Sara'})
+ ]
+ ```
+
+ Will result in the HTML structure below
+
+ ```html
+ <div class="ember-view">
+ <div class="ember-view">Hi Dave</div>
+ <div class="ember-view">Hi Mary</div>
+ <div class="ember-view">Hi Sara</div>
+ </div>
+ ```
+
+ ### Blockless Use
+
+ If you provide an `itemViewClass` option that has its own `template` you can
+ omit the block.
+
+ The following template:
+
+ ```handlebars
+ {{collection contentBinding="App.items" itemViewClass="App.AnItemView"}}
+ ```
+
+ And application code
+
+ ```javascript
+ App = Ember.Application.create();
+ App.items = [
+ Ember.Object.create({name: 'Dave'}),
+ Ember.Object.create({name: 'Mary'}),
+ Ember.Object.create({name: 'Sara'})
+ ];
+
+ App.AnItemView = Ember.View.extend({
+ template: Ember.Handlebars.compile("Greetings {{view.content.name}}")
+ });
+ ```
+
+ Will result in the HTML structure below
+
+ ```html
+ <div class="ember-view">
+ <div class="ember-view">Greetings Dave</div>
+ <div class="ember-view">Greetings Mary</div>
+ <div class="ember-view">Greetings Sara</div>
+ </div>
+ ```
+
+ ### Specifying a CollectionView subclass
+
+ By default the `{{collection}}` helper will create an instance of
+ `Ember.CollectionView`. You can supply a `Ember.CollectionView` subclass to
+ the helper by passing it as the first argument:
+
+ ```handlebars
+ {{#collection App.MyCustomCollectionClass contentBinding="App.items"}}
+ Hi {{view.content.name}}
+ {{/collection}}
+ ```
+
+ ### Forwarded `item.*`-named Options
+
+ As with the `{{view}}`, helper options passed to the `{{collection}}` will be
+ set on the resulting `Ember.CollectionView` as properties. Additionally,
+ options prefixed with `item` will be applied to the views rendered for each
+ item (note the camelcasing):
+
+ ```handlebars
+ {{#collection contentBinding="App.items"
+ itemTagName="p"
+ itemClassNames="greeting"}}
+ Howdy {{view.content.name}}
+ {{/collection}}
+ ```
+
+ Will result in the following HTML structure:
+
+ ```html
+ <div class="ember-view">
+ <p class="ember-view greeting">Howdy Dave</p>
+ <p class="ember-view greeting">Howdy Mary</p>
+ <p class="ember-view greeting">Howdy Sara</p>
+ </div>
+ ```
+
+ @method collection
+ @for Ember.Handlebars.helpers
+ @param {String} path
+ @param {Hash} options
+ @return {String} HTML string
+ @deprecated Use `{{each}}` helper instead.
+*/
+Ember.Handlebars.registerHelper('collection', function(path, options) {
+ Ember.deprecate("Using the {{collection}} helper without specifying a class has been deprecated as the {{each}} helper now supports the same functionality.", path !== 'collection');
+
+ // If no path is provided, treat path param as options.
+ if (path && path.data && path.data.isRenderData) {
+ options = path;
+ path = undefined;
+ Ember.assert("You cannot pass more than one argument to the collection helper", arguments.length === 1);
+ } else {
+ Ember.assert("You cannot pass more than one argument to the collection helper", arguments.length === 2);
+ }
+
+ var fn = options.fn;
+ var data = options.data;
+ var inverse = options.inverse;
+ var view = options.data.view;
+
+ // If passed a path string, convert that into an object.
+ // Otherwise, just default to the standard class.
+ var collectionClass;
+ collectionClass = path ? handlebarsGet(this, path, options) : Ember.CollectionView;
+ Ember.assert(fmt("%@ #collection: Could not find collection class %@", [data.view, path]), !!collectionClass);
+
+ var hash = options.hash, itemHash = {}, match;
+
+ // Extract item view class if provided else default to the standard class
+ var itemViewClass, itemViewPath = hash.itemViewClass;
+ var collectionPrototype = collectionClass.proto();
+ delete hash.itemViewClass;
+ itemViewClass = itemViewPath ? handlebarsGet(collectionPrototype, itemViewPath, options) : collectionPrototype.itemViewClass;
+ Ember.assert(fmt("%@ #collection: Could not find itemViewClass %@", [data.view, itemViewPath]), !!itemViewClass);
+
+ // Go through options passed to the {{collection}} helper and extract options
+ // that configure item views instead of the collection itself.
+ for (var prop in hash) {
+ if (hash.hasOwnProperty(prop)) {
+ match = prop.match(/^item(.)(.*)$/);
+
+ if(match && prop !== 'itemController') {
+ // Convert itemShouldFoo -> shouldFoo
+ itemHash[match[1].toLowerCase() + match[2]] = hash[prop];
+ // Delete from hash as this will end up getting passed to the
+ // {{view}} helper method.
+ delete hash[prop];
+ }
+ }
+ }
+
+ var tagName = hash.tagName || collectionPrototype.tagName;
+
+ if (fn) {
+ itemHash.template = fn;
+ delete options.fn;
+ }
+
+ var emptyViewClass;
+ if (inverse && inverse !== Handlebars.VM.noop) {
+ emptyViewClass = get(collectionPrototype, 'emptyViewClass');
+ emptyViewClass = emptyViewClass.extend({
+ template: inverse,
+ tagName: itemHash.tagName
+ });
+ } else if (hash.emptyViewClass) {
+ emptyViewClass = handlebarsGet(this, hash.emptyViewClass, options);
+ }
+ if (emptyViewClass) { hash.emptyView = emptyViewClass; }
+
+ if(!hash.keyword){
+ 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);
+
+ return Ember.Handlebars.helpers.view.call(this, collectionClass, options);
+});
+
+
+})();
+
+
+
+(function() {
+/*globals Handlebars */
+/**
+@module ember
+@submodule ember-handlebars
+*/
+
+var handlebarsGet = Ember.Handlebars.get;
+
+/**
+ `unbound` allows you to output a property without binding. *Important:* The
+ output will not be updated if the property changes. Use with caution.
+
+ ```handlebars
+ <div>{{unbound somePropertyThatDoesntChange}}</div>
+ ```
+
+ `unbound` can also be used in conjunction with a bound helper to
+ render it in its unbound form:
+
+ ```handlebars
+ <div>{{unbound helperName somePropertyThatDoesntChange}}</div>
+ ```
+
+ @method unbound
+ @for Ember.Handlebars.helpers
+ @param {String} property
+ @return {String} HTML string
+*/
+Ember.Handlebars.registerHelper('unbound', function(property, fn) {
+ var options = arguments[arguments.length - 1], helper, context, out;
+
+ if(arguments.length > 2) {
+ // Unbound helper call.
+ options.data.isUnbound = true;
+ helper = Ember.Handlebars.helpers[arguments[0]] || Ember.Handlebars.helperMissing;
+ out = helper.apply(this, Array.prototype.slice.call(arguments, 1));
+ delete options.data.isUnbound;
+ return out;
+ }
+
+ context = (fn.contexts && fn.contexts[0]) || this;
+ return handlebarsGet(context, property, fn);
+});
+
+})();
+
+
+
+(function() {
+/*jshint debug:true*/
+/**
+@module ember
+@submodule ember-handlebars
+*/
+
+var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.normalizePath;
+
+/**
+ `log` allows you to output the value of a value in the current rendering
+ context.
+
+ ```handlebars
+ {{log myVariable}}
+ ```
+
+ @method log
+ @for Ember.Handlebars.helpers
+ @param {String} property
+*/
+Ember.Handlebars.registerHelper('log', function(property, options) {
+ var context = (options.contexts && options.contexts[0]) || this,
+ normalized = normalizePath(context, property, options.data),
+ pathRoot = normalized.root,
+ path = normalized.path,
+ value = (path === 'this') ? pathRoot : handlebarsGet(pathRoot, path, options);
+ Ember.Logger.log(value);
+});
+
+/**
+ Execute the `debugger` statement in the current context.
+
+ ```handlebars
+ {{debugger}}
+ ```
+
+ @method debugger
+ @for Ember.Handlebars.helpers
+ @param {String} property
+*/
+Ember.Handlebars.registerHelper('debugger', function() {
+ debugger;
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-handlebars
+*/
+
+var get = Ember.get, set = Ember.set;
+
+Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, {
+ init: function() {
+ var itemController = get(this, 'itemController');
+ var binding;
+
+ if (itemController) {
+ var controller = Ember.ArrayController.create();
+ set(controller, 'itemController', itemController);
+ set(controller, 'container', get(this, 'controller.container'));
+ set(controller, '_eachView', this);
+ set(controller, 'target', get(this, 'controller'));
+
+ this.disableContentObservers(function() {
+ set(this, 'content', controller);
+ binding = new Ember.Binding('content', '_eachView.dataSource').oneWay();
+ binding.connect(controller);
+ });
+
+ set(this, '_arrayController', controller);
+ } else {
+ this.disableContentObservers(function() {
+ binding = new Ember.Binding('content', 'dataSource').oneWay();
+ binding.connect(this);
+ });
+ }
+
+ return this._super();
+ },
+
+ disableContentObservers: function(callback) {
+ Ember.removeBeforeObserver(this, 'content', null, '_contentWillChange');
+ Ember.removeObserver(this, 'content', null, '_contentDidChange');
+
+ callback.apply(this);
+
+ Ember.addBeforeObserver(this, 'content', null, '_contentWillChange');
+ Ember.addObserver(this, 'content', null, '_contentDidChange');
+ },
+
+ itemViewClass: Ember._MetamorphView,
+ emptyViewClass: Ember._MetamorphView,
+
+ createChildView: function(view, attrs) {
+ view = this._super(view, attrs);
+
+ // At the moment, if a container view subclass wants
+ // to insert keywords, it is responsible for cloning
+ // the keywords hash. This will be fixed momentarily.
+ var keyword = get(this, 'keyword');
+ var content = get(view, 'content');
+
+ if (keyword) {
+ var data = get(view, 'templateData');
+
+ data = Ember.copy(data);
+ data.keywords = view.cloneKeywords();
+ set(view, 'templateData', data);
+
+ // In this case, we do not bind, because the `content` of
+ // a #each item cannot change.
+ data.keywords[keyword] = content;
+ }
+
+ // If {{#each}} is looping over an array of controllers,
+ // point each child view at their respective controller.
+ if (content && get(content, 'isController')) {
+ set(view, 'controller', content);
+ }
+
+ return view;
+ },
+
+ willDestroy: function() {
+ var arrayController = get(this, '_arrayController');
+
+ if (arrayController) {
+ arrayController.destroy();
+ }
+
+ return this._super();
+ }
+});
+
+var GroupedEach = Ember.Handlebars.GroupedEach = function(context, path, options) {
+ var self = this,
+ normalized = Ember.Handlebars.normalizePath(context, path, options.data);
+
+ this.context = context;
+ this.path = path;
+ this.options = options;
+ this.template = options.fn;
+ this.containingView = options.data.view;
+ this.normalizedRoot = normalized.root;
+ this.normalizedPath = normalized.path;
+ this.content = this.lookupContent();
+
+ this.addContentObservers();
+ this.addArrayObservers();
+
+ this.containingView.on('willClearRender', function() {
+ self.destroy();
+ });
+};
+
+GroupedEach.prototype = {
+ contentWillChange: function() {
+ this.removeArrayObservers();
+ },
+
+ contentDidChange: function() {
+ this.content = this.lookupContent();
+ this.addArrayObservers();
+ this.rerenderContainingView();
+ },
+
+ contentArrayWillChange: Ember.K,
+
+ contentArrayDidChange: function() {
+ this.rerenderContainingView();
+ },
+
+ lookupContent: function() {
+ return Ember.Handlebars.get(this.normalizedRoot, this.normalizedPath, this.options);
+ },
+
+ addArrayObservers: function() {
+ this.content.addArrayObserver(this, {
+ willChange: 'contentArrayWillChange',
+ didChange: 'contentArrayDidChange'
+ });
+ },
+
+ removeArrayObservers: function() {
+ this.content.removeArrayObserver(this, {
+ willChange: 'contentArrayWillChange',
+ didChange: 'contentArrayDidChange'
+ });
+ },
+
+ addContentObservers: function() {
+ Ember.addBeforeObserver(this.normalizedRoot, this.normalizedPath, this, this.contentWillChange);
+ Ember.addObserver(this.normalizedRoot, this.normalizedPath, this, this.contentDidChange);
+ },
+
+ removeContentObservers: function() {
+ Ember.removeBeforeObserver(this.normalizedRoot, this.normalizedPath, this.contentWillChange);
+ Ember.removeObserver(this.normalizedRoot, this.normalizedPath, this.contentDidChange);
+ },
+
+ render: function() {
+ var content = this.content,
+ contentLength = get(content, 'length'),
+ data = this.options.data,
+ template = this.template;
+
+ data.insideEach = true;
+ for (var i = 0; i < contentLength; i++) {
+ template(content.objectAt(i), { data: data });
+ }
+ },
+
+ rerenderContainingView: function() {
+ Ember.run.scheduleOnce('render', this.containingView, 'rerender');
+ },
+
+ destroy: function() {
+ this.removeContentObservers();
+ this.removeArrayObservers();
+ }
+};
+
+/**
+ The `{{#each}}` helper loops over elements in a collection, rendering its
+ block once for each item. It is an extension of the base Handlebars `{{#each}}`
+ helper:
+
+ ```javascript
+ Developers = [{name: 'Yehuda'},{name: 'Tom'}, {name: 'Paul'}];
+ ```
+
+ ```handlebars
+ {{#each Developers}}
+ {{name}}
+ {{/each}}
+ ```
+
+ `{{each}}` supports an alternative syntax with element naming:
+
+ ```handlebars
+ {{#each person in Developers}}
+ {{person.name}}
+ {{/each}}
+ ```
+
+ When looping over objects that do not have properties, `{{this}}` can be used
+ to render the object:
+
+ ```javascript
+ DeveloperNames = ['Yehuda', 'Tom', 'Paul']
+ ```
+
+ ```handlebars
+ {{#each DeveloperNames}}
+ {{this}}
+ {{/each}}
+ ```
+ ### {{else}} condition
+ `{{#each}}` can have a matching `{{else}}`. The contents of this block will render
+ if the collection is empty.
+
+ ```
+ {{#each person in Developers}}
+ {{person.name}}
+ {{else}}
+ <p>Sorry, nobody is available for this task.</p>
+ {{/each}}
+ ```
+ ### Specifying a View class for items
+ If you provide an `itemViewClass` option that references a view class
+ with its own `template` you can omit the block.
+
+ The following template:
+
+ ```handlebars
+ {{#view App.MyView }}
+ {{each view.items itemViewClass="App.AnItemView"}}
+ {{/view}}
+ ```
+
+ And application code
+
+ ```javascript
+ App = Ember.Application.create({
+ MyView: Ember.View.extend({
+ items: [
+ Ember.Object.create({name: 'Dave'}),
+ Ember.Object.create({name: 'Mary'}),
+ Ember.Object.create({name: 'Sara'})
+ ]
+ })
+ });
+
+ App.AnItemView = Ember.View.extend({
+ template: Ember.Handlebars.compile("Greetings {{name}}")
+ });
+ ```
+
+ Will result in the HTML structure below
+
+ ```html
+ <div class="ember-view">
+ <div class="ember-view">Greetings Dave</div>
+ <div class="ember-view">Greetings Mary</div>
+ <div class="ember-view">Greetings Sara</div>
+ </div>
+ ```
+
+ ### Representing each item with a Controller.
+ By default the controller lookup within an `{{#each}}` block will be
+ the controller of the template where the `{{#each}}` was used. If each
+ item needs to be presented by a custom controller you can provide a
+ `itemController` option which references a controller by lookup name.
+ Each item in the loop will be wrapped in an instance of this controller
+ and the item itself will be set to the `content` property of that controller.
+
+ This is useful in cases where properties of model objects need transformation
+ or synthesis for display:
+
+ ```javascript
+ App.DeveloperController = Ember.ObjectController.extend({
+ isAvailableForHire: function(){
+ return !this.get('content.isEmployed') && this.get('content.isSeekingWork');
+ }.property('isEmployed', 'isSeekingWork')
+ })
+ ```
+
+ ```handlebars
+ {{#each person in Developers itemController="developer"}}
+ {{person.name}} {{#if person.isAvailableForHire}}Hire me!{{/if}}
+ {{/each}}
+ ```
+
+ @method each
+ @for Ember.Handlebars.helpers
+ @param [name] {String} name for item (used with `in`)
+ @param path {String} path
+ @param [options] {Object} Handlebars key/value pairs of options
+ @param [options.itemViewClass] {String} a path to a view class used for each item
+ @param [options.itemController] {String} name of a controller to be created for each item
+*/
+Ember.Handlebars.registerHelper('each', function(path, options) {
+ if (arguments.length === 4) {
+ Ember.assert("If you pass more than one argument to the each helper, it must be in the form #each foo in bar", arguments[1] === "in");
+
+ var keywordName = arguments[0];
+
+ options = arguments[3];
+ path = arguments[2];
+ if (path === '') { path = "this"; }
+
+ options.hash.keyword = keywordName;
+ }
+
+ options.hash.dataSourceBinding = path;
+ // Set up emptyView as a metamorph with no tag
+ //options.hash.emptyViewClass = Ember._MetamorphView;
+
+ if (options.data.insideGroup && !options.hash.groupedRows && !options.hash.itemViewClass) {
+ new Ember.Handlebars.GroupedEach(this, path, options).render();
+ } else {
+ return Ember.Handlebars.helpers.collection.call(this, 'Ember.Handlebars.EachView', options);
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-handlebars
+*/
+
+/**
+ `template` allows you to render a template from inside another template.
+ This allows you to re-use the same template in multiple places. For example:
+
+ ```html
+ <script type="text/x-handlebars" data-template-name="logged_in_user">
+ {{#with loggedInUser}}
+ Last Login: {{lastLogin}}
+ User Info: {{template "user_info"}}
+ {{/with}}
+ </script>
+ ```
+
+ ```html
+ <script type="text/x-handlebars" data-template-name="user_info">
+ Name: <em>{{name}}</em>
+ Karma: <em>{{karma}}</em>
+ </script>
+ ```
+
+ This helper looks for templates in the global `Ember.TEMPLATES` hash. If you
+ add `<script>` tags to your page with the `data-template-name` attribute set,
+ they will be compiled and placed in this hash automatically.
+
+ You can also manually register templates by adding them to the hash:
+
+ ```javascript
+ Ember.TEMPLATES["my_cool_template"] = Ember.Handlebars.compile('<b>{{user}}</b>');
+ ```
+
+ @method template
+ @for Ember.Handlebars.helpers
+ @param {String} templateName the template to render
+*/
+
+Ember.Handlebars.registerHelper('template', function(name, options) {
+ var template = Ember.TEMPLATES[name];
+
+ Ember.assert("Unable to find template with name '"+name+"'.", !!template);
+
+ Ember.TEMPLATES[name](this, { data: options.data });
+});
+
+Ember.Handlebars.registerHelper('partial', function(name, options) {
+ var nameParts = name.split("/"),
+ lastPart = nameParts[nameParts.length - 1];
+
+ nameParts[nameParts.length - 1] = "_" + lastPart;
+
+ var underscoredName = nameParts.join("/");
+
+ var template = Ember.TEMPLATES[underscoredName],
+ deprecatedTemplate = Ember.TEMPLATES[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);
+
+ template = template || deprecatedTemplate;
+
+ template(this, { data: options.data });
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-handlebars
+*/
+
+var get = Ember.get, set = Ember.set;
+
+/**
+ When used in a Handlebars template that is assigned to an `Ember.View`
+ instance's `layout` property Ember will render the layout template first,
+ inserting the view's own rendered output at the `{{yield}}` location.
+
+ An empty `<body>` and the following application code:
+
+ ```javascript
+ AView = Ember.View.extend({
+ classNames: ['a-view-with-layout'],
+ layout: Ember.Handlebars.compile('<div class="wrapper">{{yield}}</div>'),
+ template: Ember.Handlebars.compile('<span>I am wrapped</span>')
+ });
+
+ aView = AView.create();
+ aView.appendTo('body');
+ ```
+
+ Will result in the following HTML output:
+
+ ```html
+ <body>
+ <div class='ember-view a-view-with-layout'>
+ <div class="wrapper">
+ <span>I am wrapped</span>
+ </div>
+ </div>
+ </body>
+ ```
+
+ The `yield` helper cannot be used outside of a template assigned to an
+ `Ember.View`'s `layout` property and will throw an error if attempted.
+
+ ```javascript
+ BView = Ember.View.extend({
+ classNames: ['a-view-with-layout'],
+ template: Ember.Handlebars.compile('{{yield}}')
+ });
+
+ bView = BView.create();
+ bView.appendTo('body');
+
+ // throws
+ // Uncaught Error: assertion failed: You called yield in a template that was not a layout
+ ```
+
+ @method yield
+ @for Ember.Handlebars.helpers
+ @param {Hash} options
+ @return {String} HTML string
+*/
+Ember.Handlebars.registerHelper('yield', function(options) {
+ var view = options.data.view, template;
+
+ while (view && !get(view, 'layout')) {
+ view = get(view, 'parentView');
+ }
+
+ Ember.assert("You called yield in a template that was not a layout", !!view);
+
+ template = get(view, 'template');
+
+ if (template) { template(this, options); }
+});
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-handlebars
+*/
+
+var set = Ember.set, get = Ember.get;
+
+/**
+ The `Ember.Checkbox` view class renders a checkbox
+ [input](https://developer.mozilla.org/en/HTML/Element/Input) element. It
+ allows for binding an Ember property (`checked`) to the status of the
+ checkbox.
+
+ Example:
+
+ ```handlebars
+ {{view Ember.Checkbox checkedBinding="receiveEmail"}}
+ ```
+
+ You can add a `label` tag yourself in the template where the `Ember.Checkbox`
+ is being used.
+
+ ```html
+ <label>
+ {{view Ember.Checkbox classNames="applicaton-specific-checkbox"}}
+ Some Title
+ </label>
+ ```
+
+ The `checked` attribute of an `Ember.Checkbox` object should always be set
+ through the Ember object or by interacting with its rendered element
+ representation via the mouse, keyboard, or touch. Updating the value of the
+ checkbox via jQuery will result in the checked value of the object and its
+ element losing synchronization.
+
+ ## Layout and LayoutName properties
+
+ Because HTML `input` elements are self closing `layout` and `layoutName`
+ properties will not be applied. See `Ember.View`'s layout section for more
+ information.
+
+ @class Checkbox
+ @namespace Ember
+ @extends Ember.View
+*/
+Ember.Checkbox = Ember.View.extend({
+ classNames: ['ember-checkbox'],
+
+ tagName: 'input',
+
+ attributeBindings: ['type', 'checked', 'disabled', 'tabindex'],
+
+ type: "checkbox",
+ checked: false,
+ disabled: false,
+
+ init: function() {
+ this._super();
+ this.on("change", this, this._updateElementValue);
+ },
+
+ _updateElementValue: function() {
+ set(this, 'checked', this.$().prop('checked'));
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-handlebars
+*/
+
+var get = Ember.get, set = Ember.set;
+
+/**
+ Shared mixin used by `Ember.TextField` and `Ember.TextArea`.
+
+ @class TextSupport
+ @namespace Ember
+ @extends Ember.Mixin
+ @private
+*/
+Ember.TextSupport = Ember.Mixin.create({
+ value: "",
+
+ attributeBindings: ['placeholder', 'disabled', 'maxlength', 'tabindex'],
+ placeholder: null,
+ disabled: false,
+ maxlength: null,
+
+ insertNewline: Ember.K,
+ cancel: Ember.K,
+
+ init: function() {
+ this._super();
+ this.on("focusOut", this, this._elementValueDidChange);
+ this.on("change", this, this._elementValueDidChange);
+ this.on("paste", this, this._elementValueDidChange);
+ this.on("cut", this, this._elementValueDidChange);
+ this.on("input", this, this._elementValueDidChange);
+ this.on("keyUp", this, this.interpretKeyEvents);
+ },
+
+ interpretKeyEvents: function(event) {
+ var map = Ember.TextSupport.KEY_EVENTS;
+ var method = map[event.keyCode];
+
+ this._elementValueDidChange();
+ if (method) { return this[method](event); }
+ },
+
+ _elementValueDidChange: function() {
+ set(this, 'value', this.$().val());
+ }
+
+});
+
+Ember.TextSupport.KEY_EVENTS = {
+ 13: 'insertNewline',
+ 27: 'cancel'
+};
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-handlebars
+*/
+
+var get = Ember.get, set = Ember.set;
+
+/**
+ The `Ember.TextField` view class renders a text
+ [input](https://developer.mozilla.org/en/HTML/Element/Input) element. It
+ allows for binding Ember properties to the text field contents (`value`),
+ live-updating as the user inputs text.
+
+ Example:
+
+ ```handlebars
+ {{view Ember.TextField valueBinding="firstName"}}
+ ```
+
+ ## Layout and LayoutName properties
+
+ Because HTML `input` elements are self closing `layout` and `layoutName`
+ properties will not be applied. See `Ember.View`'s layout section for more
+ information.
+
+ ## HTML Attributes
+
+ By default `Ember.TextField` provides support for `type`, `value`, `size`,
+ `placeholder`, `disabled`, `maxlength` and `tabindex` attributes on a
+ test field. If you need to support more attributes have a look at the
+ `attributeBindings` property in `Ember.View`'s HTML Attributes section.
+
+ To globally add support for additional attributes you can reopen
+ `Ember.TextField` or `Ember.TextSupport`.
+
+ ```javascript
+ Ember.TextSupport.reopen({
+ attributeBindings: ["required"]
+ })
+ ```
+
+ @class TextField
+ @namespace Ember
+ @extends Ember.View
+ @uses Ember.TextSupport
+*/
+Ember.TextField = Ember.View.extend(Ember.TextSupport,
+ /** @scope Ember.TextField.prototype */ {
+
+ classNames: ['ember-text-field'],
+ tagName: "input",
+ attributeBindings: ['type', 'value', 'size', 'pattern'],
+
+ /**
+ The `value` attribute of the input element. As the user inputs text, this
+ property is updated live.
+
+ @property value
+ @type String
+ @default ""
+ */
+ value: "",
+
+ /**
+ The `type` attribute of the input element.
+
+ @property type
+ @type String
+ @default "text"
+ */
+ type: "text",
+
+ /**
+ The `size` of the text field in characters.
+
+ @property size
+ @type String
+ @default null
+ */
+ size: null,
+
+ /**
+ The `pattern` the pattern attribute of input element.
+
+ @property pattern
+ @type String
+ @default null
+ */
+ pattern: null,
+
+ /**
+ The action to be sent when the user presses the return key.
+
+ This is similar to the `{{action}}` helper, but is fired when
+ the user presses the return key when editing a text field, and sends
+ the value of the field as the context.
+
+ @property action
+ @type String
+ @default null
+ */
+ action: null,
+
+ /**
+ Whether they `keyUp` event that triggers an `action` to be sent continues
+ propagating to other views.
+
+ By default, when the user presses the return key on their keyboard and
+ the text field has an `action` set, the action will be sent to the view's
+ controller and the key event will stop propagating.
+
+ If you would like parent views to receive the `keyUp` event even after an
+ action has been dispatched, set `bubbles` to true.
+
+ @property bubbles
+ @type Boolean
+ @default false
+ */
+ bubbles: false,
+
+ insertNewline: function(event) {
+ var controller = get(this, 'controller'),
+ action = get(this, 'action');
+
+ if (action) {
+ controller.send(action, get(this, 'value'), this);
+
+ if (!get(this, 'bubbles')) {
+ event.stopPropagation();
+ }
+ }
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-handlebars
+*/
+
+var get = Ember.get, set = Ember.set;
+
+/**
+ @class Button
+ @namespace Ember
+ @extends Ember.View
+ @uses Ember.TargetActionSupport
+ @deprecated
+*/
+Ember.Button = Ember.View.extend(Ember.TargetActionSupport, {
+ classNames: ['ember-button'],
+ classNameBindings: ['isActive'],
+
+ tagName: 'button',
+
+ propagateEvents: false,
+
+ attributeBindings: ['type', 'disabled', 'href', 'tabindex'],
+
+ /**
+ @private
+
+ Overrides `TargetActionSupport`'s `targetObject` computed
+ property to use Handlebars-specific path resolution.
+
+ @property targetObject
+ */
+ targetObject: Ember.computed(function() {
+ var target = get(this, 'target'),
+ root = get(this, 'context'),
+ data = get(this, 'templateData');
+
+ if (typeof target !== 'string') { return target; }
+
+ return Ember.Handlebars.get(root, target, { data: data });
+ }).property('target'),
+
+ // Defaults to 'button' if tagName is 'input' or 'button'
+ type: Ember.computed(function(key) {
+ var tagName = this.tagName;
+ if (tagName === 'input' || tagName === 'button') { return 'button'; }
+ }),
+
+ disabled: false,
+
+ // Allow 'a' tags to act like buttons
+ href: Ember.computed(function() {
+ return this.tagName === 'a' ? '#' : null;
+ }),
+
+ mouseDown: function() {
+ if (!get(this, 'disabled')) {
+ set(this, 'isActive', true);
+ this._mouseDown = true;
+ this._mouseEntered = true;
+ }
+ return get(this, 'propagateEvents');
+ },
+
+ mouseLeave: function() {
+ if (this._mouseDown) {
+ set(this, 'isActive', false);
+ this._mouseEntered = false;
+ }
+ },
+
+ mouseEnter: function() {
+ if (this._mouseDown) {
+ set(this, 'isActive', true);
+ this._mouseEntered = true;
+ }
+ },
+
+ mouseUp: function(event) {
+ if (get(this, 'isActive')) {
+ // Actually invoke the button's target and action.
+ // This method comes from the Ember.TargetActionSupport mixin.
+ this.triggerAction();
+ set(this, 'isActive', false);
+ }
+
+ this._mouseDown = false;
+ this._mouseEntered = false;
+ return get(this, 'propagateEvents');
+ },
+
+ keyDown: function(event) {
+ // Handle space or enter
+ if (event.keyCode === 13 || event.keyCode === 32) {
+ this.mouseDown();
+ }
+ },
+
+ keyUp: function(event) {
+ // Handle space or enter
+ if (event.keyCode === 13 || event.keyCode === 32) {
+ this.mouseUp();
+ }
+ },
+
+ // TODO: Handle proper touch behavior. Including should make inactive when
+ // finger moves more than 20x outside of the edge of the button (vs mouse
+ // which goes inactive as soon as mouse goes out of edges.)
+
+ touchStart: function(touch) {
+ return this.mouseDown(touch);
+ },
+
+ touchEnd: function(touch) {
+ return this.mouseUp(touch);
+ },
+
+ init: function() {
+ Ember.deprecate("Ember.Button is deprecated and will be removed from future releases. Consider using the `{{action}}` helper.");
+ this._super();
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-handlebars
+*/
+
+var get = Ember.get, set = Ember.set;
+
+/**
+ The `Ember.TextArea` view class renders a
+ [textarea](https://developer.mozilla.org/en/HTML/Element/textarea) element.
+ It allows for binding Ember properties to the text area contents (`value`),
+ live-updating as the user inputs text.
+
+ ## Layout and LayoutName properties
+
+ Because HTML `textarea` elements do not contain inner HTML the `layout` and
+ `layoutName` properties will not be applied. See `Ember.View`'s layout
+ section for more information.
+
+ ## HTML Attributes
+
+ By default `Ember.TextArea` provides support for `rows`, `cols`,
+ `placeholder`, `disabled`, `maxlength` and `tabindex` attributes on a
+ textarea. If you need to support more attributes have a look at the
+ `attributeBindings` property in `Ember.View`'s HTML Attributes section.
+
+ To globally add support for additional attributes you can reopen
+ `Ember.TextArea` or `Ember.TextSupport`.
+
+ ```javascript
+ Ember.TextSupport.reopen({
+ attributeBindings: ["required"]
+ })
+ ```
+
+ @class TextArea
+ @namespace Ember
+ @extends Ember.View
+ @uses Ember.TextSupport
+*/
+Ember.TextArea = Ember.View.extend(Ember.TextSupport, {
+ classNames: ['ember-text-area'],
+
+ tagName: "textarea",
+ attributeBindings: ['rows', 'cols'],
+ rows: null,
+ cols: null,
+
+ _updateElementValue: Ember.observer(function() {
+ // We do this check so cursor position doesn't get affected in IE
+ var value = get(this, 'value'),
+ $el = this.$();
+ if ($el && value !== $el.val()) {
+ $el.val(value);
+ }
+ }, 'value'),
+
+ init: function() {
+ this._super();
+ this.on("didInsertElement", this, this._updateElementValue);
+ }
+
+});
+
+})();
+
+
+
+(function() {
+/*jshint eqeqeq:false */
+
+/**
+@module ember
+@submodule ember-handlebars
+*/
+
+var set = Ember.set,
+ get = Ember.get,
+ indexOf = Ember.EnumerableUtils.indexOf,
+ indexesOf = Ember.EnumerableUtils.indexesOf,
+ replace = Ember.EnumerableUtils.replace,
+ isArray = Ember.isArray,
+ precompileTemplate = Ember.Handlebars.compile;
+
+/**
+ The `Ember.Select` view class renders a
+ [select](https://developer.mozilla.org/en/HTML/Element/select) HTML element,
+ allowing the user to choose from a list of options.
+
+ The text and `value` property of each `<option>` element within the
+ `<select>` element are populated from the objects in the `Element.Select`'s
+ `content` property. The underlying data object of the selected `<option>` is
+ stored in the `Element.Select`'s `value` property.
+
+ ### `content` as an array of Strings
+
+ The simplest version of an `Ember.Select` takes an array of strings as its
+ `content` property. The string will be used as both the `value` property and
+ the inner text of each `<option>` element inside the rendered `<select>`.
+
+ Example:
+
+ ```javascript
+ App.names = ["Yehuda", "Tom"];
+ ```
+
+ ```handlebars
+ {{view Ember.Select contentBinding="App.names"}}
+ ```
+
+ Would result in the following HTML:
+
+ ```html
+ <select class="ember-select">
+ <option value="Yehuda">Yehuda</option>
+ <option value="Tom">Tom</option>
+ </select>
+ ```
+
+ You can control which `<option>` is selected through the `Ember.Select`'s
+ `value` property directly or as a binding:
+
+ ```javascript
+ App.names = Ember.Object.create({
+ selected: 'Tom',
+ content: ["Yehuda", "Tom"]
+ });
+ ```
+
+ ```handlebars
+ {{view Ember.Select
+ contentBinding="App.names.content"
+ valueBinding="App.names.selected"
+ }}
+ ```
+
+ Would result in the following HTML with the `<option>` for 'Tom' selected:
+
+ ```html
+ <select class="ember-select">
+ <option value="Yehuda">Yehuda</option>
+ <option value="Tom" selected="selected">Tom</option>
+ </select>
+ ```
+
+ A user interacting with the rendered `<select>` to choose "Yehuda" would
+ update the value of `App.names.selected` to "Yehuda".
+
+ ### `content` as an Array of Objects
+
+ An `Ember.Select` can also take an array of JavaScript or Ember objects as
+ its `content` property.
+
+ When using objects you need to tell the `Ember.Select` which property should
+ be accessed on each object to supply the `value` attribute of the `<option>`
+ and which property should be used to supply the element text.
+
+ The `optionValuePath` option is used to specify the path on each object to
+ the desired property for the `value` attribute. The `optionLabelPath`
+ specifies the path on each object to the desired property for the
+ element's text. Both paths must reference each object itself as `content`:
+
+ ```javascript
+ App.programmers = [
+ Ember.Object.create({firstName: "Yehuda", id: 1}),
+ Ember.Object.create({firstName: "Tom", id: 2})
+ ];
+ ```
+
+ ```handlebars
+ {{view Ember.Select
+ contentBinding="App.programmers"
+ optionValuePath="content.id"
+ optionLabelPath="content.firstName"}}
+ ```
+
+ Would result in the following HTML:
+
+ ```html
+ <select class="ember-select">
+ <option value>Please Select</option>
+ <option value="1">Yehuda</option>
+ <option value="2">Tom</option>
+ </select>
+ ```
+
+ The `value` attribute of the selected `<option>` within an `Ember.Select`
+ can be bound to a property on another object by providing a
+ `valueBinding` option:
+
+ ```javascript
+ App.programmers = [
+ Ember.Object.create({firstName: "Yehuda", id: 1}),
+ Ember.Object.create({firstName: "Tom", id: 2})
+ ];
+
+ App.currentProgrammer = Ember.Object.create({
+ id: 2
+ });
+ ```
+
+ ```handlebars
+ {{view Ember.Select
+ contentBinding="App.programmers"
+ optionValuePath="content.id"
+ optionLabelPath="content.firstName"
+ valueBinding="App.currentProgrammer.id"}}
+ ```
+
+ Would result in the following HTML with a selected option:
+
+ ```html
+ <select class="ember-select">
+ <option value>Please Select</option>
+ <option value="1">Yehuda</option>
+ <option value="2" selected="selected">Tom</option>
+ </select>
+ ```
+
+ Interacting with the rendered element by selecting the first option
+ ('Yehuda') will update the `id` value of `App.currentProgrammer`
+ to match the `value` property of the newly selected `<option>`.
+
+ Alternatively, you can control selection through the underlying objects
+ used to render each object providing a `selectionBinding`. When the selected
+ `<option>` is changed, the property path provided to `selectionBinding`
+ will be updated to match the content object of the rendered `<option>`
+ element:
+
+ ```javascript
+ App.controller = Ember.Object.create({
+ selectedPerson: null,
+ content: [
+ Ember.Object.create({firstName: "Yehuda", id: 1}),
+ Ember.Object.create({firstName: "Tom", id: 2})
+ ]
+ });
+ ```
+
+ ```handlebars
+ {{view Ember.Select
+ contentBinding="App.controller.content"
+ optionValuePath="content.id"
+ optionLabelPath="content.firstName"
+ selectionBinding="App.controller.selectedPerson"}}
+ ```
+
+ Would result in the following HTML with a selected option:
+
+ ```html
+ <select class="ember-select">
+ <option value>Please Select</option>
+ <option value="1">Yehuda</option>
+ <option value="2" selected="selected">Tom</option>
+ </select>
+ ```
+
+ Interacting with the rendered element by selecting the first option
+ ('Yehuda') will update the `selectedPerson` value of `App.controller`
+ to match the content object of the newly selected `<option>`. In this
+ case it is the first object in the `App.content.content`
+
+ ### Supplying a Prompt
+
+ A `null` value for the `Ember.Select`'s `value` or `selection` property
+ results in there being no `<option>` with a `selected` attribute:
+
+ ```javascript
+ App.controller = Ember.Object.create({
+ selected: null,
+ content: [
+ "Yehuda",
+ "Tom"
+ ]
+ });
+ ```
+
+ ``` handlebars
+ {{view Ember.Select
+ contentBinding="App.controller.content"
+ valueBinding="App.controller.selected"
+ }}
+ ```
+
+ Would result in the following HTML:
+
+ ```html
+ <select class="ember-select">
+ <option value="Yehuda">Yehuda</option>
+ <option value="Tom">Tom</option>
+ </select>
+ ```
+
+ Although `App.controller.selected` is `null` and no `<option>`
+ has a `selected` attribute the rendered HTML will display the
+ first item as though it were selected. You can supply a string
+ value for the `Ember.Select` to display when there is no selection
+ with the `prompt` option:
+
+ ```javascript
+ App.controller = Ember.Object.create({
+ selected: null,
+ content: [
+ "Yehuda",
+ "Tom"
+ ]
+ });
+ ```
+
+ ```handlebars
+ {{view Ember.Select
+ contentBinding="App.controller.content"
+ valueBinding="App.controller.selected"
+ prompt="Please select a name"
+ }}
+ ```
+
+ Would result in the following HTML:
+
+ ```html
+ <select class="ember-select">
+ <option>Please select a name</option>
+ <option value="Yehuda">Yehuda</option>
+ <option value="Tom">Tom</option>
+ </select>
+ ```
+
+ @class Select
+ @namespace Ember
+ @extends Ember.View
+*/
+Ember.Select = Ember.View.extend(
+ /** @scope Ember.Select.prototype */ {
+
+ tagName: 'select',
+ classNames: ['ember-select'],
+ defaultTemplate: Ember.Handlebars.template(function anonymous(Handlebars,depth0,helpers,partials,data) {
+this.compilerInfo = [2,'>= 1.0.0-rc.3'];
+helpers = helpers || Ember.Handlebars.helpers; data = data || {};
+ var buffer = '', stack1, hashTypes, escapeExpression=this.escapeExpression, self=this;
+
+function program1(depth0,data) {
+
+ var buffer = '', hashTypes;
+ data.buffer.push("<option value=\"\">");
+ hashTypes = {};
+ data.buffer.push(escapeExpression(helpers._triageMustache.call(depth0, "view.prompt", {hash:{},contexts:[depth0],types:["ID"],hashTypes:hashTypes,data:data})));
+ data.buffer.push("</option>");
+ return buffer;
+ }
+
+function program3(depth0,data) {
+
+ var hashTypes;
+ hashTypes = {'contentBinding': "STRING"};
+ data.buffer.push(escapeExpression(helpers.view.call(depth0, "Ember.SelectOption", {hash:{
+ 'contentBinding': ("this")
+ },contexts:[depth0],types:["ID"],hashTypes:hashTypes,data:data})));
+ }
+
+ hashTypes = {};
+ stack1 = helpers['if'].call(depth0, "view.prompt", {hash:{},inverse:self.noop,fn:self.program(1, program1, data),contexts:[depth0],types:["ID"],hashTypes:hashTypes,data:data});
+ if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
+ hashTypes = {};
+ stack1 = helpers.each.call(depth0, "view.content", {hash:{},inverse:self.noop,fn:self.program(3, program3, data),contexts:[depth0],types:["ID"],hashTypes:hashTypes,data:data});
+ if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
+ return buffer;
+
+}),
+ attributeBindings: ['multiple', 'disabled', 'tabindex'],
+
+ /**
+ The `multiple` attribute of the select element. Indicates whether multiple
+ options can be selected.
+
+ @property multiple
+ @type Boolean
+ @default false
+ */
+ multiple: false,
+
+ disabled: false,
+
+ /**
+ The list of options.
+
+ If `optionLabelPath` and `optionValuePath` are not overridden, this should
+ be a list of strings, which will serve simultaneously as labels and values.
+
+ Otherwise, this should be a list of objects. For instance:
+
+ ```javascript
+ Ember.Select.create({
+ content: Ember.A([
+ { id: 1, firstName: 'Yehuda' },
+ { id: 2, firstName: 'Tom' }
+ ]),
+ optionLabelPath: 'content.firstName',
+ optionValuePath: 'content.id'
+ });
+ ```
+
+ @property content
+ @type Array
+ @default null
+ */
+ content: null,
+
+ /**
+ When `multiple` is `false`, the element of `content` that is currently
+ selected, if any.
+
+ When `multiple` is `true`, an array of such elements.
+
+ @property selection
+ @type Object or Array
+ @default null
+ */
+ selection: null,
+
+ /**
+ In single selection mode (when `multiple` is `false`), value can be used to
+ get the current selection's value or set the selection by it's value.
+
+ It is not currently supported in multiple selection mode.
+
+ @property value
+ @type String
+ @default null
+ */
+ value: Ember.computed(function(key, value) {
+ if (arguments.length === 2) { return value; }
+ var valuePath = get(this, 'optionValuePath').replace(/^content\.?/, '');
+ return valuePath ? get(this, 'selection.' + valuePath) : get(this, 'selection');
+ }).property('selection'),
+
+ /**
+ If given, a top-most dummy option will be rendered to serve as a user
+ prompt.
+
+ @property prompt
+ @type String
+ @default null
+ */
+ prompt: null,
+
+ /**
+ The path of the option labels. See `content`.
+
+ @property optionLabelPath
+ @type String
+ @default 'content'
+ */
+ optionLabelPath: 'content',
+
+ /**
+ The path of the option values. See `content`.
+
+ @property optionValuePath
+ @type String
+ @default 'content'
+ */
+ optionValuePath: 'content',
+
+ _change: function() {
+ if (get(this, 'multiple')) {
+ this._changeMultiple();
+ } else {
+ this._changeSingle();
+ }
+ },
+
+ selectionDidChange: Ember.observer(function() {
+ var selection = get(this, 'selection');
+ if (get(this, 'multiple')) {
+ if (!isArray(selection)) {
+ set(this, 'selection', Ember.A([selection]));
+ return;
+ }
+ this._selectionDidChangeMultiple();
+ } else {
+ this._selectionDidChangeSingle();
+ }
+ }, 'selection.@each'),
+
+ valueDidChange: Ember.observer(function() {
+ var content = get(this, 'content'),
+ value = get(this, 'value'),
+ valuePath = get(this, 'optionValuePath').replace(/^content\.?/, ''),
+ selectedValue = (valuePath ? get(this, 'selection.' + valuePath) : get(this, 'selection')),
+ selection;
+
+ if (value !== selectedValue) {
+ selection = content.find(function(obj) {
+ return value === (valuePath ? get(obj, valuePath) : obj);
+ });
+
+ this.set('selection', selection);
+ }
+ }, 'value'),
+
+
+ _triggerChange: function() {
+ var selection = get(this, 'selection');
+ var value = get(this, 'value');
+
+ if (selection) { this.selectionDidChange(); }
+ if (value) { this.valueDidChange(); }
+
+ this._change();
+ },
+
+ _changeSingle: function() {
+ var selectedIndex = this.$()[0].selectedIndex,
+ content = get(this, 'content'),
+ prompt = get(this, 'prompt');
+
+ if (!get(content, 'length')) { return; }
+ if (prompt && selectedIndex === 0) { set(this, 'selection', null); return; }
+
+ if (prompt) { selectedIndex -= 1; }
+ set(this, 'selection', content.objectAt(selectedIndex));
+ },
+
+
+ _changeMultiple: function() {
+ var options = this.$('option:selected'),
+ prompt = get(this, 'prompt'),
+ offset = prompt ? 1 : 0,
+ content = get(this, 'content'),
+ selection = get(this, 'selection');
+
+ if (!content){ return; }
+ if (options) {
+ var selectedIndexes = options.map(function(){
+ return this.index - offset;
+ }).toArray();
+ var newSelection = content.objectsAt(selectedIndexes);
+
+ if (isArray(selection)) {
+ replace(selection, 0, get(selection, 'length'), newSelection);
+ } else {
+ set(this, 'selection', newSelection);
+ }
+ }
+ },
+
+ _selectionDidChangeSingle: function() {
+ var el = this.get('element');
+ if (!el) { return; }
+
+ var content = get(this, 'content'),
+ selection = get(this, 'selection'),
+ selectionIndex = content ? indexOf(content, selection) : -1,
+ prompt = get(this, 'prompt');
+
+ if (prompt) { selectionIndex += 1; }
+ if (el) { el.selectedIndex = selectionIndex; }
+ },
+
+ _selectionDidChangeMultiple: function() {
+ var content = get(this, 'content'),
+ selection = get(this, 'selection'),
+ selectedIndexes = content ? indexesOf(content, selection) : [-1],
+ prompt = get(this, 'prompt'),
+ offset = prompt ? 1 : 0,
+ options = this.$('option'),
+ adjusted;
+
+ if (options) {
+ options.each(function() {
+ adjusted = this.index > -1 ? this.index - offset : -1;
+ this.selected = indexOf(selectedIndexes, adjusted) > -1;
+ });
+ }
+ },
+
+ init: function() {
+ this._super();
+ this.on("didInsertElement", this, this._triggerChange);
+ this.on("change", this, this._change);
+ }
+});
+
+Ember.SelectOption = Ember.View.extend({
+ tagName: 'option',
+ attributeBindings: ['value', 'selected'],
+
+ defaultTemplate: function(context, options) {
+ options = { data: options.data, hash: {} };
+ Ember.Handlebars.helpers.bind.call(context, "view.label", options);
+ },
+
+ init: function() {
+ this.labelPathDidChange();
+ this.valuePathDidChange();
+
+ this._super();
+ },
+
+ selected: Ember.computed(function() {
+ var content = get(this, 'content'),
+ selection = get(this, 'parentView.selection');
+ if (get(this, 'parentView.multiple')) {
+ return selection && indexOf(selection, content.valueOf()) > -1;
+ } else {
+ // Primitives get passed through bindings as objects... since
+ // `new Number(4) !== 4`, we use `==` below
+ return content == selection;
+ }
+ }).property('content', 'parentView.selection').volatile(),
+
+ labelPathDidChange: Ember.observer(function() {
+ var labelPath = get(this, 'parentView.optionLabelPath');
+
+ if (!labelPath) { return; }
+
+ Ember.defineProperty(this, 'label', Ember.computed(function() {
+ return get(this, labelPath);
+ }).property(labelPath));
+ }, 'parentView.optionLabelPath'),
+
+ valuePathDidChange: Ember.observer(function() {
+ var valuePath = get(this, 'parentView.optionValuePath');
+
+ if (!valuePath) { return; }
+
+ Ember.defineProperty(this, 'value', Ember.computed(function() {
+ return get(this, valuePath);
+ }).property(valuePath));
+ }, 'parentView.optionValuePath')
+});
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(function() {
+/*globals Handlebars */
+/**
+@module ember
+@submodule ember-handlebars
+*/
+
+/**
+ @private
+
+ Find templates stored in the head tag as script tags and make them available
+ to `Ember.CoreView` in the global `Ember.TEMPLATES` object. This will be run
+ as as jQuery DOM-ready callback.
+
+ Script tags with `text/x-handlebars` will be compiled
+ with Ember's Handlebars and are suitable for use as a view's template.
+ Those with type `text/x-raw-handlebars` will be compiled with regular
+ Handlebars and are suitable for use in views' computed properties.
+
+ @method bootstrap
+ @for Ember.Handlebars
+ @static
+ @param ctx
+*/
+Ember.Handlebars.bootstrap = function(ctx) {
+ var selectors = 'script[type="text/x-handlebars"], script[type="text/x-raw-handlebars"]';
+
+ Ember.$(selectors, ctx)
+ .each(function() {
+ // Get a reference to the script tag
+ var script = Ember.$(this),
+ type = script.attr('type');
+
+ var compile = (script.attr('type') === 'text/x-raw-handlebars') ?
+ Ember.$.proxy(Handlebars.compile, Handlebars) :
+ Ember.$.proxy(Ember.Handlebars.compile, Ember.Handlebars),
+ // Get the name of the script, used by Ember.View's templateName property.
+ // First look for data-template-name attribute, then fall back to its
+ // id if no name is found.
+ templateName = script.attr('data-template-name') || script.attr('id') || 'application',
+ template = compile(script.html());
+
+ // For templates which have a name, we save them and then remove them from the DOM
+ Ember.TEMPLATES[templateName] = template;
+
+ // Remove script tag from DOM
+ script.remove();
+ });
+};
+
+function bootstrap() {
+ Ember.Handlebars.bootstrap( Ember.$(document) );
+}
+
+/*
+ We tie this to application.load to ensure that we've at least
+ attempted to bootstrap at the point that the application is loaded.
+
+ We also tie this to document ready since we're guaranteed that all
+ the inline templates are present at this point.
+
+ There's no harm to running this twice, since we remove the templates
+ from the DOM after processing.
+*/
+
+Ember.onLoad('application', bootstrap);
+
+})();
+
+
+
+(function() {
+/**
+Ember Handlebars
+
+@module ember
+@submodule ember-handlebars
+@requires ember-views
+*/
+
+Ember.runLoadHooks('Ember.Handlebars', Ember.Handlebars);
+
+})();
+
+(function() {
+define("route-recognizer",
+ [],
+ function() {
+ "use strict";
+ var specials = [
+ '/', '.', '*', '+', '?', '|',
+ '(', ')', '[', ']', '{', '}', '\\'
+ ];
+
+ var escapeRegex = new RegExp('(\\' + specials.join('|\\') + ')', 'g');
+
+ // A Segment represents a segment in the original route description.
+ // Each Segment type provides an `eachChar` and `regex` method.
+ //
+ // The `eachChar` method invokes the callback with one or more character
+ // specifications. A character specification consumes one or more input
+ // characters.
+ //
+ // The `regex` method returns a regex fragment for the segment. If the
+ // segment is a dynamic of star segment, the regex fragment also includes
+ // a capture.
+ //
+ // A character specification contains:
+ //
+ // * `validChars`: a String with a list of all valid characters, or
+ // * `invalidChars`: a String with a list of all invalid characters
+ // * `repeat`: true if the character specification can repeat
+
+ function StaticSegment(string) { this.string = string; }
+ StaticSegment.prototype = {
+ eachChar: function(callback) {
+ var string = this.string, char;
+
+ for (var i=0, l=string.length; i<l; i++) {
+ char = string.charAt(i);
+ callback({ validChars: char });
+ }
+ },
+
+ regex: function() {
+ return this.string.replace(escapeRegex, '\\$1');
+ },
+
+ generate: function() {
+ return this.string;
+ }
+ };
+
+ function DynamicSegment(name) { this.name = name; }
+ DynamicSegment.prototype = {
+ eachChar: function(callback) {
+ callback({ invalidChars: "/", repeat: true });
+ },
+
+ regex: function() {
+ return "([^/]+)";
+ },
+
+ generate: function(params) {
+ return params[this.name];
+ }
+ };
+
+ function StarSegment(name) { this.name = name; }
+ StarSegment.prototype = {
+ eachChar: function(callback) {
+ callback({ invalidChars: "", repeat: true });
+ },
+
+ regex: function() {
+ return "(.+)";
+ },
+
+ generate: function(params) {
+ return params[this.name];
+ }
+ };
+
+ function EpsilonSegment() {}
+ EpsilonSegment.prototype = {
+ eachChar: function() {},
+ regex: function() { return ""; },
+ generate: function() { return ""; }
+ };
+
+ function parse(route, names, types) {
+ // normalize route as not starting with a "/". Recognition will
+ // also normalize.
+ if (route.charAt(0) === "/") { route = route.substr(1); }
+
+ var segments = route.split("/"), results = [];
+
+ for (var i=0, l=segments.length; i<l; i++) {
+ var segment = segments[i], match;
+
+ if (match = segment.match(/^:([^\/]+)$/)) {
+ results.push(new DynamicSegment(match[1]));
+ names.push(match[1]);
+ types.dynamics++;
+ } else if (match = segment.match(/^\*([^\/]+)$/)) {
+ results.push(new StarSegment(match[1]));
+ names.push(match[1]);
+ types.stars++;
+ } else if(segment === "") {
+ results.push(new EpsilonSegment());
+ } else {
+ results.push(new StaticSegment(segment));
+ types.statics++;
+ }
+ }
+
+ return results;
+ }
+
+ // A State has a character specification and (`charSpec`) and a list of possible
+ // subsequent states (`nextStates`).
+ //
+ // If a State is an accepting state, it will also have several additional
+ // properties:
+ //
+ // * `regex`: A regular expression that is used to extract parameters from paths
+ // that reached this accepting state.
+ // * `handlers`: Information on how to convert the list of captures into calls
+ // to registered handlers with the specified parameters
+ // * `types`: How many static, dynamic or star segments in this route. Used to
+ // decide which route to use if multiple registered routes match a path.
+ //
+ // Currently, State is implemented naively by looping over `nextStates` and
+ // comparing a character specification against a character. A more efficient
+ // implementation would use a hash of keys pointing at one or more next states.
+
+ function State(charSpec) {
+ this.charSpec = charSpec;
+ this.nextStates = [];
+ }
+
+ State.prototype = {
+ get: function(charSpec) {
+ var nextStates = this.nextStates;
+
+ for (var i=0, l=nextStates.length; i<l; i++) {
+ var child = nextStates[i];
+
+ var isEqual = child.charSpec.validChars === charSpec.validChars;
+ isEqual = isEqual && child.charSpec.invalidChars === charSpec.invalidChars;
+
+ if (isEqual) { return child; }
+ }
+ },
+
+ put: function(charSpec) {
+ var state;
+
+ // If the character specification already exists in a child of the current
+ // state, just return that state.
+ if (state = this.get(charSpec)) { return state; }
+
+ // Make a new state for the character spec
+ state = new State(charSpec);
+
+ // Insert the new state as a child of the current state
+ this.nextStates.push(state);
+
+ // If this character specification repeats, insert the new state as a child
+ // of itself. Note that this will not trigger an infinite loop because each
+ // transition during recognition consumes a character.
+ if (charSpec.repeat) {
+ state.nextStates.push(state);
+ }
+
+ // Return the new state
+ return state;
+ },
+
+ // Find a list of child states matching the next character
+ match: function(char) {
+ // DEBUG "Processing `" + char + "`:"
+ var nextStates = this.nextStates,
+ child, charSpec, chars;
+
+ // DEBUG " " + debugState(this)
+ var returned = [];
+
+ for (var i=0, l=nextStates.length; i<l; i++) {
+ child = nextStates[i];
+
+ charSpec = child.charSpec;
+
+ if (typeof (chars = charSpec.validChars) !== 'undefined') {
+ if (chars.indexOf(char) !== -1) { returned.push(child); }
+ } else if (typeof (chars = charSpec.invalidChars) !== 'undefined') {
+ if (chars.indexOf(char) === -1) { returned.push(child); }
+ }
+ }
+
+ return returned;
+ }
+
+ /** IF DEBUG
+ , debug: function() {
+ var charSpec = this.charSpec,
+ debug = "[",
+ chars = charSpec.validChars || charSpec.invalidChars;
+
+ if (charSpec.invalidChars) { debug += "^"; }
+ debug += chars;
+ debug += "]";
+
+ if (charSpec.repeat) { debug += "+"; }
+
+ return debug;
+ }
+ END IF **/
+ };
+
+ /** IF DEBUG
+ function debug(log) {
+ console.log(log);
+ }
+
+ function debugState(state) {
+ return state.nextStates.map(function(n) {
+ if (n.nextStates.length === 0) { return "( " + n.debug() + " [accepting] )"; }
+ return "( " + n.debug() + " <then> " + n.nextStates.map(function(s) { return s.debug() }).join(" or ") + " )";
+ }).join(", ")
+ }
+ END IF **/
+
+ // This is a somewhat naive strategy, but should work in a lot of cases
+ // A better strategy would properly resolve /posts/:id/new and /posts/edit/:id
+ function sortSolutions(states) {
+ return states.sort(function(a, b) {
+ if (a.types.stars !== b.types.stars) { return a.types.stars - b.types.stars; }
+ if (a.types.dynamics !== b.types.dynamics) { return a.types.dynamics - b.types.dynamics; }
+ if (a.types.statics !== b.types.statics) { return a.types.statics - b.types.statics; }
+
+ return 0;
+ });
+ }
+
+ function recognizeChar(states, char) {
+ var nextStates = [];
+
+ for (var i=0, l=states.length; i<l; i++) {
+ var state = states[i];
+
+ nextStates = nextStates.concat(state.match(char));
+ }
+
+ return nextStates;
+ }
+
+ function findHandler(state, path) {
+ var handlers = state.handlers, regex = state.regex;
+ var captures = path.match(regex), currentCapture = 1;
+ var result = [];
+
+ for (var i=0, l=handlers.length; i<l; i++) {
+ var handler = handlers[i], names = handler.names, params = {};
+
+ for (var j=0, m=names.length; j<m; j++) {
+ params[names[j]] = captures[currentCapture++];
+ }
+
+ result.push({ handler: handler.handler, params: params, isDynamic: !!names.length });
+ }
+
+ return result;
+ }
+
+ function addSegment(currentState, segment) {
+ segment.eachChar(function(char) {
+ var state;
+
+ currentState = currentState.put(char);
+ });
+
+ return currentState;
+ }
+
+ // The main interface
+
+ var RouteRecognizer = function() {
+ this.rootState = new State();
+ this.names = {};
+ };
+
+
+ RouteRecognizer.prototype = {
+ add: function(routes, options) {
+ var currentState = this.rootState, regex = "^",
+ types = { statics: 0, dynamics: 0, stars: 0 },
+ handlers = [], allSegments = [], name;
+
+ var isEmpty = true;
+
+ for (var i=0, l=routes.length; i<l; i++) {
+ var route = routes[i], names = [];
+
+ var segments = parse(route.path, names, types);
+
+ allSegments = allSegments.concat(segments);
+
+ for (var j=0, m=segments.length; j<m; j++) {
+ var segment = segments[j];
+
+ if (segment instanceof EpsilonSegment) { continue; }
+
+ isEmpty = false;
+
+ // Add a "/" for the new segment
+ currentState = currentState.put({ validChars: "/" });
+ regex += "/";
+
+ // Add a representation of the segment to the NFA and regex
+ currentState = addSegment(currentState, segment);
+ regex += segment.regex();
+ }
+
+ handlers.push({ handler: route.handler, names: names });
+ }
+
+ if (isEmpty) {
+ currentState = currentState.put({ validChars: "/" });
+ regex += "/";
+ }
+
+ currentState.handlers = handlers;
+ currentState.regex = new RegExp(regex + "$");
+ currentState.types = types;
+
+ if (name = options && options.as) {
+ this.names[name] = {
+ segments: allSegments,
+ handlers: handlers
+ };
+ }
+ },
+
+ handlersFor: function(name) {
+ var route = this.names[name], result = [];
+ if (!route) { throw new Error("There is no route named " + name); }
+
+ for (var i=0, l=route.handlers.length; i<l; i++) {
+ result.push(route.handlers[i]);
+ }
+
+ return result;
+ },
+
+ hasRoute: function(name) {
+ return !!this.names[name];
+ },
+
+ generate: function(name, params) {
+ var route = this.names[name], output = "";
+ if (!route) { throw new Error("There is no route named " + name); }
+
+ var segments = route.segments;
+
+ for (var i=0, l=segments.length; i<l; i++) {
+ var segment = segments[i];
+
+ if (segment instanceof EpsilonSegment) { continue; }
+
+ output += "/";
+ output += segment.generate(params);
+ }
+
+ if (output.charAt(0) !== '/') { output = '/' + output; }
+
+ return output;
+ },
+
+ recognize: function(path) {
+ var states = [ this.rootState ], i, l;
+
+ // DEBUG GROUP path
+
+ var pathLen = path.length;
+
+ if (path.charAt(0) !== "/") { path = "/" + path; }
+
+ if (pathLen > 1 && path.charAt(pathLen - 1) === "/") {
+ path = path.substr(0, pathLen - 1);
+ }
+
+ for (i=0, l=path.length; i<l; i++) {
+ states = recognizeChar(states, path.charAt(i));
+ if (!states.length) { break; }
+ }
+
+ // END DEBUG GROUP
+
+ var solutions = [];
+ for (i=0, l=states.length; i<l; i++) {
+ if (states[i].handlers) { solutions.push(states[i]); }
+ }
+
+ states = sortSolutions(solutions);
+
+ var state = solutions[0];
+
+ if (state && state.handlers) {
+ return findHandler(state, path);
+ }
+ }
+ };
+
+ function Target(path, matcher, delegate) {
+ this.path = path;
+ this.matcher = matcher;
+ this.delegate = delegate;
+ }
+
+ Target.prototype = {
+ to: function(target, callback) {
+ var delegate = this.delegate;
+
+ if (delegate && delegate.willAddRoute) {
+ target = delegate.willAddRoute(this.matcher.target, target);
+ }
+
+ this.matcher.add(this.path, target);
+
+ if (callback) {
+ if (callback.length === 0) { throw new Error("You must have an argument in the function passed to `to`"); }
+ this.matcher.addChild(this.path, target, callback, this.delegate);
+ }
+ }
+ };
+
+ function Matcher(target) {
+ this.routes = {};
+ this.children = {};
+ this.target = target;
+ }
+
+ Matcher.prototype = {
+ add: function(path, handler) {
+ this.routes[path] = handler;
+ },
+
+ addChild: function(path, target, callback, delegate) {
+ var matcher = new Matcher(target);
+ this.children[path] = matcher;
+
+ var match = generateMatch(path, matcher, delegate);
+
+ if (delegate && delegate.contextEntered) {
+ delegate.contextEntered(target, match);
+ }
+
+ callback(match);
+ }
+ };
+
+ function generateMatch(startingPath, matcher, delegate) {
+ return function(path, nestedCallback) {
+ var fullPath = startingPath + path;
+
+ if (nestedCallback) {
+ nestedCallback(generateMatch(fullPath, matcher, delegate));
+ } else {
+ return new Target(startingPath + path, matcher, delegate);
+ }
+ };
+ }
+
+ function addRoute(routeArray, path, handler) {
+ var len = 0;
+ for (var i=0, l=routeArray.length; i<l; i++) {
+ len += routeArray[i].path.length;
+ }
+
+ path = path.substr(len);
+ routeArray.push({ path: path, handler: handler });
+ }
+
+ function eachRoute(baseRoute, matcher, callback, binding) {
+ var routes = matcher.routes;
+
+ for (var path in routes) {
+ if (routes.hasOwnProperty(path)) {
+ var routeArray = baseRoute.slice();
+ addRoute(routeArray, path, routes[path]);
+
+ if (matcher.children[path]) {
+ eachRoute(routeArray, matcher.children[path], callback, binding);
+ } else {
+ callback.call(binding, routeArray);
+ }
+ }
+ }
+ }
+
+ RouteRecognizer.prototype.map = function(callback, addRouteCallback) {
+ var matcher = new Matcher();
+
+ callback(generateMatch("", matcher, this.delegate));
+
+ eachRoute([], matcher, function(route) {
+ if (addRouteCallback) { addRouteCallback(this, route); }
+ else { this.add(route); }
+ }, this);
+ };
+ return RouteRecognizer;
+ });
+
+})();
+
+
+
+(function() {
+define("router",
+ ["route-recognizer"],
+ function(RouteRecognizer) {
+ "use strict";
+ /**
+ @private
+
+ This file references several internal structures:
+
+ ## `RecognizedHandler`
+
+ * `{String} handler`: A handler name
+ * `{Object} params`: A hash of recognized parameters
+
+ ## `UnresolvedHandlerInfo`
+
+ * `{Boolean} isDynamic`: whether a handler has any dynamic segments
+ * `{String} name`: the name of a handler
+ * `{Object} context`: the active context for the handler
+
+ ## `HandlerInfo`
+
+ * `{Boolean} isDynamic`: whether a handler has any dynamic segments
+ * `{String} name`: the original unresolved handler name
+ * `{Object} handler`: a handler object
+ * `{Object} context`: the active context for the handler
+ */
+
+
+ function Router() {
+ this.recognizer = new RouteRecognizer();
+ }
+
+
+ Router.prototype = {
+ /**
+ The main entry point into the router. The API is essentially
+ the same as the `map` method in `route-recognizer`.
+
+ This method extracts the String handler at the last `.to()`
+ call and uses it as the name of the whole route.
+
+ @param {Function} callback
+ */
+ map: function(callback) {
+ this.recognizer.delegate = this.delegate;
+
+ this.recognizer.map(callback, function(recognizer, route) {
+ var lastHandler = route[route.length - 1].handler;
+ var args = [route, { as: lastHandler }];
+ recognizer.add.apply(recognizer, args);
+ });
+ },
+
+ hasRoute: function(route) {
+ return this.recognizer.hasRoute(route);
+ },
+
+ /**
+ The entry point for handling a change to the URL (usually
+ via the back and forward button).
+
+ Returns an Array of handlers and the parameters associated
+ with those parameters.
+
+ @param {String} url a URL to process
+
+ @return {Array} an Array of `[handler, parameter]` tuples
+ */
+ handleURL: function(url) {
+ var results = this.recognizer.recognize(url),
+ objects = [];
+
+ if (!results) {
+ throw new Error("No route matched the URL '" + url + "'");
+ }
+
+ collectObjects(this, results, 0, []);
+ },
+
+ /**
+ Hook point for updating the URL.
+
+ @param {String} url a URL to update to
+ */
+ updateURL: function() {
+ throw "updateURL is not implemented";
+ },
+
+ /**
+ Hook point for replacing the current URL, i.e. with replaceState
+
+ By default this behaves the same as `updateURL`
+
+ @param {String} url a URL to update to
+ */
+ replaceURL: function(url) {
+ this.updateURL(url);
+ },
+
+ /**
+ Transition into the specified named route.
+
+ If necessary, trigger the exit callback on any handlers
+ that are no longer represented by the target route.
+
+ @param {String} name the name of the route
+ */
+ transitionTo: function(name) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ doTransition(this, name, this.updateURL, args);
+ },
+
+ /**
+ Identical to `transitionTo` except that the current URL will be replaced
+ if possible.
+
+ This method is intended primarily for use with `replaceState`.
+
+ @param {String} name the name of the route
+ */
+ replaceWith: function(name) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ doTransition(this, name, this.replaceURL, args);
+ },
+
+ /**
+ @private
+
+ This method takes a handler name and a list of contexts and returns
+ a serialized parameter hash suitable to pass to `recognizer.generate()`.
+
+ @param {String} handlerName
+ @param {Array[Object]} contexts
+ @return {Object} a serialized parameter hash
+ */
+ paramsForHandler: function(handlerName, callback) {
+ var output = this._paramsForHandler(handlerName, [].slice.call(arguments, 1));
+ return output.params;
+ },
+
+ /**
+ Take a named route and context objects and generate a
+ URL.
+
+ @param {String} name the name of the route to generate
+ a URL for
+ @param {...Object} objects a list of objects to serialize
+
+ @return {String} a URL
+ */
+ generate: function(handlerName) {
+ var params = this.paramsForHandler.apply(this, arguments);
+ return this.recognizer.generate(handlerName, params);
+ },
+
+ /**
+ @private
+
+ Used internally by `generate` and `transitionTo`.
+ */
+ _paramsForHandler: function(handlerName, objects, doUpdate) {
+ var handlers = this.recognizer.handlersFor(handlerName),
+ params = {},
+ toSetup = [],
+ startIdx = handlers.length,
+ objectsToMatch = objects.length,
+ object, objectChanged, handlerObj, handler, names, i, len;
+
+ // Find out which handler to start matching at
+ for (i=handlers.length-1; i>=0 && objectsToMatch>0; i--) {
+ if (handlers[i].names.length) {
+ objectsToMatch--;
+ startIdx = i;
+ }
+ }
+
+ if (objectsToMatch > 0) {
+ throw "More objects were passed than dynamic segments";
+ }
+
+ // Connect the objects to the routes
+ for (i=0, len=handlers.length; i<len; i++) {
+ handlerObj = handlers[i];
+ handler = this.getHandler(handlerObj.handler);
+ names = handlerObj.names;
+ objectChanged = false;
+
+ // If it's a dynamic segment
+ if (names.length) {
+ // If we have objects, use them
+ if (i >= startIdx) {
+ object = objects.shift();
+ objectChanged = true;
+ // Otherwise use existing context
+ } else {
+ object = handler.context;
+ }
+
+ // Serialize to generate params
+ if (handler.serialize) {
+ merge(params, handler.serialize(object, names));
+ }
+ // If it's not a dynamic segment and we're updating
+ } else if (doUpdate) {
+ // If we've passed the match point we need to deserialize again
+ // or if we never had a context
+ if (i > startIdx || !handler.hasOwnProperty('context')) {
+ if (handler.deserialize) {
+ object = handler.deserialize({});
+ objectChanged = true;
+ }
+ // Otherwise use existing context
+ } else {
+ object = handler.context;
+ }
+ }
+
+ // Make sure that we update the context here so it's available to
+ // subsequent deserialize calls
+ if (doUpdate && objectChanged) {
+ // TODO: It's a bit awkward to set the context twice, see if we can DRY things up
+ setContext(handler, object);
+ }
+
+ toSetup.push({
+ isDynamic: !!handlerObj.names.length,
+ handler: handlerObj.handler,
+ name: handlerObj.name,
+ context: object
+ });
+ }
+
+ return { params: params, toSetup: toSetup };
+ },
+
+ isActive: function(handlerName) {
+ var contexts = [].slice.call(arguments, 1);
+
+ var currentHandlerInfos = this.currentHandlerInfos,
+ found = false, names, object, handlerInfo, handlerObj;
+
+ for (var i=currentHandlerInfos.length-1; i>=0; i--) {
+ handlerInfo = currentHandlerInfos[i];
+ if (handlerInfo.name === handlerName) { found = true; }
+
+ if (found) {
+ if (contexts.length === 0) { break; }
+
+ if (handlerInfo.isDynamic) {
+ object = contexts.pop();
+ if (handlerInfo.context !== object) { return false; }
+ }
+ }
+ }
+
+ return contexts.length === 0 && found;
+ },
+
+ trigger: function(name) {
+ var args = [].slice.call(arguments);
+ trigger(this, args);
+ }
+ };
+
+ function merge(hash, other) {
+ for (var prop in other) {
+ if (other.hasOwnProperty(prop)) { hash[prop] = other[prop]; }
+ }
+ }
+
+ function isCurrent(currentHandlerInfos, handlerName) {
+ return currentHandlerInfos[currentHandlerInfos.length - 1].name === handlerName;
+ }
+
+ /**
+ @private
+
+ This function is called the first time the `collectObjects`
+ function encounters a promise while converting URL parameters
+ into objects.
+
+ It triggers the `enter` and `setup` methods on the `loading`
+ handler.
+
+ @param {Router} router
+ */
+ function loading(router) {
+ if (!router.isLoading) {
+ router.isLoading = true;
+ var handler = router.getHandler('loading');
+
+ if (handler) {
+ if (handler.enter) { handler.enter(); }
+ if (handler.setup) { handler.setup(); }
+ }
+ }
+ }
+
+ /**
+ @private
+
+ This function is called if a promise was previously
+ encountered once all promises are resolved.
+
+ It triggers the `exit` method on the `loading` handler.
+
+ @param {Router} router
+ */
+ function loaded(router) {
+ router.isLoading = false;
+ var handler = router.getHandler('loading');
+ if (handler && handler.exit) { handler.exit(); }
+ }
+
+ /**
+ @private
+
+ This function is called if any encountered promise
+ is rejected.
+
+ It triggers the `exit` method on the `loading` handler,
+ the `enter` method on the `failure` handler, and the
+ `setup` method on the `failure` handler with the
+ `error`.
+
+ @param {Router} router
+ @param {Object} error the reason for the promise
+ rejection, to pass into the failure handler's
+ `setup` method.
+ */
+ function failure(router, error) {
+ loaded(router);
+ var handler = router.getHandler('failure');
+ if (handler && handler.setup) { handler.setup(error); }
+ }
+
+ /**
+ @private
+ */
+ function doTransition(router, name, method, args) {
+ var output = router._paramsForHandler(name, args, true);
+ var params = output.params, toSetup = output.toSetup;
+
+ var url = router.recognizer.generate(name, params);
+ method.call(router, url);
+
+ setupContexts(router, toSetup);
+ }
+
+ /**
+ @private
+
+ This function is called after a URL change has been handled
+ by `router.handleURL`.
+
+ Takes an Array of `RecognizedHandler`s, and converts the raw
+ params hashes into deserialized objects by calling deserialize
+ on the handlers. This process builds up an Array of
+ `HandlerInfo`s. It then calls `setupContexts` with the Array.
+
+ If the `deserialize` method on a handler returns a promise
+ (i.e. has a method called `then`), this function will pause
+ building up the `HandlerInfo` Array until the promise is
+ resolved. It will use the resolved value as the context of
+ `HandlerInfo`.
+ */
+ function collectObjects(router, results, index, objects) {
+ if (results.length === index) {
+ loaded(router);
+ setupContexts(router, objects);
+ return;
+ }
+
+ var result = results[index];
+ var handler = router.getHandler(result.handler);
+ var object = handler.deserialize && handler.deserialize(result.params);
+
+ if (object && typeof object.then === 'function') {
+ loading(router);
+
+ // The chained `then` means that we can also catch errors that happen in `proceed`
+ object.then(proceed).then(null, function(error) {
+ failure(router, error);
+ });
+ } else {
+ proceed(object);
+ }
+
+ function proceed(value) {
+ if (handler.context !== object) {
+ setContext(handler, object);
+ }
+
+ var updatedObjects = objects.concat([{
+ context: value,
+ handler: result.handler,
+ isDynamic: result.isDynamic
+ }]);
+ collectObjects(router, results, index + 1, updatedObjects);
+ }
+ }
+
+ /**
+ @private
+
+ Takes an Array of `UnresolvedHandlerInfo`s, resolves the handler names
+ into handlers, and then figures out what to do with each of the handlers.
+
+ For example, consider the following tree of handlers. Each handler is
+ followed by the URL segment it handles.
+
+ ```
+ |~index ("/")
+ | |~posts ("/posts")
+ | | |-showPost ("/:id")
+ | | |-newPost ("/new")
+ | | |-editPost ("/edit")
+ | |~about ("/about/:id")
+ ```
+
+ Consider the following transitions:
+
+ 1. A URL transition to `/posts/1`.
+ 1. Triggers the `deserialize` callback on the
+ `index`, `posts`, and `showPost` handlers
+ 2. Triggers the `enter` callback on the same
+ 3. Triggers the `setup` callback on the same
+ 2. A direct transition to `newPost`
+ 1. Triggers the `exit` callback on `showPost`
+ 2. Triggers the `enter` callback on `newPost`
+ 3. Triggers the `setup` callback on `newPost`
+ 3. A direct transition to `about` with a specified
+ context object
+ 1. Triggers the `exit` callback on `newPost`
+ and `posts`
+ 2. Triggers the `serialize` callback on `about`
+ 3. Triggers the `enter` callback on `about`
+ 4. Triggers the `setup` callback on `about`
+
+ @param {Router} router
+ @param {Array[UnresolvedHandlerInfo]} handlerInfos
+ */
+ function setupContexts(router, handlerInfos) {
+ resolveHandlers(router, handlerInfos);
+
+ var partition =
+ partitionHandlers(router.currentHandlerInfos || [], handlerInfos);
+
+ router.currentHandlerInfos = handlerInfos;
+
+ eachHandler(partition.exited, function(handler, context) {
+ delete handler.context;
+ if (handler.exit) { handler.exit(); }
+ });
+
+ eachHandler(partition.updatedContext, function(handler, context) {
+ setContext(handler, context);
+ if (handler.setup) { handler.setup(context); }
+ });
+
+ var aborted = false;
+ eachHandler(partition.entered, function(handler, context) {
+ if (aborted) { return; }
+ if (handler.enter) { handler.enter(); }
+ setContext(handler, context);
+ if (handler.setup) {
+ if (false === handler.setup(context)) {
+ aborted = true;
+ }
+ }
+ });
+
+ if (router.didTransition) {
+ router.didTransition(handlerInfos);
+ }
+ }
+
+ /**
+ @private
+
+ Iterates over an array of `HandlerInfo`s, passing the handler
+ and context into the callback.
+
+ @param {Array[HandlerInfo]} handlerInfos
+ @param {Function(Object, Object)} callback
+ */
+ function eachHandler(handlerInfos, callback) {
+ for (var i=0, l=handlerInfos.length; i<l; i++) {
+ var handlerInfo = handlerInfos[i],
+ handler = handlerInfo.handler,
+ context = handlerInfo.context;
+
+ callback(handler, context);
+ }
+ }
+
+ /**
+ @private
+
+ Updates the `handler` field in each element in an Array of
+ `UnresolvedHandlerInfo`s from a handler name to a resolved handler.
+
+ When done, the Array will contain `HandlerInfo` structures.
+
+ @param {Router} router
+ @param {Array[UnresolvedHandlerInfo]} handlerInfos
+ */
+ function resolveHandlers(router, handlerInfos) {
+ var handlerInfo;
+
+ for (var i=0, l=handlerInfos.length; i<l; i++) {
+ handlerInfo = handlerInfos[i];
+
+ handlerInfo.name = handlerInfo.handler;
+ handlerInfo.handler = router.getHandler(handlerInfo.handler);
+ }
+ }
+
+ /**
+ @private
+
+ This function is called when transitioning from one URL to
+ another to determine which handlers are not longer active,
+ which handlers are newly active, and which handlers remain
+ active but have their context changed.
+
+ Take a list of old handlers and new handlers and partition
+ them into four buckets:
+
+ * unchanged: the handler was active in both the old and
+ new URL, and its context remains the same
+ * updated context: the handler was active in both the
+ old and new URL, but its context changed. The handler's
+ `setup` method, if any, will be called with the new
+ context.
+ * exited: the handler was active in the old URL, but is
+ no longer active.
+ * entered: the handler was not active in the old URL, but
+ is now active.
+
+ The PartitionedHandlers structure has three fields:
+
+ * `updatedContext`: a list of `HandlerInfo` objects that
+ represent handlers that remain active but have a changed
+ context
+ * `entered`: a list of `HandlerInfo` objects that represent
+ handlers that are newly active
+ * `exited`: a list of `HandlerInfo` objects that are no
+ longer active.
+
+ @param {Array[HandlerInfo]} oldHandlers a list of the handler
+ information for the previous URL (or `[]` if this is the
+ first handled transition)
+ @param {Array[HandlerInfo]} newHandlers a list of the handler
+ information for the new URL
+
+ @return {Partition}
+ */
+ function partitionHandlers(oldHandlers, newHandlers) {
+ var handlers = {
+ updatedContext: [],
+ exited: [],
+ entered: []
+ };
+
+ var handlerChanged, contextChanged, i, l;
+
+ for (i=0, l=newHandlers.length; i<l; i++) {
+ var oldHandler = oldHandlers[i], newHandler = newHandlers[i];
+
+ if (!oldHandler || oldHandler.handler !== newHandler.handler) {
+ handlerChanged = true;
+ }
+
+ if (handlerChanged) {
+ handlers.entered.push(newHandler);
+ if (oldHandler) { handlers.exited.unshift(oldHandler); }
+ } else if (contextChanged || oldHandler.context !== newHandler.context) {
+ contextChanged = true;
+ handlers.updatedContext.push(newHandler);
+ }
+ }
+
+ for (i=newHandlers.length, l=oldHandlers.length; i<l; i++) {
+ handlers.exited.unshift(oldHandlers[i]);
+ }
+
+ return handlers;
+ }
+
+ function trigger(router, args) {
+ var currentHandlerInfos = router.currentHandlerInfos;
+
+ var name = args.shift();
+
+ if (!currentHandlerInfos) {
+ throw new Error("Could not trigger event '" + name + "'. There are no active handlers");
+ }
+
+ for (var i=currentHandlerInfos.length-1; i>=0; i--) {
+ var handlerInfo = currentHandlerInfos[i],
+ handler = handlerInfo.handler;
+
+ if (handler.events && handler.events[name]) {
+ handler.events[name].apply(handler, args);
+ return;
+ }
+ }
+
+ throw new Error("Nothing handled the event '" + name + "'.");
+ }
+
+ function setContext(handler, context) {
+ handler.context = context;
+ if (handler.contextDidChange) { handler.contextDidChange(); }
+ }
+ return Router;
+ });
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-routing
+*/
+
+function DSL(name) {
+ this.parent = name;
+ this.matches = [];
+}
+
+DSL.prototype = {
+ resource: function(name, options, callback) {
+ if (arguments.length === 2 && typeof options === 'function') {
+ callback = options;
+ options = {};
+ }
+
+ if (arguments.length === 1) {
+ options = {};
+ }
+
+ if (typeof options.path !== 'string') {
+ options.path = "/" + name;
+ }
+
+ if (callback) {
+ var dsl = new DSL(name);
+ callback.call(dsl);
+ this.push(options.path, name, dsl.generate());
+ } else {
+ this.push(options.path, name);
+ }
+ },
+
+ push: function(url, name, callback) {
+ if (url === "" || url === "/") { this.explicitIndex = true; }
+
+ this.matches.push([url, name, callback]);
+ },
+
+ route: function(name, options) {
+ Ember.assert("You must use `this.resource` to nest", typeof options !== 'function');
+
+ options = options || {};
+
+ if (typeof options.path !== 'string') {
+ options.path = "/" + name;
+ }
+
+ if (this.parent && this.parent !== 'application') {
+ name = this.parent + "." + name;
+ }
+
+ this.push(options.path, name);
+ },
+
+ generate: function() {
+ var dslMatches = this.matches;
+
+ if (!this.explicitIndex) {
+ this.route("index", { path: "/" });
+ }
+
+ return function(match) {
+ for (var i=0, l=dslMatches.length; i<l; i++) {
+ var dslMatch = dslMatches[i];
+ match(dslMatch[0]).to(dslMatch[1], dslMatch[2]);
+ }
+ };
+ }
+};
+
+DSL.map = function(callback) {
+ var dsl = new DSL();
+ callback.call(dsl);
+ return dsl;
+};
+
+Ember.RouterDSL = DSL;
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-routing
+*/
+
+Ember.controllerFor = function(container, controllerName, context) {
+ return container.lookup('controller:' + controllerName) ||
+ Ember.generateController(container, controllerName, context);
+};
+
+Ember.generateController = function(container, controllerName, context) {
+ var controller;
+
+ if (context && Ember.isArray(context)) {
+ controller = Ember.ArrayController.extend({
+ content: context
+ });
+ } else if (context) {
+ controller = Ember.ObjectController.extend({
+ content: context
+ });
+ } else {
+ controller = Ember.Controller.extend();
+ }
+
+ controller.toString = function() {
+ return "(generated " + controllerName + " controller)";
+ };
+
+ container.register('controller', controllerName, controller);
+ return container.lookup('controller:' + controllerName);
+};
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-routing
+*/
+
+var Router = requireModule("router");
+var get = Ember.get, set = Ember.set, classify = Ember.String.classify;
+
+var DefaultView = Ember._MetamorphView;
+function setupLocation(router) {
+ var location = get(router, 'location'),
+ rootURL = get(router, 'rootURL');
+
+ if ('string' === typeof location) {
+ location = set(router, 'location', Ember.Location.create({
+ implementation: location
+ }));
+
+ if (typeof rootURL === 'string') {
+ set(location, 'rootURL', rootURL);
+ }
+ }
+}
+
+/**
+ The `Ember.Router` class manages the application state and URLs. Refer to
+ the [routing guide](http://emberjs.com/guides/routing/) for documentation.
+
+ @class Router
+ @namespace Ember
+ @extends Ember.Object
+*/
+Ember.Router = Ember.Object.extend({
+ location: 'hash',
+
+ init: function() {
+ this.router = this.constructor.router;
+ this._activeViews = {};
+ setupLocation(this);
+ },
+
+ url: Ember.computed(function() {
+ return get(this, 'location').getURL();
+ }),
+
+ startRouting: function() {
+ this.router = this.router || this.constructor.map(Ember.K);
+
+ var router = this.router,
+ location = get(this, 'location'),
+ container = this.container,
+ self = this;
+
+ setupRouter(this, router, location);
+
+ container.register('view', 'default', DefaultView);
+ container.register('view', 'toplevel', Ember.View.extend());
+
+ location.onUpdateURL(function(url) {
+ self.handleURL(url);
+ });
+
+ this.handleURL(location.getURL());
+ },
+
+ didTransition: function(infos) {
+ // Don't do any further action here if we redirected
+ for (var i=0, l=infos.length; i<l; i++) {
+ if (infos[i].handler.redirected) { return; }
+ }
+
+ var appController = this.container.lookup('controller:application'),
+ path = routePath(infos);
+
+ set(appController, 'currentPath', path);
+ this.notifyPropertyChange('url');
+
+ if (get(this, 'namespace').LOG_TRANSITIONS) {
+ Ember.Logger.log("Transitioned into '" + path + "'");
+ }
+ },
+
+ handleURL: function(url) {
+ this.router.handleURL(url);
+ this.notifyPropertyChange('url');
+ },
+
+ transitionTo: function(name) {
+ var args = [].slice.call(arguments);
+ doTransition(this, 'transitionTo', args);
+ },
+
+ replaceWith: function() {
+ var args = [].slice.call(arguments);
+ doTransition(this, 'replaceWith', args);
+ },
+
+ generate: function() {
+ var url = this.router.generate.apply(this.router, arguments);
+ return this.location.formatURL(url);
+ },
+
+ isActive: function(routeName) {
+ var router = this.router;
+ return router.isActive.apply(router, arguments);
+ },
+
+ send: function(name, context) {
+ this.router.trigger.apply(this.router, arguments);
+ },
+
+ hasRoute: function(route) {
+ return this.router.hasRoute(route);
+ },
+
+ _lookupActiveView: function(templateName) {
+ var active = this._activeViews[templateName];
+ return active && active[0];
+ },
+
+ _connectActiveView: function(templateName, view) {
+ var existing = this._activeViews[templateName];
+
+ if (existing) {
+ existing[0].off('willDestroyElement', this, existing[1]);
+ }
+
+ var disconnect = function() {
+ delete this._activeViews[templateName];
+ };
+
+ this._activeViews[templateName] = [view, disconnect];
+ view.one('willDestroyElement', this, disconnect);
+ }
+});
+
+Ember.Router.reopenClass({
+ defaultFailureHandler: {
+ setup: function(error) {
+ Ember.Logger.error('Error while loading route:', error);
+
+ // Using setTimeout allows us to escape from the Promise's try/catch block
+ setTimeout(function() { throw error; });
+ }
+ }
+});
+
+function getHandlerFunction(router) {
+ var seen = {}, container = router.container;
+
+ return function(name) {
+ var handler = container.lookup('route:' + name);
+ if (seen[name]) { return handler; }
+
+ seen[name] = true;
+
+ if (!handler) {
+ if (name === 'loading') { return {}; }
+ if (name === 'failure') { return router.constructor.defaultFailureHandler; }
+
+ container.register('route', name, Ember.Route.extend());
+ handler = container.lookup('route:' + name);
+ }
+
+ handler.routeName = name;
+ return handler;
+ };
+}
+
+function handlerIsActive(router, handlerName) {
+ var handler = router.container.lookup('route:' + handlerName),
+ currentHandlerInfos = router.router.currentHandlerInfos,
+ handlerInfo;
+
+ for (var i=0, l=currentHandlerInfos.length; i<l; i++) {
+ handlerInfo = currentHandlerInfos[i];
+ if (handlerInfo.handler === handler) { return true; }
+ }
+
+ return false;
+}
+
+function routePath(handlerInfos) {
+ var path = [];
+
+ for (var i=1, l=handlerInfos.length; i<l; i++) {
+ var name = handlerInfos[i].name,
+ nameParts = name.split(".");
+
+ path.push(nameParts[nameParts.length - 1]);
+ }
+
+ return path.join(".");
+}
+
+function setupRouter(emberRouter, router, location) {
+ var lastURL;
+
+ router.getHandler = getHandlerFunction(emberRouter);
+
+ var doUpdateURL = function() {
+ location.setURL(lastURL);
+ };
+
+ router.updateURL = function(path) {
+ lastURL = path;
+ Ember.run.once(doUpdateURL);
+ };
+
+ if (location.replaceURL) {
+ var doReplaceURL = function() {
+ location.replaceURL(lastURL);
+ };
+
+ router.replaceURL = function(path) {
+ lastURL = path;
+ Ember.run.once(doReplaceURL);
+ };
+ }
+
+ router.didTransition = function(infos) {
+ emberRouter.didTransition(infos);
+ };
+}
+
+function doTransition(router, method, args) {
+ var passedName = args[0], name;
+
+ if (!router.router.hasRoute(args[0])) {
+ name = args[0] = passedName + '.index';
+ } else {
+ name = passedName;
+ }
+
+ Ember.assert("The route " + passedName + " was not found", router.router.hasRoute(name));
+
+ router.router[method].apply(router.router, args);
+ router.notifyPropertyChange('url');
+}
+
+Ember.Router.reopenClass({
+ map: function(callback) {
+ var router = this.router = new Router();
+
+ var dsl = Ember.RouterDSL.map(function() {
+ this.resource('application', { path: "/" }, function() {
+ callback.call(this);
+ });
+ });
+
+ router.map(dsl.generate());
+ return router;
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-routing
+*/
+
+var get = Ember.get, set = Ember.set,
+ classify = Ember.String.classify,
+ decamelize = Ember.String.decamelize;
+
+/**
+ The `Ember.Route` class is used to define individual routes. Refer to
+ the [routing guide](http://emberjs.com/guides/routing/) for documentation.
+
+ @class Route
+ @namespace Ember
+ @extends Ember.Object
+*/
+Ember.Route = Ember.Object.extend({
+ /**
+ @private
+
+ @method exit
+ */
+ exit: function() {
+ this.deactivate();
+ teardownView(this);
+ },
+
+ /**
+ @private
+
+ @method enter
+ */
+ enter: function() {
+ this.activate();
+ },
+
+ /**
+ The collection of functions keyed by name available on this route as
+ action targets.
+
+ These functions will be invoked when a matching `{{action}}` is triggered
+ from within a template and the application's current route is this route.
+
+ Events can also be invoked from other parts of your application via `Route#send`.
+
+ The context of event will be the this route.
+
+ @see {Ember.Route#send}
+ @see {Handlebars.helpers.action}
+
+ @property events
+ @type Hash
+ @default null
+ */
+ events: null,
+
+ /**
+ This hook is executed when the router completely exits this route. It is
+ not executed when the model for the route changes.
+
+ @method deactivate
+ */
+ deactivate: Ember.K,
+
+ /**
+ This hook is executed when the router enters the route for the first time.
+ It is not executed when the model for the route changes.
+
+ @method activate
+ */
+ activate: Ember.K,
+
+ /**
+ Transition into another route. Optionally supply a model for the
+ route in question. The model will be serialized into the URL
+ using the `serialize` hook.
+
+ @method transitionTo
+ @param {String} name the name of the route
+ @param {...Object} models the
+ */
+ transitionTo: function() {
+ if (this._checkingRedirect) { this.redirected = true; }
+ return this.router.transitionTo.apply(this.router, arguments);
+ },
+
+ /**
+ Transition into another route while replacing the current URL if
+ possible. Identical to `transitionTo` in all other respects.
+
+ @method replaceWith
+ @param {String} name the name of the route
+ @param {...Object} models the
+ */
+ replaceWith: function() {
+ if (this._checkingRedirect) { this.redirected = true; }
+ return this.router.replaceWith.apply(this.router, arguments);
+ },
+
+ send: function() {
+ return this.router.send.apply(this.router, arguments);
+ },
+
+ /**
+ @private
+
+ This hook is the entry point for router.js
+
+ @method setup
+ */
+ setup: function(context) {
+ this.redirected = false;
+ this._checkingRedirect = true;
+
+ this.redirect(context);
+
+ this._checkingRedirect = false;
+ if (this.redirected) { return false; }
+
+ var controller = this.controllerFor(this.routeName, context);
+
+ if (controller) {
+ this.controller = controller;
+ set(controller, 'model', context);
+ }
+
+ if (this.setupControllers) {
+ Ember.deprecate("Ember.Route.setupControllers is deprecated. Please use Ember.Route.setupController(controller, model) instead.");
+ this.setupControllers(controller, context);
+ } else {
+ this.setupController(controller, context);
+ }
+
+ if (this.renderTemplates) {
+ Ember.deprecate("Ember.Route.renderTemplates is deprecated. Please use Ember.Route.renderTemplate(controller, model) instead.");
+ this.renderTemplates(context);
+ } else {
+ this.renderTemplate(controller, context);
+ }
+ },
+
+ /**
+ A hook you can implement to optionally redirect to another route.
+
+ If you call `this.transitionTo` from inside of this hook, this route
+ will not be entered in favor of the other hook.
+
+ @method redirect
+ @param {Object} model the model for this route
+ */
+ redirect: Ember.K,
+
+ /**
+ @private
+
+ The hook called by `router.js` to convert parameters into the context
+ for this handler. The public Ember hook is `model`.
+
+ @method deserialize
+ */
+ deserialize: function(params) {
+ var model = this.model(params);
+ return this.currentModel = model;
+ },
+
+ /**
+ @private
+
+ Called when the context is changed by router.js.
+ */
+ contextDidChange: function() {
+ this.currentModel = this.context;
+ },
+
+ /**
+ A hook you can implement to convert the URL into the model for
+ this route.
+
+ ```js
+ App.Route.map(function(match) {
+ match("/posts/:post_id").to("post");
+ });
+ ```
+
+ The model for the `post` route is `App.Post.find(params.post_id)`.
+
+ By default, if your route has a dynamic segment ending in `_id`:
+
+ * The model class is determined from the segment (`post_id`'s
+ class is `App.Post`)
+ * The find method is called on the model class with the value of
+ the dynamic segment.
+
+ @method model
+ @param {Object} params the parameters extracted from the URL
+ */
+ model: function(params) {
+ var match, name, sawParams, value;
+
+ for (var prop in params) {
+ if (match = prop.match(/^(.*)_id$/)) {
+ name = match[1];
+ value = params[prop];
+ }
+ sawParams = true;
+ }
+
+ if (!name && sawParams) { return params; }
+ else if (!name) { return; }
+
+ var className = classify(name),
+ namespace = this.router.namespace,
+ modelClass = namespace[className];
+
+ Ember.assert("You used the dynamic segment " + name + "_id in your router, but " + namespace + "." + className + " did not exist and you did not override your state's `model` hook.", modelClass);
+ return modelClass.find(value);
+ },
+
+ /**
+ A hook you can implement to convert the route's model into parameters
+ for the URL.
+
+ ```js
+ App.Route.map(function(match) {
+ match("/posts/:post_id").to("post");
+ });
+
+ App.PostRoute = Ember.Route.extend({
+ model: function(params) {
+ // the server returns `{ id: 12 }`
+ return jQuery.getJSON("/posts/" + params.post_id);
+ },
+
+ serialize: function(model) {
+ // this will make the URL `/posts/12`
+ return { post_id: model.id };
+ }
+ });
+ ```
+
+ The default `serialize` method inserts the model's `id` into the
+ route's dynamic segment (in this case, `:post_id`).
+
+ This method is called when `transitionTo` is called with a context
+ in order to populate the URL.
+
+ @method serialize
+ @param {Object} model the route's model
+ @param {Array} params an Array of parameter names for the current
+ route (in the example, `['post_id']`.
+ @return {Object} the serialized parameters
+ */
+ serialize: function(model, params) {
+ if (params.length !== 1) { return; }
+
+ var name = params[0], object = {};
+
+ if (/_id$/.test(name)) {
+ object[name] = get(model, 'id');
+ } else {
+ object[name] = model;
+ }
+
+ return object;
+ },
+
+ /**
+ A hook you can use to setup the controller for the current route.
+
+ This method is called with the controller for the current route and the
+ model supplied by the `model` hook.
+
+ ```js
+ App.Route.map(function(match) {
+ match("/posts/:post_id").to("post");
+ });
+ ```
+
+ For the `post` route, the controller is `App.PostController`.
+
+ By default, the `setupController` hook sets the `content` property of
+ the controller to the `model`.
+
+ If no explicit controller is defined, the route will automatically create
+ an appropriate controller for the model:
+
+ * if the model is an `Ember.Array` (including record arrays from Ember
+ Data), the controller is an `Ember.ArrayController`.
+ * otherwise, the controller is an `Ember.ObjectController`.
+
+ This means that your template will get a proxy for the model as its
+ context, and you can act as though the model itself was the context.
+
+ @method setupController
+ */
+ setupController: Ember.K,
+
+ /**
+ Returns the controller for a particular route.
+
+ ```js
+ App.PostRoute = Ember.Route.extend({
+ setupController: function(controller, post) {
+ this._super(controller, post);
+ this.controllerFor('posts').set('currentPost', post);
+ }
+ });
+ ```
+
+ By default, the controller for `post` is the shared instance of
+ `App.PostController`.
+
+ @method controllerFor
+ @param {String} name the name of the route
+ @param {Object} model the model associated with the route (optional)
+ @return {Ember.Controller}
+ */
+ controllerFor: function(name, model) {
+ var container = this.router.container,
+ controller = container.lookup('controller:' + name);
+
+ if (!controller) {
+ model = model || this.modelFor(name);
+
+ Ember.assert("You are trying to look up a controller that you did not define, and for which Ember does not know the model.\n\nThis is not a controller for a route, so you must explicitly define the controller ("+this.router.namespace.toString() + "." + Ember.String.capitalize(Ember.String.camelize(name))+"Controller) or pass a model as the second parameter to `controllerFor`, so that Ember knows which type of controller to create for you.", model || this.container.lookup('route:' + name));
+
+ controller = Ember.generateController(container, name, model);
+ }
+
+ return controller;
+ },
+
+ /**
+ Returns the current model for a given route.
+
+ This is the object returned by the `model` hook of the route
+ in question.
+
+ @method modelFor
+ @param {String} name the name of the route
+ @return {Object} the model object
+ */
+ modelFor: function(name) {
+ var route = this.container.lookup('route:' + name);
+ return route && route.currentModel;
+ },
+
+ /**
+ A hook you can use to render the template for the current route.
+
+ This method is called with the controller for the current route and the
+ model supplied by the `model` hook. By default, it renders the route's
+ template, configured with the controller for the route.
+
+ This method can be overridden to set up and render additional or
+ alternative templates.
+
+ @method renderTemplate
+ @param {Object} controller the route's controller
+ @param {Object} model the route's model
+ */
+ renderTemplate: function(controller, model) {
+ this.render();
+ },
+
+ /**
+ Renders a template into an outlet.
+
+ This method has a number of defaults, based on the name of the
+ route specified in the router.
+
+ For example:
+
+ ```js
+ App.Router.map(function(match) {
+ match("/").to("index");
+ match("/posts/:post_id").to("post");
+ });
+
+ App.PostRoute = App.Route.extend({
+ renderTemplate: function() {
+ this.render();
+ }
+ });
+ ```
+
+ The name of the `PostRoute`, as defined by the router, is `post`.
+
+ By default, render will:
+
+ * render the `post` template
+ * with the `post` view (`PostView`) for event handling, if one exists
+ * and the `post` controller (`PostController`), if one exists
+ * into the `main` outlet of the `application` template
+
+ You can override this behavior:
+
+ ```js
+ App.PostRoute = App.Route.extend({
+ renderTemplate: function() {
+ this.render('myPost', { // the template to render
+ into: 'index', // the template to render into
+ outlet: 'detail', // the name of the outlet in that template
+ controller: 'blogPost' // the controller to use for the template
+ });
+ }
+ });
+ ```
+
+ Remember that the controller's `content` will be the route's model. In
+ this case, the default model will be `App.Post.find(params.post_id)`.
+
+ @method render
+ @param {String} name the name of the template to render
+ @param {Object} options the options
+ */
+ render: function(name, options) {
+ if (typeof name === 'object' && !options) {
+ options = name;
+ name = this.routeName;
+ }
+
+ name = name ? name.replace(/\//g, '.') : this.routeName;
+
+ var container = this.container,
+ view = container.lookup('view:' + name),
+ template = container.lookup('template:' + name);
+
+ if (!view && !template) { return; }
+
+ options = normalizeOptions(this, name, template, options);
+ view = setupView(view, container, options);
+
+ if (options.outlet === 'main') { this.lastRenderedTemplate = name; }
+
+ appendView(this, view, options);
+ },
+
+ willDestroy: function() {
+ teardownView(this);
+ }
+});
+
+function parentRoute(route) {
+ var handlerInfos = route.router.router.currentHandlerInfos;
+
+ var parent, current;
+
+ for (var i=0, l=handlerInfos.length; i<l; i++) {
+ current = handlerInfos[i].handler;
+ if (current === route) { return parent; }
+ parent = current;
+ }
+}
+
+function parentTemplate(route, isRecursive) {
+ var parent = parentRoute(route), template;
+
+ if (!parent) { return; }
+
+ Ember.warn("The immediate parent route did not render into the main outlet and the default 'into' option may not be expected", !isRecursive);
+
+ if (template = parent.lastRenderedTemplate) {
+ return template;
+ } else {
+ return parentTemplate(parent, true);
+ }
+}
+
+function normalizeOptions(route, name, template, options) {
+ options = options || {};
+ options.into = options.into ? options.into.replace(/\//g, '.') : parentTemplate(route);
+ options.outlet = options.outlet || 'main';
+ options.name = name;
+ options.template = template;
+
+ Ember.assert("An outlet ("+options.outlet+") was specified but this view will render at the root level.", options.outlet === 'main' || options.into);
+
+ var controller = options.controller, namedController;
+
+ if (options.controller) {
+ controller = options.controller;
+ } else if (namedController = route.container.lookup('controller:' + name)) {
+ controller = namedController;
+ } else {
+ controller = route.routeName;
+ }
+
+ if (typeof controller === 'string') {
+ controller = route.container.lookup('controller:' + controller);
+ }
+
+ options.controller = controller;
+
+ return options;
+}
+
+function setupView(view, container, options) {
+ var defaultView = options.into ? 'view:default' : 'view:toplevel';
+
+ view = view || container.lookup(defaultView);
+
+ if (!get(view, 'templateName')) {
+ set(view, 'template', options.template);
+
+ set(view, '_debugTemplateName', options.name);
+ }
+
+ set(view, 'renderedName', options.name);
+ set(view, 'controller', options.controller);
+
+ return view;
+}
+
+function appendView(route, view, options) {
+ if (options.into) {
+ var parentView = route.router._lookupActiveView(options.into);
+ route.teardownView = teardownOutlet(parentView, options.outlet);
+ parentView.connectOutlet(options.outlet, view);
+ } else {
+ var rootElement = get(route, 'router.namespace.rootElement');
+ route.router._connectActiveView(options.name, view);
+ route.teardownView = teardownTopLevel(view);
+ view.appendTo(rootElement);
+ }
+}
+
+function teardownTopLevel(view) {
+ return function() { view.destroy(); };
+}
+
+function teardownOutlet(parentView, outlet) {
+ return function() { parentView.disconnectOutlet(outlet); };
+}
+
+function teardownView(route) {
+ if (route.teardownView) { route.teardownView(); }
+
+ delete route.teardownView;
+ delete route.lastRenderedTemplate;
+}
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-routing
+*/
+
+var get = Ember.get, set = Ember.set;
+Ember.onLoad('Ember.Handlebars', function(Handlebars) {
+
+ var resolveParams = Ember.Handlebars.resolveParams,
+ isSimpleClick = Ember.ViewUtils.isSimpleClick;
+
+ function fullRouteName(router, name) {
+ if (!router.hasRoute(name)) {
+ name = name + '.index';
+ }
+
+ return name;
+ }
+
+ function resolvedPaths(options) {
+ var types = options.options.types.slice(1),
+ data = options.options.data;
+
+ return resolveParams(options.context, options.params, { types: types, data: data });
+ }
+
+ function args(linkView, router, route) {
+ var passedRouteName = route || linkView.namedRoute, routeName;
+
+ routeName = fullRouteName(router, passedRouteName);
+
+ Ember.assert("The route " + passedRouteName + " was not found", router.hasRoute(routeName));
+
+ var ret = [ routeName ];
+ return ret.concat(resolvedPaths(linkView.parameters));
+ }
+
+ var LinkView = Ember.View.extend({
+ tagName: 'a',
+ namedRoute: null,
+ currentWhen: null,
+ title: null,
+ activeClass: 'active',
+ replace: false,
+ attributeBindings: ['href', 'title'],
+ classNameBindings: 'active',
+
+ // Even though this isn't a virtual view, we want to treat it as if it is
+ // so that you can access the parent with {{view.prop}}
+ concreteView: Ember.computed(function() {
+ return get(this, 'parentView');
+ }).property('parentView').volatile(),
+
+ active: Ember.computed(function() {
+ var router = this.get('router'),
+ params = resolvedPaths(this.parameters),
+ currentWithIndex = this.currentWhen + '.index',
+ isActive = router.isActive.apply(router, [this.currentWhen].concat(params)) ||
+ router.isActive.apply(router, [currentWithIndex].concat(params));
+
+ if (isActive) { return get(this, 'activeClass'); }
+ }).property('namedRoute', 'router.url'),
+
+ router: Ember.computed(function() {
+ return this.get('controller').container.lookup('router:main');
+ }),
+
+ click: function(event) {
+ if (!isSimpleClick(event)) { return true; }
+
+ event.preventDefault();
+ if (this.bubbles === false) { event.stopPropagation(); }
+
+ var router = this.get('router');
+
+ if (this.get('replace')) {
+ router.replaceWith.apply(router, args(this, router));
+ } else {
+ router.transitionTo.apply(router, args(this, router));
+ }
+ },
+
+ href: Ember.computed(function() {
+ var router = this.get('router');
+ return router.generate.apply(router, args(this, router));
+ })
+ });
+
+ LinkView.toString = function() { return "LinkView"; };
+
+ /**
+ @method linkTo
+ @for Ember.Handlebars.helpers
+ @param {String} routeName
+ @param {Object} [context]*
+ @return {String} HTML string
+ */
+ Ember.Handlebars.registerHelper('linkTo', function(name) {
+ var options = [].slice.call(arguments, -1)[0];
+ var params = [].slice.call(arguments, 1, -1);
+
+ var hash = options.hash;
+
+ hash.namedRoute = name;
+ hash.currentWhen = hash.currentWhen || name;
+
+ hash.parameters = {
+ context: this,
+ options: options,
+ params: params
+ };
+
+ return Ember.Handlebars.helpers.view.call(this, LinkView, options);
+ });
+
+});
+
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-routing
+*/
+
+var get = Ember.get, set = Ember.set;
+Ember.onLoad('Ember.Handlebars', function(Handlebars) {
+ /**
+ @module ember
+ @submodule ember-handlebars
+ */
+
+ Handlebars.OutletView = Ember.ContainerView.extend(Ember._Metamorph);
+
+ /**
+ The `outlet` helper allows you to specify that the current
+ view's controller will fill in the view for a given area.
+
+ ``` handlebars
+ {{outlet}}
+ ```
+
+ By default, when the the current controller's `view` property changes, the
+ outlet will replace its current view with the new view. You can set the
+ `view` property directly, but it's normally best to use `connectOutlet`.
+
+ ``` javascript
+ # Instantiate App.PostsView and assign to `view`, so as to render into outlet.
+ controller.connectOutlet('posts');
+ ```
+
+ You can also specify a particular name other than `view`:
+
+ ``` handlebars
+ {{outlet masterView}}
+ {{outlet detailView}}
+ ```
+
+ Then, you can control several outlets from a single controller.
+
+ ``` javascript
+ # Instantiate App.PostsView and assign to controller.masterView.
+ controller.connectOutlet('masterView', 'posts');
+ # Also, instantiate App.PostInfoView and assign to controller.detailView.
+ controller.connectOutlet('detailView', 'postInfo');
+ ```
+
+ @method outlet
+ @for Ember.Handlebars.helpers
+ @param {String} property the property on the controller
+ that holds the view for this outlet
+ */
+ Handlebars.registerHelper('outlet', function(property, options) {
+ var outletSource;
+
+ if (property && property.data && property.data.isRenderData) {
+ options = property;
+ property = 'main';
+ }
+
+ outletSource = options.data.view;
+ while (!(outletSource.get('template.isTop'))){
+ outletSource = outletSource.get('_parentView');
+ }
+
+ options.data.view.set('outletSource', outletSource);
+ options.hash.currentViewBinding = '_view.outletSource._outlets.' + property;
+
+ return Handlebars.helpers.view.call(this, Handlebars.OutletView, options);
+ });
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-routing
+*/
+
+var get = Ember.get, set = Ember.set;
+Ember.onLoad('Ember.Handlebars', function(Handlebars) {
+
+ /**
+ Renders the named template in the current context using the singleton
+ instance of the same-named controller.
+
+ If a view class with the same name exists, uses the view class.
+
+ If a `model` is specified, it becomes the model for that controller.
+
+ The default target for `{{action}}`s in the rendered template is the
+ named controller.
+
+ @method action
+ @for Ember.Handlebars.helpers
+ @param {String} actionName
+ @param {Object?} model
+ @param {Hash} options
+ */
+ Ember.Handlebars.registerHelper('render', function(name, contextString, options) {
+ Ember.assert("You must pass a template to render", arguments.length >= 2);
+ var container, router, controller, view, context;
+
+ if (arguments.length === 2) {
+ options = contextString;
+ contextString = undefined;
+ }
+
+ if (typeof contextString === 'string') {
+ context = Ember.Handlebars.get(options.contexts[1], contextString, options);
+ }
+
+ name = name.replace(/\//g, '.');
+ container = options.data.keywords.controller.container;
+ router = container.lookup('router:main');
+
+ Ember.assert("This view is already rendered", !router || !router._lookupActiveView(name));
+
+ view = container.lookup('view:' + name) || container.lookup('view:default');
+
+ if (controller = options.hash.controller) {
+ controller = container.lookup('controller:' + controller);
+ } else {
+ controller = Ember.controllerFor(container, name, context);
+ }
+
+ if (controller && context) {
+ controller.set('model', context);
+ }
+
+ var root = options.contexts[1];
+
+ if (root) {
+ view.registerObserver(root, contextString, function() {
+ controller.set('model', Ember.Handlebars.get(root, contextString, options));
+ });
+ }
+
+ controller.set('target', options.data.keywords.controller);
+
+ options.hash.viewName = Ember.String.camelize(name);
+ options.hash.template = container.lookup('template:' + name);
+ options.hash.controller = controller;
+
+ if (router) {
+ router._connectActiveView(name, view);
+ }
+
+ Ember.Handlebars.helpers.view.call(this, view, options);
+ });
+
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-routing
+*/
+Ember.onLoad('Ember.Handlebars', function(Handlebars) {
+
+ var resolveParams = Ember.Handlebars.resolveParams,
+ isSimpleClick = Ember.ViewUtils.isSimpleClick;
+
+ var EmberHandlebars = Ember.Handlebars,
+ handlebarsGet = EmberHandlebars.get,
+ SafeString = EmberHandlebars.SafeString,
+ get = Ember.get,
+ a_slice = Array.prototype.slice;
+
+ function args(options, actionName) {
+ var ret = [];
+ if (actionName) { ret.push(actionName); }
+
+ var types = options.options.types.slice(1),
+ data = options.options.data;
+
+ return ret.concat(resolveParams(options.context, options.params, { types: types, data: data }));
+ }
+
+ var ActionHelper = EmberHandlebars.ActionHelper = {
+ registeredActions: {}
+ };
+
+ ActionHelper.registerAction = function(actionName, options) {
+ var actionId = (++Ember.uuid).toString();
+
+ ActionHelper.registeredActions[actionId] = {
+ eventName: options.eventName,
+ handler: function(event) {
+ if (!isSimpleClick(event)) { return true; }
+ event.preventDefault();
+
+ if (options.bubbles === false) {
+ event.stopPropagation();
+ }
+
+ var view = options.view,
+ contexts = options.contexts,
+ target = options.target;
+
+ if (target.target) {
+ target = handlebarsGet(target.root, target.target, target.options);
+ } else {
+ target = target.root;
+ }
+
+ Ember.run(function() {
+ if (target.send) {
+ target.send.apply(target, args(options.parameters, actionName));
+ } else {
+ Ember.assert("The action '" + actionName + "' did not exist on " + target, typeof target[actionName] === 'function');
+ target[actionName].apply(target, args(options.parameters));
+ }
+ });
+ }
+ };
+
+ options.view.on('willClearRender', function() {
+ delete ActionHelper.registeredActions[actionId];
+ });
+
+ return actionId;
+ };
+
+ /**
+ The `{{action}}` helper registers an HTML element within a template for DOM
+ event handling and forwards that interaction to the view's controller
+ or supplied `target` option (see 'Specifying a Target').
+
+ If the view's controller does not implement the event, the event is sent
+ to the current route, and it bubbles up the route hierarchy from there.
+
+ User interaction with that element will invoke the supplied action name on
+ the appropriate target.
+
+ Given the following Handlebars template on the page
+
+ ```handlebars
+ <script type="text/x-handlebars" data-template-name='a-template'>
+ <div {{action anActionName}}>
+ click me
+ </div>
+ </script>
+ ```
+
+ And application code
+
+ ```javascript
+ AController = Ember.Controller.extend({
+ anActionName: function() {}
+ });
+
+ AView = Ember.View.extend({
+ controller: AController.create(),
+ templateName: 'a-template'
+ });
+
+ aView = AView.create();
+ aView.appendTo('body');
+ ```
+
+ Will results in the following rendered HTML
+
+ ```html
+ <div class="ember-view">
+ <div data-ember-action="1">
+ click me
+ </div>
+ </div>
+ ```
+
+ Clicking "click me" will trigger the `anActionName` method of the
+ `AController`. In this case, no additional parameters will be passed.
+
+ If you provide additional parameters to the helper:
+
+ ```handlebars
+ <button {{action 'edit' post}}>Edit</button>
+ ```
+
+ Those parameters will be passed along as arguments to the JavaScript
+ function implementing the action.
+
+ ### Event Propagation
+
+ Events triggered through the action helper will automatically have
+ `.preventDefault()` called on them. You do not need to do so in your event
+ handlers.
+
+ To also disable bubbling, pass `bubbles=false` to the helper:
+
+ ```handlebars
+ <button {{action 'edit' post bubbles=false}}>Edit</button>
+ ```
+
+ If you need the default handler to trigger you should either register your
+ own event handler, or use event methods on your view class. See `Ember.View`
+ 'Responding to Browser Events' for more information.
+
+ ### Specifying DOM event type
+
+ By default the `{{action}}` helper registers for DOM `click` events. You can
+ supply an `on` option to the helper to specify a different DOM event name:
+
+ ```handlebars
+ <script type="text/x-handlebars" data-template-name='a-template'>
+ <div {{action anActionName on="doubleClick"}}>
+ click me
+ </div>
+ </script>
+ ```
+
+ See `Ember.View` 'Responding to Browser Events' for a list of
+ acceptable DOM event names.
+
+ NOTE: Because `{{action}}` depends on Ember's event dispatch system it will
+ only function if an `Ember.EventDispatcher` instance is available. An
+ `Ember.EventDispatcher` instance will be created when a new `Ember.Application`
+ is created. Having an instance of `Ember.Application` will satisfy this
+ requirement.
+
+ ### Specifying a Target
+
+ There are several possible target objects for `{{action}}` helpers:
+
+ In a typical Ember application, where views are managed through use of the
+ `{{outlet}}` helper, actions will bubble to the current controller, then
+ to the current route, and then up the route hierarchy.
+
+ Alternatively, a `target` option can be provided to the helper to change
+ which object will receive the method call. This option must be a path
+ path to an object, accessible in the current context:
+
+ ```handlebars
+ <script type="text/x-handlebars" data-template-name='a-template'>
+ <div {{action anActionName target="MyApplication.someObject"}}>
+ click me
+ </div>
+ </script>
+ ```
+
+ Clicking "click me" in the rendered HTML of the above template will trigger
+ the `anActionName` method of the object at `MyApplication.someObject`.
+
+ If an action's target does not implement a method that matches the supplied
+ action name an error will be thrown.
+
+ ```handlebars
+ <script type="text/x-handlebars" data-template-name='a-template'>
+ <div {{action aMethodNameThatIsMissing}}>
+ click me
+ </div>
+ </script>
+ ```
+
+ With the following application code
+
+ ```javascript
+ AView = Ember.View.extend({
+ templateName; 'a-template',
+ // note: no method 'aMethodNameThatIsMissing'
+ anActionName: function(event) {}
+ });
+
+ aView = AView.create();
+ aView.appendTo('body');
+ ```
+
+ Will throw `Uncaught TypeError: Cannot call method 'call' of undefined` when
+ "click me" is clicked.
+
+ ### Additional Parameters
+
+ You may specify additional parameters to the `{{action}}` helper. These
+ parameters are passed along as the arguments to the JavaScript function
+ implementing the action.
+
+ ```handlebars
+ <script type="text/x-handlebars" data-template-name='a-template'>
+ {{#each person in people}}
+ <div {{action edit person}}>
+ click me
+ </div>
+ {{/each}}
+ </script>
+ ```
+
+ Clicking "click me" will trigger the `edit` method on the current view's
+ controller with the current person as a parameter.
+
+ @method action
+ @for Ember.Handlebars.helpers
+ @param {String} actionName
+ @param {Object} [context]*
+ @param {Hash} options
+ */
+ EmberHandlebars.registerHelper('action', function(actionName) {
+ var options = arguments[arguments.length - 1],
+ contexts = a_slice.call(arguments, 1, -1);
+
+ var hash = options.hash,
+ view = options.data.view,
+ controller, link;
+
+ // create a hash to pass along to registerAction
+ var action = {
+ eventName: hash.on || "click"
+ };
+
+ action.parameters = {
+ context: this,
+ options: options,
+ params: contexts
+ };
+
+ action.view = view = get(view, 'concreteView');
+
+ var root, target;
+
+ if (hash.target) {
+ root = this;
+ target = hash.target;
+ } else if (controller = options.data.keywords.controller) {
+ root = controller;
+ }
+
+ action.target = { root: root, target: target, options: options };
+ action.bubbles = hash.bubbles;
+
+ var actionId = ActionHelper.registerAction(actionName, action);
+ return new SafeString('data-ember-action="' + actionId + '"');
+ });
+
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-routing
+*/
+
+if (Ember.ENV.EXPERIMENTAL_CONTROL_HELPER) {
+ var get = Ember.get, set = Ember.set;
+
+ /**
+ The control helper is currently under development and is considered experimental.
+ To enable it, set `ENV.EXPERIMENTAL_CONTROL_HELPER = true` before requiring Ember.
+
+ @method control
+ @for Ember.Handlebars.helpers
+ @param {String} path
+ @param {String} modelPath
+ @param {Hash} options
+ @return {String} HTML string
+ */
+ Ember.Handlebars.registerHelper('control', function(path, modelPath, options) {
+ if (arguments.length === 2) {
+ options = modelPath;
+ modelPath = undefined;
+ }
+
+ var model;
+
+ if (modelPath) {
+ model = Ember.Handlebars.get(this, modelPath, options);
+ }
+
+ var controller = options.data.keywords.controller,
+ view = options.data.keywords.view,
+ children = get(controller, '_childContainers'),
+ controlID = options.hash.controlID,
+ container, subContainer;
+
+ if (children.hasOwnProperty(controlID)) {
+ subContainer = children[controlID];
+ } else {
+ container = get(controller, 'container'),
+ subContainer = container.child();
+ children[controlID] = subContainer;
+ }
+
+ var normalizedPath = path.replace(/\//g, '.');
+
+ var childView = subContainer.lookup('view:' + normalizedPath) || subContainer.lookup('view:default'),
+ childController = subContainer.lookup('controller:' + normalizedPath),
+ childTemplate = subContainer.lookup('template:' + path);
+
+ Ember.assert("Could not find controller for path: " + normalizedPath, childController);
+ Ember.assert("Could not find view for path: " + normalizedPath, childView);
+
+ set(childController, 'target', controller);
+ set(childController, 'model', model);
+
+ options.hash.template = childTemplate;
+ options.hash.controller = childController;
+
+ function observer() {
+ var model = Ember.Handlebars.get(this, modelPath, options);
+ set(childController, 'model', model);
+ childView.rerender();
+ }
+
+ Ember.addObserver(this, modelPath, observer);
+ childView.one('willDestroyElement', this, function() {
+ Ember.removeObserver(this, modelPath, observer);
+ });
+
+ Ember.Handlebars.helpers.view.call(this, childView, options);
+ });
+}
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-routing
+*/
+
+var get = Ember.get, set = Ember.set;
+
+Ember.ControllerMixin.reopen({
+ transitionToRoute: function() {
+ // target may be either another controller or a router
+ var target = get(this, 'target'),
+ method = target.transitionToRoute || target.transitionTo;
+ return method.apply(target, arguments);
+ },
+
+ transitionTo: function() {
+ Ember.deprecate("transitionTo is deprecated. Please use transitionToRoute.");
+ return this.transitionToRoute.apply(this, arguments);
+ },
+
+ replaceRoute: function() {
+ // target may be either another controller or a router
+ var target = get(this, 'target'),
+ method = target.replaceRoute || target.replaceWith;
+ return method.apply(target, arguments);
+ },
+
+ replaceWith: function() {
+ Ember.deprecate("replaceWith is deprecated. Please use replaceRoute.");
+ return this.replaceRoute.apply(this, arguments);
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-routing
+*/
+
+var get = Ember.get, set = Ember.set;
+
+Ember.View.reopen({
+ init: function() {
+ set(this, '_outlets', {});
+ this._super();
+ },
+
+ connectOutlet: function(outletName, view) {
+ var outlets = get(this, '_outlets'),
+ container = get(this, 'container'),
+ router = container && container.lookup('router:main'),
+ renderedName = get(view, 'renderedName');
+
+ set(outlets, outletName, view);
+
+ if (router && renderedName) {
+ router._connectActiveView(renderedName, view);
+ }
+ },
+
+ disconnectOutlet: function(outletName) {
+ var outlets = get(this, '_outlets');
+
+ set(outlets, outletName, null);
+ }
+});
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-routing
+*/
+
+var get = Ember.get, set = Ember.set;
+
+/*
+ This file implements the `location` API used by Ember's router.
+
+ That API is:
+
+ getURL: returns the current URL
+ setURL(path): sets the current URL
+ replaceURL(path): replace the current URL (optional)
+ onUpdateURL(callback): triggers the callback when the URL changes
+ formatURL(url): formats `url` to be placed into `href` attribute
+
+ Calling setURL or replaceURL will not trigger onUpdateURL callbacks.
+
+ TODO: This should perhaps be moved so that it's visible in the doc output.
+*/
+
+/**
+ Ember.Location returns an instance of the correct implementation of
+ the `location` API.
+
+ You can pass it a `implementation` ('hash', 'history', 'none') to force a
+ particular implementation.
+
+ @class Location
+ @namespace Ember
+ @static
+*/
+Ember.Location = {
+ create: function(options) {
+ var implementation = options && options.implementation;
+ Ember.assert("Ember.Location.create: you must specify a 'implementation' option", !!implementation);
+
+ var implementationClass = this.implementations[implementation];
+ Ember.assert("Ember.Location.create: " + implementation + " is not a valid implementation", !!implementationClass);
+
+ return implementationClass.create.apply(implementationClass, arguments);
+ },
+
+ registerImplementation: function(name, implementation) {
+ this.implementations[name] = implementation;
+ },
+
+ implementations: {}
+};
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-routing
+*/
+
+var get = Ember.get, set = Ember.set;
+
+/**
+ Ember.NoneLocation does not interact with the browser. It is useful for
+ testing, or when you need to manage state with your Router, but temporarily
+ don't want it to muck with the URL (for example when you embed your
+ application in a larger page).
+
+ @class NoneLocation
+ @namespace Ember
+ @extends Ember.Object
+*/
+Ember.NoneLocation = Ember.Object.extend({
+ path: '',
+
+ getURL: function() {
+ return get(this, 'path');
+ },
+
+ setURL: function(path) {
+ set(this, 'path', path);
+ },
+
+ onUpdateURL: function(callback) {
+ this.updateCallback = callback;
+ },
+
+ handleURL: function(url) {
+ set(this, 'path', url);
+ this.updateCallback(url);
+ },
+
+ formatURL: function(url) {
+ // The return value is not overly meaningful, but we do not want to throw
+ // errors when test code renders templates containing {{action href=true}}
+ // helpers.
+ return url;
+ }
+});
+
+Ember.Location.registerImplementation('none', Ember.NoneLocation);
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-routing
+*/
+
+var get = Ember.get, set = Ember.set;
+
+/**
+ Ember.HashLocation implements the location API using the browser's
+ hash. At present, it relies on a hashchange event existing in the
+ browser.
+
+ @class HashLocation
+ @namespace Ember
+ @extends Ember.Object
+*/
+Ember.HashLocation = Ember.Object.extend({
+
+ init: function() {
+ set(this, 'location', get(this, 'location') || window.location);
+ },
+
+ /**
+ @private
+
+ Returns the current `location.hash`, minus the '#' at the front.
+
+ @method getURL
+ */
+ getURL: function() {
+ return get(this, 'location').hash.substr(1);
+ },
+
+ /**
+ @private
+
+ Set the `location.hash` and remembers what was set. This prevents
+ `onUpdateURL` callbacks from triggering when the hash was set by
+ `HashLocation`.
+
+ @method setURL
+ @param path {String}
+ */
+ setURL: function(path) {
+ get(this, 'location').hash = path;
+ set(this, 'lastSetURL', path);
+ },
+
+ /**
+ @private
+
+ Register a callback to be invoked when the hash changes. These
+ callbacks will execute when the user presses the back or forward
+ button, but not after `setURL` is invoked.
+
+ @method onUpdateURL
+ @param callback {Function}
+ */
+ onUpdateURL: function(callback) {
+ var self = this;
+ var guid = Ember.guidFor(this);
+
+ Ember.$(window).bind('hashchange.ember-location-'+guid, function() {
+ Ember.run(function() {
+ var path = location.hash.substr(1);
+ if (get(self, 'lastSetURL') === path) { return; }
+
+ set(self, 'lastSetURL', null);
+
+ callback(location.hash.substr(1));
+ });
+ });
+ },
+
+ /**
+ @private
+
+ Given a URL, formats it to be placed into the page as part
+ of an element's `href` attribute.
+
+ This is used, for example, when using the {{action}} helper
+ to generate a URL based on an event.
+
+ @method formatURL
+ @param url {String}
+ */
+ formatURL: function(url) {
+ return '#'+url;
+ },
+
+ willDestroy: function() {
+ var guid = Ember.guidFor(this);
+
+ Ember.$(window).unbind('hashchange.ember-location-'+guid);
+ }
+});
+
+Ember.Location.registerImplementation('hash', Ember.HashLocation);
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-routing
+*/
+
+var get = Ember.get, set = Ember.set;
+var popstateReady = false;
+
+/**
+ Ember.HistoryLocation implements the location API using the browser's
+ history.pushState API.
+
+ @class HistoryLocation
+ @namespace Ember
+ @extends Ember.Object
+*/
+Ember.HistoryLocation = Ember.Object.extend({
+
+ init: function() {
+ set(this, 'location', get(this, 'location') || window.location);
+ this.initState();
+ },
+
+ /**
+ @private
+
+ Used to set state on first call to setURL
+
+ @method initState
+ */
+ initState: function() {
+ this.replaceState(this.formatURL(this.getURL()));
+ set(this, 'history', window.history);
+ },
+
+ /**
+ Will be pre-pended to path upon state change
+
+ @property rootURL
+ @default '/'
+ */
+ rootURL: '/',
+
+ /**
+ @private
+
+ Returns the current `location.pathname` without rootURL
+
+ @method getURL
+ */
+ getURL: function() {
+ var rootURL = get(this, 'rootURL'),
+ url = get(this, 'location').pathname;
+
+ rootURL = rootURL.replace(/\/$/, '');
+ url = url.replace(rootURL, '');
+
+ return url;
+ },
+
+ /**
+ @private
+
+ Uses `history.pushState` to update the url without a page reload.
+
+ @method setURL
+ @param path {String}
+ */
+ setURL: function(path) {
+ path = this.formatURL(path);
+
+ if (this.getState() && this.getState().path !== path) {
+ popstateReady = true;
+ this.pushState(path);
+ }
+ },
+
+ /**
+ @private
+
+ Uses `history.replaceState` to update the url without a page reload
+ or history modification.
+
+ @method replaceURL
+ @param path {String}
+ */
+ replaceURL: function(path) {
+ path = this.formatURL(path);
+
+ if (this.getState() && this.getState().path !== path) {
+ popstateReady = true;
+ this.replaceState(path);
+ }
+ },
+
+ /**
+ @private
+
+ Get the current `history.state`
+
+ @method getState
+ */
+ getState: function() {
+ return get(this, 'history').state;
+ },
+
+ /**
+ @private
+
+ Pushes a new state
+
+ @method pushState
+ @param path {String}
+ */
+ pushState: function(path) {
+ window.history.pushState({ path: path }, null, path);
+ },
+
+ /**
+ @private
+
+ Replaces the current state
+
+ @method replaceState
+ @param path {String}
+ */
+ replaceState: function(path) {
+ window.history.replaceState({ path: path }, null, path);
+ },
+
+ /**
+ @private
+
+ Register a callback to be invoked whenever the browser
+ history changes, including using forward and back buttons.
+
+ @method onUpdateURL
+ @param callback {Function}
+ */
+ onUpdateURL: function(callback) {
+ var guid = Ember.guidFor(this),
+ self = this;
+
+ Ember.$(window).bind('popstate.ember-location-'+guid, function(e) {
+ if(!popstateReady) {
+ return;
+ }
+ callback(self.getURL());
+ });
+ },
+
+ /**
+ @private
+
+ Used when using `{{action}}` helper. The url is always appended to the rootURL.
+
+ @method formatURL
+ @param url {String}
+ */
+ formatURL: function(url) {
+ var rootURL = get(this, 'rootURL');
+
+ if (url !== '') {
+ rootURL = rootURL.replace(/\/$/, '');
+ }
+
+ return rootURL + url;
+ },
+
+ willDestroy: function() {
+ var guid = Ember.guidFor(this);
+
+ Ember.$(window).unbind('popstate.ember-location-'+guid);
+ }
+});
+
+Ember.Location.registerImplementation('history', Ember.HistoryLocation);
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(function() {
+/**
+Ember Routing
+
+@module ember
+@submodule ember-routing
+@requires ember-states
+@requires ember-views
+*/
+
+})();
+
+(function() {
+function visit(vertex, fn, visited, path) {
+ var name = vertex.name,
+ vertices = vertex.incoming,
+ names = vertex.incomingNames,
+ len = names.length,
+ i;
+ if (!visited) {
+ visited = {};
+ }
+ if (!path) {
+ path = [];
+ }
+ if (visited.hasOwnProperty(name)) {
+ return;
+ }
+ path.push(name);
+ visited[name] = true;
+ for (i = 0; i < len; i++) {
+ visit(vertices[names[i]], fn, visited, path);
+ }
+ fn(vertex, path);
+ path.pop();
+}
+
+function DAG() {
+ this.names = [];
+ this.vertices = {};
+}
+
+DAG.prototype.add = function(name) {
+ if (!name) { return; }
+ if (this.vertices.hasOwnProperty(name)) {
+ return this.vertices[name];
+ }
+ var vertex = {
+ name: name, incoming: {}, incomingNames: [], hasOutgoing: false, value: null
+ };
+ this.vertices[name] = vertex;
+ this.names.push(name);
+ return vertex;
+};
+
+DAG.prototype.map = function(name, value) {
+ this.add(name).value = value;
+};
+
+DAG.prototype.addEdge = function(fromName, toName) {
+ if (!fromName || !toName || fromName === toName) {
+ return;
+ }
+ var from = this.add(fromName), to = this.add(toName);
+ if (to.incoming.hasOwnProperty(fromName)) {
+ return;
+ }
+ function checkCycle(vertex, path) {
+ if (vertex.name === toName) {
+ throw new Error("cycle detected: " + toName + " <- " + path.join(" <- "));
+ }
+ }
+ visit(from, checkCycle);
+ from.hasOutgoing = true;
+ to.incoming[fromName] = from;
+ to.incomingNames.push(fromName);
+};
+
+DAG.prototype.topsort = function(fn) {
+ var visited = {},
+ vertices = this.vertices,
+ names = this.names,
+ len = names.length,
+ i, vertex;
+ for (i = 0; i < len; i++) {
+ vertex = vertices[names[i]];
+ if (!vertex.hasOutgoing) {
+ visit(vertex, fn, visited);
+ }
+ }
+};
+
+DAG.prototype.addEdges = function(name, value, before, after) {
+ var i;
+ this.map(name, value);
+ if (before) {
+ if (typeof before === 'string') {
+ this.addEdge(name, before);
+ } else {
+ for (i = 0; i < before.length; i++) {
+ this.addEdge(name, before[i]);
+ }
+ }
+ }
+ if (after) {
+ if (typeof after === 'string') {
+ this.addEdge(after, name);
+ } else {
+ for (i = 0; i < after.length; i++) {
+ this.addEdge(after[i], name);
+ }
+ }
+ }
+};
+
+Ember.DAG = DAG;
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-application
+*/
+
+var get = Ember.get, set = Ember.set,
+ classify = Ember.String.classify,
+ decamelize = Ember.String.decamelize;
+
+/**
+ An instance of `Ember.Application` is the starting point for every Ember
+ application. It helps to instantiate, initialize and coordinate the many
+ objects that make up your app.
+
+ Each Ember app has one and only one `Ember.Application` object. In fact, the
+ very first thing you should do in your application is create the instance:
+
+ ```javascript
+ window.App = Ember.Application.create();
+ ```
+
+ Typically, the application object is the only global variable. All other
+ classes in your app should be properties on the `Ember.Application` instance,
+ which highlights its first role: a global namespace.
+
+ For example, if you define a view class, it might look like this:
+
+ ```javascript
+ App.MyView = Ember.View.extend();
+ ```
+
+ By default, calling `Ember.Application.create()` will automatically initialize
+ your application by calling the `Ember.Application.initialize()` method. If
+ you need to delay initialization, you can call your app's `deferReadiness()`
+ method. When you are ready for your app to be initialized, call its
+ `advanceReadiness()` method.
+
+ Because `Ember.Application` inherits from `Ember.Namespace`, any classes
+ you create will have useful string representations when calling `toString()`.
+ See the `Ember.Namespace` documentation for more information.
+
+ While you can think of your `Ember.Application` as a container that holds the
+ other classes in your application, there are several other responsibilities
+ going on under-the-hood that you may want to understand.
+
+ ### Event Delegation
+
+ Ember uses a technique called _event delegation_. This allows the framework
+ to set up a global, shared event listener instead of requiring each view to
+ do it manually. For example, instead of each view registering its own
+ `mousedown` listener on its associated element, Ember sets up a `mousedown`
+ listener on the `body`.
+
+ If a `mousedown` event occurs, Ember will look at the target of the event and
+ start walking up the DOM node tree, finding corresponding views and invoking
+ their `mouseDown` method as it goes.
+
+ `Ember.Application` has a number of default events that it listens for, as
+ well as a mapping from lowercase events to camel-cased view method names. For
+ example, the `keypress` event causes the `keyPress` method on the view to be
+ called, the `dblclick` event causes `doubleClick` to be called, and so on.
+
+ If there is a browser event that Ember does not listen for by default, you
+ can specify custom events and their corresponding view method names by
+ setting the application's `customEvents` property:
+
+ ```javascript
+ App = Ember.Application.create({
+ customEvents: {
+ // add support for the loadedmetadata media
+ // player event
+ 'loadedmetadata': "loadedMetadata"
+ }
+ });
+ ```
+
+ By default, the application sets up these event listeners on the document
+ body. However, in cases where you are embedding an Ember application inside
+ an existing page, you may want it to set up the listeners on an element
+ inside the body.
+
+ For example, if only events inside a DOM element with the ID of `ember-app`
+ should be delegated, set your application's `rootElement` property:
+
+ ```javascript
+ window.App = Ember.Application.create({
+ rootElement: '#ember-app'
+ });
+ ```
+
+ The `rootElement` can be either a DOM element or a jQuery-compatible selector
+ string. Note that *views appended to the DOM outside the root element will
+ not receive events.* If you specify a custom root element, make sure you only
+ append views inside it!
+
+ To learn more about the advantages of event delegation and the Ember view
+ layer, and a list of the event listeners that are setup by default, visit the
+ [Ember View Layer guide](http://emberjs.com/guides/view_layer#toc_event-delegation).
+
+ ### Initializers
+
+ Libraries on top of Ember can register additional initializers, like so:
+
+ ```javascript
+ Ember.Application.initializer({
+ name: "store",
+
+ initialize: function(container, application) {
+ container.register('store', 'main', application.Store);
+ }
+ });
+ ```
+
+ ### Routing
+
+ In addition to creating your application's router, `Ember.Application` is
+ also responsible for telling the router when to start routing. Transitions
+ between routes can be logged with the LOG_TRANSITIONS flag:
+
+ ```javascript
+ window.App = Ember.Application.create({
+ LOG_TRANSITIONS: true
+ });
+ ```
+
+ By default, the router will begin trying to translate the current URL into
+ application state once the browser emits the `DOMContentReady` event. If you
+ need to defer routing, you can call the application's `deferReadiness()`
+ method. Once routing can begin, call the `advanceReadiness()` method.
+
+ If there is any setup required before routing begins, you can implement a
+ `ready()` method on your app that will be invoked immediately before routing
+ begins.
+
+ To begin routing, you must have at a minimum a top-level controller and view.
+ You define these as `App.ApplicationController` and `App.ApplicationView`,
+ respectively. Your application will not work if you do not define these two
+ mandatory classes. For example:
+
+ ```javascript
+ App.ApplicationView = Ember.View.extend({
+ templateName: 'application'
+ });
+ App.ApplicationController = Ember.Controller.extend();
+ ```
+
+ @class Application
+ @namespace Ember
+ @extends Ember.Namespace
+*/
+var Application = Ember.Application = Ember.Namespace.extend({
+
+ /**
+ The root DOM element of the Application. This can be specified as an
+ element or a
+ [jQuery-compatible selector string](http://api.jquery.com/category/selectors/).
+
+ This is the element that will be passed to the Application's,
+ `eventDispatcher`, which sets up the listeners for event delegation. Every
+ view in your application should be a child of the element you specify here.
+
+ @property rootElement
+ @type DOMElement
+ @default 'body'
+ */
+ rootElement: 'body',
+
+ /**
+ The `Ember.EventDispatcher` responsible for delegating events to this
+ application's views.
+
+ The event dispatcher is created by the application at initialization time
+ and sets up event listeners on the DOM element described by the
+ application's `rootElement` property.
+
+ See the documentation for `Ember.EventDispatcher` for more information.
+
+ @property eventDispatcher
+ @type Ember.EventDispatcher
+ @default null
+ */
+ eventDispatcher: null,
+
+ /**
+ The DOM events for which the event dispatcher should listen.
+
+ By default, the application's `Ember.EventDispatcher` listens
+ for a set of standard DOM events, such as `mousedown` and
+ `keyup`, and delegates them to your application's `Ember.View`
+ instances.
+
+ If you would like additional events to be delegated to your
+ views, set your `Ember.Application`'s `customEvents` property
+ to a hash containing the DOM event name as the key and the
+ corresponding view method name as the value. For example:
+
+ ```javascript
+ App = Ember.Application.create({
+ customEvents: {
+ // add support for the loadedmetadata media
+ // player event
+ 'loadedmetadata': "loadedMetadata"
+ }
+ });
+ ```
+
+ @property customEvents
+ @type Object
+ @default null
+ */
+ customEvents: null,
+
+ isInitialized: false,
+
+ // Start off the number of deferrals at 1. This will be
+ // decremented by the Application's own `initialize` method.
+ _readinessDeferrals: 1,
+
+ init: function() {
+ if (!this.$) { this.$ = Ember.$; }
+ this.__container__ = this.buildContainer();
+
+ this.Router = this.Router || this.defaultRouter();
+ if (this.Router) { this.Router.namespace = this; }
+
+ this._super();
+
+ this.deferUntilDOMReady();
+ this.scheduleInitialize();
+
+ Ember.debug('-------------------------------');
+ Ember.debug('Ember.VERSION : ' + Ember.VERSION);
+ Ember.debug('Handlebars.VERSION : ' + Ember.Handlebars.VERSION);
+ Ember.debug('jQuery.VERSION : ' + Ember.$().jquery);
+ Ember.debug('-------------------------------');
+ },
+
+ /**
+ @private
+
+ Build the container for the current application.
+
+ Also register a default application view in case the application
+ itself does not.
+
+ @method buildContainer
+ @return {Ember.Container} the configured container
+ */
+ buildContainer: function() {
+ var container = this.__container__ = Application.buildContainer(this);
+
+ return container;
+ },
+
+ /**
+ @private
+
+ If the application has not opted out of routing and has not explicitly
+ defined a router, supply a default router for the application author
+ to configure.
+
+ This allows application developers to do:
+
+ ```javascript
+ App = Ember.Application.create();
+
+ App.Router.map(function(match) {
+ match("/").to("index");
+ });
+ ```
+
+ @method defaultRouter
+ @return {Ember.Router} the default router
+ */
+ defaultRouter: function() {
+ // Create a default App.Router if one was not supplied to make
+ // it possible to do App.Router.map(...) without explicitly
+ // creating a router first.
+ if (this.router === undefined) {
+ return Ember.Router.extend();
+ }
+ },
+
+ /**
+ @private
+
+ Defer Ember readiness until DOM readiness. By default, Ember
+ will wait for both DOM readiness and application initialization,
+ as well as any deferrals registered by initializers.
+
+ @method deferUntilDOMReady
+ */
+ deferUntilDOMReady: function() {
+ this.deferReadiness();
+
+ var self = this;
+ this.$().ready(function() {
+ self.advanceReadiness();
+ });
+ },
+
+ /**
+ @private
+
+ Automatically initialize the application once the DOM has
+ become ready.
+
+ The initialization itself is deferred using Ember.run.once,
+ which ensures that application loading finishes before
+ booting.
+
+ If you are asynchronously loading code, you should call
+ `deferReadiness()` to defer booting, and then call
+ `advanceReadiness()` once all of your code has finished
+ loading.
+
+ @method scheduleInitialize
+ */
+ scheduleInitialize: function() {
+ var self = this;
+ this.$().ready(function() {
+ if (self.isDestroyed || self.isInitialized) return;
+ Ember.run.once(self, 'initialize');
+ });
+ },
+
+ /**
+ Use this to defer readiness until some condition is true.
+
+ Example:
+
+ ```javascript
+ App = Ember.Application.create();
+ App.deferReadiness();
+
+ jQuery.getJSON("/auth-token", function(token) {
+ App.token = token;
+ App.advanceReadiness();
+ });
+ ```
+
+ This allows you to perform asynchronous setup logic and defer
+ booting your application until the setup has finished.
+
+ However, if the setup requires a loading UI, it might be better
+ to use the router for this purpose.
+
+ @method deferReadiness
+ */
+ deferReadiness: function() {
+ Ember.assert("You cannot defer readiness since the `ready()` hook has already been called.", this._readinessDeferrals > 0);
+ this._readinessDeferrals++;
+ },
+
+ /**
+ @method advanceReadiness
+ @see {Ember.Application#deferReadiness}
+ */
+ advanceReadiness: function() {
+ this._readinessDeferrals--;
+
+ if (this._readinessDeferrals === 0) {
+ Ember.run.once(this, this.didBecomeReady);
+ }
+ },
+
+ /**
+ registers a factory for later injection
+
+ Example:
+
+ ```javascript
+ App = Ember.Application.create();
+
+ App.Person = Ember.Object.extend({});
+ App.Orange = Ember.Object.extend({});
+ App.Email = Ember.Object.extend({});
+
+ App.register('model:user', App.Person, {singleton: false });
+ App.register('fruit:favorite', App.Orange);
+ App.register('communication:main', App.Email, {singleton: false});
+ ```
+
+ @method register
+ @param type {String}
+ @param name {String}
+ @param factory {String}
+ @param options {String} (optional)
+ **/
+ register: function() {
+ var container = this.__container__;
+ container.register.apply(container, arguments);
+ },
+ /**
+ defines an injection or typeInjection
+
+ Example:
+
+ ```javascript
+ App.inject(<full_name or type>, <property name>, <full_name>)
+ App.inject('model:user', 'email', 'model:email')
+ App.inject('model', 'source', 'source:main')
+ ```
+
+ @method inject
+ @param factoryNameOrType {String}
+ @param property {String}
+ @param injectionName {String}
+ **/
+ inject: function(){
+ var container = this.__container__;
+ container.injection.apply(container, arguments);
+ },
+
+ /**
+ @private
+
+ Initialize the application. This happens automatically.
+
+ Run any initializers and run the application load hook. These hooks may
+ choose to defer readiness. For example, an authentication hook might want
+ to defer readiness until the auth token has been retrieved.
+
+ @method initialize
+ */
+ initialize: function() {
+ Ember.assert("Application initialize may only be called once", !this.isInitialized);
+ Ember.assert("Cannot initialize a destroyed application", !this.isDestroyed);
+ this.isInitialized = true;
+
+ // At this point, the App.Router must already be assigned
+ this.__container__.register('router', 'main', this.Router);
+
+ this.runInitializers();
+ Ember.runLoadHooks('application', this);
+
+ // At this point, any initializers or load hooks that would have wanted
+ // to defer readiness have fired. In general, advancing readiness here
+ // will proceed to didBecomeReady.
+ this.advanceReadiness();
+
+ return this;
+ },
+
+ reset: function() {
+ get(this, '__container__').destroy();
+ this.buildContainer();
+
+ this.isInitialized = false;
+ this.initialize();
+ this.startRouting();
+ },
+
+ /**
+ @private
+ @method runInitializers
+ */
+ runInitializers: function() {
+ var initializers = get(this.constructor, 'initializers'),
+ container = this.__container__,
+ graph = new Ember.DAG(),
+ namespace = this,
+ properties, i, initializer;
+
+ for (i=0; i<initializers.length; i++) {
+ initializer = initializers[i];
+ graph.addEdges(initializer.name, initializer.initialize, initializer.before, initializer.after);
+ }
+
+ graph.topsort(function (vertex) {
+ var initializer = vertex.value;
+ initializer(container, namespace);
+ });
+ },
+
+ /**
+ @private
+ @method didBecomeReady
+ */
+ didBecomeReady: function() {
+ this.setupEventDispatcher();
+ this.ready(); // user hook
+ this.startRouting();
+
+ if (!Ember.testing) {
+ // Eagerly name all classes that are already loaded
+ Ember.Namespace.processAll();
+ Ember.BOOTED = true;
+ }
+ },
+
+ /**
+ @private
+
+ Setup up the event dispatcher to receive events on the
+ application's `rootElement` with any registered
+ `customEvents`.
+
+ @method setupEventDispatcher
+ */
+ setupEventDispatcher: function() {
+ var eventDispatcher = this.createEventDispatcher(),
+ customEvents = get(this, 'customEvents');
+
+ eventDispatcher.setup(customEvents);
+ },
+
+ /**
+ @private
+
+ Create an event dispatcher for the application's `rootElement`.
+
+ @method createEventDispatcher
+ */
+ createEventDispatcher: function() {
+ var rootElement = get(this, 'rootElement'),
+ eventDispatcher = Ember.EventDispatcher.create({
+ rootElement: rootElement
+ });
+
+ set(this, 'eventDispatcher', eventDispatcher);
+ return eventDispatcher;
+ },
+
+ /**
+ @private
+
+ If the application has a router, use it to route to the current URL, and
+ trigger a new call to `route` whenever the URL changes.
+
+ @method startRouting
+ @property router {Ember.Router}
+ */
+ startRouting: function() {
+ var router = this.__container__.lookup('router:main');
+ if (!router) { return; }
+
+ router.startRouting();
+ },
+
+ handleURL: function(url) {
+ var router = this.__container__.lookup('router:main');
+
+ router.handleURL(url);
+ },
+
+ /**
+ Called when the Application has become ready.
+ The call will be delayed until the DOM has become ready.
+
+ @event ready
+ */
+ ready: Ember.K,
+
+ willDestroy: function() {
+ Ember.BOOTED = false;
+
+ var eventDispatcher = get(this, 'eventDispatcher');
+ if (eventDispatcher) { eventDispatcher.destroy(); }
+
+ get(this, '__container__').destroy();
+ },
+
+ initializer: function(options) {
+ this.constructor.initializer(options);
+ }
+});
+
+Ember.Application.reopenClass({
+ concatenatedProperties: ['initializers'],
+ initializers: Ember.A(),
+ initializer: function(initializer) {
+ var initializers = get(this, 'initializers');
+
+ Ember.assert("The initializer '" + initializer.name + "' has already been registered", !initializers.findProperty('name', initializers.name));
+ Ember.assert("An injection cannot be registered with both a before and an after", !(initializer.before && initializer.after));
+ Ember.assert("An injection cannot be registered without an injection function", Ember.canInvoke(initializer, 'initialize'));
+
+ initializers.push(initializer);
+ },
+
+ /**
+ @private
+
+ This creates a container with the default Ember naming conventions.
+
+ It also configures the container:
+
+ * registered views are created every time they are looked up (they are
+ not singletons)
+ * registered templates are not factories; the registered value is
+ returned directly.
+ * the router receives the application as its `namespace` property
+ * all controllers receive the router as their `target` and `controllers`
+ properties
+ * all controllers receive the application as their `namespace` property
+ * the application view receives the application controller as its
+ `controller` property
+ * the application view receives the application template as its
+ `defaultTemplate` property
+
+ @method buildContainer
+ @static
+ @param {Ember.Application} namespace the application to build the
+ container for.
+ @return {Ember.Container} the built container
+ */
+ buildContainer: function(namespace) {
+ var container = new Ember.Container();
+ Ember.Container.defaultContainer = Ember.Container.defaultContainer || container;
+
+ container.set = Ember.set;
+ container.resolver = resolverFor(namespace);
+ container.optionsForType('view', { singleton: false });
+ container.optionsForType('template', { instantiate: false });
+ container.register('application', 'main', namespace, { instantiate: false });
+ container.injection('router:main', 'namespace', 'application:main');
+
+ container.typeInjection('controller', 'target', 'router:main');
+ container.typeInjection('controller', 'namespace', 'application:main');
+
+ container.typeInjection('route', 'router', 'router:main');
+
+ return container;
+ }
+});
+
+/**
+ @private
+
+ This function defines the default lookup rules for container lookups:
+
+ * templates are looked up on `Ember.TEMPLATES`
+ * other names are looked up on the application after classifying the name.
+ For example, `controller:post` looks up `App.PostController` by default.
+ * if the default lookup fails, look for registered classes on the container
+
+ This allows the application to register default injections in the container
+ that could be overridden by the normal naming convention.
+
+ @param {Ember.Namespace} namespace the namespace to look for classes
+ @return {any} the resolved value for a given lookup
+*/
+function resolverFor(namespace) {
+ return function(fullName) {
+ var nameParts = fullName.split(":"),
+ type = nameParts[0], name = nameParts[1];
+
+ if (type === 'template') {
+ var templateName = name.replace(/\./g, '/');
+ if (Ember.TEMPLATES[templateName]) {
+ return Ember.TEMPLATES[templateName];
+ }
+
+ templateName = decamelize(templateName);
+ if (Ember.TEMPLATES[templateName]) {
+ return Ember.TEMPLATES[templateName];
+ }
+ }
+
+ if (type === 'controller' || type === 'route' || type === 'view') {
+ name = name.replace(/\./g, '_');
+ }
+
+ var className = classify(name) + classify(type);
+ var factory = get(namespace, className);
+
+ if (factory) { return factory; }
+ };
+}
+
+Ember.runLoadHooks('Ember.Application', Ember.Application);
+
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-routing
+*/
+
+var get = Ember.get, set = Ember.set;
+var ControllersProxy = Ember.Object.extend({
+ controller: null,
+
+ unknownProperty: function(controllerName) {
+ var controller = get(this, 'controller'),
+ needs = get(controller, 'needs'),
+ container = controller.get('container'),
+ dependency;
+
+ for (var i=0, l=needs.length; i<l; i++) {
+ dependency = needs[i];
+ if (dependency === controllerName) {
+ return container.lookup('controller:' + controllerName);
+ }
+ }
+ }
+});
+
+function verifyDependencies(controller) {
+ var needs = get(controller, 'needs'),
+ container = get(controller, 'container'),
+ dependency, satisfied = true;
+
+ for (var i=0, l=needs.length; i<l; i++) {
+ dependency = needs[i];
+ if (dependency.indexOf(':') === -1) {
+ dependency = "controller:" + dependency;
+ }
+
+ if (!container.has(dependency)) {
+ satisfied = false;
+ Ember.assert(controller + " needs " + dependency + " but it does not exist", false);
+ }
+ }
+
+ return satisfied;
+}
+
+Ember.ControllerMixin.reopen({
+ concatenatedProperties: ['needs'],
+ needs: [],
+
+ init: function() {
+ this._super.apply(this, arguments);
+
+ // Structure asserts to still do verification but not string concat in production
+ if(!verifyDependencies(this)) {
+ Ember.assert("Missing dependencies", false);
+ }
+ },
+
+ controllerFor: function(controllerName) {
+ Ember.deprecate("Controller#controllerFor is depcrecated, please use Controller#needs instead");
+ var container = get(this, 'container');
+ return container.lookup('controller:' + controllerName);
+ },
+
+ controllers: Ember.computed(function() {
+ return ControllersProxy.create({ controller: this });
+ })
+});
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(function() {
+/**
+Ember Application
+
+@module ember
+@submodule ember-application
+@requires ember-views, ember-states, ember-routing
+*/
+
+})();
+
+(function() {
+var get = Ember.get, set = Ember.set;
+
+/**
+@module ember
+@submodule ember-states
+*/
+
+/**
+ @class State
+ @namespace Ember
+ @extends Ember.Object
+ @uses Ember.Evented
+*/
+Ember.State = Ember.Object.extend(Ember.Evented,
+/** @scope Ember.State.prototype */{
+ isState: true,
+
+ /**
+ A reference to the parent state.
+
+ @property parentState
+ @type Ember.State
+ */
+ parentState: null,
+ start: null,
+
+ /**
+ The name of this state.
+
+ @property name
+ @type String
+ */
+ name: null,
+
+ /**
+ The full path to this state.
+
+ @property path
+ @type String
+ */
+ path: Ember.computed(function() {
+ var parentPath = get(this, 'parentState.path'),
+ path = get(this, 'name');
+
+ if (parentPath) {
+ path = parentPath + '.' + path;
+ }
+
+ return path;
+ }),
+
+ /**
+ @private
+
+ Override the default event firing from `Ember.Evented` to
+ also call methods with the given name.
+
+ @method trigger
+ @param name
+ */
+ trigger: function(name) {
+ if (this[name]) {
+ this[name].apply(this, [].slice.call(arguments, 1));
+ }
+ this._super.apply(this, arguments);
+ },
+
+ init: function() {
+ var states = get(this, 'states'), foundStates;
+ set(this, 'childStates', Ember.A());
+ set(this, 'eventTransitions', get(this, 'eventTransitions') || {});
+
+ var name, value, transitionTarget;
+
+ // As a convenience, loop over the properties
+ // of this state and look for any that are other
+ // Ember.State instances or classes, and move them
+ // to the `states` hash. This avoids having to
+ // create an explicit separate hash.
+
+ if (!states) {
+ states = {};
+
+ for (name in this) {
+ if (name === "constructor") { continue; }
+
+ if (value = this[name]) {
+ if (transitionTarget = value.transitionTarget) {
+ this.eventTransitions[name] = transitionTarget;
+ }
+
+ this.setupChild(states, name, value);
+ }
+ }
+
+ set(this, 'states', states);
+ } else {
+ for (name in states) {
+ this.setupChild(states, name, states[name]);
+ }
+ }
+
+ set(this, 'pathsCache', {});
+ set(this, 'pathsCacheNoContext', {});
+ },
+
+ setupChild: function(states, name, value) {
+ if (!value) { return false; }
+
+ if (value.isState) {
+ set(value, 'name', name);
+ } else if (Ember.State.detect(value)) {
+ value = value.create({
+ name: name
+ });
+ }
+
+ if (value.isState) {
+ set(value, 'parentState', this);
+ get(this, 'childStates').pushObject(value);
+ states[name] = value;
+ return value;
+ }
+ },
+
+ lookupEventTransition: function(name) {
+ var path, state = this;
+
+ while(state && !path) {
+ path = state.eventTransitions[name];
+ state = state.get('parentState');
+ }
+
+ return path;
+ },
+
+ /**
+ A Boolean value indicating whether the state is a leaf state
+ in the state hierarchy. This is `false` if the state has child
+ states; otherwise it is true.
+
+ @property isLeaf
+ @type Boolean
+ */
+ isLeaf: Ember.computed(function() {
+ return !get(this, 'childStates').length;
+ }),
+
+ /**
+ A boolean value indicating whether the state takes a context.
+ By default we assume all states take contexts.
+
+ @property hasContext
+ @default true
+ */
+ hasContext: true,
+
+ /**
+ This is the default transition event.
+
+ @event setup
+ @param {Ember.StateManager} manager
+ @param context
+ @see Ember.StateManager#transitionEvent
+ */
+ setup: Ember.K,
+
+ /**
+ This event fires when the state is entered.
+
+ @event enter
+ @param {Ember.StateManager} manager
+ */
+ enter: Ember.K,
+
+ /**
+ This event fires when the state is exited.
+
+ @event exit
+ @param {Ember.StateManager} manager
+ */
+ exit: Ember.K
+});
+
+Ember.State.reopenClass({
+
+ /**
+ Creates an action function for transitioning to the named state while
+ preserving context.
+
+ The following example StateManagers are equivalent:
+
+ ```javascript
+ aManager = Ember.StateManager.create({
+ stateOne: Ember.State.create({
+ changeToStateTwo: Ember.State.transitionTo('stateTwo')
+ }),
+ stateTwo: Ember.State.create({})
+ })
+
+ bManager = Ember.StateManager.create({
+ stateOne: Ember.State.create({
+ changeToStateTwo: function(manager, context){
+ manager.transitionTo('stateTwo', context)
+ }
+ }),
+ stateTwo: Ember.State.create({})
+ })
+ ```
+
+ @method transitionTo
+ @static
+ @param {String} target
+ */
+
+ transitionTo: function(target) {
+
+ var transitionFunction = function(stateManager, contextOrEvent) {
+ var contexts = [], transitionArgs,
+ Event = Ember.$ && Ember.$.Event;
+
+ if (contextOrEvent && (Event && contextOrEvent instanceof Event)) {
+ if (contextOrEvent.hasOwnProperty('contexts')) {
+ contexts = contextOrEvent.contexts.slice();
+ }
+ }
+ else {
+ contexts = [].slice.call(arguments, 1);
+ }
+
+ contexts.unshift(target);
+ stateManager.transitionTo.apply(stateManager, contexts);
+ };
+
+ transitionFunction.transitionTarget = target;
+
+ return transitionFunction;
+ }
+
+});
+
+})();
+
+
+
+(function() {
+/**
+@module ember
+@submodule ember-states
+*/
+
+var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt;
+var arrayForEach = Ember.ArrayPolyfills.forEach;
+/**
+ A Transition takes the enter, exit and resolve states and normalizes
+ them:
+
+ * takes any passed in contexts into consideration
+ * adds in `initialState`s
+
+ @class Transition
+ @private
+*/
+var Transition = function(raw) {
+ this.enterStates = raw.enterStates.slice();
+ this.exitStates = raw.exitStates.slice();
+ this.resolveState = raw.resolveState;
+
+ this.finalState = raw.enterStates[raw.enterStates.length - 1] || raw.resolveState;
+};
+
+Transition.prototype = {
+ /**
+ Normalize the passed in enter, exit and resolve states.
+
+ This process also adds `finalState` and `contexts` to the Transition object.
+
+ @method normalize
+ @param {Ember.StateManager} manager the state manager running the transition
+ @param {Array} contexts a list of contexts passed into `transitionTo`
+ */
+ normalize: function(manager, contexts) {
+ this.matchContextsToStates(contexts);
+ this.addInitialStates();
+ this.removeUnchangedContexts(manager);
+ return this;
+ },
+
+ /**
+ Match each of the contexts passed to `transitionTo` to a state.
+ This process may also require adding additional enter and exit
+ states if there are more contexts than enter states.
+
+ @method matchContextsToStates
+ @param {Array} contexts a list of contexts passed into `transitionTo`
+ */
+ matchContextsToStates: function(contexts) {
+ var stateIdx = this.enterStates.length - 1,
+ matchedContexts = [],
+ state,
+ context;
+
+ // Next, we will match the passed in contexts to the states they
+ // represent.
+ //
+ // First, assign a context to each enter state in reverse order. If
+ // any contexts are left, add a parent state to the list of states
+ // to enter and exit, and assign a context to the parent state.
+ //
+ // If there are still contexts left when the state manager is
+ // reached, raise an exception.
+ //
+ // This allows the following:
+ //
+ // |- root
+ // | |- post
+ // | | |- comments
+ // | |- about (* current state)
+ //
+ // For `transitionTo('post.comments', post, post.get('comments')`,
+ // the first context (`post`) will be assigned to `root.post`, and
+ // the second context (`post.get('comments')`) will be assigned
+ // to `root.post.comments`.
+ //
+ // For the following:
+ //
+ // |- root
+ // | |- post
+ // | | |- index (* current state)
+ // | | |- comments
+ //
+ // For `transitionTo('post.comments', otherPost, otherPost.get('comments')`,
+ // the `<root.post>` state will be added to the list of enter and exit
+ // states because its context has changed.
+
+ while (contexts.length > 0) {
+ if (stateIdx >= 0) {
+ state = this.enterStates[stateIdx--];
+ } else {
+ if (this.enterStates.length) {
+ state = get(this.enterStates[0], 'parentState');
+ if (!state) { throw "Cannot match all contexts to states"; }
+ } else {
+ // If re-entering the current state with a context, the resolve
+ // state will be the current state.
+ state = this.resolveState;
+ }
+
+ this.enterStates.unshift(state);
+ this.exitStates.unshift(state);
+ }
+
+ // in routers, only states with dynamic segments have a context
+ if (get(state, 'hasContext')) {
+ context = contexts.pop();
+ } else {
+ context = null;
+ }
+
+ matchedContexts.unshift(context);
+ }
+
+ this.contexts = matchedContexts;
+ },
+
+ /**
+ Add any `initialState`s to the list of enter states.
+
+ @method addInitialStates
+ */
+ addInitialStates: function() {
+ var finalState = this.finalState, initialState;
+
+ while(true) {
+ initialState = get(finalState, 'initialState') || 'start';
+ finalState = get(finalState, 'states.' + initialState);
+
+ if (!finalState) { break; }
+
+ this.finalState = finalState;
+ this.enterStates.push(finalState);
+ this.contexts.push(undefined);
+ }
+ },
+
+ /**
+ Remove any states that were added because the number of contexts
+ exceeded the number of explicit enter states, but the context has
+ not changed since the last time the state was entered.
+
+ @method removeUnchangedContexts
+ @param {Ember.StateManager} manager passed in to look up the last
+ context for a states
+ */
+ removeUnchangedContexts: function(manager) {
+ // Start from the beginning of the enter states. If the state was added
+ // to the list during the context matching phase, make sure the context
+ // has actually changed since the last time the state was entered.
+ while (this.enterStates.length > 0) {
+ if (this.enterStates[0] !== this.exitStates[0]) { break; }
+
+ if (this.enterStates.length === this.contexts.length) {
+ if (manager.getStateMeta(this.enterStates[0], 'context') !== this.contexts[0]) { break; }
+ this.contexts.shift();
+ }
+
+ this.resolveState = this.enterStates.shift();
+ this.exitStates.shift();
+ }
+ }
+};
+
+var sendRecursively = function(event, currentState, isUnhandledPass) {
+ var log = this.enableLogging,
+ eventName = isUnhandledPass ? 'unhandledEvent' : event,
+ action = currentState[eventName],
+ contexts, sendRecursiveArguments, actionArguments;
+
+ contexts = [].slice.call(arguments, 3);
+
+ // Test to see if the action is a method that
+ // can be invoked. Don't blindly check just for
+ // existence, because it is possible the state
+ // manager has a child state of the given name,
+ // and we should still raise an exception in that
+ // case.
+ if (typeof action === 'function') {
+ if (log) {
+ if (isUnhandledPass) {
+ Ember.Logger.log(fmt("STATEMANAGER: Unhandled event '%@' being sent to state %@.", [event, get(currentState, 'path')]));
+ } else {
+ Ember.Logger.log(fmt("STATEMANAGER: Sending event '%@' to state %@.", [event, get(currentState, 'path')]));
+ }
+ }
+
+ actionArguments = contexts;
+ if (isUnhandledPass) {
+ actionArguments.unshift(event);
+ }
+ actionArguments.unshift(this);
+
+ return action.apply(currentState, actionArguments);
+ } else {
+ var parentState = get(currentState, 'parentState');
+ if (parentState) {
+
+ sendRecursiveArguments = contexts;
+ sendRecursiveArguments.unshift(event, parentState, isUnhandledPass);
+
+ return sendRecursively.apply(this, sendRecursiveArguments);
+ } else if (!isUnhandledPass) {
+ return sendEvent.call(this, event, contexts, true);
+ }
+ }
+};
+
+var sendEvent = function(eventName, sendRecursiveArguments, isUnhandledPass) {
+ sendRecursiveArguments.unshift(eventName, get(this, 'currentState'), isUnhandledPass);
+ return sendRecursively.apply(this, sendRecursiveArguments);
+};
+
+/**
+ StateManager is part of Ember's implementation of a finite state machine. A
+ StateManager instance manages a number of properties that are instances of
+ `Ember.State`,
+ tracks the current active state, and triggers callbacks when states have changed.
+
+ ## Defining States
+
+ The states of StateManager can be declared in one of two ways. First, you can
+ define a `states` property that contains all the states:
+
+ ```javascript
+ managerA = Ember.StateManager.create({
+ states: {
+ stateOne: Ember.State.create(),
+ stateTwo: Ember.State.create()
+ }
+ })
+
+ managerA.get('states')
+ // {
+ // stateOne: Ember.State.create(),
+ // stateTwo: Ember.State.create()
+ // }
+ ```
+
+ You can also add instances of `Ember.State` (or an `Ember.State` subclass)
+ directly as properties of a StateManager. These states will be collected into
+ the `states` property for you.
+
+ ```javascript
+ managerA = Ember.StateManager.create({
+ stateOne: Ember.State.create(),
+ stateTwo: Ember.State.create()
+ })
+
+ managerA.get('states')
+ // {
+ // stateOne: Ember.State.create(),
+ // stateTwo: Ember.State.create()
+ // }
+ ```
+
+ ## The Initial State
+
+ When created a StateManager instance will immediately enter into the state
+ defined as its `start` property or the state referenced by name in its
+ `initialState` property:
+
+ ```javascript
+ managerA = Ember.StateManager.create({
+ start: Ember.State.create({})
+ })
+
+ managerA.get('currentState.name') // 'start'
+
+ managerB = Ember.StateManager.create({
+ initialState: 'beginHere',
+ beginHere: Ember.State.create({})
+ })
+
+ managerB.get('currentState.name') // 'beginHere'
+ ```
+
+ Because it is a property you may also provide a computed function if you wish
+ to derive an `initialState` programmatically:
+
+ ```javascript
+ managerC = Ember.StateManager.create({
+ initialState: function(){
+ if (someLogic) {
+ return 'active';
+ } else {
+ return 'passive';
+ }
+ }.property(),
+ active: Ember.State.create({}),
+ passive: Ember.State.create({})
+ })
+ ```
+
+ ## Moving Between States
+
+ A StateManager can have any number of `Ember.State` objects as properties
+ and can have a single one of these states as its current state.
+
+ Calling `transitionTo` transitions between states:
+
+ ```javascript
+ robotManager = Ember.StateManager.create({
+ initialState: 'poweredDown',
+ poweredDown: Ember.State.create({}),
+ poweredUp: Ember.State.create({})
+ })
+
+ robotManager.get('currentState.name') // 'poweredDown'
+ robotManager.transitionTo('poweredUp')
+ robotManager.get('currentState.name') // 'poweredUp'
+ ```
+
+ Before transitioning into a new state the existing `currentState` will have
+ its `exit` method called with the StateManager instance as its first argument
+ and an object representing the transition as its second argument.
+
+ After transitioning into a new state the new `currentState` will have its
+ `enter` method called with the StateManager instance as its first argument
+ and an object representing the transition as its second argument.
+
+ ```javascript
+ robotManager = Ember.StateManager.create({
+ initialState: 'poweredDown',
+ poweredDown: Ember.State.create({
+ exit: function(stateManager){
+ console.log("exiting the poweredDown state")
+ }
+ }),
+ poweredUp: Ember.State.create({
+ enter: function(stateManager){
+ console.log("entering the poweredUp state. Destroy all humans.")
+ }
+ })
+ })
+
+ robotManager.get('currentState.name') // 'poweredDown'
+ robotManager.transitionTo('poweredUp')
+
+ // will log
+ // 'exiting the poweredDown state'
+ // 'entering the poweredUp state. Destroy all humans.'
+ ```
+
+ Once a StateManager is already in a state, subsequent attempts to enter that
+ state will not trigger enter or exit method calls. Attempts to transition
+ into a state that the manager does not have will result in no changes in the
+ StateManager's current state:
+
+ ```javascript
+ robotManager = Ember.StateManager.create({
+ initialState: 'poweredDown',
+ poweredDown: Ember.State.create({
+ exit: function(stateManager){
+ console.log("exiting the poweredDown state")
+ }
+ }),
+ poweredUp: Ember.State.create({
+ enter: function(stateManager){
+ console.log("entering the poweredUp state. Destroy all humans.")
+ }
+ })
+ })
+
+ robotManager.get('currentState.name') // 'poweredDown'
+ robotManager.transitionTo('poweredUp')
+ // will log
+ // 'exiting the poweredDown state'
+ // 'entering the poweredUp state. Destroy all humans.'
+ robotManager.transitionTo('poweredUp') // no logging, no state change
+
+ robotManager.transitionTo('someUnknownState') // silently fails
+ robotManager.get('currentState.name') // 'poweredUp'
+ ```
+
+ Each state property may itself contain properties that are instances of
+ `Ember.State`. The StateManager can transition to specific sub-states in a
+ series of transitionTo method calls or via a single transitionTo with the
+ full path to the specific state. The StateManager will also keep track of the
+ full path to its currentState
+
+ ```javascript
+ robotManager = Ember.StateManager.create({
+ initialState: 'poweredDown',
+ poweredDown: Ember.State.create({
+ charging: Ember.State.create(),
+ charged: Ember.State.create()
+ }),
+ poweredUp: Ember.State.create({
+ mobile: Ember.State.create(),
+ stationary: Ember.State.create()
+ })
+ })
+
+ robotManager.get('currentState.name') // 'poweredDown'
+
+ robotManager.transitionTo('poweredUp')
+ robotManager.get('currentState.name') // 'poweredUp'
+
+ robotManager.transitionTo('mobile')
+ robotManager.get('currentState.name') // 'mobile'
+
+ // transition via a state path
+ robotManager.transitionTo('poweredDown.charging')
+ robotManager.get('currentState.name') // 'charging'
+
+ robotManager.get('currentState.path') // 'poweredDown.charging'
+ ```
+
+ Enter transition methods will be called for each state and nested child state
+ in their hierarchical order. Exit methods will be called for each state and
+ its nested states in reverse hierarchical order.
+
+ Exit transitions for a parent state are not called when entering into one of
+ its child states, only when transitioning to a new section of possible states
+ in the hierarchy.
+
+ ```javascript
+ robotManager = Ember.StateManager.create({
+ initialState: 'poweredDown',
+ poweredDown: Ember.State.create({
+ enter: function(){},
+ exit: function(){
+ console.log("exited poweredDown state")
+ },
+ charging: Ember.State.create({
+ enter: function(){},
+ exit: function(){}
+ }),
+ charged: Ember.State.create({
+ enter: function(){
+ console.log("entered charged state")
+ },
+ exit: function(){
+ console.log("exited charged state")
+ }
+ })
+ }),
+ poweredUp: Ember.State.create({
+ enter: function(){
+ console.log("entered poweredUp state")
+ },
+ exit: function(){},
+ mobile: Ember.State.create({
+ enter: function(){
+ console.log("entered mobile state")
+ },
+ exit: function(){}
+ }),
+ stationary: Ember.State.create({
+ enter: function(){},
+ exit: function(){}
+ })
+ })
+ })
+
+
+ robotManager.get('currentState.path') // 'poweredDown'
+ robotManager.transitionTo('charged')
+ // logs 'entered charged state'
+ // but does *not* log 'exited poweredDown state'
+ robotManager.get('currentState.name') // 'charged
+
+ robotManager.transitionTo('poweredUp.mobile')
+ // logs
+ // 'exited charged state'
+ // 'exited poweredDown state'
+ // 'entered poweredUp state'
+ // 'entered mobile state'
+ ```
+
+ During development you can set a StateManager's `enableLogging` property to
+ `true` to receive console messages of state transitions.
+
+ ```javascript
+ robotManager = Ember.StateManager.create({
+ enableLogging: true
+ })
+ ```
+
+ ## Managing currentState with Actions
+
+ To control which transitions are possible for a given state, and
+ appropriately handle external events, the StateManager can receive and
+ route action messages to its states via the `send` method. Calling to
+ `send` with an action name will begin searching for a method with the same
+ name starting at the current state and moving up through the parent states
+ in a state hierarchy until an appropriate method is found or the StateManager
+ instance itself is reached.
+
+ If an appropriately named method is found it will be called with the state
+ manager as the first argument and an optional `context` object as the second
+ argument.
+
+ ```javascript
+ managerA = Ember.StateManager.create({
+ initialState: 'stateOne.substateOne.subsubstateOne',
+ stateOne: Ember.State.create({
+ substateOne: Ember.State.create({
+ anAction: function(manager, context){
+ console.log("an action was called")
+ },
+ subsubstateOne: Ember.State.create({})
+ })
+ })
+ })
+
+ managerA.get('currentState.name') // 'subsubstateOne'
+ managerA.send('anAction')
+ // 'stateOne.substateOne.subsubstateOne' has no anAction method
+ // so the 'anAction' method of 'stateOne.substateOne' is called
+ // and logs "an action was called"
+ // with managerA as the first argument
+ // and no second argument
+
+ someObject = {}
+ managerA.send('anAction', someObject)
+ // the 'anAction' method of 'stateOne.substateOne' is called again
+ // with managerA as the first argument and
+ // someObject as the second argument.
+ ```
+
+ If the StateManager attempts to send an action but does not find an appropriately named
+ method in the current state or while moving upwards through the state hierarchy, it will
+ repeat the process looking for a `unhandledEvent` method. If an `unhandledEvent` method is
+ found, it will be called with the original event name as the second argument. If an
+ `unhandledEvent` method is not found, the StateManager will throw a new Ember.Error.
+
+ ```javascript
+ managerB = Ember.StateManager.create({
+ initialState: 'stateOne.substateOne.subsubstateOne',
+ stateOne: Ember.State.create({
+ substateOne: Ember.State.create({
+ subsubstateOne: Ember.State.create({}),
+ unhandledEvent: function(manager, eventName, context) {
+ console.log("got an unhandledEvent with name " + eventName);
+ }
+ })
+ })
+ })
+
+ managerB.get('currentState.name') // 'subsubstateOne'
+ managerB.send('anAction')
+ // neither `stateOne.substateOne.subsubstateOne` nor any of it's
+ // parent states have a handler for `anAction`. `subsubstateOne`
+ // also does not have a `unhandledEvent` method, but its parent
+ // state, `substateOne`, does, and it gets fired. It will log
+ // "got an unhandledEvent with name anAction"
+ ```
+
+ Action detection only moves upwards through the state hierarchy from the current state.
+ It does not search in other portions of the hierarchy.
+
+ ```javascript
+ managerC = Ember.StateManager.create({
+ initialState: 'stateOne.substateOne.subsubstateOne',
+ stateOne: Ember.State.create({
+ substateOne: Ember.State.create({
+ subsubstateOne: Ember.State.create({})
+ })
+ }),
+ stateTwo: Ember.State.create({
+ anAction: function(manager, context){
+ // will not be called below because it is
+ // not a parent of the current state
+ }
+ })
+ })
+
+ managerC.get('currentState.name') // 'subsubstateOne'
+ managerC.send('anAction')
+ // Error: <Ember.StateManager:ember132> could not
+ // respond to event anAction in state stateOne.substateOne.subsubstateOne.
+ ```
+
+ Inside of an action method the given state should delegate `transitionTo` calls on its
+ StateManager.
+
+ ```javascript
+ robotManager = Ember.StateManager.create({
+ initialState: 'poweredDown.charging',
+ poweredDown: Ember.State.create({
+ charging: Ember.State.create({
+ chargeComplete: function(manager, context){
+ manager.transitionTo('charged')
+ }
+ }),
+ charged: Ember.State.create({
+ boot: function(manager, context){
+ manager.transitionTo('poweredUp')
+ }
+ })
+ }),
+ poweredUp: Ember.State.create({
+ beginExtermination: function(manager, context){
+ manager.transitionTo('rampaging')
+ },
+ rampaging: Ember.State.create()
+ })
+ })
+
+ robotManager.get('currentState.name') // 'charging'
+ robotManager.send('boot') // throws error, no boot action
+ // in current hierarchy
+ robotManager.get('currentState.name') // remains 'charging'
+
+ robotManager.send('beginExtermination') // throws error, no beginExtermination
+ // action in current hierarchy
+ robotManager.get('currentState.name') // remains 'charging'
+
+ robotManager.send('chargeComplete')
+ robotManager.get('currentState.name') // 'charged'
+
+ robotManager.send('boot')
+ robotManager.get('currentState.name') // 'poweredUp'
+
+ robotManager.send('beginExtermination', allHumans)
+ robotManager.get('currentState.name') // 'rampaging'
+ ```
+
+ Transition actions can also be created using the `transitionTo` method of the `Ember.State` class. The
+ following example StateManagers are equivalent:
+
+ ```javascript
+ aManager = Ember.StateManager.create({
+ stateOne: Ember.State.create({
+ changeToStateTwo: Ember.State.transitionTo('stateTwo')
+ }),
+ stateTwo: Ember.State.create({})
+ })
+
+ bManager = Ember.StateManager.create({
+ stateOne: Ember.State.create({
+ changeToStateTwo: function(manager, context){
+ manager.transitionTo('stateTwo', context)
+ }
+ }),
+ stateTwo: Ember.State.create({})
+ })
+ ```
+
+ @class StateManager
+ @namespace Ember
+ @extends Ember.State
+**/
+Ember.StateManager = Ember.State.extend({
+ /**
+ @private
+
+ When creating a new statemanager, look for a default state to transition
+ into. This state can either be named `start`, or can be specified using the
+ `initialState` property.
+
+ @method init
+ */
+ init: function() {
+ this._super();
+
+ set(this, 'stateMeta', Ember.Map.create());
+
+ var initialState = get(this, 'initialState');
+
+ if (!initialState && get(this, 'states.start')) {
+ initialState = 'start';
+ }
+
+ if (initialState) {
+ this.transitionTo(initialState);
+ Ember.assert('Failed to transition to initial state "' + initialState + '"', !!get(this, 'currentState'));
+ }
+ },
+
+ stateMetaFor: function(state) {
+ var meta = get(this, 'stateMeta'),
+ stateMeta = meta.get(state);
+
+ if (!stateMeta) {
+ stateMeta = {};
+ meta.set(state, stateMeta);
+ }
+
+ return stateMeta;
+ },
+
+ setStateMeta: function(state, key, value) {
+ return set(this.stateMetaFor(state), key, value);
+ },
+
+ getStateMeta: function(state, key) {
+ return get(this.stateMetaFor(state), key);
+ },
+
+ /**
+ The current state from among the manager's possible states. This property should
+ not be set directly. Use `transitionTo` to move between states by name.
+
+ @property currentState
+ @type Ember.State
+ */
+ currentState: null,
+
+ /**
+ The path of the current state. Returns a string representation of the current
+ state.
+
+ @property currentPath
+ @type String
+ */
+ currentPath: Ember.computed.alias('currentState.path'),
+
+ /**
+ The name of transitionEvent that this stateManager will dispatch
+
+ @property transitionEvent
+ @type String
+ @default 'setup'
+ */
+ transitionEvent: 'setup',
+
+ /**
+ If set to true, `errorOnUnhandledEvents` will cause an exception to be
+ raised if you attempt to send an event to a state manager that is not
+ handled by the current state or any of its parent states.
+
+ @property errorOnUnhandledEvents
+ @type Boolean
+ @default true
+ */
+ errorOnUnhandledEvent: true,
+
+ send: function(event) {
+ var contexts = [].slice.call(arguments, 1);
+ Ember.assert('Cannot send event "' + event + '" while currentState is ' + get(this, 'currentState'), get(this, 'currentState'));
+ return sendEvent.call(this, event, contexts, false);
+ },
+ unhandledEvent: function(manager, event) {
+ if (get(this, 'errorOnUnhandledEvent')) {
+ throw new Ember.Error(this.toString() + " could not respond to event " + event + " in state " + get(this, 'currentState.path') + ".");
+ }
+ },
+
+ /**
+ Finds a state by its state path.
+
+ Example:
+
+ ```javascript
+ manager = Ember.StateManager.create({
+ root: Ember.State.create({
+ dashboard: Ember.State.create()
+ })
+ });
+
+ manager.getStateByPath(manager, "root.dashboard")
+
+ // returns the dashboard state
+ ```
+
+ @method getStateByPath
+ @param {Ember.State} root the state to start searching from
+ @param {String} path the state path to follow
+ @return {Ember.State} the state at the end of the path
+ */
+ getStateByPath: function(root, path) {
+ var parts = path.split('.'),
+ state = root;
+
+ for (var i=0, len=parts.length; i<len; i++) {
+ state = get(get(state, 'states'), parts[i]);
+ if (!state) { break; }
+ }
+
+ return state;
+ },
+
+ findStateByPath: function(state, path) {
+ var possible;
+
+ while (!possible && state) {
+ possible = this.getStateByPath(state, path);
+ state = get(state, 'parentState');
+ }
+
+ return possible;
+ },
+
+ /**
+ A state stores its child states in its `states` hash.
+ This code takes a path like `posts.show` and looks
+ up `root.states.posts.states.show`.
+
+ It returns a list of all of the states from the
+ root, which is the list of states to call `enter`
+ on.
+
+ @method getStatesInPath
+ @param root
+ @param path
+ */
+ getStatesInPath: function(root, path) {
+ if (!path || path === "") { return undefined; }
+ var parts = path.split('.'),
+ result = [],
+ states,
+ state;
+
+ for (var i=0, len=parts.length; i<len; i++) {
+ states = get(root, 'states');
+ if (!states) { return undefined; }
+ state = get(states, parts[i]);
+ if (state) { root = state; result.push(state); }
+ else { return undefined; }
+ }
+
+ return result;
+ },
+
+ goToState: function() {
+ // not deprecating this yet so people don't constantly need to
+ // make trivial changes for little reason.
+ return this.transitionTo.apply(this, arguments);
+ },
+
+ transitionTo: function(path, context) {
+ // XXX When is transitionTo called with no path
+ if (Ember.isEmpty(path)) { return; }
+
+ // The ES6 signature of this function is `path, ...contexts`
+ var contexts = context ? Array.prototype.slice.call(arguments, 1) : [],
+ currentState = get(this, 'currentState') || this;
+
+ // First, get the enter, exit and resolve states for the current state
+ // and specified path. If possible, use an existing cache.
+ var hash = this.contextFreeTransition(currentState, path);
+
+ // Next, process the raw state information for the contexts passed in.
+ var transition = new Transition(hash).normalize(this, contexts);
+
+ this.enterState(transition);
+ this.triggerSetupContext(transition);
+ },
+
+ contextFreeTransition: function(currentState, path) {
+ var cache = currentState.pathsCache[path];
+ if (cache) { return cache; }
+
+ var enterStates = this.getStatesInPath(currentState, path),
+ exitStates = [],
+ resolveState = currentState;
+
+ // Walk up the states. For each state, check whether a state matching
+ // the `path` is nested underneath. This will find the closest
+ // parent state containing `path`.
+ //
+ // This allows the user to pass in a relative path. For example, for
+ // the following state hierarchy:
+ //
+ // | |root
+ // | |- posts
+ // | | |- show (* current)
+ // | |- comments
+ // | | |- show
+ //
+ // If the current state is `<root.posts.show>`, an attempt to
+ // transition to `comments.show` will match `<root.comments.show>`.
+ //
+ // First, this code will look for root.posts.show.comments.show.
+ // Next, it will look for root.posts.comments.show. Finally,
+ // it will look for `root.comments.show`, and find the state.
+ //
+ // After this process, the following variables will exist:
+ //
+ // * resolveState: a common parent state between the current
+ // and target state. In the above example, `<root>` is the
+ // `resolveState`.
+ // * enterStates: a list of all of the states represented
+ // by the path from the `resolveState`. For example, for
+ // the path `root.comments.show`, `enterStates` would have
+ // `[<root.comments>, <root.comments.show>]`
+ // * exitStates: a list of all of the states from the
+ // `resolveState` to the `currentState`. In the above
+ // example, `exitStates` would have
+ // `[<root.posts>`, `<root.posts.show>]`.
+ while (resolveState && !enterStates) {
+ exitStates.unshift(resolveState);
+
+ resolveState = get(resolveState, 'parentState');
+ if (!resolveState) {
+ enterStates = this.getStatesInPath(this, path);
+ if (!enterStates) {
+ Ember.assert('Could not find state for path: "'+path+'"');
+ return;
+ }
+ }
+ enterStates = this.getStatesInPath(resolveState, path);
+ }
+
+ // If the path contains some states that are parents of both the
+ // current state and the target state, remove them.
+ //
+ // For example, in the following hierarchy:
+ //
+ // |- root
+ // | |- post
+ // | | |- index (* current)
+ // | | |- show
+ //
+ // If the `path` is `root.post.show`, the three variables will
+ // be:
+ //
+ // * resolveState: `<state manager>`
+ // * enterStates: `[<root>, <root.post>, <root.post.show>]`
+ // * exitStates: `[<root>, <root.post>, <root.post.index>]`
+ //
+ // The goal of this code is to remove the common states, so we
+ // have:
+ //
+ // * resolveState: `<root.post>`
+ // * enterStates: `[<root.post.show>]`
+ // * exitStates: `[<root.post.index>]`
+ //
+ // This avoid unnecessary calls to the enter and exit transitions.
+ while (enterStates.length > 0 && enterStates[0] === exitStates[0]) {
+ resolveState = enterStates.shift();
+ exitStates.shift();
+ }
+
+ // Cache the enterStates, exitStates, and resolveState for the
+ // current state and the `path`.
+ var transitions = currentState.pathsCache[path] = {
+ exitStates: exitStates,
+ enterStates: enterStates,
+ resolveState: resolveState
+ };
+
+ return transitions;
+ },
+
+ triggerSetupContext: function(transitions) {
+ var contexts = transitions.contexts,
+ offset = transitions.enterStates.length - contexts.length,
+ enterStates = transitions.enterStates,
+ transitionEvent = get(this, 'transitionEvent');
+
+ Ember.assert("More contexts provided than states", offset >= 0);
+
+ arrayForEach.call(enterStates, function(state, idx) {
+ state.trigger(transitionEvent, this, contexts[idx-offset]);
+ }, this);
+ },
+
+ getState: function(name) {
+ var state = get(this, name),
+ parentState = get(this, 'parentState');
+
+ if (state) {
+ return state;
+ } else if (parentState) {
+ return parentState.getState(name);
+ }
+ },
+
+ enterState: function(transition) {
+ var log = this.enableLogging;
+
+ var exitStates = transition.exitStates.slice(0).reverse();
+ arrayForEach.call(exitStates, function(state) {
+ state.trigger('exit', this);
+ }, this);
+
+ arrayForEach.call(transition.enterStates, function(state) {
+ if (log) { Ember.Logger.log("STATEMANAGER: Entering " + get(state, 'path')); }
+ state.trigger('enter', this);
+ }, this);
+
+ set(this, 'currentState', transition.finalState);
+ }
+});
+
+})();
+
+
+
+(function() {
+/**
+Ember States
+
+@module ember
+@submodule ember-states
+@requires ember-runtime
+*/
+
+})();
+
+
+})();
+// Version: v1.0.0-rc.1
+// Last commit: 8b061b4 (2013-02-15 12:10:22 -0800)
+
+
+(function() {
+/**
+Ember
+
+@module ember
+*/
+
+})();
+
diff --git a/public/js/libs/handlebars.js b/public/js/libs/handlebars.js
new file mode 100644
index 0000000..9c653ee
--- /dev/null
+++ b/public/js/libs/handlebars.js
@@ -0,0 +1,2201 @@
+/*
+
+Copyright (C) 2011 by Yehuda Katz
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+*/
+
+// lib/handlebars/base.js
+
+/*jshint eqnull:true*/
+this.Handlebars = {};
+
+(function(Handlebars) {
+
+Handlebars.VERSION = "1.0.0-rc.3";
+Handlebars.COMPILER_REVISION = 2;
+
+Handlebars.REVISION_CHANGES = {
+ 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it
+ 2: '>= 1.0.0-rc.3'
+};
+
+Handlebars.helpers = {};
+Handlebars.partials = {};
+
+Handlebars.registerHelper = function(name, fn, inverse) {
+ if(inverse) { fn.not = inverse; }
+ this.helpers[name] = fn;
+};
+
+Handlebars.registerPartial = function(name, str) {
+ this.partials[name] = str;
+};
+
+Handlebars.registerHelper('helperMissing', function(arg) {
+ if(arguments.length === 2) {
+ return undefined;
+ } else {
+ throw new Error("Could not find property '" + arg + "'");
+ }
+});
+
+var toString = Object.prototype.toString, functionType = "[object Function]";
+
+Handlebars.registerHelper('blockHelperMissing', function(context, options) {
+ var inverse = options.inverse || function() {}, fn = options.fn;
+
+
+ var ret = "";
+ var type = toString.call(context);
+
+ if(type === functionType) { context = context.call(this); }
+
+ if(context === true) {
+ return fn(this);
+ } else if(context === false || context == null) {
+ return inverse(this);
+ } else if(type === "[object Array]") {
+ if(context.length > 0) {
+ return Handlebars.helpers.each(context, options);
+ } else {
+ return inverse(this);
+ }
+ } else {
+ return fn(context);
+ }
+});
+
+Handlebars.K = function() {};
+
+Handlebars.createFrame = Object.create || function(object) {
+ Handlebars.K.prototype = object;
+ var obj = new Handlebars.K();
+ Handlebars.K.prototype = null;
+ return obj;
+};
+
+Handlebars.logger = {
+ DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3,
+
+ methodMap: {0: 'debug', 1: 'info', 2: 'warn', 3: 'error'},
+
+ // can be overridden in the host environment
+ log: function(level, obj) {
+ if (Handlebars.logger.level <= level) {
+ var method = Handlebars.logger.methodMap[level];
+ if (typeof console !== 'undefined' && console[method]) {
+ console[method].call(console, obj);
+ }
+ }
+ }
+};
+
+Handlebars.log = function(level, obj) { Handlebars.logger.log(level, obj); };
+
+Handlebars.registerHelper('each', function(context, options) {
+ var fn = options.fn, inverse = options.inverse;
+ var i = 0, ret = "", data;
+
+ if (options.data) {
+ data = Handlebars.createFrame(options.data);
+ }
+
+ if(context && typeof context === 'object') {
+ if(context instanceof Array){
+ for(var j = context.length; i<j; i++) {
+ if (data) { data.index = i; }
+ ret = ret + fn(context[i], { data: data });
+ }
+ } else {
+ for(var key in context) {
+ if(context.hasOwnProperty(key)) {
+ if(data) { data.key = key; }
+ ret = ret + fn(context[key], {data: data});
+ i++;
+ }
+ }
+ }
+ }
+
+ if(i === 0){
+ ret = inverse(this);
+ }
+
+ return ret;
+});
+
+Handlebars.registerHelper('if', function(context, options) {
+ var type = toString.call(context);
+ if(type === functionType) { context = context.call(this); }
+
+ if(!context || Handlebars.Utils.isEmpty(context)) {
+ return options.inverse(this);
+ } else {
+ return options.fn(this);
+ }
+});
+
+Handlebars.registerHelper('unless', function(context, options) {
+ var fn = options.fn, inverse = options.inverse;
+ options.fn = inverse;
+ options.inverse = fn;
+
+ return Handlebars.helpers['if'].call(this, context, options);
+});
+
+Handlebars.registerHelper('with', function(context, options) {
+ return options.fn(context);
+});
+
+Handlebars.registerHelper('log', function(context, options) {
+ var level = options.data && options.data.level != null ? parseInt(options.data.level, 10) : 1;
+ Handlebars.log(level, context);
+});
+
+}(this.Handlebars));
+;
+// lib/handlebars/compiler/parser.js
+/* Jison generated parser */
+var handlebars = (function(){
+var parser = {trace: function trace() { },
+yy: {},
+symbols_: {"error":2,"root":3,"program":4,"EOF":5,"simpleInverse":6,"statements":7,"statement":8,"openInverse":9,"closeBlock":10,"openBlock":11,"mustache":12,"partial":13,"CONTENT":14,"COMMENT":15,"OPEN_BLOCK":16,"inMustache":17,"CLOSE":18,"OPEN_INVERSE":19,"OPEN_ENDBLOCK":20,"path":21,"OPEN":22,"OPEN_UNESCAPED":23,"OPEN_PARTIAL":24,"partialName":25,"params":26,"hash":27,"DATA":28,"param":29,"STRING":30,"INTEGER":31,"BOOLEAN":32,"hashSegments":33,"hashSegment":34,"ID":35,"EQUALS":36,"PARTIAL_NAME":37,"pathSegments":38,"SEP":39,"$accept":0,"$end":1},
+terminals_: {2:"error",5:"EOF",14:"CONTENT",15:"COMMENT",16:"OPEN_BLOCK",18:"CLOSE",19:"OPEN_INVERSE",20:"OPEN_ENDBLOCK",22:"OPEN",23:"OPEN_UNESCAPED",24:"OPEN_PARTIAL",28:"DATA",30:"STRING",31:"INTEGER",32:"BOOLEAN",35:"ID",36:"EQUALS",37:"PARTIAL_NAME",39:"SEP"},
+productions_: [0,[3,2],[4,2],[4,3],[4,2],[4,1],[4,1],[4,0],[7,1],[7,2],[8,3],[8,3],[8,1],[8,1],[8,1],[8,1],[11,3],[9,3],[10,3],[12,3],[12,3],[13,3],[13,4],[6,2],[17,3],[17,2],[17,2],[17,1],[17,1],[26,2],[26,1],[29,1],[29,1],[29,1],[29,1],[29,1],[27,1],[33,2],[33,1],[34,3],[34,3],[34,3],[34,3],[34,3],[25,1],[21,1],[38,3],[38,1]],
+performAction: function anonymous(yytext,yyleng,yylineno,yy,yystate,$$,_$) {
+
+var $0 = $$.length - 1;
+switch (yystate) {
+case 1: return $$[$0-1];
+break;
+case 2: this.$ = new yy.ProgramNode([], $$[$0]);
+break;
+case 3: this.$ = new yy.ProgramNode($$[$0-2], $$[$0]);
+break;
+case 4: this.$ = new yy.ProgramNode($$[$0-1], []);
+break;
+case 5: this.$ = new yy.ProgramNode($$[$0]);
+break;
+case 6: this.$ = new yy.ProgramNode([], []);
+break;
+case 7: this.$ = new yy.ProgramNode([]);
+break;
+case 8: this.$ = [$$[$0]];
+break;
+case 9: $$[$0-1].push($$[$0]); this.$ = $$[$0-1];
+break;
+case 10: this.$ = new yy.BlockNode($$[$0-2], $$[$0-1].inverse, $$[$0-1], $$[$0]);
+break;
+case 11: this.$ = new yy.BlockNode($$[$0-2], $$[$0-1], $$[$0-1].inverse, $$[$0]);
+break;
+case 12: this.$ = $$[$0];
+break;
+case 13: this.$ = $$[$0];
+break;
+case 14: this.$ = new yy.ContentNode($$[$0]);
+break;
+case 15: this.$ = new yy.CommentNode($$[$0]);
+break;
+case 16: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1]);
+break;
+case 17: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1]);
+break;
+case 18: this.$ = $$[$0-1];
+break;
+case 19: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1]);
+break;
+case 20: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], true);
+break;
+case 21: this.$ = new yy.PartialNode($$[$0-1]);
+break;
+case 22: this.$ = new yy.PartialNode($$[$0-2], $$[$0-1]);
+break;
+case 23:
+break;
+case 24: this.$ = [[$$[$0-2]].concat($$[$0-1]), $$[$0]];
+break;
+case 25: this.$ = [[$$[$0-1]].concat($$[$0]), null];
+break;
+case 26: this.$ = [[$$[$0-1]], $$[$0]];
+break;
+case 27: this.$ = [[$$[$0]], null];
+break;
+case 28: this.$ = [[new yy.DataNode($$[$0])], null];
+break;
+case 29: $$[$0-1].push($$[$0]); this.$ = $$[$0-1];
+break;
+case 30: this.$ = [$$[$0]];
+break;
+case 31: this.$ = $$[$0];
+break;
+case 32: this.$ = new yy.StringNode($$[$0]);
+break;
+case 33: this.$ = new yy.IntegerNode($$[$0]);
+break;
+case 34: this.$ = new yy.BooleanNode($$[$0]);
+break;
+case 35: this.$ = new yy.DataNode($$[$0]);
+break;
+case 36: this.$ = new yy.HashNode($$[$0]);
+break;
+case 37: $$[$0-1].push($$[$0]); this.$ = $$[$0-1];
+break;
+case 38: this.$ = [$$[$0]];
+break;
+case 39: this.$ = [$$[$0-2], $$[$0]];
+break;
+case 40: this.$ = [$$[$0-2], new yy.StringNode($$[$0])];
+break;
+case 41: this.$ = [$$[$0-2], new yy.IntegerNode($$[$0])];
+break;
+case 42: this.$ = [$$[$0-2], new yy.BooleanNode($$[$0])];
+break;
+case 43: this.$ = [$$[$0-2], new yy.DataNode($$[$0])];
+break;
+case 44: this.$ = new yy.PartialNameNode($$[$0]);
+break;
+case 45: this.$ = new yy.IdNode($$[$0]);
+break;
+case 46: $$[$0-2].push($$[$0]); this.$ = $$[$0-2];
+break;
+case 47: this.$ = [$$[$0]];
+break;
+}
+},
+table: [{3:1,4:2,5:[2,7],6:3,7:4,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],22:[1,14],23:[1,15],24:[1,16]},{1:[3]},{5:[1,17]},{5:[2,6],7:18,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,6],22:[1,14],23:[1,15],24:[1,16]},{5:[2,5],6:20,8:21,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],20:[2,5],22:[1,14],23:[1,15],24:[1,16]},{17:23,18:[1,22],21:24,28:[1,25],35:[1,27],38:26},{5:[2,8],14:[2,8],15:[2,8],16:[2,8],19:[2,8],20:[2,8],22:[2,8],23:[2,8],24:[2,8]},{4:28,6:3,7:4,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],20:[2,7],22:[1,14],23:[1,15],24:[1,16]},{4:29,6:3,7:4,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],20:[2,7],22:[1,14],23:[1,15],24:[1,16]},{5:[2,12],14:[2,12],15:[2,12],16:[2,12],19:[2,12],20:[2,12],22:[2,12],23:[2,12],24:[2,12]},{5:[2,13],14:[2,13],15:[2,13],16:[2,13],19:[2,13],20:[2,13],22:[2,13],23:[2,13],24:[2,13]},{5:[2,14],14:[2,14],15:[2,14],16:[2,14],19:[2,14],20:[2,14],22:[2,14],23:[2,14],24:[2,14]},{5:[2,15],14:[2,15],15:[2,15],16:[2,15],19:[2,15],20:[2,15],22:[2,15],23:[2,15],24:[2,15]},{17:30,21:24,28:[1,25],35:[1,27],38:26},{17:31,21:24,28:[1,25],35:[1,27],38:26},{17:32,21:24,28:[1,25],35:[1,27],38:26},{25:33,37:[1,34]},{1:[2,1]},{5:[2,2],8:21,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,2],22:[1,14],23:[1,15],24:[1,16]},{17:23,21:24,28:[1,25],35:[1,27],38:26},{5:[2,4],7:35,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,4],22:[1,14],23:[1,15],24:[1,16]},{5:[2,9],14:[2,9],15:[2,9],16:[2,9],19:[2,9],20:[2,9],22:[2,9],23:[2,9],24:[2,9]},{5:[2,23],14:[2,23],15:[2,23],16:[2,23],19:[2,23],20:[2,23],22:[2,23],23:[2,23],24:[2,23]},{18:[1,36]},{18:[2,27],21:41,26:37,27:38,28:[1,45],29:39,30:[1,42],31:[1,43],32:[1,44],33:40,34:46,35:[1,47],38:26},{18:[2,28]},{18:[2,45],28:[2,45],30:[2,45],31:[2,45],32:[2,45],35:[2,45],39:[1,48]},{18:[2,47],28:[2,47],30:[2,47],31:[2,47],32:[2,47],35:[2,47],39:[2,47]},{10:49,20:[1,50]},{10:51,20:[1,50]},{18:[1,52]},{18:[1,53]},{18:[1,54]},{18:[1,55],21:56,35:[1,27],38:26},{18:[2,44],35:[2,44]},{5:[2,3],8:21,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,3],22:[1,14],23:[1,15],24:[1,16]},{14:[2,17],15:[2,17],16:[2,17],19:[2,17],20:[2,17],22:[2,17],23:[2,17],24:[2,17]},{18:[2,25],21:41,27:57,28:[1,45],29:58,30:[1,42],31:[1,43],32:[1,44],33:40,34:46,35:[1,47],38:26},{18:[2,26]},{18:[2,30],28:[2,30],30:[2,30],31:[2,30],32:[2,30],35:[2,30]},{18:[2,36],34:59,35:[1,60]},{18:[2,31],28:[2,31],30:[2,31],31:[2,31],32:[2,31],35:[2,31]},{18:[2,32],28:[2,32],30:[2,32],31:[2,32],32:[2,32],35:[2,32]},{18:[2,33],28:[2,33],30:[2,33],31:[2,33],32:[2,33],35:[2,33]},{18:[2,34],28:[2,34],30:[2,34],31:[2,34],32:[2,34],35:[2,34]},{18:[2,35],28:[2,35],30:[2,35],31:[2,35],32:[2,35],35:[2,35]},{18:[2,38],35:[2,38]},{18:[2,47],28:[2,47],30:[2,47],31:[2,47],32:[2,47],35:[2,47],36:[1,61],39:[2,47]},{35:[1,62]},{5:[2,10],14:[2,10],15:[2,10],16:[2,10],19:[2,10],20:[2,10],22:[2,10],23:[2,10],24:[2,10]},{21:63,35:[1,27],38:26},{5:[2,11],14:[2,11],15:[2,11],16:[2,11],19:[2,11],20:[2,11],22:[2,11],23:[2,11],24:[2,11]},{14:[2,16],15:[2,16],16:[2,16],19:[2,16],20:[2,16],22:[2,16],23:[2,16],24:[2,16]},{5:[2,19],14:[2,19],15:[2,19],16:[2,19],19:[2,19],20:[2,19],22:[2,19],23:[2,19],24:[2,19]},{5:[2,20],14:[2,20],15:[2,20],16:[2,20],19:[2,20],20:[2,20],22:[2,20],23:[2,20],24:[2,20]},{5:[2,21],14:[2,21],15:[2,21],16:[2,21],19:[2,21],20:[2,21],22:[2,21],23:[2,21],24:[2,21]},{18:[1,64]},{18:[2,24]},{18:[2,29],28:[2,29],30:[2,29],31:[2,29],32:[2,29],35:[2,29]},{18:[2,37],35:[2,37]},{36:[1,61]},{21:65,28:[1,69],30:[1,66],31:[1,67],32:[1,68],35:[1,27],38:26},{18:[2,46],28:[2,46],30:[2,46],31:[2,46],32:[2,46],35:[2,46],39:[2,46]},{18:[1,70]},{5:[2,22],14:[2,22],15:[2,22],16:[2,22],19:[2,22],20:[2,22],22:[2,22],23:[2,22],24:[2,22]},{18:[2,39],35:[2,39]},{18:[2,40],35:[2,40]},{18:[2,41],35:[2,41]},{18:[2,42],35:[2,42]},{18:[2,43],35:[2,43]},{5:[2,18],14:[2,18],15:[2,18],16:[2,18],19:[2,18],20:[2,18],22:[2,18],23:[2,18],24:[2,18]}],
+defaultActions: {17:[2,1],25:[2,28],38:[2,26],57:[2,24]},
+parseError: function parseError(str, hash) {
+ throw new Error(str);
+},
+parse: function parse(input) {
+ var self = this, stack = [0], vstack = [null], lstack = [], table = this.table, yytext = "", yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1;
+ this.lexer.setInput(input);
+ this.lexer.yy = this.yy;
+ this.yy.lexer = this.lexer;
+ this.yy.parser = this;
+ if (typeof this.lexer.yylloc == "undefined")
+ this.lexer.yylloc = {};
+ var yyloc = this.lexer.yylloc;
+ lstack.push(yyloc);
+ var ranges = this.lexer.options && this.lexer.options.ranges;
+ if (typeof this.yy.parseError === "function")
+ this.parseError = this.yy.parseError;
+ function popStack(n) {
+ stack.length = stack.length - 2 * n;
+ vstack.length = vstack.length - n;
+ lstack.length = lstack.length - n;
+ }
+ function lex() {
+ var token;
+ token = self.lexer.lex() || 1;
+ if (typeof token !== "number") {
+ token = self.symbols_[token] || token;
+ }
+ return token;
+ }
+ var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected;
+ while (true) {
+ state = stack[stack.length - 1];
+ if (this.defaultActions[state]) {
+ action = this.defaultActions[state];
+ } else {
+ if (symbol === null || typeof symbol == "undefined") {
+ symbol = lex();
+ }
+ action = table[state] && table[state][symbol];
+ }
+ if (typeof action === "undefined" || !action.length || !action[0]) {
+ var errStr = "";
+ if (!recovering) {
+ expected = [];
+ for (p in table[state])
+ if (this.terminals_[p] && p > 2) {
+ expected.push("'" + this.terminals_[p] + "'");
+ }
+ if (this.lexer.showPosition) {
+ errStr = "Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", ") + ", got '" + (this.terminals_[symbol] || symbol) + "'";
+ } else {
+ errStr = "Parse error on line " + (yylineno + 1) + ": Unexpected " + (symbol == 1?"end of input":"'" + (this.terminals_[symbol] || symbol) + "'");
+ }
+ this.parseError(errStr, {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected});
+ }
+ }
+ if (action[0] instanceof Array && action.length > 1) {
+ throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol);
+ }
+ switch (action[0]) {
+ case 1:
+ stack.push(symbol);
+ vstack.push(this.lexer.yytext);
+ lstack.push(this.lexer.yylloc);
+ stack.push(action[1]);
+ symbol = null;
+ if (!preErrorSymbol) {
+ yyleng = this.lexer.yyleng;
+ yytext = this.lexer.yytext;
+ yylineno = this.lexer.yylineno;
+ yyloc = this.lexer.yylloc;
+ if (recovering > 0)
+ recovering--;
+ } else {
+ symbol = preErrorSymbol;
+ preErrorSymbol = null;
+ }
+ break;
+ case 2:
+ len = this.productions_[action[1]][1];
+ yyval.$ = vstack[vstack.length - len];
+ yyval._$ = {first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column};
+ if (ranges) {
+ yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]];
+ }
+ r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack);
+ if (typeof r !== "undefined") {
+ return r;
+ }
+ if (len) {
+ stack = stack.slice(0, -1 * len * 2);
+ vstack = vstack.slice(0, -1 * len);
+ lstack = lstack.slice(0, -1 * len);
+ }
+ stack.push(this.productions_[action[1]][0]);
+ vstack.push(yyval.$);
+ lstack.push(yyval._$);
+ newState = table[stack[stack.length - 2]][stack[stack.length - 1]];
+ stack.push(newState);
+ break;
+ case 3:
+ return true;
+ }
+ }
+ return true;
+}
+};
+/* Jison generated lexer */
+var lexer = (function(){
+var lexer = ({EOF:1,
+parseError:function parseError(str, hash) {
+ if (this.yy.parser) {
+ this.yy.parser.parseError(str, hash);
+ } else {
+ throw new Error(str);
+ }
+ },
+setInput:function (input) {
+ this._input = input;
+ this._more = this._less = this.done = false;
+ this.yylineno = this.yyleng = 0;
+ this.yytext = this.matched = this.match = '';
+ this.conditionStack = ['INITIAL'];
+ this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0};
+ if (this.options.ranges) this.yylloc.range = [0,0];
+ this.offset = 0;
+ return this;
+ },
+input:function () {
+ var ch = this._input[0];
+ this.yytext += ch;
+ this.yyleng++;
+ this.offset++;
+ this.match += ch;
+ this.matched += ch;
+ var lines = ch.match(/(?:\r\n?|\n).*/g);
+ if (lines) {
+ this.yylineno++;
+ this.yylloc.last_line++;
+ } else {
+ this.yylloc.last_column++;
+ }
+ if (this.options.ranges) this.yylloc.range[1]++;
+
+ this._input = this._input.slice(1);
+ return ch;
+ },
+unput:function (ch) {
+ var len = ch.length;
+ var lines = ch.split(/(?:\r\n?|\n)/g);
+
+ this._input = ch + this._input;
+ this.yytext = this.yytext.substr(0, this.yytext.length-len-1);
+ //this.yyleng -= len;
+ this.offset -= len;
+ var oldLines = this.match.split(/(?:\r\n?|\n)/g);
+ this.match = this.match.substr(0, this.match.length-1);
+ this.matched = this.matched.substr(0, this.matched.length-1);
+
+ if (lines.length-1) this.yylineno -= lines.length-1;
+ var r = this.yylloc.range;
+
+ this.yylloc = {first_line: this.yylloc.first_line,
+ last_line: this.yylineno+1,
+ first_column: this.yylloc.first_column,
+ last_column: lines ?
+ (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length:
+ this.yylloc.first_column - len
+ };
+
+ if (this.options.ranges) {
+ this.yylloc.range = [r[0], r[0] + this.yyleng - len];
+ }
+ return this;
+ },
+more:function () {
+ this._more = true;
+ return this;
+ },
+less:function (n) {
+ this.unput(this.match.slice(n));
+ },
+pastInput:function () {
+ var past = this.matched.substr(0, this.matched.length - this.match.length);
+ return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, "");
+ },
+upcomingInput:function () {
+ var next = this.match;
+ if (next.length < 20) {
+ next += this._input.substr(0, 20-next.length);
+ }
+ return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, "");
+ },
+showPosition:function () {
+ var pre = this.pastInput();
+ var c = new Array(pre.length + 1).join("-");
+ return pre + this.upcomingInput() + "\n" + c+"^";
+ },
+next:function () {
+ if (this.done) {
+ return this.EOF;
+ }
+ if (!this._input) this.done = true;
+
+ var token,
+ match,
+ tempMatch,
+ index,
+ col,
+ lines;
+ if (!this._more) {
+ this.yytext = '';
+ this.match = '';
+ }
+ var rules = this._currentRules();
+ for (var i=0;i < rules.length; i++) {
+ tempMatch = this._input.match(this.rules[rules[i]]);
+ if (tempMatch && (!match || tempMatch[0].length > match[0].length)) {
+ match = tempMatch;
+ index = i;
+ if (!this.options.flex) break;
+ }
+ }
+ if (match) {
+ lines = match[0].match(/(?:\r\n?|\n).*/g);
+ if (lines) this.yylineno += lines.length;
+ this.yylloc = {first_line: this.yylloc.last_line,
+ last_line: this.yylineno+1,
+ first_column: this.yylloc.last_column,
+ last_column: lines ? lines[lines.length-1].length-lines[lines.length-1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length};
+ this.yytext += match[0];
+ this.match += match[0];
+ this.matches = match;
+ this.yyleng = this.yytext.length;
+ if (this.options.ranges) {
+ this.yylloc.range = [this.offset, this.offset += this.yyleng];
+ }
+ this._more = false;
+ this._input = this._input.slice(match[0].length);
+ this.matched += match[0];
+ token = this.performAction.call(this, this.yy, this, rules[index],this.conditionStack[this.conditionStack.length-1]);
+ if (this.done && this._input) this.done = false;
+ if (token) return token;
+ else return;
+ }
+ if (this._input === "") {
+ return this.EOF;
+ } else {
+ return this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(),
+ {text: "", token: null, line: this.yylineno});
+ }
+ },
+lex:function lex() {
+ var r = this.next();
+ if (typeof r !== 'undefined') {
+ return r;
+ } else {
+ return this.lex();
+ }
+ },
+begin:function begin(condition) {
+ this.conditionStack.push(condition);
+ },
+popState:function popState() {
+ return this.conditionStack.pop();
+ },
+_currentRules:function _currentRules() {
+ return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules;
+ },
+topState:function () {
+ return this.conditionStack[this.conditionStack.length-2];
+ },
+pushState:function begin(condition) {
+ this.begin(condition);
+ }});
+lexer.options = {};
+lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) {
+
+var YYSTATE=YY_START
+switch($avoiding_name_collisions) {
+case 0:
+ if(yy_.yytext.slice(-1) !== "\\") this.begin("mu");
+ if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1), this.begin("emu");
+ if(yy_.yytext) return 14;
+
+break;
+case 1: return 14;
+break;
+case 2:
+ if(yy_.yytext.slice(-1) !== "\\") this.popState();
+ if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1);
+ return 14;
+
+break;
+case 3: yy_.yytext = yy_.yytext.substr(0, yy_.yyleng-4); this.popState(); return 15;
+break;
+case 4: this.begin("par"); return 24;
+break;
+case 5: return 16;
+break;
+case 6: return 20;
+break;
+case 7: return 19;
+break;
+case 8: return 19;
+break;
+case 9: return 23;
+break;
+case 10: return 23;
+break;
+case 11: this.popState(); this.begin('com');
+break;
+case 12: yy_.yytext = yy_.yytext.substr(3,yy_.yyleng-5); this.popState(); return 15;
+break;
+case 13: return 22;
+break;
+case 14: return 36;
+break;
+case 15: return 35;
+break;
+case 16: return 35;
+break;
+case 17: return 39;
+break;
+case 18: /*ignore whitespace*/
+break;
+case 19: this.popState(); return 18;
+break;
+case 20: this.popState(); return 18;
+break;
+case 21: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 30;
+break;
+case 22: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\'/g,"'"); return 30;
+break;
+case 23: yy_.yytext = yy_.yytext.substr(1); return 28;
+break;
+case 24: return 32;
+break;
+case 25: return 32;
+break;
+case 26: return 31;
+break;
+case 27: return 35;
+break;
+case 28: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2); return 35;
+break;
+case 29: return 'INVALID';
+break;
+case 30: /*ignore whitespace*/
+break;
+case 31: this.popState(); return 37;
+break;
+case 32: return 5;
+break;
+}
+};
+lexer.rules = [/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|$)))/,/^(?:[\s\S]*?--\}\})/,/^(?:\{\{>)/,/^(?:\{\{#)/,/^(?:\{\{\/)/,/^(?:\{\{\^)/,/^(?:\{\{\s*else\b)/,/^(?:\{\{\{)/,/^(?:\{\{&)/,/^(?:\{\{!--)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{)/,/^(?:=)/,/^(?:\.(?=[} ]))/,/^(?:\.\.)/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}\}\})/,/^(?:\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@[a-zA-Z]+)/,/^(?:true(?=[}\s]))/,/^(?:false(?=[}\s]))/,/^(?:[0-9]+(?=[}\s]))/,/^(?:[a-zA-Z0-9_$-]+(?=[=}\s\/.]))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:\s+)/,/^(?:[a-zA-Z0-9_$-/]+)/,/^(?:$)/];
+lexer.conditions = {"mu":{"rules":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,32],"inclusive":false},"emu":{"rules":[2],"inclusive":false},"com":{"rules":[3],"inclusive":false},"par":{"rules":[30,31],"inclusive":false},"INITIAL":{"rules":[0,1,32],"inclusive":true}};
+return lexer;})()
+parser.lexer = lexer;
+function Parser () { this.yy = {}; }Parser.prototype = parser;parser.Parser = Parser;
+return new Parser;
+})();;
+// lib/handlebars/compiler/base.js
+Handlebars.Parser = handlebars;
+
+Handlebars.parse = function(input) {
+
+ // Just return if an already-compile AST was passed in.
+ if(input.constructor === Handlebars.AST.ProgramNode) { return input; }
+
+ Handlebars.Parser.yy = Handlebars.AST;
+ return Handlebars.Parser.parse(input);
+};
+
+Handlebars.print = function(ast) {
+ return new Handlebars.PrintVisitor().accept(ast);
+};;
+// lib/handlebars/compiler/ast.js
+(function() {
+
+ Handlebars.AST = {};
+
+ Handlebars.AST.ProgramNode = function(statements, inverse) {
+ this.type = "program";
+ this.statements = statements;
+ if(inverse) { this.inverse = new Handlebars.AST.ProgramNode(inverse); }
+ };
+
+ Handlebars.AST.MustacheNode = function(rawParams, hash, unescaped) {
+ this.type = "mustache";
+ this.escaped = !unescaped;
+ this.hash = hash;
+
+ var id = this.id = rawParams[0];
+ var params = this.params = rawParams.slice(1);
+
+ // a mustache is an eligible helper if:
+ // * its id is simple (a single part, not `this` or `..`)
+ var eligibleHelper = this.eligibleHelper = id.isSimple;
+
+ // a mustache is definitely a helper if:
+ // * it is an eligible helper, and
+ // * it has at least one parameter or hash segment
+ this.isHelper = eligibleHelper && (params.length || hash);
+
+ // if a mustache is an eligible helper but not a definite
+ // helper, it is ambiguous, and will be resolved in a later
+ // pass or at runtime.
+ };
+
+ Handlebars.AST.PartialNode = function(partialName, context) {
+ this.type = "partial";
+ this.partialName = partialName;
+ this.context = context;
+ };
+
+ var verifyMatch = function(open, close) {
+ if(open.original !== close.original) {
+ throw new Handlebars.Exception(open.original + " doesn't match " + close.original);
+ }
+ };
+
+ Handlebars.AST.BlockNode = function(mustache, program, inverse, close) {
+ verifyMatch(mustache.id, close);
+ this.type = "block";
+ this.mustache = mustache;
+ this.program = program;
+ this.inverse = inverse;
+
+ if (this.inverse && !this.program) {
+ this.isInverse = true;
+ }
+ };
+
+ Handlebars.AST.ContentNode = function(string) {
+ this.type = "content";
+ this.string = string;
+ };
+
+ Handlebars.AST.HashNode = function(pairs) {
+ this.type = "hash";
+ this.pairs = pairs;
+ };
+
+ Handlebars.AST.IdNode = function(parts) {
+ this.type = "ID";
+ this.original = parts.join(".");
+
+ var dig = [], depth = 0;
+
+ for(var i=0,l=parts.length; i<l; i++) {
+ var part = parts[i];
+
+ if (part === ".." || part === "." || part === "this") {
+ if (dig.length > 0) { throw new Handlebars.Exception("Invalid path: " + this.original); }
+ else if (part === "..") { depth++; }
+ else { this.isScoped = true; }
+ }
+ else { dig.push(part); }
+ }
+
+ this.parts = dig;
+ this.string = dig.join('.');
+ this.depth = depth;
+
+ // an ID is simple if it only has one part, and that part is not
+ // `..` or `this`.
+ this.isSimple = parts.length === 1 && !this.isScoped && depth === 0;
+
+ this.stringModeValue = this.string;
+ };
+
+ Handlebars.AST.PartialNameNode = function(name) {
+ this.type = "PARTIAL_NAME";
+ this.name = name;
+ };
+
+ Handlebars.AST.DataNode = function(id) {
+ this.type = "DATA";
+ this.id = id;
+ };
+
+ Handlebars.AST.StringNode = function(string) {
+ this.type = "STRING";
+ this.string = string;
+ this.stringModeValue = string;
+ };
+
+ Handlebars.AST.IntegerNode = function(integer) {
+ this.type = "INTEGER";
+ this.integer = integer;
+ this.stringModeValue = Number(integer);
+ };
+
+ Handlebars.AST.BooleanNode = function(bool) {
+ this.type = "BOOLEAN";
+ this.bool = bool;
+ this.stringModeValue = bool === "true";
+ };
+
+ Handlebars.AST.CommentNode = function(comment) {
+ this.type = "comment";
+ this.comment = comment;
+ };
+
+})();;
+// lib/handlebars/utils.js
+
+var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack'];
+
+Handlebars.Exception = function(message) {
+ var tmp = Error.prototype.constructor.apply(this, arguments);
+
+ // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work.
+ for (var idx = 0; idx < errorProps.length; idx++) {
+ this[errorProps[idx]] = tmp[errorProps[idx]];
+ }
+};
+Handlebars.Exception.prototype = new Error();
+
+// Build out our basic SafeString type
+Handlebars.SafeString = function(string) {
+ this.string = string;
+};
+Handlebars.SafeString.prototype.toString = function() {
+ return this.string.toString();
+};
+
+(function() {
+ var escape = {
+ "&": "&amp;",
+ "<": "&lt;",
+ ">": "&gt;",
+ '"': "&quot;",
+ "'": "&#x27;",
+ "`": "&#x60;"
+ };
+
+ var badChars = /[&<>"'`]/g;
+ var possible = /[&<>"'`]/;
+
+ var escapeChar = function(chr) {
+ return escape[chr] || "&amp;";
+ };
+
+ Handlebars.Utils = {
+ escapeExpression: function(string) {
+ // don't escape SafeStrings, since they're already safe
+ if (string instanceof Handlebars.SafeString) {
+ return string.toString();
+ } else if (string == null || string === false) {
+ return "";
+ }
+
+ if(!possible.test(string)) { return string; }
+ return string.replace(badChars, escapeChar);
+ },
+
+ isEmpty: function(value) {
+ if (!value && value !== 0) {
+ return true;
+ } else if(Object.prototype.toString.call(value) === "[object Array]" && value.length === 0) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ };
+})();;
+// lib/handlebars/compiler/compiler.js
+
+/*jshint eqnull:true*/
+Handlebars.Compiler = function() {};
+Handlebars.JavaScriptCompiler = function() {};
+
+(function(Compiler, JavaScriptCompiler) {
+ // the foundHelper register will disambiguate helper lookup from finding a
+ // function in a context. This is necessary for mustache compatibility, which
+ // requires that context functions in blocks are evaluated by blockHelperMissing,
+ // and then proceed as if the resulting value was provided to blockHelperMissing.
+
+ Compiler.prototype = {
+ compiler: Compiler,
+
+ disassemble: function() {
+ var opcodes = this.opcodes, opcode, out = [], params, param;
+
+ for (var i=0, l=opcodes.length; i<l; i++) {
+ opcode = opcodes[i];
+
+ if (opcode.opcode === 'DECLARE') {
+ out.push("DECLARE " + opcode.name + "=" + opcode.value);
+ } else {
+ params = [];
+ for (var j=0; j<opcode.args.length; j++) {
+ param = opcode.args[j];
+ if (typeof param === "string") {
+ param = "\"" + param.replace("\n", "\\n") + "\"";
+ }
+ params.push(param);
+ }
+ out.push(opcode.opcode + " " + params.join(" "));
+ }
+ }
+
+ return out.join("\n");
+ },
+ equals: function(other) {
+ var len = this.opcodes.length;
+ if (other.opcodes.length !== len) {
+ return false;
+ }
+
+ for (var i = 0; i < len; i++) {
+ var opcode = this.opcodes[i],
+ otherOpcode = other.opcodes[i];
+ if (opcode.opcode !== otherOpcode.opcode || opcode.args.length !== otherOpcode.args.length) {
+ return false;
+ }
+ for (var j = 0; j < opcode.args.length; j++) {
+ if (opcode.args[j] !== otherOpcode.args[j]) {
+ return false;
+ }
+ }
+ }
+ return true;
+ },
+
+ guid: 0,
+
+ compile: function(program, options) {
+ this.children = [];
+ this.depths = {list: []};
+ this.options = options;
+
+ // These changes will propagate to the other compiler components
+ var knownHelpers = this.options.knownHelpers;
+ this.options.knownHelpers = {
+ 'helperMissing': true,
+ 'blockHelperMissing': true,
+ 'each': true,
+ 'if': true,
+ 'unless': true,
+ 'with': true,
+ 'log': true
+ };
+ if (knownHelpers) {
+ for (var name in knownHelpers) {
+ this.options.knownHelpers[name] = knownHelpers[name];
+ }
+ }
+
+ return this.program(program);
+ },
+
+ accept: function(node) {
+ return this[node.type](node);
+ },
+
+ program: function(program) {
+ var statements = program.statements, statement;
+ this.opcodes = [];
+
+ for(var i=0, l=statements.length; i<l; i++) {
+ statement = statements[i];
+ this[statement.type](statement);
+ }
+ this.isSimple = l === 1;
+
+ this.depths.list = this.depths.list.sort(function(a, b) {
+ return a - b;
+ });
+
+ return this;
+ },
+
+ compileProgram: function(program) {
+ var result = new this.compiler().compile(program, this.options);
+ var guid = this.guid++, depth;
+
+ this.usePartial = this.usePartial || result.usePartial;
+
+ this.children[guid] = result;
+
+ for(var i=0, l=result.depths.list.length; i<l; i++) {
+ depth = result.depths.list[i];
+
+ if(depth < 2) { continue; }
+ else { this.addDepth(depth - 1); }
+ }
+
+ return guid;
+ },
+
+ block: function(block) {
+ var mustache = block.mustache,
+ program = block.program,
+ inverse = block.inverse;
+
+ if (program) {
+ program = this.compileProgram(program);
+ }
+
+ if (inverse) {
+ inverse = this.compileProgram(inverse);
+ }
+
+ var type = this.classifyMustache(mustache);
+
+ if (type === "helper") {
+ this.helperMustache(mustache, program, inverse);
+ } else if (type === "simple") {
+ this.simpleMustache(mustache);
+
+ // now that the simple mustache is resolved, we need to
+ // evaluate it by executing `blockHelperMissing`
+ this.opcode('pushProgram', program);
+ this.opcode('pushProgram', inverse);
+ this.opcode('emptyHash');
+ this.opcode('blockValue');
+ } else {
+ this.ambiguousMustache(mustache, program, inverse);
+
+ // now that the simple mustache is resolved, we need to
+ // evaluate it by executing `blockHelperMissing`
+ this.opcode('pushProgram', program);
+ this.opcode('pushProgram', inverse);
+ this.opcode('emptyHash');
+ this.opcode('ambiguousBlockValue');
+ }
+
+ this.opcode('append');
+ },
+
+ hash: function(hash) {
+ var pairs = hash.pairs, pair, val;
+
+ this.opcode('pushHash');
+
+ for(var i=0, l=pairs.length; i<l; i++) {
+ pair = pairs[i];
+ val = pair[1];
+
+ if (this.options.stringParams) {
+ this.opcode('pushStringParam', val.stringModeValue, val.type);
+ } else {
+ this.accept(val);
+ }
+
+ this.opcode('assignToHash', pair[0]);
+ }
+ this.opcode('popHash');
+ },
+
+ partial: function(partial) {
+ var partialName = partial.partialName;
+ this.usePartial = true;
+
+ if(partial.context) {
+ this.ID(partial.context);
+ } else {
+ this.opcode('push', 'depth0');
+ }
+
+ this.opcode('invokePartial', partialName.name);
+ this.opcode('append');
+ },
+
+ content: function(content) {
+ this.opcode('appendContent', content.string);
+ },
+
+ mustache: function(mustache) {
+ var options = this.options;
+ var type = this.classifyMustache(mustache);
+
+ if (type === "simple") {
+ this.simpleMustache(mustache);
+ } else if (type === "helper") {
+ this.helperMustache(mustache);
+ } else {
+ this.ambiguousMustache(mustache);
+ }
+
+ if(mustache.escaped && !options.noEscape) {
+ this.opcode('appendEscaped');
+ } else {
+ this.opcode('append');
+ }
+ },
+
+ ambiguousMustache: function(mustache, program, inverse) {
+ var id = mustache.id,
+ name = id.parts[0],
+ isBlock = program != null || inverse != null;
+
+ this.opcode('getContext', id.depth);
+
+ this.opcode('pushProgram', program);
+ this.opcode('pushProgram', inverse);
+
+ this.opcode('invokeAmbiguous', name, isBlock);
+ },
+
+ simpleMustache: function(mustache) {
+ var id = mustache.id;
+
+ if (id.type === 'DATA') {
+ this.DATA(id);
+ } else if (id.parts.length) {
+ this.ID(id);
+ } else {
+ // Simplified ID for `this`
+ this.addDepth(id.depth);
+ this.opcode('getContext', id.depth);
+ this.opcode('pushContext');
+ }
+
+ this.opcode('resolvePossibleLambda');
+ },
+
+ helperMustache: function(mustache, program, inverse) {
+ var params = this.setupFullMustacheParams(mustache, program, inverse),
+ name = mustache.id.parts[0];
+
+ if (this.options.knownHelpers[name]) {
+ this.opcode('invokeKnownHelper', params.length, name);
+ } else if (this.knownHelpersOnly) {
+ throw new Error("You specified knownHelpersOnly, but used the unknown helper " + name);
+ } else {
+ this.opcode('invokeHelper', params.length, name);
+ }
+ },
+
+ ID: function(id) {
+ this.addDepth(id.depth);
+ this.opcode('getContext', id.depth);
+
+ var name = id.parts[0];
+ if (!name) {
+ this.opcode('pushContext');
+ } else {
+ this.opcode('lookupOnContext', id.parts[0]);
+ }
+
+ for(var i=1, l=id.parts.length; i<l; i++) {
+ this.opcode('lookup', id.parts[i]);
+ }
+ },
+
+ DATA: function(data) {
+ this.options.data = true;
+ this.opcode('lookupData', data.id);
+ },
+
+ STRING: function(string) {
+ this.opcode('pushString', string.string);
+ },
+
+ INTEGER: function(integer) {
+ this.opcode('pushLiteral', integer.integer);
+ },
+
+ BOOLEAN: function(bool) {
+ this.opcode('pushLiteral', bool.bool);
+ },
+
+ comment: function() {},
+
+ // HELPERS
+ opcode: function(name) {
+ this.opcodes.push({ opcode: name, args: [].slice.call(arguments, 1) });
+ },
+
+ declare: function(name, value) {
+ this.opcodes.push({ opcode: 'DECLARE', name: name, value: value });
+ },
+
+ addDepth: function(depth) {
+ if(isNaN(depth)) { throw new Error("EWOT"); }
+ if(depth === 0) { return; }
+
+ if(!this.depths[depth]) {
+ this.depths[depth] = true;
+ this.depths.list.push(depth);
+ }
+ },
+
+ classifyMustache: function(mustache) {
+ var isHelper = mustache.isHelper;
+ var isEligible = mustache.eligibleHelper;
+ var options = this.options;
+
+ // if ambiguous, we can possibly resolve the ambiguity now
+ if (isEligible && !isHelper) {
+ var name = mustache.id.parts[0];
+
+ if (options.knownHelpers[name]) {
+ isHelper = true;
+ } else if (options.knownHelpersOnly) {
+ isEligible = false;
+ }
+ }
+
+ if (isHelper) { return "helper"; }
+ else if (isEligible) { return "ambiguous"; }
+ else { return "simple"; }
+ },
+
+ pushParams: function(params) {
+ var i = params.length, param;
+
+ while(i--) {
+ param = params[i];
+
+ if(this.options.stringParams) {
+ if(param.depth) {
+ this.addDepth(param.depth);
+ }
+
+ this.opcode('getContext', param.depth || 0);
+ this.opcode('pushStringParam', param.stringModeValue, param.type);
+ } else {
+ this[param.type](param);
+ }
+ }
+ },
+
+ setupMustacheParams: function(mustache) {
+ var params = mustache.params;
+ this.pushParams(params);
+
+ if(mustache.hash) {
+ this.hash(mustache.hash);
+ } else {
+ this.opcode('emptyHash');
+ }
+
+ return params;
+ },
+
+ // this will replace setupMustacheParams when we're done
+ setupFullMustacheParams: function(mustache, program, inverse) {
+ var params = mustache.params;
+ this.pushParams(params);
+
+ this.opcode('pushProgram', program);
+ this.opcode('pushProgram', inverse);
+
+ if(mustache.hash) {
+ this.hash(mustache.hash);
+ } else {
+ this.opcode('emptyHash');
+ }
+
+ return params;
+ }
+ };
+
+ var Literal = function(value) {
+ this.value = value;
+ };
+
+ JavaScriptCompiler.prototype = {
+ // PUBLIC API: You can override these methods in a subclass to provide
+ // alternative compiled forms for name lookup and buffering semantics
+ nameLookup: function(parent, name /* , type*/) {
+ if (/^[0-9]+$/.test(name)) {
+ return parent + "[" + name + "]";
+ } else if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
+ return parent + "." + name;
+ }
+ else {
+ return parent + "['" + name + "']";
+ }
+ },
+
+ appendToBuffer: function(string) {
+ if (this.environment.isSimple) {
+ return "return " + string + ";";
+ } else {
+ return {
+ appendToBuffer: true,
+ content: string,
+ toString: function() { return "buffer += " + string + ";"; }
+ };
+ }
+ },
+
+ initializeBuffer: function() {
+ return this.quotedString("");
+ },
+
+ namespace: "Handlebars",
+ // END PUBLIC API
+
+ compile: function(environment, options, context, asObject) {
+ this.environment = environment;
+ this.options = options || {};
+
+ Handlebars.log(Handlebars.logger.DEBUG, this.environment.disassemble() + "\n\n");
+
+ this.name = this.environment.name;
+ this.isChild = !!context;
+ this.context = context || {
+ programs: [],
+ environments: [],
+ aliases: { }
+ };
+
+ this.preamble();
+
+ this.stackSlot = 0;
+ this.stackVars = [];
+ this.registers = { list: [] };
+ this.compileStack = [];
+ this.inlineStack = [];
+
+ this.compileChildren(environment, options);
+
+ var opcodes = environment.opcodes, opcode;
+
+ this.i = 0;
+
+ for(l=opcodes.length; this.i<l; this.i++) {
+ opcode = opcodes[this.i];
+
+ if(opcode.opcode === 'DECLARE') {
+ this[opcode.name] = opcode.value;
+ } else {
+ this[opcode.opcode].apply(this, opcode.args);
+ }
+ }
+
+ return this.createFunctionContext(asObject);
+ },
+
+ nextOpcode: function() {
+ var opcodes = this.environment.opcodes;
+ return opcodes[this.i + 1];
+ },
+
+ eat: function() {
+ this.i = this.i + 1;
+ },
+
+ preamble: function() {
+ var out = [];
+
+ if (!this.isChild) {
+ var namespace = this.namespace;
+ var copies = "helpers = helpers || " + namespace + ".helpers;";
+ if (this.environment.usePartial) { copies = copies + " partials = partials || " + namespace + ".partials;"; }
+ if (this.options.data) { copies = copies + " data = data || {};"; }
+ out.push(copies);
+ } else {
+ out.push('');
+ }
+
+ if (!this.environment.isSimple) {
+ out.push(", buffer = " + this.initializeBuffer());
+ } else {
+ out.push("");
+ }
+
+ // track the last context pushed into place to allow skipping the
+ // getContext opcode when it would be a noop
+ this.lastContext = 0;
+ this.source = out;
+ },
+
+ createFunctionContext: function(asObject) {
+ var locals = this.stackVars.concat(this.registers.list);
+
+ if(locals.length > 0) {
+ this.source[1] = this.source[1] + ", " + locals.join(", ");
+ }
+
+ // Generate minimizer alias mappings
+ if (!this.isChild) {
+ for (var alias in this.context.aliases) {
+ this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias];
+ }
+ }
+
+ if (this.source[1]) {
+ this.source[1] = "var " + this.source[1].substring(2) + ";";
+ }
+
+ // Merge children
+ if (!this.isChild) {
+ this.source[1] += '\n' + this.context.programs.join('\n') + '\n';
+ }
+
+ if (!this.environment.isSimple) {
+ this.source.push("return buffer;");
+ }
+
+ var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"];
+
+ for(var i=0, l=this.environment.depths.list.length; i<l; i++) {
+ params.push("depth" + this.environment.depths.list[i]);
+ }
+
+ // Perform a second pass over the output to merge content when possible
+ var source = this.mergeSource();
+
+ if (!this.isChild) {
+ var revision = Handlebars.COMPILER_REVISION,
+ versions = Handlebars.REVISION_CHANGES[revision];
+ source = "this.compilerInfo = ["+revision+",'"+versions+"'];\n"+source;
+ }
+
+ if (asObject) {
+ params.push(source);
+
+ return Function.apply(this, params);
+ } else {
+ var functionSource = 'function ' + (this.name || '') + '(' + params.join(',') + ') {\n ' + source + '}';
+ Handlebars.log(Handlebars.logger.DEBUG, functionSource + "\n\n");
+ return functionSource;
+ }
+ },
+ mergeSource: function() {
+ // WARN: We are not handling the case where buffer is still populated as the source should
+ // not have buffer append operations as their final action.
+ var source = '',
+ buffer;
+ for (var i = 0, len = this.source.length; i < len; i++) {
+ var line = this.source[i];
+ if (line.appendToBuffer) {
+ if (buffer) {
+ buffer = buffer + '\n + ' + line.content;
+ } else {
+ buffer = line.content;
+ }
+ } else {
+ if (buffer) {
+ source += 'buffer += ' + buffer + ';\n ';
+ buffer = undefined;
+ }
+ source += line + '\n ';
+ }
+ }
+ return source;
+ },
+
+ // [blockValue]
+ //
+ // On stack, before: hash, inverse, program, value
+ // On stack, after: return value of blockHelperMissing
+ //
+ // The purpose of this opcode is to take a block of the form
+ // `{{#foo}}...{{/foo}}`, resolve the value of `foo`, and
+ // replace it on the stack with the result of properly
+ // invoking blockHelperMissing.
+ blockValue: function() {
+ this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing';
+
+ var params = ["depth0"];
+ this.setupParams(0, params);
+
+ this.replaceStack(function(current) {
+ params.splice(1, 0, current);
+ return "blockHelperMissing.call(" + params.join(", ") + ")";
+ });
+ },
+
+ // [ambiguousBlockValue]
+ //
+ // On stack, before: hash, inverse, program, value
+ // Compiler value, before: lastHelper=value of last found helper, if any
+ // On stack, after, if no lastHelper: same as [blockValue]
+ // On stack, after, if lastHelper: value
+ ambiguousBlockValue: function() {
+ this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing';
+
+ var params = ["depth0"];
+ this.setupParams(0, params);
+
+ var current = this.topStack();
+ params.splice(1, 0, current);
+
+ // Use the options value generated from the invocation
+ params[params.length-1] = 'options';
+
+ this.source.push("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }");
+ },
+
+ // [appendContent]
+ //
+ // On stack, before: ...
+ // On stack, after: ...
+ //
+ // Appends the string value of `content` to the current buffer
+ appendContent: function(content) {
+ this.source.push(this.appendToBuffer(this.quotedString(content)));
+ },
+
+ // [append]
+ //
+ // On stack, before: value, ...
+ // On stack, after: ...
+ //
+ // Coerces `value` to a String and appends it to the current buffer.
+ //
+ // If `value` is truthy, or 0, it is coerced into a string and appended
+ // Otherwise, the empty string is appended
+ append: function() {
+ // Force anything that is inlined onto the stack so we don't have duplication
+ // when we examine local
+ this.flushInline();
+ var local = this.popStack();
+ this.source.push("if(" + local + " || " + local + " === 0) { " + this.appendToBuffer(local) + " }");
+ if (this.environment.isSimple) {
+ this.source.push("else { " + this.appendToBuffer("''") + " }");
+ }
+ },
+
+ // [appendEscaped]
+ //
+ // On stack, before: value, ...
+ // On stack, after: ...
+ //
+ // Escape `value` and append it to the buffer
+ appendEscaped: function() {
+ this.context.aliases.escapeExpression = 'this.escapeExpression';
+
+ this.source.push(this.appendToBuffer("escapeExpression(" + this.popStack() + ")"));
+ },
+
+ // [getContext]
+ //
+ // On stack, before: ...
+ // On stack, after: ...
+ // Compiler value, after: lastContext=depth
+ //
+ // Set the value of the `lastContext` compiler value to the depth
+ getContext: function(depth) {
+ if(this.lastContext !== depth) {
+ this.lastContext = depth;
+ }
+ },
+
+ // [lookupOnContext]
+ //
+ // On stack, before: ...
+ // On stack, after: currentContext[name], ...
+ //
+ // Looks up the value of `name` on the current context and pushes
+ // it onto the stack.
+ lookupOnContext: function(name) {
+ this.push(this.nameLookup('depth' + this.lastContext, name, 'context'));
+ },
+
+ // [pushContext]
+ //
+ // On stack, before: ...
+ // On stack, after: currentContext, ...
+ //
+ // Pushes the value of the current context onto the stack.
+ pushContext: function() {
+ this.pushStackLiteral('depth' + this.lastContext);
+ },
+
+ // [resolvePossibleLambda]
+ //
+ // On stack, before: value, ...
+ // On stack, after: resolved value, ...
+ //
+ // If the `value` is a lambda, replace it on the stack by
+ // the return value of the lambda
+ resolvePossibleLambda: function() {
+ this.context.aliases.functionType = '"function"';
+
+ this.replaceStack(function(current) {
+ return "typeof " + current + " === functionType ? " + current + ".apply(depth0) : " + current;
+ });
+ },
+
+ // [lookup]
+ //
+ // On stack, before: value, ...
+ // On stack, after: value[name], ...
+ //
+ // Replace the value on the stack with the result of looking
+ // up `name` on `value`
+ lookup: function(name) {
+ this.replaceStack(function(current) {
+ return current + " == null || " + current + " === false ? " + current + " : " + this.nameLookup(current, name, 'context');
+ });
+ },
+
+ // [lookupData]
+ //
+ // On stack, before: ...
+ // On stack, after: data[id], ...
+ //
+ // Push the result of looking up `id` on the current data
+ lookupData: function(id) {
+ this.push(this.nameLookup('data', id, 'data'));
+ },
+
+ // [pushStringParam]
+ //
+ // On stack, before: ...
+ // On stack, after: string, currentContext, ...
+ //
+ // This opcode is designed for use in string mode, which
+ // provides the string value of a parameter along with its
+ // depth rather than resolving it immediately.
+ pushStringParam: function(string, type) {
+ this.pushStackLiteral('depth' + this.lastContext);
+
+ this.pushString(type);
+
+ if (typeof string === 'string') {
+ this.pushString(string);
+ } else {
+ this.pushStackLiteral(string);
+ }
+ },
+
+ emptyHash: function() {
+ this.pushStackLiteral('{}');
+
+ if (this.options.stringParams) {
+ this.register('hashTypes', '{}');
+ }
+ },
+ pushHash: function() {
+ this.hash = {values: [], types: []};
+ },
+ popHash: function() {
+ var hash = this.hash;
+ this.hash = undefined;
+
+ if (this.options.stringParams) {
+ this.register('hashTypes', '{' + hash.types.join(',') + '}');
+ }
+ this.push('{\n ' + hash.values.join(',\n ') + '\n }');
+ },
+
+ // [pushString]
+ //
+ // On stack, before: ...
+ // On stack, after: quotedString(string), ...
+ //
+ // Push a quoted version of `string` onto the stack
+ pushString: function(string) {
+ this.pushStackLiteral(this.quotedString(string));
+ },
+
+ // [push]
+ //
+ // On stack, before: ...
+ // On stack, after: expr, ...
+ //
+ // Push an expression onto the stack
+ push: function(expr) {
+ this.inlineStack.push(expr);
+ return expr;
+ },
+
+ // [pushLiteral]
+ //
+ // On stack, before: ...
+ // On stack, after: value, ...
+ //
+ // Pushes a value onto the stack. This operation prevents
+ // the compiler from creating a temporary variable to hold
+ // it.
+ pushLiteral: function(value) {
+ this.pushStackLiteral(value);
+ },
+
+ // [pushProgram]
+ //
+ // On stack, before: ...
+ // On stack, after: program(guid), ...
+ //
+ // Push a program expression onto the stack. This takes
+ // a compile-time guid and converts it into a runtime-accessible
+ // expression.
+ pushProgram: function(guid) {
+ if (guid != null) {
+ this.pushStackLiteral(this.programExpression(guid));
+ } else {
+ this.pushStackLiteral(null);
+ }
+ },
+
+ // [invokeHelper]
+ //
+ // On stack, before: hash, inverse, program, params..., ...
+ // On stack, after: result of helper invocation
+ //
+ // Pops off the helper's parameters, invokes the helper,
+ // and pushes the helper's return value onto the stack.
+ //
+ // If the helper is not found, `helperMissing` is called.
+ invokeHelper: function(paramSize, name) {
+ this.context.aliases.helperMissing = 'helpers.helperMissing';
+
+ var helper = this.lastHelper = this.setupHelper(paramSize, name, true);
+
+ this.push(helper.name);
+ this.replaceStack(function(name) {
+ return name + ' ? ' + name + '.call(' +
+ helper.callParams + ") " + ": helperMissing.call(" +
+ helper.helperMissingParams + ")";
+ });
+ },
+
+ // [invokeKnownHelper]
+ //
+ // On stack, before: hash, inverse, program, params..., ...
+ // On stack, after: result of helper invocation
+ //
+ // This operation is used when the helper is known to exist,
+ // so a `helperMissing` fallback is not required.
+ invokeKnownHelper: function(paramSize, name) {
+ var helper = this.setupHelper(paramSize, name);
+ this.push(helper.name + ".call(" + helper.callParams + ")");
+ },
+
+ // [invokeAmbiguous]
+ //
+ // On stack, before: hash, inverse, program, params..., ...
+ // On stack, after: result of disambiguation
+ //
+ // This operation is used when an expression like `{{foo}}`
+ // is provided, but we don't know at compile-time whether it
+ // is a helper or a path.
+ //
+ // This operation emits more code than the other options,
+ // and can be avoided by passing the `knownHelpers` and
+ // `knownHelpersOnly` flags at compile-time.
+ invokeAmbiguous: function(name, helperCall) {
+ this.context.aliases.functionType = '"function"';
+
+ this.pushStackLiteral('{}'); // Hash value
+ var helper = this.setupHelper(0, name, helperCall);
+
+ var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper');
+
+ var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context');
+ var nextStack = this.nextStack();
+
+ this.source.push('if (' + nextStack + ' = ' + helperName + ') { ' + nextStack + ' = ' + nextStack + '.call(' + helper.callParams + '); }');
+ this.source.push('else { ' + nextStack + ' = ' + nonHelper + '; ' + nextStack + ' = typeof ' + nextStack + ' === functionType ? ' + nextStack + '.apply(depth0) : ' + nextStack + '; }');
+ },
+
+ // [invokePartial]
+ //
+ // On stack, before: context, ...
+ // On stack after: result of partial invocation
+ //
+ // This operation pops off a context, invokes a partial with that context,
+ // and pushes the result of the invocation back.
+ invokePartial: function(name) {
+ var params = [this.nameLookup('partials', name, 'partial'), "'" + name + "'", this.popStack(), "helpers", "partials"];
+
+ if (this.options.data) {
+ params.push("data");
+ }
+
+ this.context.aliases.self = "this";
+ this.push("self.invokePartial(" + params.join(", ") + ")");
+ },
+
+ // [assignToHash]
+ //
+ // On stack, before: value, hash, ...
+ // On stack, after: hash, ...
+ //
+ // Pops a value and hash off the stack, assigns `hash[key] = value`
+ // and pushes the hash back onto the stack.
+ assignToHash: function(key) {
+ var value = this.popStack(),
+ type;
+
+ if (this.options.stringParams) {
+ type = this.popStack();
+ this.popStack();
+ }
+
+ var hash = this.hash;
+ if (type) {
+ hash.types.push("'" + key + "': " + type);
+ }
+ hash.values.push("'" + key + "': (" + value + ")");
+ },
+
+ // HELPERS
+
+ compiler: JavaScriptCompiler,
+
+ compileChildren: function(environment, options) {
+ var children = environment.children, child, compiler;
+
+ for(var i=0, l=children.length; i<l; i++) {
+ child = children[i];
+ compiler = new this.compiler();
+
+ var index = this.matchExistingProgram(child);
+
+ if (index == null) {
+ this.context.programs.push(''); // Placeholder to prevent name conflicts for nested children
+ index = this.context.programs.length;
+ child.index = index;
+ child.name = 'program' + index;
+ this.context.programs[index] = compiler.compile(child, options, this.context);
+ this.context.environments[index] = child;
+ } else {
+ child.index = index;
+ child.name = 'program' + index;
+ }
+ }
+ },
+ matchExistingProgram: function(child) {
+ for (var i = 0, len = this.context.environments.length; i < len; i++) {
+ var environment = this.context.environments[i];
+ if (environment && environment.equals(child)) {
+ return i;
+ }
+ }
+ },
+
+ programExpression: function(guid) {
+ this.context.aliases.self = "this";
+
+ if(guid == null) {
+ return "self.noop";
+ }
+
+ var child = this.environment.children[guid],
+ depths = child.depths.list, depth;
+
+ var programParams = [child.index, child.name, "data"];
+
+ for(var i=0, l = depths.length; i<l; i++) {
+ depth = depths[i];
+
+ if(depth === 1) { programParams.push("depth0"); }
+ else { programParams.push("depth" + (depth - 1)); }
+ }
+
+ if(depths.length === 0) {
+ return "self.program(" + programParams.join(", ") + ")";
+ } else {
+ programParams.shift();
+ return "self.programWithDepth(" + programParams.join(", ") + ")";
+ }
+ },
+
+ register: function(name, val) {
+ this.useRegister(name);
+ this.source.push(name + " = " + val + ";");
+ },
+
+ useRegister: function(name) {
+ if(!this.registers[name]) {
+ this.registers[name] = true;
+ this.registers.list.push(name);
+ }
+ },
+
+ pushStackLiteral: function(item) {
+ return this.push(new Literal(item));
+ },
+
+ pushStack: function(item) {
+ this.flushInline();
+
+ var stack = this.incrStack();
+ if (item) {
+ this.source.push(stack + " = " + item + ";");
+ }
+ this.compileStack.push(stack);
+ return stack;
+ },
+
+ replaceStack: function(callback) {
+ var prefix = '',
+ inline = this.isInline(),
+ stack;
+
+ // If we are currently inline then we want to merge the inline statement into the
+ // replacement statement via ','
+ if (inline) {
+ var top = this.popStack(true);
+
+ if (top instanceof Literal) {
+ // Literals do not need to be inlined
+ stack = top.value;
+ } else {
+ // Get or create the current stack name for use by the inline
+ var name = this.stackSlot ? this.topStackName() : this.incrStack();
+
+ prefix = '(' + this.push(name) + ' = ' + top + '),';
+ stack = this.topStack();
+ }
+ } else {
+ stack = this.topStack();
+ }
+
+ var item = callback.call(this, stack);
+
+ if (inline) {
+ if (this.inlineStack.length || this.compileStack.length) {
+ this.popStack();
+ }
+ this.push('(' + prefix + item + ')');
+ } else {
+ // Prevent modification of the context depth variable. Through replaceStack
+ if (!/^stack/.test(stack)) {
+ stack = this.nextStack();
+ }
+
+ this.source.push(stack + " = (" + prefix + item + ");");
+ }
+ return stack;
+ },
+
+ nextStack: function() {
+ return this.pushStack();
+ },
+
+ incrStack: function() {
+ this.stackSlot++;
+ if(this.stackSlot > this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); }
+ return this.topStackName();
+ },
+ topStackName: function() {
+ return "stack" + this.stackSlot;
+ },
+ flushInline: function() {
+ var inlineStack = this.inlineStack;
+ if (inlineStack.length) {
+ this.inlineStack = [];
+ for (var i = 0, len = inlineStack.length; i < len; i++) {
+ var entry = inlineStack[i];
+ if (entry instanceof Literal) {
+ this.compileStack.push(entry);
+ } else {
+ this.pushStack(entry);
+ }
+ }
+ }
+ },
+ isInline: function() {
+ return this.inlineStack.length;
+ },
+
+ popStack: function(wrapped) {
+ var inline = this.isInline(),
+ item = (inline ? this.inlineStack : this.compileStack).pop();
+
+ if (!wrapped && (item instanceof Literal)) {
+ return item.value;
+ } else {
+ if (!inline) {
+ this.stackSlot--;
+ }
+ return item;
+ }
+ },
+
+ topStack: function(wrapped) {
+ var stack = (this.isInline() ? this.inlineStack : this.compileStack),
+ item = stack[stack.length - 1];
+
+ if (!wrapped && (item instanceof Literal)) {
+ return item.value;
+ } else {
+ return item;
+ }
+ },
+
+ quotedString: function(str) {
+ return '"' + str
+ .replace(/\\/g, '\\\\')
+ .replace(/"/g, '\\"')
+ .replace(/\n/g, '\\n')
+ .replace(/\r/g, '\\r') + '"';
+ },
+
+ setupHelper: function(paramSize, name, missingParams) {
+ var params = [];
+ this.setupParams(paramSize, params, missingParams);
+ var foundHelper = this.nameLookup('helpers', name, 'helper');
+
+ return {
+ params: params,
+ name: foundHelper,
+ callParams: ["depth0"].concat(params).join(", "),
+ helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ")
+ };
+ },
+
+ // the params and contexts arguments are passed in arrays
+ // to fill in
+ setupParams: function(paramSize, params, useRegister) {
+ var options = [], contexts = [], types = [], param, inverse, program;
+
+ options.push("hash:" + this.popStack());
+
+ inverse = this.popStack();
+ program = this.popStack();
+
+ // Avoid setting fn and inverse if neither are set. This allows
+ // helpers to do a check for `if (options.fn)`
+ if (program || inverse) {
+ if (!program) {
+ this.context.aliases.self = "this";
+ program = "self.noop";
+ }
+
+ if (!inverse) {
+ this.context.aliases.self = "this";
+ inverse = "self.noop";
+ }
+
+ options.push("inverse:" + inverse);
+ options.push("fn:" + program);
+ }
+
+ for(var i=0; i<paramSize; i++) {
+ param = this.popStack();
+ params.push(param);
+
+ if(this.options.stringParams) {
+ types.push(this.popStack());
+ contexts.push(this.popStack());
+ }
+ }
+
+ if (this.options.stringParams) {
+ options.push("contexts:[" + contexts.join(",") + "]");
+ options.push("types:[" + types.join(",") + "]");
+ options.push("hashTypes:hashTypes");
+ }
+
+ if(this.options.data) {
+ options.push("data:data");
+ }
+
+ options = "{" + options.join(",") + "}";
+ if (useRegister) {
+ this.register('options', options);
+ params.push('options');
+ } else {
+ params.push(options);
+ }
+ return params.join(", ");
+ }
+ };
+
+ var reservedWords = (
+ "break else new var" +
+ " case finally return void" +
+ " catch for switch while" +
+ " continue function this with" +
+ " default if throw" +
+ " delete in try" +
+ " do instanceof typeof" +
+ " abstract enum int short" +
+ " boolean export interface static" +
+ " byte extends long super" +
+ " char final native synchronized" +
+ " class float package throws" +
+ " const goto private transient" +
+ " debugger implements protected volatile" +
+ " double import public let yield"
+ ).split(" ");
+
+ var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {};
+
+ for(var i=0, l=reservedWords.length; i<l; i++) {
+ compilerWords[reservedWords[i]] = true;
+ }
+
+ JavaScriptCompiler.isValidJavaScriptVariableName = function(name) {
+ if(!JavaScriptCompiler.RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]+$/.test(name)) {
+ return true;
+ }
+ return false;
+ };
+
+})(Handlebars.Compiler, Handlebars.JavaScriptCompiler);
+
+Handlebars.precompile = function(input, options) {
+ if (!input || (typeof input !== 'string' && input.constructor !== Handlebars.AST.ProgramNode)) {
+ throw new Handlebars.Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed " + input);
+ }
+
+ options = options || {};
+ if (!('data' in options)) {
+ options.data = true;
+ }
+ var ast = Handlebars.parse(input);
+ var environment = new Handlebars.Compiler().compile(ast, options);
+ return new Handlebars.JavaScriptCompiler().compile(environment, options);
+};
+
+Handlebars.compile = function(input, options) {
+ if (!input || (typeof input !== 'string' && input.constructor !== Handlebars.AST.ProgramNode)) {
+ throw new Handlebars.Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed " + input);
+ }
+
+ options = options || {};
+ if (!('data' in options)) {
+ options.data = true;
+ }
+ var compiled;
+ function compile() {
+ var ast = Handlebars.parse(input);
+ var environment = new Handlebars.Compiler().compile(ast, options);
+ var templateSpec = new Handlebars.JavaScriptCompiler().compile(environment, options, undefined, true);
+ return Handlebars.template(templateSpec);
+ }
+
+ // Template is only compiled on first use and cached after that point.
+ return function(context, options) {
+ if (!compiled) {
+ compiled = compile();
+ }
+ return compiled.call(this, context, options);
+ };
+};
+;
+// lib/handlebars/runtime.js
+Handlebars.VM = {
+ template: function(templateSpec) {
+ // Just add water
+ var container = {
+ escapeExpression: Handlebars.Utils.escapeExpression,
+ invokePartial: Handlebars.VM.invokePartial,
+ programs: [],
+ program: function(i, fn, data) {
+ var programWrapper = this.programs[i];
+ if(data) {
+ return Handlebars.VM.program(fn, data);
+ } else if(programWrapper) {
+ return programWrapper;
+ } else {
+ programWrapper = this.programs[i] = Handlebars.VM.program(fn);
+ return programWrapper;
+ }
+ },
+ programWithDepth: Handlebars.VM.programWithDepth,
+ noop: Handlebars.VM.noop,
+ compilerInfo: null
+ };
+
+ return function(context, options) {
+ options = options || {};
+ var result = templateSpec.call(container, Handlebars, context, options.helpers, options.partials, options.data);
+
+ var compilerInfo = container.compilerInfo || [],
+ compilerRevision = compilerInfo[0] || 1,
+ currentRevision = Handlebars.COMPILER_REVISION;
+
+ if (compilerRevision !== currentRevision) {
+ if (compilerRevision < currentRevision) {
+ var runtimeVersions = Handlebars.REVISION_CHANGES[currentRevision],
+ compilerVersions = Handlebars.REVISION_CHANGES[compilerRevision];
+ throw "Template was precompiled with an older version of Handlebars than the current runtime. "+
+ "Please update your precompiler to a newer version ("+runtimeVersions+") or downgrade your runtime to an older version ("+compilerVersions+").";
+ } else {
+ // Use the embedded version info since the runtime doesn't know about this revision yet
+ throw "Template was precompiled with a newer version of Handlebars than the current runtime. "+
+ "Please update your runtime to a newer version ("+compilerInfo[1]+").";
+ }
+ }
+
+ return result;
+ };
+ },
+
+ programWithDepth: function(fn, data, $depth) {
+ var args = Array.prototype.slice.call(arguments, 2);
+
+ return function(context, options) {
+ options = options || {};
+
+ return fn.apply(this, [context, options.data || data].concat(args));
+ };
+ },
+ program: function(fn, data) {
+ return function(context, options) {
+ options = options || {};
+
+ return fn(context, options.data || data);
+ };
+ },
+ noop: function() { return ""; },
+ invokePartial: function(partial, name, context, helpers, partials, data) {
+ var options = { helpers: helpers, partials: partials, data: data };
+
+ if(partial === undefined) {
+ throw new Handlebars.Exception("The partial " + name + " could not be found");
+ } else if(partial instanceof Function) {
+ return partial(context, options);
+ } else if (!Handlebars.compile) {
+ throw new Handlebars.Exception("The partial " + name + " could not be compiled when running in runtime-only mode");
+ } else {
+ partials[name] = Handlebars.compile(partial, {data: data !== undefined});
+ return partials[name](context, options);
+ }
+ }
+};
+
+Handlebars.template = Handlebars.VM.template;
+;
diff --git a/public/js/libs/new-ember-data.js b/public/js/libs/new-ember-data.js
new file mode 100644
index 0000000..e07c21b
--- /dev/null
+++ b/public/js/libs/new-ember-data.js
@@ -0,0 +1,8431 @@
+// Last commit: 57d6c01 (2013-03-18 11:27:29 -0700)
+
+
+(function() {
+window.DS = Ember.Namespace.create({
+ // this one goes past 11
+ CURRENT_API_REVISION: 12
+});
+
+})();
+
+
+
+(function() {
+var DeferredMixin = Ember.DeferredMixin, // ember-runtime/mixins/deferred
+ Evented = Ember.Evented, // ember-runtime/mixins/evented
+ run = Ember.run, // ember-metal/run-loop
+ get = Ember.get; // ember-metal/accessors
+
+var LoadPromise = Ember.Mixin.create(Evented, DeferredMixin, {
+ init: function() {
+ this._super.apply(this, arguments);
+ this.one('didLoad', function() {
+ run(this, 'resolve', this);
+ });
+
+ if (get(this, 'isLoaded')) {
+ this.trigger('didLoad');
+ }
+ }
+});
+
+DS.LoadPromise = LoadPromise;
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set;
+
+var LoadPromise = DS.LoadPromise; // system/mixins/load_promise
+
+/**
+ A record array is an array that contains records of a certain type. The record
+ array materializes records as needed when they are retrieved for the first
+ time. You should not create record arrays yourself. Instead, an instance of
+ DS.RecordArray or its subclasses will be returned by your application's store
+ in response to queries.
+*/
+
+DS.RecordArray = Ember.ArrayProxy.extend(Ember.Evented, LoadPromise, {
+ /**
+ The model type contained by this record array.
+
+ @type DS.Model
+ */
+ type: null,
+
+ // The array of client ids backing the record array. When a
+ // record is requested from the record array, the record
+ // for the client id at the same index is materialized, if
+ // necessary, by the store.
+ content: null,
+
+ isLoaded: false,
+ isUpdating: false,
+
+ // The store that created this record array.
+ store: null,
+
+ objectAtContent: function(index) {
+ var content = get(this, 'content'),
+ reference = content.objectAt(index),
+ store = get(this, 'store');
+
+ if (reference) {
+ return store.recordForReference(reference);
+ }
+ },
+
+ materializedObjectAt: function(index) {
+ var reference = get(this, 'content').objectAt(index);
+ if (!reference) { return; }
+
+ if (get(this, 'store').recordIsMaterialized(reference)) {
+ return this.objectAt(index);
+ }
+ },
+
+ update: function() {
+ if (get(this, 'isUpdating')) { return; }
+
+ var store = get(this, 'store'),
+ type = get(this, 'type');
+
+ store.fetchAll(type, this);
+ },
+
+ addReference: function(reference) {
+ get(this, 'content').addObject(reference);
+ },
+
+ removeReference: function(reference) {
+ get(this, 'content').removeObject(reference);
+ }
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get;
+
+DS.FilteredRecordArray = DS.RecordArray.extend({
+ filterFunction: null,
+ isLoaded: true,
+
+ replace: function() {
+ var type = get(this, 'type').toString();
+ throw new Error("The result of a client-side filter (on " + type + ") is immutable.");
+ },
+
+ updateFilter: Ember.observer(function() {
+ var store = get(this, 'store');
+ store.updateRecordArrayFilter(this, get(this, 'type'), get(this, 'filterFunction'));
+ }, 'filterFunction')
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set;
+
+DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({
+ query: null,
+
+ replace: function() {
+ var type = get(this, 'type').toString();
+ throw new Error("The result of a server query (on " + type + ") is immutable.");
+ },
+
+ load: function(references) {
+ var store = get(this, 'store'), type = get(this, 'type');
+
+ this.beginPropertyChanges();
+ set(this, 'content', Ember.A(references));
+ set(this, 'isLoaded', true);
+ this.endPropertyChanges();
+
+ var self = this;
+ // TODO: does triggering didLoad event should be the last action of the runLoop?
+ Ember.run.once(function() {
+ self.trigger('didLoad');
+ });
+ }
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set;
+
+/**
+ A ManyArray is a RecordArray that represents the contents of a has-many
+ relationship.
+
+ The ManyArray is instantiated lazily the first time the relationship is
+ requested.
+
+ ### Inverses
+
+ Often, the relationships in Ember Data applications will have
+ an inverse. For example, imagine the following models are
+ defined:
+
+ App.Post = DS.Model.extend({
+ comments: DS.hasMany('App.Comment')
+ });
+
+ App.Comment = DS.Model.extend({
+ post: DS.belongsTo('App.Post')
+ });
+
+ If you created a new instance of `App.Post` and added
+ a `App.Comment` record to its `comments` has-many
+ relationship, you would expect the comment's `post`
+ property to be set to the post that contained
+ the has-many.
+
+ We call the record to which a relationship belongs the
+ relationship's _owner_.
+*/
+DS.ManyArray = DS.RecordArray.extend({
+ init: function() {
+ this._super.apply(this, arguments);
+ this._changesToSync = Ember.OrderedSet.create();
+ },
+
+ /**
+ @private
+
+ The record to which this relationship belongs.
+
+ @property {DS.Model}
+ */
+ owner: null,
+
+ // LOADING STATE
+
+ isLoaded: false,
+
+ loadingRecordsCount: function(count) {
+ this.loadingRecordsCount = count;
+ },
+
+ loadedRecord: function() {
+ this.loadingRecordsCount--;
+ if (this.loadingRecordsCount === 0) {
+ set(this, 'isLoaded', true);
+ this.trigger('didLoad');
+ }
+ },
+
+ fetch: function() {
+ var references = get(this, 'content'),
+ store = get(this, 'store'),
+ type = get(this, 'type'),
+ owner = get(this, 'owner');
+
+ store.fetchUnloadedReferences(type, 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));
+ return get(record, '_reference');
+ }, this);
+
+ this._super(index, removed, added);
+ },
+
+ arrangedContentDidChange: function() {
+ this.fetch();
+ },
+
+ arrayContentWillChange: function(index, removed, added) {
+ var owner = get(this, 'owner'),
+ name = get(this, 'name');
+
+ if (!owner._suspendedRelationships) {
+ // This code is the first half of code that continues inside
+ // of arrayContentDidChange. It gets or creates a change from
+ // the child object, adds the current owner as the old
+ // parent if this is the first time the object was removed
+ // from a ManyArray, and sets `newParent` to null.
+ //
+ // Later, if the object is added to another ManyArray,
+ // the `arrayContentDidChange` will set `newParent` on
+ // the change.
+ for (var i=index; i<index+removed; i++) {
+ var reference = get(this, 'content').objectAt(i);
+
+ var change = DS.RelationshipChange.createChange(owner.get('_reference'), reference, get(this, 'store'), {
+ parentType: owner.constructor,
+ changeType: "remove",
+ kind: "hasMany",
+ key: name
+ });
+
+ this._changesToSync.add(change);
+ }
+ }
+
+ return this._super.apply(this, arguments);
+ },
+
+ arrayContentDidChange: function(index, removed, added) {
+ this._super.apply(this, arguments);
+
+ var owner = get(this, 'owner'),
+ name = get(this, 'name'),
+ store = get(this, 'store');
+
+ if (!owner._suspendedRelationships) {
+ // This code is the second half of code that started in
+ // `arrayContentWillChange`. It gets or creates a change
+ // from the child object, and adds the current owner as
+ // the new parent.
+ for (var i=index; i<index+added; i++) {
+ var reference = get(this, 'content').objectAt(i);
+
+ var change = DS.RelationshipChange.createChange(owner.get('_reference'), reference, store, {
+ parentType: owner.constructor,
+ changeType: "add",
+ kind:"hasMany",
+ key: name
+ });
+ change.hasManyName = name;
+
+ this._changesToSync.add(change);
+ }
+
+ // We wait until the array has finished being
+ // mutated before syncing the OneToManyChanges created
+ // in arrayContentWillChange, so that the array
+ // membership test in the sync() logic operates
+ // on the final results.
+ this._changesToSync.forEach(function(change) {
+ change.sync();
+ });
+ DS.OneToManyChange.ensureSameTransaction(this._changesToSync, store);
+ this._changesToSync.clear();
+ }
+ },
+
+ // Create a child record within the owner
+ createRecord: function(hash, transaction) {
+ var owner = get(this, 'owner'),
+ store = get(owner, 'store'),
+ type = get(this, 'type'),
+ record;
+
+ transaction = transaction || get(owner, 'transaction');
+
+ record = store.createRecord.call(store, type, hash, transaction);
+ this.pushObject(record);
+
+ return record;
+ }
+
+});
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt,
+ removeObject = Ember.EnumerableUtils.removeObject, forEach = Ember.EnumerableUtils.forEach;
+
+/**
+ A transaction allows you to collect multiple records into a unit of work
+ that can be committed or rolled back as a group.
+
+ For example, if a record has local modifications that have not yet
+ been saved, calling `commit()` on its transaction will cause those
+ modifications to be sent to the adapter to be saved. Calling
+ `rollback()` on its transaction would cause all of the modifications to
+ be discarded and the record to return to the last known state before
+ changes were made.
+
+ If a newly created record's transaction is rolled back, it will
+ immediately transition to the deleted state.
+
+ If you do not explicitly create a transaction, a record is assigned to
+ an implicit transaction called the default transaction. In these cases,
+ you can treat your application's instance of `DS.Store` as a transaction
+ and call the `commit()` and `rollback()` methods on the store itself.
+
+ Once a record has been successfully committed or rolled back, it will
+ be moved back to the implicit transaction. Because it will now be in
+ a clean state, it can be moved to a new transaction if you wish.
+
+ ### Creating a Transaction
+
+ To create a new transaction, call the `transaction()` method of your
+ application's `DS.Store` instance:
+
+ var transaction = App.store.transaction();
+
+ This will return a new instance of `DS.Transaction` with no records
+ yet assigned to it.
+
+ ### Adding Existing Records
+
+ Add records to a transaction using the `add()` method:
+
+ record = App.store.find(App.Person, 1);
+ transaction.add(record);
+
+ Note that only records whose `isDirty` flag is `false` may be added
+ to a transaction. Once modifications to a record have been made
+ (its `isDirty` flag is `true`), it is not longer able to be added to
+ a transaction.
+
+ ### Creating New Records
+
+ Because newly created records are dirty from the time they are created,
+ and because dirty records can not be added to a transaction, you must
+ use the `createRecord()` method to assign new records to a transaction.
+
+ For example, instead of this:
+
+ var transaction = store.transaction();
+ var person = App.Person.createRecord({ name: "Steve" });
+
+ // won't work because person is dirty
+ transaction.add(person);
+
+ Call `createRecord()` on the transaction directly:
+
+ var transaction = store.transaction();
+ transaction.createRecord(App.Person, { name: "Steve" });
+
+ ### Asynchronous Commits
+
+ Typically, all of the records in a transaction will be committed
+ together. However, new records that have a dependency on other new
+ records need to wait for their parent record to be saved and assigned an
+ ID. In that case, the child record will continue to live in the
+ transaction until its parent is saved, at which time the transaction will
+ attempt to commit again.
+
+ For this reason, you should not re-use transactions once you have committed
+ them. Always make a new transaction and move the desired records to it before
+ calling commit.
+*/
+
+var arrayDefault = function() { return []; };
+
+DS.Transaction = Ember.Object.extend({
+ /**
+ @private
+
+ Creates the bucket data structure used to segregate records by
+ 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());
+ },
+
+ /**
+ Creates a new record of the given type and assigns it to the transaction
+ on which the method was called.
+
+ This is useful as only clean records can be added to a transaction and
+ new records created using other methods immediately become dirty.
+
+ @param {DS.Model} type the model type to create
+ @param {Object} hash the data hash to assign the new record
+ */
+ createRecord: function(type, hash) {
+ var store = get(this, 'store');
+
+ return store.createRecord(type, hash, this);
+ },
+
+ isEqualOrDefault: function(other) {
+ if (this === other || other === get(this, 'store.defaultTransaction')) {
+ return true;
+ }
+ },
+
+ isDefault: Ember.computed(function() {
+ return this === get(this, 'store.defaultTransaction');
+ }),
+
+ /**
+ Adds an existing record to this transaction. Only records without
+ modficiations (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
+ */
+ 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');
+
+ // Make `add` idempotent
+ if (recordTransaction === this) { return; }
+
+ // XXX it should be possible to move a dirty transaction from the default transaction
+
+ // 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'));
+
+ Ember.assert("Models cannot belong to more than one transaction at a time.", recordTransaction === defaultTransaction);
+
+ this.adoptRecord(record);
+ },
+
+ relationshipBecameDirty: function(relationship) {
+ get(this, 'relationships').add(relationship);
+ },
+
+ relationshipBecameClean: function(relationship) {
+ get(this, 'relationships').remove(relationship);
+ },
+
+ /**
+ Commits the transaction, which causes all of the modified records that
+ belong to the transaction to be sent to the adapter to be saved.
+
+ Once you call `commit()` on a transaction, you should not re-use it.
+
+ When a record is saved, it will be removed from this transaction and
+ moved back to the store's default transaction.
+ */
+ 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) {
+ 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); }
+ }
+
+ // Once we've committed the transaction, there is no need to
+ // keep the OneToManyChanges around. Destroy them so they
+ // can be garbage collected.
+ relationships.forEach(function(relationship) {
+ relationship.destroy();
+ });
+ },
+
+ /**
+ Rolling back a transaction resets the records that belong to
+ that transaction.
+
+ Updated records have their properties reset to the last known
+ value from the persistence layer. Deleted records are reverted
+ to a clean, non-deleted state. Newly created records immediately
+ become deleted, and are not sent to the adapter to be persisted.
+
+ After the transaction is rolled back, any records that belong
+ to it will return to the store's default transaction, and the
+ 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);
+
+ // Now that all records in the transaction are guaranteed to be
+ // clean, migrate them all to the store's default transaction.
+ this.removeCleanRecords();
+ },
+
+ /**
+ @private
+
+ Removes a record from this transaction and back to the store's
+ default transaction.
+
+ Note: This method is private for now, but should probably be exposed
+ in the future once we have stricter error checking (for example, in the
+ case of the record being dirty).
+
+ @param {DS.Model} record
+ */
+ remove: function(record) {
+ var defaultTransaction = get(this, 'store.defaultTransaction');
+ defaultTransaction.adoptRecord(record);
+ },
+
+ /**
+ @private
+
+ 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);
+ },
+
+ /**
+ @private
+
+ This method moves a record into a different transaction without the normal
+ checks that ensure that the user is not doing something weird, like moving
+ a dirty record into a new transaction.
+
+ It is designed for internal use, such as when we are moving a clean record
+ into a new transaction when the transaction is committed.
+
+ This method must not be called unless the record is clean.
+
+ @param {DS.Model} record
+ */
+ adoptRecord: function(record) {
+ var oldTransaction = get(record, 'transaction');
+
+ if (oldTransaction) {
+ oldTransaction.removeFromBucket('clean', record);
+ }
+
+ this.addToBucket('clean', 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.
+
+ @param {String} bucketType one of `created`, `updated`, or `deleted`
+ */
+ 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.
+
+ @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);
+ },
+
+ /**
+ @private
+
+ 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.
+
+ @param {String} bucketType one of `created`, `updated`, or `deleted`
+ */
+ recordBecameClean: function(kind, record) {
+ this.removeFromBucket(kind, record);
+ this.remove(record);
+ }
+});
+
+})();
+
+
+
+(function() {
+var classify = Ember.String.classify, get = Ember.get;
+
+/**
+@private
+
+ The Mappable mixin is designed for classes that would like to
+ behave as a map for configuration purposes.
+
+ For example, the DS.Adapter class can behave like a map, with
+ more semantic API, via the `map` API:
+
+ DS.Adapter.map('App.Person', { firstName: { key: 'FIRST' } });
+
+ Class configuration via a map-like API has a few common requirements
+ that differentiate it from the standard Ember.Map implementation.
+
+ First, values often are provided as strings that should be normalized
+ into classes the first time the configuration options are used.
+
+ Second, the values configured on parent classes should also be taken
+ into account.
+
+ Finally, setting the value of a key sometimes should merge with the
+ previous value, rather than replacing it.
+
+ This mixin provides a instance method, `createInstanceMapFor`, that
+ will reify all of the configuration options set on an instance's
+ constructor and provide it for the instance to use.
+
+ Classes can implement certain hooks that allow them to customize
+ the requirements listed above:
+
+ * `resolveMapConflict` - called when a value is set for an existing
+ value
+ * `transformMapKey` - allows a key name (for example, a global path
+ to a class) to be normalized
+ * `transformMapValue` - allows a value (for example, a class that
+ should be instantiated) to be normalized
+
+ Classes that implement this mixin should also implement a class
+ method built using the `generateMapFunctionFor` method:
+
+ DS.Adapter.reopenClass({
+ map: DS.Mappable.generateMapFunctionFor('attributes', function(key, newValue, map) {
+ var existingValue = map.get(key);
+
+ for (var prop in newValue) {
+ if (!newValue.hasOwnProperty(prop)) { continue; }
+ existingValue[prop] = newValue[prop];
+ }
+ })
+ });
+
+ The function passed to `generateMapFunctionFor` is invoked every time a
+ new value is added to the map.
+**/
+
+var resolveMapConflict = function(oldValue, newValue, mappingsKey) {
+ return oldValue;
+};
+
+var transformMapKey = function(key, value) {
+ return key;
+};
+
+var transformMapValue = function(key, value) {
+ return value;
+};
+
+DS._Mappable = Ember.Mixin.create({
+ createInstanceMapFor: function(mapName) {
+ var instanceMeta = Ember.metaPath(this, ['DS.Mappable'], true);
+
+ instanceMeta.values = instanceMeta.values || {};
+
+ if (instanceMeta.values[mapName]) { return instanceMeta.values[mapName]; }
+
+ var instanceMap = instanceMeta.values[mapName] = new Ember.Map();
+
+ var klass = this.constructor;
+
+ while (klass && klass !== DS.Store) {
+ this._copyMap(mapName, klass, instanceMap);
+ klass = klass.superclass;
+ }
+
+ instanceMeta.values[mapName] = instanceMap;
+ return instanceMap;
+ },
+
+ _copyMap: function(mapName, klass, instanceMap) {
+ var classMeta = Ember.metaPath(klass, ['DS.Mappable'], true);
+
+ var classMap = classMeta[mapName];
+ if (classMap) {
+ classMap.forEach(eachMap, this);
+ }
+
+ function eachMap(key, value) {
+ var transformedKey = (klass.transformMapKey || transformMapKey)(key, value);
+ var transformedValue = (klass.transformMapValue || transformMapValue)(key, value);
+
+ var oldValue = instanceMap.get(transformedKey);
+ var newValue = transformedValue;
+
+ if (oldValue) {
+ newValue = (this.constructor.resolveMapConflict || resolveMapConflict)(oldValue, newValue, mapName);
+ }
+
+ instanceMap.set(transformedKey, newValue);
+ }
+ }
+
+
+});
+
+DS._Mappable.generateMapFunctionFor = function(mapName, transform) {
+ return function(key, value) {
+ var meta = Ember.metaPath(this, ['DS.Mappable'], true);
+ var map = meta[mapName] || Ember.MapWithDefault.create({
+ defaultValue: function() { return {}; }
+ });
+
+ transform.call(this, key, value, map);
+
+ meta[mapName] = map;
+ };
+};
+
+})();
+
+
+
+(function() {
+/*globals Ember*/
+/*jshint eqnull:true*/
+var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt, once = Ember.run.once;
+var forEach = Ember.EnumerableUtils.forEach;
+// These values are used in the data cache when clientIds are
+// needed but the underlying data has not yet been loaded by
+// the server.
+var UNLOADED = 'unloaded';
+var LOADING = 'loading';
+var MATERIALIZED = { materialized: true };
+var CREATED = { created: true };
+
+// Implementors Note:
+//
+// The variables in this file are consistently named according to the following
+// scheme:
+//
+// * +id+ means an identifier managed by an external source, provided inside
+// the data provided by that source.
+// * +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.
+// * +type+ means a subclass of DS.Model.
+
+// Used by the store to normalize IDs entering the store. Despite the fact
+// that developers may provide IDs as numbers (e.g., `store.find(Person, 1)`),
+// it is important that internally we use strings, since IDs may be serialized
+// and lose type information. For example, Ember's router may put a record's
+// ID into the URL, and if we later try to deserialize that URL and find the
+// corresponding record, we will not know if it is a string or a number.
+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
+ the individual data for a record, so that they can be bound to in your
+ Handlebars templates.
+
+ Create a new store like this:
+
+ MyApp.store = DS.Store.create();
+
+ 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 record = MyApp.store.find(MyApp.Contact, 123);
+
+ By default, the store will talk to your backend using a standard REST mechanism.
+ You can customize how the store talks to your backend by specifying a custom adapter:
+
+ MyApp.store = DS.Store.create({
+ adapter: 'MyApp.CustomAdapter'
+ });
+
+ You can learn more about writing a custom adapter by reading the `DS.Adapter`
+ documentation.
+*/
+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.
+
+ 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.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, 'defaultTransaction', this.transaction());
+ },
+
+ /**
+ Returns a new transaction scoped to this store. This delegates
+ responsibility for invoking the adapter's commit mechanism to
+ a transaction.
+
+ Transaction are responsible for tracking changes to records
+ added to them, and supporting `commit` and `rollback`
+ functionality. Committing a transaction invokes the store's
+ adapter, while rolling back a transaction reverses all
+ changes made to records added to the transaction.
+
+ A store has an implicit (default) transaction, which tracks changes
+ made to records not explicitly added to a transaction.
+
+ @see {DS.Transaction}
+ @returns DS.Transaction
+ */
+ transaction: function() {
+ 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
+
+ Instructs the store to materialize the data for a given record.
+
+ To materialize a record, the store first retrieves the opaque data that was
+ passed to either `load()` or `loadMany()`. Then, the data and the record
+ are passed to the adapter's `materialize()` method, which allows the adapter
+ to translate arbitrary data structures from the adapter into the normalized
+ form the record expects.
+
+ The adapter's `materialize()` method will invoke `materializeAttribute()`,
+ `materializeHasMany()` and `materializeBelongsTo()` on the record to
+ populate it with normalized values.
+
+ @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 prematerialized = this.clientIdToPrematerializedData[clientId];
+
+ // 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);
+ }
+ },
+
+ /**
+ @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.
+
+ This can be specified as an instance, a class, or a property path that specifies
+ where the adapter can be located.
+
+ @property {DS.Adapter|String}
+ */
+ adapter: 'DS.RESTAdapter',
+
+ /**
+ @private
+
+ Returns a JSON representation of the record using the adapter's
+ serialization strategy. This method exists primarily to enable
+ a record, which has access to its store (but not the store's
+ adapter) to provide a `serialize()` convenience.
+
+ The available options are:
+
+ * `includeId`: `true` if the record's ID should be included in
+ the JSON representation
+
+ @param {DS.Model} record the record to serialize
+ @param {Object} options an options hash
+ */
+ serialize: function(record, options) {
+ return this.adapterForType(record.constructor).serialize(record, options);
+ },
+
+ /**
+ @private
+
+ This property returns the adapter, after resolving a possible
+ property path.
+
+ If the supplied `adapter` was a class, or a String property
+ path resolved to a class, this property will instantiate the
+ class.
+
+ This property is cacheable, so the same instance of a specified
+ adapter class should be used for the lifetime of the store.
+
+ @returns DS.Adapter
+ */
+ _adapter: Ember.computed(function() {
+ var adapter = get(this, 'adapter');
+ if (typeof adapter === 'string') {
+ adapter = get(this, adapter, false) || get(Ember.lookup, adapter);
+ }
+
+ if (DS.Adapter.detect(adapter)) {
+ adapter = adapter.create();
+ }
+
+ return adapter;
+ }).property('adapter'),
+
+ /**
+ @private
+
+ A monotonically increasing number to be used to uniquely identify
+ data and records.
+
+ It starts at 1 so other parts of the code can test for truthiness
+ when provided a `clientId` instead of having to explicitly test
+ for undefined.
+ */
+ clientIdCounter: 1,
+
+ // .....................
+ // . CREATE NEW RECORD .
+ // .....................
+
+ /**
+ Create a new record in the current store. The properties passed
+ to this method are set on the newly created record.
+
+ Note: The third `transaction` property is for internal use only.
+ If you want to create a record inside of a given transaction,
+ use `transaction.createRecord()` instead of `store.createRecord()`.
+
+ @param {subclass of DS.Model} type
+ @param {Object} properties a hash of properties to set on the
+ newly created record.
+ @returns DS.Model
+ */
+ createRecord: function(type, properties, transaction) {
+ properties = properties || {};
+
+ // Create a new instance of the model `type` and put it
+ // into the specified `transaction`. If no transaction is
+ // specified, the default transaction will be used.
+ var record = type._create({
+ store: this
+ });
+
+ transaction = transaction || get(this, 'defaultTransaction');
+
+ // adoptRecord is an internal API that allows records to move
+ // into a transaction without assertions designed for app
+ // code. It is used here to ensure that regardless of new
+ // restrictions on the use of the public `transaction.add()`
+ // API, we will always be able to insert new records into
+ // their transaction.
+ transaction.adoptRecord(record);
+
+ // `id` is a special property that may not be a `DS.attr`
+ var id = properties.id;
+
+ // If the passed properties do not include a primary key,
+ // give the adapter an opportunity to generate one. Typically,
+ // client-side ID generators will use something like uuid.js
+ // to avoid conflicts.
+ var adapter;
+ if (Ember.isNone(id)) {
+ adapter = this.adapterForType(type);
+ if (adapter && adapter.generateIdForRecord) {
+ id = coerceId(adapter.generateIdForRecord(this, record));
+ properties.id = id;
+ }
+ }
+
+ id = coerceId(id);
+
+ // Create a new `clientId` and associate it with the
+ // specified (or generated) `id`. Since we don't have
+ // any data for the server yet (by definition), store
+ // 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);
+
+ // Now that we have a clientId, attach it to the record we
+ // just created.
+ set(record, 'clientId', clientId);
+
+ // 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);
+
+ // Resolve record promise
+ Ember.run(record, 'resolve', record);
+
+ return record;
+ },
+
+ // .................
+ // . DELETE RECORD .
+ // .................
+
+ /**
+ For symmetry, a record can be deleted via the store.
+
+ @param {DS.Model} record
+ */
+ deleteRecord: function(record) {
+ record.deleteRecord();
+ },
+
+ /**
+ For symmetry, a record can be unloaded via the store.
+
+ @param {DS.Model} record
+ */
+ unloadRecord: function(record) {
+ record.unloadRecord();
+ },
+
+ // ................
+ // . FIND RECORDS .
+ // ................
+
+ /**
+ This is the main entry point into finding records. The first parameter to
+ this method is always a subclass of `DS.Model`.
+
+ You can use the `find` method on a subclass of `DS.Model` directly if your
+ application only has one store. For example, instead of
+ `store.find(App.Person, 1)`, you could say `App.Person.find(1)`.
+
+ ---
+
+ To find a record by ID, pass the `id` as the second parameter:
+
+ store.find(App.Person, 1);
+ App.Person.find(1);
+
+ If the record with that `id` had not previously been loaded, the store will
+ return an empty record immediately and ask the adapter to find the data by
+ calling the adapter's `find` method.
+
+ The `find` method will always return the same object for a given type and
+ `id`. To check whether the adapter has populated a record, you can check
+ its `isLoaded` property.
+
+ ---
+
+ To find all records for a type, call `find` with no additional parameters:
+
+ store.find(App.Person);
+ App.Person.find();
+
+ This will return a `RecordArray` representing all known records for the
+ given type and kick off a request to the adapter's `findAll` method to load
+ any additional records for the type.
+
+ The `RecordArray` returned by `find()` is live. If any more records for the
+ type are added at a later time through any mechanism, it will automatically
+ update to reflect the change.
+
+ ---
+
+ To find a record by a query, call `find` with a hash as the second
+ parameter:
+
+ store.find(App.Person, { page: 1 });
+ App.Person.find({ page: 1 });
+
+ This will return a `RecordArray` immediately, but it will always be an
+ empty `RecordArray` at first. It will call the adapter's `findQuery`
+ method, which will populate the `RecordArray` once the server has returned
+ results.
+
+ You can check whether a query results `RecordArray` has loaded by checking
+ its `isLoaded` property.
+ */
+ find: function(type, id) {
+ if (id === undefined) {
+ return this.findAll(type);
+ }
+
+ // We are passed a query instead of an id.
+ if (Ember.typeOf(id) === 'object') {
+ return this.findQuery(type, id);
+ }
+
+ return this.findById(type, coerceId(id));
+ },
+
+ /**
+ @private
+
+ This method returns a record for a given type and id combination.
+
+ If the store has never seen this combination of type and id before, it
+ creates a new `clientId` with the LOADING sentinel and asks the adapter to
+ load the data.
+
+ If the store has seen the combination, this method delegates to
+ `getByReference`.
+ */
+ findById: function(type, id) {
+ var clientId = this.typeMapFor(type).idToCid[id];
+
+ if (clientId) {
+ return this.findByClientId(type, clientId);
+ }
+
+ clientId = this.pushData(LOADING, id, type);
+
+ // create a new instance of the model type in the
+ // 'isLoading' state
+ var record = this.materializeRecord(type, clientId, id);
+
+ // 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"; }
+
+ return record;
+ },
+
+ reloadRecord: function(record) {
+ var type = record.constructor,
+ adapter = this.adapterForType(type),
+ 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);
+ },
+
+ /**
+ @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.
+
+ 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
+
+ In short, it's a convenient way to get a record for a known
+ clientId, materializing it if necessary.
+
+ @param {Class} type
+ @param {Number|String} clientId
+ */
+ findByClientId: function(type, clientId) {
+ var cidToData, record, id;
+
+ record = this.recordCache[clientId];
+
+ 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();
+ }
+ }
+
+ return record;
+ },
+
+ /**
+ @private
+
+ Given a type and array of `clientId`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;
+
+ for (var i=0, l=references.length; i<l; i++) {
+ reference = references[i];
+
+ if (cidToData[reference.clientId] === UNLOADED) {
+ neededReferences.push(reference);
+ cidToData[reference.clientId] = LOADING;
+ }
+ }
+
+ return neededReferences;
+ },
+
+ /**
+ @private
+
+ This method is the entry point that relationships use to update
+ themselves when their underlying data changes.
+
+ First, it determines which of its `clientId`s are still unloaded,
+ then converts the needed `clientId`s to IDs and invokes `findMany`
+ on the adapter.
+ */
+ fetchUnloadedReferences: function(type, references, owner) {
+ var neededReferences = this.neededReferences(type, references);
+ this.fetchMany(type, neededReferences, owner);
+ },
+
+ /**
+ @private
+
+ This method takes a type and list of `clientId`s, converts the
+ `clientId`s into IDs, and then invokes the adapter's `findMany`
+ method.
+
+ It is used both by a brand new relationship (via the `findMany`
+ method) or when the data underlying an existing relationship
+ changes (via the `fetchUnloadedReferences` method).
+ */
+ fetchMany: function(type, references, owner) {
+ if (!references.length) { return; }
+
+ var ids = map(references, function(reference) {
+ return reference.id;
+ });
+
+ var adapter = this.adapterForType(type);
+ if (adapter && adapter.findMany) { adapter.findMany(this, type, ids, owner); }
+ else { throw "Adapter is either null or does not implement `findMany` method"; }
+ },
+
+ referenceForId: function(type, id) {
+ var clientId = this.clientIdForId(type, id);
+ return this.referenceForClientId(clientId);
+ },
+
+ referenceForClientId: function(clientId) {
+ var references = this.recordReferences;
+
+ if (references[clientId]) {
+ return references[clientId];
+ }
+
+ var type = this.clientIdToType[clientId];
+
+ return references[clientId] = {
+ id: this.idForClientId(clientId),
+ clientId: clientId,
+ type: type
+ };
+ },
+
+ recordForReference: function(reference) {
+ return this.findByClientId(reference.type, reference.clientId);
+ },
+
+ /**
+ @private
+
+ `findMany` is the entry point that relationships use to generate a
+ new `ManyArray` for the list of IDs specified by the server for
+ the relationship.
+
+ Its responsibilities are:
+
+ * convert the IDs into clientIds
+ * determine which of the clientIds still need to be loaded
+ * create a new ManyArray whose content is *all* of the clientIds
+ * notify the ManyArray of the number of its elements that are
+ already loaded
+ * insert the unloaded clientIds into the `loadingRecordArrays`
+ bookkeeping structure, which will allow the `ManyArray` to know
+ when all of its loading elements are loaded from the server.
+ * ask the adapter to load the unloaded elements, by invoking
+ findMany with the still-unloaded IDs.
+ */
+ findMany: function(type, ids, record, relationship) {
+ // 1. Convert ids to client ids
+ // 2. Determine which of the client ids need to be loaded
+ // 3. Create a new ManyArray whose content is ALL of the clientIds
+ // 4. Decrement the ManyArray's counter by the number of loaded clientIds
+ // 5. Put the ManyArray into our bookkeeping data structure, keyed on
+ // the needed clientIds
+ // 6. Ask the adapter to load the records for the unloaded clientIds (but
+ // convert them back to ids)
+
+ if (!Ember.isArray(ids)) {
+ var adapter = this.adapterForType(type);
+ if (adapter && adapter.findHasMany) { adapter.findHasMany(this, record, relationship, ids); }
+ else if (ids !== undefined) { throw fmt("Adapter is either null or does not implement `findHasMany` method", this); }
+
+ return this.createManyArray(type, Ember.A());
+ }
+
+ // Coerce server IDs into Record Reference
+ var references = map(ids, function(reference) {
+ if (typeof reference !== 'object' && reference !== null) {
+ return this.referenceForId(type, reference);
+ }
+
+ return reference;
+ }, this);
+
+ var neededReferences = this.neededReferences(type, references),
+ manyArray = this.createManyArray(type, Ember.A(references)),
+ loadingRecordArrays = this.loadingRecordArrays,
+ reference, clientId, i, l;
+
+ // Start the decrementing counter on the ManyArray at the number of
+ // records we need to load from the adapter
+ manyArray.loadingRecordsCount(neededReferences.length);
+
+ if (neededReferences.length) {
+ for (i=0, l=neededReferences.length; i<l; i++) {
+ reference = neededReferences[i];
+ clientId = reference.clientId;
+
+ // keep track of the record arrays that a given loading record
+ // is part of. This way, if the same record is in multiple
+ // ManyArrays, all of their loading records counters will be
+ // decremented when the adapter provides the data.
+ if (loadingRecordArrays[clientId]) {
+ loadingRecordArrays[clientId].push(manyArray);
+ } else {
+ this.loadingRecordArrays[clientId] = [ manyArray ];
+ }
+ }
+
+ this.fetchMany(type, neededReferences, record);
+ } else {
+ // all requested records are available
+ manyArray.set('isLoaded', true);
+
+ Ember.run.once(function() {
+ manyArray.trigger('didLoad');
+ });
+ }
+
+ return manyArray;
+ },
+
+ /**
+ @private
+
+ This method delegates a query to the adapter. This is the one place where
+ adapter-level semantics are exposed to the application.
+
+ Exposing queries this way seems preferable to creating an abstract query
+ language for all server-side queries, and then require all adapters to
+ implement them.
+
+ @param {Class} type
+ @param {Object} query an opaque query to be used by the adapter
+ @return {DS.AdapterPopulatedRecordArray}
+ */
+ findQuery: function(type, query) {
+ var array = DS.AdapterPopulatedRecordArray.create({ type: type, query: query, content: Ember.A([]), store: this });
+ var adapter = this.adapterForType(type);
+ if (adapter && adapter.findQuery) { adapter.findQuery(this, type, query, array); }
+ else { throw "Adapter is either null or does not implement `findQuery` method"; }
+ return array;
+ },
+
+ /**
+ @private
+
+ This method returns an array of all records adapter can find.
+ It triggers the adapter's `findAll` method to give it an opportunity to populate
+ the array with records of that type.
+
+ @param {Class} type
+ @return {DS.AdapterPopulatedRecordArray}
+ */
+ findAll: function(type) {
+ var array = this.all(type);
+ this.fetchAll(type, array);
+ return array;
+ },
+
+ /**
+ @private
+ */
+ fetchAll: function(type, array) {
+ var sinceToken = this.typeMapFor(type).sinceToken,
+ adapter = this.adapterForType(type);
+
+ set(array, 'isUpdating', true);
+
+ if (adapter && adapter.findAll) { adapter.findAll(this, type, sinceToken); }
+ else { throw "Adapter is either null or does not implement `findAll` method"; }
+ },
+
+ /**
+ */
+ sinceForType: function(type, sinceToken) {
+ this.typeMapFor(type).sinceToken = sinceToken;
+ },
+
+ /**
+ */
+ didUpdateAll: function(type) {
+ var findAllCache = this.typeMapFor(type).findAllCache;
+ set(findAllCache, 'isUpdating', false);
+ },
+
+ /**
+ This method returns a filtered array that contains all of the known records
+ for a given type.
+
+ Note that because it's just a filter, it will have any locally
+ created records of the type.
+
+ Also note that multiple calls to `all` for a given type will always
+ return the same RecordArray.
+
+ @param {Class} type
+ @return {DS.RecordArray}
+ */
+ all: function(type) {
+ var typeMap = this.typeMapFor(type),
+ findAllCache = typeMap.findAllCache;
+
+ if (findAllCache) { return findAllCache; }
+
+ var array = DS.RecordArray.create({ type: type, content: Ember.A([]), store: this, isLoaded: true });
+ this.registerRecordArray(array, type);
+
+ typeMap.findAllCache = array;
+ return array;
+ },
+
+ /**
+ Takes a type and filter function, and returns a live RecordArray that
+ remains up to date as new records are loaded into the store or created
+ locally.
+
+ The callback function takes a materialized record, and returns true
+ if the record should be included in the filter and false if it should
+ not.
+
+ The filter function is called once on all records for the type when
+ it is created, and then once on each newly loaded or created record.
+
+ If any of a record's properties change, or if it changes state, the
+ filter function will be invoked again to determine whether it should
+ still be in the array.
+
+ Note that the existence of a filter on a type will trigger immediate
+ materialization of all loaded data for a given type, so you might
+ not want to use filters for a type if you are loading many records
+ into the store, many of which are not active at any given time.
+
+ In this scenario, you might want to consider filtering the raw
+ data before loading it into the store.
+
+ @param {Class} type
+ @param {Function} filter
+
+ @return {DS.FilteredRecordArray}
+ */
+ filter: function(type, query, filter) {
+ // allow an optional server query
+ if (arguments.length === 3) {
+ this.findQuery(type, query);
+ } else if (arguments.length === 2) {
+ filter = query;
+ }
+
+ var array = DS.FilteredRecordArray.create({ type: type, content: Ember.A([]), store: this, filterFunction: filter });
+
+ this.registerRecordArray(array, type, filter);
+
+ return array;
+ },
+
+ /**
+ This method returns if a certain record is already loaded
+ in the store. Use this function to know beforehand if a find()
+ will result in a request or that it will be a cache hit.
+
+ @param {Class} type
+ @param {string} id
+ @return {boolean}
+ */
+ recordIsLoaded: function(type, id) {
+ return !Ember.isNone(this.typeMapFor(type).idToCid[id]);
+ },
+
+ // ............
+ // . UPDATING .
+ // ............
+
+ /**
+ @private
+
+ If the adapter updates attributes or acknowledges creation
+ or deletion, the record will notify the store to update its
+ membership in any filters.
+
+ To avoid thrashing, this method is invoked only once per
+ run loop per record.
+
+ @param {Class} type
+ @param {Number|String} clientId
+ @param {DS.Model} record
+ */
+ dataWasUpdated: function(type, reference, record) {
+ // Because data updates are invoked at the end of the run loop,
+ // it is possible that a record might be deleted after its data
+ // has been modified and this method was scheduled to be called.
+ //
+ // If that's the case, the record would have already been removed
+ // from all record arrays; calling updateRecordArrays would just
+ // add it back. If the record is deleted, just bail. It shouldn't
+ // give us any more trouble after this.
+
+ if (get(record, 'isDeleted')) { return; }
+
+ var cidToData = this.clientIdToData,
+ clientId = reference.clientId,
+ data = cidToData[clientId];
+
+ if (typeof data === "object") {
+ this.updateRecordArrays(type, clientId);
+ }
+ },
+
+ // .................
+ // . BASIC ADAPTER .
+ // .................
+
+ scheduleSave: function(record) {
+ this._recordsToSave.add(record);
+ Ember.run.once(this, 'flushSavedRecords');
+ },
+
+ flushSavedRecords: function() {
+ var created = Ember.OrderedSet.create();
+ var updated = Ember.OrderedSet.create();
+ var deleted = Ember.OrderedSet.create();
+
+ this._recordsToSave.forEach(function(record) {
+ if (get(record, 'isNew')) {
+ created.add(record);
+ } else if (get(record, 'isDeleted')) {
+ deleted.add(record);
+ } else {
+ updated.add(record);
+ }
+ });
+
+ this._recordsToSave.clear();
+
+ get(this, '_adapter').commit(this, {
+ created: created,
+ updated: updated,
+ deleted: deleted
+ });
+ },
+
+ // ..............
+ // . PERSISTING .
+ // ..............
+
+ /**
+ This method delegates committing to the store's implicit
+ transaction.
+
+ Calling this method is essentially a request to persist
+ any changes to records that were not explicitly added to
+ a transaction.
+ */
+ commit: function() {
+ get(this, 'defaultTransaction').commit();
+ },
+
+ /**
+ Adapters should call this method if they would like to acknowledge
+ that all changes related to a record (other than relationship
+ changes) have persisted.
+
+ Because relationship changes affect multiple records, the adapter
+ is responsible for acknowledging the change to the relationship
+ directly (using `store.didUpdateRelationship`) when all aspects
+ of the relationship change have persisted.
+
+ It can be called for created, deleted or updated records.
+
+ If the adapter supplies new data, that data will become the new
+ canonical data for the record. That will result in blowing away
+ all local changes and rematerializing the record with the new
+ data (the "sledgehammer" approach).
+
+ Alternatively, if the adapter does not supply new data, the record
+ will collapse all local changes into its saved data. Subsequent
+ rollbacks of the record will roll back to this point.
+
+ If an adapter is acknowledging receipt of a newly created record
+ that did not generate an id in the client, it *must* either
+ provide data or explicitly invoke `store.didReceiveId` with
+ the server-provided id.
+
+ Note that an adapter may not supply new data when acknowledging
+ a deleted record.
+
+ @see DS.Store#didUpdateRelationship
+
+ @param {DS.Model} record the in-flight record
+ @param {Object} data optional data (see above)
+ */
+ didSaveRecord: function(record, data) {
+ record.adapterDidCommit();
+
+ if (data) {
+ this.updateId(record, data);
+ this.updateRecordData(record, data);
+ } else {
+ this.didUpdateAttributes(record);
+ }
+ },
+
+ /**
+ For convenience, if an adapter is performing a bulk commit, it can also
+ acknowledge all of the records at once.
+
+ If the adapter supplies an array of data, they must be in the same order as
+ the array of records passed in as the first parameter.
+
+ @param {#forEach} list a list of records whose changes the
+ adapter is acknowledging. You can pass any object that
+ has an ES5-like `forEach` method, including the
+ `OrderedSet` objects passed into the adapter at commit
+ time.
+ @param {Array[Object]} dataList an Array of data. This
+ parameter must be an integer-indexed Array-like.
+ */
+ didSaveRecords: function(list, dataList) {
+ var i = 0;
+ list.forEach(function(record) {
+ this.didSaveRecord(record, dataList && dataList[i++]);
+ }, this);
+ },
+
+ /**
+ This method allows the adapter to specify that a record
+ could not be saved because it had backend-supplied validation
+ errors.
+
+ The errors object must have keys that correspond to the
+ attribute names. Once each of the specified attributes have
+ changed, the record will automatically move out of the
+ invalid state and be ready to commit again.
+
+ TODO: We should probably automate the process of converting
+ server names to attribute names using the existing serializer
+ infrastructure.
+
+ @param {DS.Model} record
+ @param {Object} errors
+ */
+ recordWasInvalid: function(record, errors) {
+ record.adapterDidInvalidate(errors);
+ },
+
+ /**
+ This method allows the adapter to specify that a record
+ could not be saved because the server returned an unhandled
+ error.
+
+ @param {DS.Model} record
+ */
+ recordWasError: function(record) {
+ record.adapterDidError();
+ },
+
+ /**
+ This is a lower-level API than `didSaveRecord` that allows an
+ adapter to acknowledge the persistence of a single attribute.
+
+ This is useful if an adapter needs to make multiple asynchronous
+ calls to fully persist a record. The record will keep track of
+ which attributes and relationships are still outstanding and
+ automatically move into the `saved` state once the adapter has
+ acknowledged everything.
+
+ If a value is provided, it clobbers the locally specified value.
+ Otherwise, the local value becomes the record's last known
+ saved value (which is used when rolling back a record).
+
+ Note that the specified attributeName is the normalized name
+ specified in the definition of the `DS.Model`, not a key in
+ the server-provided data.
+
+ Also note that the adapter is responsible for performing any
+ transformations on the value using the serializer API.
+
+ @param {DS.Model} record
+ @param {String} attributeName
+ @param {Object} value
+ */
+ didUpdateAttribute: function(record, attributeName, value) {
+ record.adapterDidUpdateAttribute(attributeName, value);
+ },
+
+ /**
+ This method allows an adapter to acknowledge persistence
+ of all attributes of a record but not relationships or
+ other factors.
+
+ It loops through the record's defined attributes and
+ notifies the record that they are all acknowledged.
+
+ This method does not take optional values, because
+ the adapter is unlikely to have a hash of normalized
+ keys and transformed values, and instead of building
+ one up, it should just call `didUpdateAttribute` as
+ needed.
+
+ This method is intended as a middle-ground between
+ `didSaveRecord`, which acknowledges all changes to
+ a record, and `didUpdateAttribute`, which allows an
+ adapter fine-grained control over updates.
+
+ @param {DS.Model} record
+ */
+ didUpdateAttributes: function(record) {
+ record.eachAttribute(function(attributeName) {
+ this.didUpdateAttribute(record, attributeName);
+ }, this);
+ },
+
+ /**
+ This allows an adapter to acknowledge that it has saved all
+ necessary aspects of a relationship change.
+
+ This is separated from acknowledging the record itself
+ (via `didSaveRecord`) because a relationship change can
+ involve as many as three separate records. Records should
+ only move out of the in-flight state once the server has
+ acknowledged all of their relationships, and this differs
+ based upon the adapter's semantics.
+
+ There are three basic scenarios by which an adapter can
+ save a relationship.
+
+ ### Foreign Key
+
+ An adapter can save all relationship changes by updating
+ a foreign key on the child record. If it does this, it
+ should acknowledge the changes when the child record is
+ saved.
+
+ record.eachRelationship(function(name, meta) {
+ if (meta.kind === 'belongsTo') {
+ store.didUpdateRelationship(record, name);
+ }
+ });
+
+ store.didSaveRecord(record, data);
+
+ ### Embedded in Parent
+
+ An adapter can save one-to-many relationships by embedding
+ IDs (or records) in the parent object. In this case, the
+ relationship is not considered acknowledged until both the
+ old parent and new parent have acknowledged the change.
+
+ In this case, the adapter should keep track of the old
+ parent and new parent, and acknowledge the relationship
+ change once both have acknowledged. If one of the two
+ sides does not exist (e.g. the new parent does not exist
+ because of nulling out the belongs-to relationship),
+ the adapter should acknowledge the relationship once
+ the other side has acknowledged.
+
+ ### Separate Entity
+
+ An adapter can save relationships as separate entities
+ on the server. In this case, they should acknowledge
+ the relationship as saved once the server has
+ acknowledged the entity.
+
+ @see DS.Store#didSaveRecord
+
+ @param {DS.Model} record
+ @param {DS.Model} relationshipName
+ */
+ didUpdateRelationship: function(record, relationshipName) {
+ var relationship = this.relationshipChangeFor(get(record, 'clientId'), relationshipName);
+ //TODO(Igor)
+ if (relationship) { relationship.adapterDidUpdate(); }
+ },
+
+ /**
+ This allows an adapter to acknowledge all relationship changes
+ for a given record.
+
+ Like `didUpdateAttributes`, this is intended as a middle ground
+ between `didSaveRecord` and fine-grained control via the
+ `didUpdateRelationship` API.
+ */
+ didUpdateRelationships: function(record) {
+ var changes = this.relationshipChangesFor(get(record, '_reference'));
+
+ for (var name in changes) {
+ if (!changes.hasOwnProperty(name)) { continue; }
+ changes[name].adapterDidUpdate();
+ }
+ },
+
+ /**
+ When acknowledging the creation of a locally created record,
+ adapters must supply an id (if they did not implement
+ `generateIdForRecord` to generate an id locally).
+
+ If an adapter does not use `didSaveRecord` and supply a hash
+ (for example, if it needs to make multiple HTTP requests to
+ create and then update the record), it will need to invoke
+ `didReceiveId` with the backend-supplied id.
+
+ When not using `didSaveRecord`, an adapter will need to
+ invoke:
+
+ * didReceiveId (unless the id was generated locally)
+ * didCreateRecord
+ * didUpdateAttribute(s)
+ * didUpdateRelationship(s)
+
+ @param {DS.Model} record
+ @param {Number|String} id
+ */
+ didReceiveId: function(record, id) {
+ var typeMap = this.typeMapFor(record.constructor),
+ clientId = get(record, 'clientId'),
+ oldId = get(record, 'id');
+
+ Ember.assert("An adapter cannot assign a new id to a record that already has an id. " + record + " had id: " + oldId + " and you tried to update it with " + id + ". This likely happened because your server returned data in response to a find or update that had a different id than the one you sent.", oldId === undefined || id === oldId);
+
+ typeMap.idToCid[id] = clientId;
+ this.clientIdToId[clientId] = id;
+ },
+
+ /**
+ @private
+
+ This method re-indexes the data by its clientId in the store
+ and then notifies the record that it should rematerialize
+ itself.
+
+ @param {DS.Model} record
+ @param {Object} data
+ */
+ updateRecordData: function(record, data) {
+ var clientId = get(record, 'clientId'),
+ cidToData = this.clientIdToData;
+
+ cidToData[clientId] = data;
+
+ record.didChangeData();
+ },
+
+ /**
+ @private
+
+ If an adapter invokes `didSaveRecord` with data, this method
+ extracts the id from the supplied data (using the adapter's
+ `extractId()` method) and indexes the clientId with that id.
+
+ @param {DS.Model} record
+ @param {Object} data
+ */
+ updateId: function(record, data) {
+ var typeMap = this.typeMapFor(record.constructor),
+ clientId = get(record, 'clientId'),
+ oldId = get(record, 'id'),
+ type = record.constructor,
+ id = this.preprocessData(type, data);
+
+ Ember.assert("An adapter cannot assign a new id to a record that already has an id. " + record + " had id: " + oldId + " and you tried to update it with " + id + ". This likely happened because your server returned data in response to a find or update that had a different id than the one you sent.", oldId === null || id === oldId);
+
+ typeMap.idToCid[id] = clientId;
+ this.clientIdToId[clientId] = id;
+ this.referenceForClientId(clientId).id = id;
+ },
+
+ /**
+ @private
+
+ This method receives opaque data provided by the adapter and
+ preprocesses it, returning an ID.
+
+ The actual preprocessing takes place in the adapter. If you would
+ like to change the default behavior, you should override the
+ appropriate hooks in `DS.Serializer`.
+
+ @see {DS.Serializer}
+ @return {String} id the id represented by the data
+ */
+ preprocessData: function(type, data) {
+ return this.adapterForType(type).extractId(type, data);
+ },
+
+ // .................
+ // . RECORD ARRAYS .
+ // .................
+
+ /**
+ @private
+
+ Register a RecordArray for a given type to be backed by
+ a filter function. This will cause the array to update
+ automatically when records of that type change attribute
+ values or states.
+
+ @param {DS.RecordArray} array
+ @param {Class} type
+ @param {Function} filter
+ */
+ registerRecordArray: function(array, type, filter) {
+ var recordArrays = this.typeMapFor(type).recordArrays;
+
+ recordArrays.push(array);
+
+ this.updateRecordArrayFilter(array, type, filter);
+ },
+
+ /**
+ @private
+
+ Create a `DS.ManyArray` for a type and list of clientIds
+ and index the `ManyArray` under each clientId. This allows
+ us to efficiently remove records from `ManyArray`s when
+ they are deleted.
+
+ @param {Class} type
+ @param {Array} clientIds
+
+ @return {DS.ManyArray}
+ */
+ createManyArray: function(type, clientIds) {
+ var array = DS.ManyArray.create({ type: type, content: clientIds, store: this });
+
+ clientIds.forEach(function(clientId) {
+ var recordArrays = this.recordArraysForClientId(clientId);
+ recordArrays.add(array);
+ }, this);
+
+ return array;
+ },
+
+ /**
+ @private
+
+ This method is invoked if the `filterFunction` property is
+ changed on a `DS.FilteredRecordArray`.
+
+ It essentially re-runs the filter from scratch. This same
+ method is invoked when the filter is created in th first place.
+ */
+ updateRecordArrayFilter: function(array, type, filter) {
+ var typeMap = this.typeMapFor(type),
+ cidToData = this.clientIdToData,
+ clientIds = typeMap.clientIds,
+ clientId, data, shouldFilter, record;
+
+ for (var i=0, l=clientIds.length; i<l; i++) {
+ clientId = clientIds[i];
+ shouldFilter = false;
+
+ data = cidToData[clientId];
+
+ if (typeof data === 'object') {
+ if (record = this.recordCache[clientId]) {
+ if (!get(record, 'isDeleted')) { shouldFilter = true; }
+ } else {
+ shouldFilter = true;
+ }
+
+ if (shouldFilter) {
+ this.updateRecordArray(array, filter, type, clientId);
+ }
+ }
+ }
+ },
+
+ updateRecordArraysLater: function(type, clientId) {
+ Ember.run.once(this, function() {
+ this.updateRecordArrays(type, clientId);
+ });
+ },
+
+ /**
+ @private
+
+ This method is invoked whenever data is loaded into the store
+ by the adapter or updated by the adapter, or when an attribute
+ changes on a record.
+
+ It updates all filters that a record belongs to.
+
+ To avoid thrashing, it only runs once per run loop per record.
+
+ @param {Class} type
+ @param {Number|String} clientId
+ */
+ updateRecordArrays: function(type, clientId) {
+ var recordArrays = this.typeMapFor(type).recordArrays,
+ filter;
+
+ recordArrays.forEach(function(array) {
+ filter = get(array, 'filterFunction');
+ this.updateRecordArray(array, filter, type, clientId);
+ }, this);
+
+ // loop through all manyArrays containing an unloaded copy of this
+ // clientId and notify them that the record was loaded.
+ var manyArrays = this.loadingRecordArrays[clientId];
+
+ if (manyArrays) {
+ for (var i=0, l=manyArrays.length; i<l; i++) {
+ manyArrays[i].loadedRecord();
+ }
+
+ this.loadingRecordArrays[clientId] = null;
+ }
+ },
+
+ /**
+ @private
+
+ Update an individual filter.
+
+ @param {DS.FilteredRecordArray} array
+ @param {Function} filter
+ @param {Class} type
+ @param {Number|String} clientId
+ */
+ updateRecordArray: function(array, filter, type, clientId) {
+ var shouldBeInArray, record;
+
+ if (!filter) {
+ shouldBeInArray = true;
+ } else {
+ record = this.findByClientId(type, clientId);
+ shouldBeInArray = filter(record);
+ }
+
+ var content = get(array, 'content');
+
+ var recordArrays = this.recordArraysForClientId(clientId);
+ var reference = this.referenceForClientId(clientId);
+
+ if (shouldBeInArray) {
+ recordArrays.add(array);
+ array.addReference(reference);
+ } else if (!shouldBeInArray) {
+ recordArrays.remove(array);
+ array.removeReference(reference);
+ }
+ },
+
+ /**
+ @private
+
+ When a record is deleted, it is removed from all its
+ record arrays.
+
+ @param {DS.Model} record
+ */
+ removeFromRecordArrays: function(record) {
+ var reference = get(record, '_reference');
+ var recordArrays = this.recordArraysForClientId(reference.clientId);
+
+ recordArrays.forEach(function(array) {
+ array.removeReference(reference);
+ });
+ },
+
+ // ............
+ // . INDEXING .
+ // ............
+
+ /**
+ @private
+
+ Return a list of all `DS.RecordArray`s a clientId is
+ part of.
+
+ @return {Object(clientId: Ember.OrderedSet)}
+ */
+ recordArraysForClientId: function(clientId) {
+ var recordArrays = get(this, 'recordArraysByClientId');
+ var ret = recordArrays[clientId];
+
+ if (!ret) {
+ ret = recordArrays[clientId] = Ember.OrderedSet.create();
+ }
+
+ return ret;
+ },
+
+ typeMapFor: function(type) {
+ var typeMaps = get(this, 'typeMaps');
+ var guidForType = Ember.guidFor(type);
+
+ var typeMap = typeMaps[guidForType];
+
+ if (typeMap) {
+ return typeMap;
+ } else {
+ return (typeMaps[guidForType] =
+ {
+ idToCid: {},
+ clientIds: [],
+ recordArrays: []
+ });
+ }
+ },
+
+ /** @private
+
+ For a given type and id combination, returns the client id used by the store.
+ If no client id has been assigned yet, one will be created and returned.
+
+ @param {DS.Model} type
+ @param {String|Number} id
+ */
+ clientIdForId: function(type, id) {
+ id = coerceId(id);
+
+ var clientId = this.typeMapFor(type).idToCid[id];
+ if (clientId !== undefined) { return clientId; }
+
+ return this.pushData(UNLOADED, id, type);
+ },
+
+ /**
+ @private
+
+ This method works exactly like `clientIdForId`, but does not
+ require looking up the `typeMap` for every `clientId` and
+ invoking a method per `clientId`.
+ */
+ clientIdsForIds: function(type, ids) {
+ var typeMap = this.typeMapFor(type),
+ idToClientIdMap = typeMap.idToCid;
+
+ return map(ids, function(id) {
+ id = coerceId(id);
+
+ var clientId = idToClientIdMap[id];
+ if (clientId) { return clientId; }
+ return this.pushData(UNLOADED, id, type);
+ }, this);
+ },
+
+ typeForClientId: function(clientId) {
+ return this.clientIdToType[clientId];
+ },
+
+ idForClientId: function(clientId) {
+ return this.clientIdToId[clientId];
+ },
+
+ // ................
+ // . LOADING DATA .
+ // ................
+
+ /**
+ Load new data into the store for a given id and type combination.
+ If data for that record had been loaded previously, the new information
+ overwrites the old.
+
+ If the record you are loading data for has outstanding changes that have not
+ yet been saved, an exception will be thrown.
+
+ @param {DS.Model} type
+ @param {String|Number} id
+ @param {Object} data the data to load
+ */
+ load: function(type, data, prematerialized) {
+ var id;
+
+ if (typeof data === 'number' || typeof data === 'string') {
+ id = data;
+ data = prematerialized;
+ prematerialized = null;
+ }
+
+ if (prematerialized && prematerialized.id) {
+ id = prematerialized.id;
+ } else if (id === undefined) {
+ id = this.preprocessData(type, data);
+ }
+
+ id = coerceId(id);
+
+ var typeMap = this.typeMapFor(type),
+ cidToData = this.clientIdToData,
+ clientId = typeMap.idToCid[id],
+ cidToPrematerialized = this.clientIdToPrematerializedData;
+
+ if (clientId !== undefined) {
+ cidToData[clientId] = data;
+ cidToPrematerialized[clientId] = prematerialized;
+
+ var record = this.recordCache[clientId];
+ if (record) {
+ once(record, 'loadedData');
+ }
+ } else {
+ clientId = this.pushData(data, id, type);
+ cidToPrematerialized[clientId] = prematerialized;
+ }
+
+ this.updateRecordArraysLater(type, clientId);
+
+ return this.referenceForClientId(clientId);
+ },
+
+ prematerialize: function(reference, prematerialized) {
+ this.clientIdToPrematerializedData[reference.clientId] = prematerialized;
+ },
+
+ loadMany: function(type, ids, dataList) {
+ if (dataList === undefined) {
+ dataList = ids;
+ ids = map(dataList, function(data) {
+ return this.preprocessData(type, data);
+ }, this);
+ }
+
+ return map(ids, function(id, i) {
+ return this.load(type, id, dataList[i]);
+ }, this);
+ },
+
+ loadHasMany: function(record, key, ids) {
+ record.materializeHasMany(key, ids);
+
+ // Update any existing many arrays that use the previous IDs,
+ // if necessary.
+ record.hasManyDidChange(key);
+
+ var relationship = record.cacheFor(key);
+
+ // TODO (tomdale) this assumes that loadHasMany *always* means
+ // that the records for the provided IDs are loaded.
+ if (relationship) {
+ set(relationship, 'isLoaded', true);
+ relationship.trigger('didLoad');
+ }
+ },
+
+ /** @private
+
+ Stores data for the specified type and id combination and returns
+ the client id.
+
+ @param {Object} data
+ @param {String|Number} id
+ @param {DS.Model} type
+ @returns {Number}
+ */
+ pushData: function(data, id, type) {
+ var typeMap = this.typeMapFor(type);
+
+ var idToClientIdMap = typeMap.idToCid,
+ clientIdToIdMap = this.clientIdToId,
+ clientIdToTypeMap = this.clientIdToType,
+ clientIds = typeMap.clientIds,
+ cidToData = this.clientIdToData;
+
+ Ember.assert('The id ' + id + ' has already been used with another record of type ' + type.toString() + '.', !id || !idToClientIdMap[id]);
+
+ var clientId = ++this.clientIdCounter;
+
+ cidToData[clientId] = data;
+ clientIdToTypeMap[clientId] = type;
+
+ // if we're creating an item, this process will be done
+ // later, once the object has been persisted.
+ if (id) {
+ idToClientIdMap[id] = clientId;
+ clientIdToIdMap[clientId] = id;
+ }
+
+ clientIds.push(clientId);
+
+ return clientId;
+ },
+
+ // ..........................
+ // . RECORD MATERIALIZATION .
+ // ..........................
+
+ materializeRecord: function(type, clientId, id) {
+ var record;
+
+ this.recordCache[clientId] = record = type._create({
+ store: this,
+ clientId: clientId
+ });
+
+ set(record, 'id', id);
+
+ get(this, 'defaultTransaction').adoptRecord(record);
+
+ record.loadingData();
+ return record;
+ },
+
+ dematerializeRecord: function(record) {
+ var id = get(record, 'id'),
+ clientId = get(record, 'clientId'),
+ type = this.typeForClientId(clientId),
+ typeMap = this.typeMapFor(type);
+
+ record.updateRecordArrays();
+
+ delete this.recordCache[clientId];
+ delete this.clientIdToId[clientId];
+ delete this.clientIdToType[clientId];
+ delete this.clientIdToData[clientId];
+ delete this.recordArraysByClientId[clientId];
+
+ if (id) { delete typeMap.idToCid[id]; }
+ },
+
+ willDestroy: function() {
+ if (get(DS, 'defaultStore') === this) {
+ set(DS, 'defaultStore', null);
+ }
+ },
+
+ // ........................
+ // . RELATIONSHIP CHANGES .
+ // ........................
+
+ addRelationshipChangeFor: function(clientReference, childKey, parentReference, parentKey, change) {
+ var clientId = clientReference.clientId,
+ parentClientId = parentReference ? parentReference.clientId : parentReference;
+ var key = childKey + parentKey;
+ var changes = this.relationshipChanges;
+ if (!(clientId in changes)) {
+ changes[clientId] = {};
+ }
+ if (!(parentClientId in changes[clientId])) {
+ changes[clientId][parentClientId] = {};
+ }
+ if (!(key in changes[clientId][parentClientId])) {
+ changes[clientId][parentClientId][key] = {};
+ }
+ changes[clientId][parentClientId][key][change.changeType] = change;
+ },
+
+ removeRelationshipChangeFor: function(clientReference, childKey, parentReference, parentKey, type) {
+ var clientId = clientReference.clientId,
+ parentClientId = parentReference ? parentReference.clientId : parentReference;
+ var changes = this.relationshipChanges;
+ var key = childKey + parentKey;
+ if (!(clientId in changes) || !(parentClientId in changes[clientId]) || !(key in changes[clientId][parentClientId])){
+ return;
+ }
+ delete changes[clientId][parentClientId][key][type];
+ },
+
+ relationshipChangeFor: function(clientId, childKey, parentClientId, parentKey, type) {
+ var changes = this.relationshipChanges;
+ var key = childKey + parentKey;
+ if (!(clientId in changes) || !(parentClientId in changes[clientId])){
+ return;
+ }
+ if(type){
+ return changes[clientId][parentClientId][key][type];
+ }
+ else{
+ //TODO(Igor) what if both present
+ return changes[clientId][parentClientId][key]["add"] || changes[clientId][parentClientId][key]["remove"];
+ }
+ },
+
+ relationshipChangePairsFor: function(reference){
+ var toReturn = [];
+
+ if( !reference ) { return toReturn; }
+
+ //TODO(Igor) What about the other side
+ var changesObject = this.relationshipChanges[reference.clientId];
+ for (var objKey in changesObject){
+ if(changesObject.hasOwnProperty(objKey)){
+ for (var changeKey in changesObject[objKey]){
+ if(changesObject[objKey].hasOwnProperty(changeKey)){
+ toReturn.push(changesObject[objKey][changeKey]);
+ }
+ }
+ }
+ }
+ return toReturn;
+ },
+
+ relationshipChangesFor: function(reference) {
+ var toReturn = [];
+
+ if( !reference ) { return toReturn; }
+
+ var relationshipPairs = this.relationshipChangePairsFor(reference);
+ forEach(relationshipPairs, function(pair){
+ var addedChange = pair["add"];
+ var removedChange = pair["remove"];
+ if(addedChange){
+ toReturn.push(addedChange);
+ }
+ if(removedChange){
+ toReturn.push(removedChange);
+ }
+ });
+ return toReturn;
+ },
+ // ......................
+ // . PER-TYPE ADAPTERS
+ // ......................
+
+ adapterForType: function(type) {
+ this._adaptersMap = this.createInstanceMapFor('adapters');
+
+ var adapter = this._adaptersMap.get(type);
+ if (adapter) { return adapter; }
+
+ return this.get('_adapter');
+ },
+
+ // ..............................
+ // . RECORD CHANGE NOTIFICATION .
+ // ..............................
+
+ recordAttributeDidChange: function(reference, attributeName, newValue, oldValue) {
+ var record = this.recordForReference(reference),
+ dirtySet = new Ember.OrderedSet(),
+ adapter = this.adapterForType(record.constructor);
+
+ if (adapter.dirtyRecordsForAttributeChange) {
+ adapter.dirtyRecordsForAttributeChange(dirtySet, record, attributeName, newValue, oldValue);
+ }
+
+ dirtySet.forEach(function(record) {
+ record.adapterDidDirty();
+ });
+ },
+
+ recordBelongsToDidChange: function(dirtySet, child, relationship) {
+ var adapter = this.adapterForType(child.constructor);
+
+ if (adapter.dirtyRecordsForBelongsToChange) {
+ adapter.dirtyRecordsForBelongsToChange(dirtySet, child, relationship);
+ }
+
+ // adapterDidDirty is called by the RelationshipChange that created
+ // the dirtySet.
+ },
+
+ recordHasManyDidChange: function(dirtySet, parent, relationship) {
+ var adapter = this.adapterForType(parent.constructor);
+
+ if (adapter.dirtyRecordsForHasManyChange) {
+ adapter.dirtyRecordsForHasManyChange(dirtySet, parent, relationship);
+ }
+
+ // adapterDidDirty is called by the RelationshipChange that created
+ // the dirtySet.
+ }
+});
+
+DS.Store.reopenClass({
+ registerAdapter: DS._Mappable.generateMapFunctionFor('adapters', function(type, adapter, map) {
+ map.set(type, adapter);
+ }),
+
+ transformMapKey: function(key) {
+ if (typeof key === 'string') {
+ var transformedKey;
+ transformedKey = get(Ember.lookup, key);
+ Ember.assert("Could not find model at path " + key, transformedKey);
+ return transformedKey;
+ } else {
+ return key;
+ }
+ },
+
+ transformMapValue: function(key, value) {
+ if (Ember.Object.detect(value)) {
+ return value.create();
+ }
+
+ return value;
+ }
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set,
+ once = Ember.run.once, arrayMap = Ember.ArrayPolyfills.map;
+
+/**
+ This file encapsulates the various states that a record can transition
+ through during its lifecycle.
+
+ ### State Manager
+
+ A record's state manager explicitly tracks what state a record is in
+ at any given time. For instance, if a record is newly created and has
+ not yet been sent to the adapter to be saved, it would be in the
+ `created.uncommitted` state. If a record has had local modifications
+ made to it that are in the process of being saved, the record would be
+ in the `updated.inFlight` state. (These state paths will be explained
+ in more detail below.)
+
+ Events are sent by the record or its store to the record's state manager.
+ How the state manager reacts to these events is dependent on which state
+ it is in. In some states, certain events will be invalid and will cause
+ an exception to be raised.
+
+ States are hierarchical. For example, a record can be in the
+ `deleted.start` state, then transition into the `deleted.inFlight` state.
+ If a child state does not implement an event handler, the state manager
+ will attempt to invoke the event on all parent states until the root state is
+ reached. The state hierarchy of a record is described in terms of a path
+ string. You can determine a record's current state by getting its manager's
+ current state path:
+
+ record.get('stateManager.currentPath');
+ //=> "created.uncommitted"
+
+ The `DS.Model` states are themselves stateless. What we mean is that,
+ though each instance of a record also has a unique instance of a
+ `DS.StateManager`, the hierarchical states that each of *those* points
+ to is a shared data structure. For performance reasons, instead of each
+ record getting its own copy of the hierarchy of states, each state
+ manager points to this global, immutable shared instance. How does a
+ state know which record it should be acting on? We pass a reference to
+ the current state manager as the first parameter to every method invoked
+ on a state.
+
+ The state manager passed as the first parameter is where you should stash
+ state about the record if needed; you should never store data on the state
+ object itself. If you need access to the record being acted on, you can
+ retrieve the state manager's `record` property. For example, if you had
+ an event handler `myEvent`:
+
+ myEvent: function(manager) {
+ var record = manager.get('record');
+ record.doSomething();
+ }
+
+ For more information about state managers in general, see the Ember.js
+ documentation on `Ember.StateManager`.
+
+ ### Events, Flags, and Transitions
+
+ A state may implement zero or more events, flags, or transitions.
+
+ #### Events
+
+ Events are named functions that are invoked when sent to a record. The
+ state manager will first look for a method with the given name on the
+ current state. If no method is found, it will search the current state's
+ parent, and then its grandparent, and so on until reaching the top of
+ the hierarchy. If the root is reached without an event handler being found,
+ an exception will be raised. This can be very helpful when debugging new
+ features.
+
+ Here's an example implementation of a state with a `myEvent` event handler:
+
+ aState: DS.State.create({
+ myEvent: function(manager, param) {
+ console.log("Received myEvent with "+param);
+ }
+ })
+
+ To trigger this event:
+
+ record.send('myEvent', 'foo');
+ //=> "Received myEvent with foo"
+
+ Note that an optional parameter can be sent to a record's `send()` method,
+ which will be passed as the second parameter to the event handler.
+
+ Events should transition to a different state if appropriate. This can be
+ done by calling the state manager's `transitionTo()` method with a path to the
+ desired state. The state manager will attempt to resolve the state path
+ relative to the current state. If no state is found at that path, it will
+ attempt to resolve it relative to the current state's parent, and then its
+ parent, and so on until the root is reached. For example, imagine a hierarchy
+ like this:
+
+ * created
+ * start <-- currentState
+ * inFlight
+ * updated
+ * inFlight
+
+ If we are currently in the `start` state, calling
+ `transitionTo('inFlight')` would transition to the `created.inFlight` state,
+ while calling `transitionTo('updated.inFlight')` would transition to
+ the `updated.inFlight` state.
+
+ Remember that *only events* should ever cause a state transition. You should
+ never call `transitionTo()` from outside a state's event handler. If you are
+ tempted to do so, create a new event and send that to the state manager.
+
+ #### Flags
+
+ Flags are Boolean values that can be used to introspect a record's current
+ state in a more user-friendly way than examining its state path. For example,
+ instead of doing this:
+
+ var statePath = record.get('stateManager.currentPath');
+ if (statePath === 'created.inFlight') {
+ doSomething();
+ }
+
+ You can say:
+
+ if (record.get('isNew') && record.get('isSaving')) {
+ doSomething();
+ }
+
+ If your state does not set a value for a given flag, the value will
+ be inherited from its parent (or the first place in the state hierarchy
+ where it is defined).
+
+ The current set of flags are defined below. If you want to add a new flag,
+ in addition to the area below, you will also need to declare it in the
+ `DS.Model` class.
+
+ #### Transitions
+
+ Transitions are like event handlers but are called automatically upon
+ entering or exiting a state. To implement a transition, just call a method
+ either `enter` or `exit`:
+
+ myState: DS.State.create({
+ // Gets called automatically when entering
+ // this state.
+ enter: function(manager) {
+ console.log("Entered myState");
+ }
+ })
+
+ Note that enter and exit events are called once per transition. If the
+ current state changes, but changes to another child state of the parent,
+ the transition event on the parent will not be triggered.
+*/
+
+var stateProperty = Ember.computed(function(key) {
+ var parent = get(this, 'parentState');
+ if (parent) {
+ return get(parent, key);
+ }
+}).property();
+
+var hasDefinedProperties = function(object) {
+ for (var name in object) {
+ if (object.hasOwnProperty(name) && object[name]) { return true; }
+ }
+
+ return false;
+};
+
+var didChangeData = function(manager) {
+ var record = get(manager, 'record');
+ record.materializeData();
+};
+
+var willSetProperty = function(manager, context) {
+ context.oldValue = get(get(manager, 'record'), context.name);
+
+ var change = DS.AttributeChange.createChange(context);
+ get(manager, 'record')._changesToSync[context.attributeName] = change;
+};
+
+var didSetProperty = function(manager, context) {
+ var change = get(manager, 'record')._changesToSync[context.attributeName];
+ change.value = get(get(manager, 'record'), context.name);
+ change.sync();
+};
+
+DS.State = Ember.State.extend({
+ isLoaded: stateProperty,
+ isReloading: stateProperty,
+ isDirty: stateProperty,
+ isSaving: stateProperty,
+ isDeleted: stateProperty,
+ isError: stateProperty,
+ isNew: stateProperty,
+ isValid: stateProperty,
+
+ // For states that are substates of a
+ // DirtyState (updated or created), it is
+ // useful to be able to determine which
+ // type of dirty state it is.
+ dirtyType: stateProperty
+});
+
+// Implementation notes:
+//
+// Each state has a boolean value for all of the following flags:
+//
+// * isLoaded: The record has a populated `data` property. When a
+// record is loaded via `store.find`, `isLoaded` is false
+// until the adapter sets it. When a record is created locally,
+// its `isLoaded` property is always true.
+// * isDirty: The record has local changes that have not yet been
+// saved by the adapter. This includes records that have been
+// created (but not yet saved) or deleted.
+// * isSaving: The record's transaction has been committed, but
+// the adapter has not yet acknowledged that the changes have
+// been persisted to the backend.
+// * isDeleted: The record was marked for deletion. When `isDeleted`
+// is true and `isDirty` is true, the record is deleted locally
+// but the deletion was not yet persisted. When `isSaving` is
+// true, the change is in-flight. When both `isDirty` and
+// `isSaving` are false, the change has persisted.
+// * isError: The adapter reported that it was unable to save
+// local changes to the backend. This may also result in the
+// record having its `isValid` property become false if the
+// adapter reported that server-side validations failed.
+// * isNew: The record was created on the client and the adapter
+// did not yet report that it was successfully saved.
+// * isValid: No client-side validations have failed and the
+// adapter did not report any server-side validation failures.
+
+// The dirty state is a abstract state whose functionality is
+// shared between the `created` and `updated` states.
+//
+// The deleted state shares the `isDirty` flag with the
+// subclasses of `DirtyState`, but with a very different
+// implementation.
+//
+// Dirty states have three child states:
+//
+// `uncommitted`: the store has not yet handed off the record
+// to be saved.
+// `inFlight`: the store has handed off the record to be saved,
+// but the adapter has not yet acknowledged success.
+// `invalid`: the record has invalid information and cannot be
+// send to the adapter yet.
+var DirtyState = DS.State.extend({
+ initialState: 'uncommitted',
+
+ // FLAGS
+ isDirty: true,
+
+ // SUBSTATES
+
+ // When a record first becomes dirty, it is `uncommitted`.
+ // This means that there are local pending changes, but they
+ // have not yet begun to be saved, and are not invalid.
+ uncommitted: DS.State.extend({
+ // TRANSITIONS
+ enter: function(manager) {
+ var dirtyType = get(this, 'dirtyType'),
+ record = get(manager, 'record');
+
+ record.withTransaction(function (t) {
+ t.recordBecameDirty(dirtyType, record);
+ });
+ },
+
+ // EVENTS
+ willSetProperty: willSetProperty,
+ didSetProperty: didSetProperty,
+
+ becomeDirty: Ember.K,
+
+ willCommit: function(manager) {
+ manager.transitionTo('inFlight');
+ },
+
+ becameClean: function(manager) {
+ var record = get(manager, 'record'),
+ dirtyType = get(this, 'dirtyType');
+
+ record.withTransaction(function(t) {
+ t.recordBecameClean(dirtyType, record);
+ });
+
+ manager.transitionTo('loaded.materializing');
+ },
+
+ becameInvalid: function(manager) {
+ var dirtyType = get(this, 'dirtyType'),
+ record = get(manager, 'record');
+
+ record.withTransaction(function (t) {
+ t.recordBecameInFlight(dirtyType, record);
+ });
+
+ manager.transitionTo('invalid');
+ },
+
+ rollback: function(manager) {
+ get(manager, 'record').rollback();
+ }
+ }),
+
+ // Once a record has been handed off to the adapter to be
+ // saved, it is in the 'in flight' state. Changes to the
+ // record cannot be made during this window.
+ inFlight: DS.State.extend({
+ // FLAGS
+ isSaving: true,
+
+ // TRANSITIONS
+ enter: function(manager) {
+ var dirtyType = get(this, 'dirtyType'),
+ record = get(manager, 'record');
+
+ record.becameInFlight();
+
+ record.withTransaction(function (t) {
+ t.recordBecameInFlight(dirtyType, record);
+ });
+ },
+
+ // EVENTS
+ didCommit: function(manager) {
+ var dirtyType = get(this, 'dirtyType'),
+ record = get(manager, 'record');
+
+ record.withTransaction(function(t) {
+ t.recordBecameClean('inflight', record);
+ });
+
+ manager.transitionTo('saved');
+ manager.send('invokeLifecycleCallbacks', dirtyType);
+ },
+
+ becameInvalid: function(manager, errors) {
+ var record = get(manager, 'record');
+
+ set(record, 'errors', errors);
+
+ manager.transitionTo('invalid');
+ manager.send('invokeLifecycleCallbacks');
+ },
+
+ becameError: function(manager) {
+ manager.transitionTo('error');
+ manager.send('invokeLifecycleCallbacks');
+ }
+ }),
+
+ // A record is in the `invalid` state when its client-side
+ // invalidations have failed, or if the adapter has indicated
+ // the the record failed server-side invalidations.
+ invalid: DS.State.extend({
+ // FLAGS
+ isValid: false,
+
+ exit: function(manager) {
+ var record = get(manager, 'record');
+
+ record.withTransaction(function (t) {
+ t.recordBecameClean('inflight', record);
+ });
+ },
+
+ // EVENTS
+ deleteRecord: function(manager) {
+ manager.transitionTo('deleted');
+ get(manager, 'record').clearRelationships();
+ },
+
+ willSetProperty: willSetProperty,
+
+ didSetProperty: function(manager, context) {
+ var record = get(manager, 'record'),
+ errors = get(record, 'errors'),
+ key = context.name;
+
+ set(errors, key, null);
+
+ if (!hasDefinedProperties(errors)) {
+ manager.send('becameValid');
+ }
+
+ didSetProperty(manager, context);
+ },
+
+ becomeDirty: Ember.K,
+
+ rollback: function(manager) {
+ manager.send('becameValid');
+ manager.send('rollback');
+ },
+
+ becameValid: function(manager) {
+ manager.transitionTo('uncommitted');
+ },
+
+ invokeLifecycleCallbacks: function(manager) {
+ var record = get(manager, 'record');
+ record.trigger('becameInvalid', record);
+ }
+ })
+});
+
+// The created and updated states are created outside the state
+// chart so we can reopen their substates and add mixins as
+// necessary.
+
+var createdState = DirtyState.create({
+ dirtyType: 'created',
+
+ // FLAGS
+ isNew: true
+});
+
+var updatedState = DirtyState.create({
+ dirtyType: 'updated'
+});
+
+createdState.states.uncommitted.reopen({
+ deleteRecord: function(manager) {
+ var record = get(manager, 'record');
+
+ record.withTransaction(function(t) {
+ t.recordIsMoving('created', record);
+ });
+
+ record.clearRelationships();
+ manager.transitionTo('deleted.saved');
+ }
+});
+
+createdState.states.uncommitted.reopen({
+ rollback: function(manager) {
+ this._super(manager);
+ manager.transitionTo('deleted.saved');
+ }
+});
+
+updatedState.states.uncommitted.reopen({
+ deleteRecord: function(manager) {
+ var record = get(manager, 'record');
+
+ record.withTransaction(function(t) {
+ t.recordIsMoving('updated', record);
+ });
+
+ manager.transitionTo('deleted');
+ get(manager, 'record').clearRelationships();
+ }
+});
+
+var states = {
+ rootState: Ember.State.create({
+ // FLAGS
+ isLoaded: false,
+ isReloading: false,
+ isDirty: false,
+ isSaving: false,
+ isDeleted: false,
+ isError: false,
+ isNew: false,
+ isValid: true,
+
+ // SUBSTATES
+
+ // A record begins its lifecycle in the `empty` state.
+ // If its data will come from the adapter, it will
+ // transition into the `loading` state. Otherwise, if
+ // the record is being created on the client, it will
+ // transition into the `created` state.
+ empty: DS.State.create({
+ // EVENTS
+ loadingData: function(manager) {
+ manager.transitionTo('loading');
+ },
+
+ loadedData: function(manager) {
+ manager.transitionTo('loaded.created');
+ }
+ }),
+
+ // A record enters this state when the store askes
+ // the adapter for its data. It remains in this state
+ // until the adapter provides the requested data.
+ //
+ // Usually, this process is asynchronous, using an
+ // XHR to retrieve the data.
+ loading: DS.State.create({
+ // EVENTS
+ loadedData: didChangeData,
+
+ materializingData: function(manager) {
+ manager.transitionTo('loaded.materializing.firstTime');
+ }
+ }),
+
+ // A record enters this state when its data is populated.
+ // Most of a record's lifecycle is spent inside substates
+ // of the `loaded` state.
+ loaded: DS.State.create({
+ initialState: 'saved',
+
+ // FLAGS
+ isLoaded: true,
+
+ // SUBSTATES
+
+ materializing: DS.State.create({
+ // FLAGS
+ isLoaded: false,
+
+ // EVENTS
+ willSetProperty: Ember.K,
+ didSetProperty: Ember.K,
+
+ didChangeData: didChangeData,
+
+ finishedMaterializing: function(manager) {
+ manager.transitionTo('loaded.saved');
+ },
+
+ // SUBSTATES
+ firstTime: DS.State.create({
+ exit: function(manager) {
+ var record = get(manager, 'record');
+
+ once(function() {
+ record.trigger('didLoad');
+ });
+ }
+ })
+ }),
+
+ reloading: DS.State.create({
+ // FLAGS
+ isReloading: true,
+
+ // TRANSITIONS
+ enter: function(manager) {
+ var record = get(manager, 'record'),
+ store = get(record, 'store');
+
+ store.reloadRecord(record);
+ },
+
+ exit: function(manager) {
+ var record = get(manager, 'record');
+
+ once(record, 'trigger', 'didReload');
+ },
+
+ // EVENTS
+ loadedData: didChangeData,
+
+ materializingData: function(manager) {
+ manager.transitionTo('loaded.materializing');
+ }
+ }),
+
+ // If there are no local changes to a record, it remains
+ // in the `saved` state.
+ saved: DS.State.create({
+ // EVENTS
+ willSetProperty: willSetProperty,
+ didSetProperty: didSetProperty,
+
+ didChangeData: didChangeData,
+ loadedData: didChangeData,
+
+ reloadRecord: function(manager) {
+ manager.transitionTo('loaded.reloading');
+ },
+
+ materializingData: function(manager) {
+ manager.transitionTo('loaded.materializing');
+ },
+
+ becomeDirty: function(manager) {
+ manager.transitionTo('updated');
+ },
+
+ deleteRecord: function(manager) {
+ manager.transitionTo('deleted');
+ get(manager, 'record').clearRelationships();
+ },
+
+ unloadRecord: function(manager) {
+ manager.transitionTo('deleted.saved');
+ get(manager, 'record').clearRelationships();
+ },
+
+ invokeLifecycleCallbacks: function(manager, dirtyType) {
+ var record = get(manager, 'record');
+ if (dirtyType === 'created') {
+ record.trigger('didCreate', record);
+ } else {
+ record.trigger('didUpdate', record);
+ }
+ }
+ }),
+
+ // A record is in this state after it has been locally
+ // created but before the adapter has indicated that
+ // it has been saved.
+ created: createdState,
+
+ // A record is in this state if it has already been
+ // saved to the server, but there are new local changes
+ // that have not yet been saved.
+ updated: updatedState
+ }),
+
+ // A record is in this state if it was deleted from the store.
+ deleted: DS.State.create({
+ initialState: 'uncommitted',
+ dirtyType: 'deleted',
+
+ // FLAGS
+ isDeleted: true,
+ isLoaded: true,
+ isDirty: true,
+
+ // TRANSITIONS
+ setup: function(manager) {
+ var record = get(manager, 'record'),
+ store = get(record, 'store');
+
+ store.removeFromRecordArrays(record);
+ },
+
+ // SUBSTATES
+
+ // When a record is deleted, it enters the `start`
+ // state. It will exit this state when the record's
+ // transaction starts to commit.
+ uncommitted: DS.State.create({
+ // TRANSITIONS
+ enter: function(manager) {
+ var record = get(manager, 'record');
+
+ record.withTransaction(function(t) {
+ t.recordBecameDirty('deleted', record);
+ });
+ },
+
+ // EVENTS
+ willCommit: function(manager) {
+ manager.transitionTo('inFlight');
+ },
+
+ rollback: function(manager) {
+ get(manager, 'record').rollback();
+ },
+
+ becomeDirty: Ember.K,
+
+ becameClean: function(manager) {
+ var record = get(manager, 'record');
+
+ record.withTransaction(function(t) {
+ t.recordBecameClean('deleted', record);
+ });
+
+ manager.transitionTo('loaded.materializing');
+ }
+ }),
+
+ // After a record's transaction is committing, but
+ // before the adapter indicates that the deletion
+ // has saved to the server, a record is in the
+ // `inFlight` substate of `deleted`.
+ inFlight: DS.State.create({
+ // FLAGS
+ isSaving: true,
+
+ // TRANSITIONS
+ enter: function(manager) {
+ var record = get(manager, 'record');
+
+ record.becameInFlight();
+
+ record.withTransaction(function (t) {
+ t.recordBecameInFlight('deleted', record);
+ });
+ },
+
+ // EVENTS
+ didCommit: function(manager) {
+ var record = get(manager, 'record');
+
+ record.withTransaction(function(t) {
+ t.recordBecameClean('inflight', record);
+ });
+
+ manager.transitionTo('saved');
+
+ manager.send('invokeLifecycleCallbacks');
+ }
+ }),
+
+ // Once the adapter indicates that the deletion has
+ // been saved, the record enters the `saved` substate
+ // of `deleted`.
+ saved: DS.State.create({
+ // FLAGS
+ isDirty: false,
+
+ setup: function(manager) {
+ var record = get(manager, 'record'),
+ store = get(record, 'store');
+
+ store.dematerializeRecord(record);
+ },
+
+ invokeLifecycleCallbacks: function(manager) {
+ var record = get(manager, 'record');
+ record.trigger('didDelete', record);
+ }
+ })
+ }),
+
+ // If the adapter indicates that there was an unknown
+ // error saving a record, the record enters the `error`
+ // state.
+ error: DS.State.create({
+ isError: true,
+
+ // EVENTS
+
+ invokeLifecycleCallbacks: function(manager) {
+ var record = get(manager, 'record');
+ record.trigger('becameError', record);
+ }
+ })
+ })
+};
+
+DS.StateManager = Ember.StateManager.extend({
+ record: null,
+ initialState: 'rootState',
+ states: states,
+ unhandledEvent: function(manager, originalEvent) {
+ var record = manager.get('record'),
+ contexts = [].slice.call(arguments, 2),
+ errorMessage;
+ errorMessage = "Attempted to handle event `" + originalEvent + "` ";
+ errorMessage += "on " + record.toString() + " while in state ";
+ errorMessage += get(manager, 'currentState.path') + ". Called with ";
+ errorMessage += arrayMap.call(contexts, function(context){
+ return Ember.inspect(context);
+ }).join(', ');
+ throw new Ember.Error(errorMessage);
+ }
+});
+
+})();
+
+
+
+(function() {
+var LoadPromise = DS.LoadPromise; // system/mixins/load_promise
+
+var get = Ember.get, set = Ember.set, map = Ember.EnumerableUtils.map;
+
+var retrieveFromCurrentState = Ember.computed(function(key, value) {
+ return get(get(this, 'stateManager.currentState'), key);
+}).property('stateManager.currentState').readOnly();
+
+DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, {
+ isLoaded: retrieveFromCurrentState,
+ isReloading: retrieveFromCurrentState,
+ isDirty: retrieveFromCurrentState,
+ isSaving: retrieveFromCurrentState,
+ isDeleted: retrieveFromCurrentState,
+ isError: retrieveFromCurrentState,
+ isNew: retrieveFromCurrentState,
+ isValid: retrieveFromCurrentState,
+
+ clientId: null,
+ id: null,
+ transaction: null,
+ stateManager: null,
+ errors: null,
+
+ /**
+ Create a JSON representation of the record, using the serialization
+ strategy of the store's adapter.
+
+ Available options:
+
+ * `includeId`: `true` if the record's ID should be included in the
+ JSON representation.
+
+ @param {Object} options
+ @returns {Object} an object whose values are primitive JSON values only
+ */
+ serialize: function(options) {
+ var store = get(this, 'store');
+ return store.serialize(this, options);
+ },
+
+ toJSON: function() {
+ var serializer = DS.JSONSerializer.create();
+ return serializer.serialize(this);
+ },
+
+ didLoad: Ember.K,
+ didReload: Ember.K,
+ didUpdate: Ember.K,
+ didCreate: Ember.K,
+ didDelete: Ember.K,
+ becameInvalid: Ember.K,
+ becameError: Ember.K,
+
+ data: Ember.computed(function() {
+ if (!this._data) {
+ this.materializeData();
+ }
+
+ return this._data;
+ }).property(),
+
+ materializeData: function() {
+ this.send('materializingData');
+
+ get(this, 'store').materializeData(this);
+
+ this.suspendRelationshipObservers(function() {
+ this.notifyPropertyChange('data');
+ });
+ },
+
+ _data: null,
+
+ init: function() {
+ this._super();
+
+ var stateManager = DS.StateManager.create({ record: this });
+ set(this, 'stateManager', stateManager);
+
+ this._setup();
+
+ stateManager.goToState('empty');
+ },
+
+ _setup: function() {
+ this._relationshipChanges = {};
+ this._changesToSync = {};
+ },
+
+ send: function(name, context) {
+ return get(this, 'stateManager').send(name, context);
+ },
+
+ withTransaction: function(fn) {
+ var transaction = get(this, 'transaction');
+ if (transaction) { fn(transaction); }
+ },
+
+ loadingData: function() {
+ this.send('loadingData');
+ },
+
+ loadedData: function() {
+ this.send('loadedData');
+ },
+
+ didChangeData: function() {
+ this.send('didChangeData');
+ },
+
+ setProperty: function(key, value, oldValue) {
+ this.send('setProperty', { key: key, value: value, oldValue: oldValue });
+ },
+
+ /**
+ 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`).
+ */
+ reload: function() {
+ this.send('reloadRecord');
+ },
+
+ deleteRecord: function() {
+ this.send('deleteRecord');
+ },
+
+ unloadRecord: function() {
+ Ember.assert("You can only unload a loaded, non-dirty record.", !get(this, 'isDirty'));
+
+ this.send('unloadRecord');
+ },
+
+ clearRelationships: function() {
+ this.eachRelationship(function(name, relationship) {
+ if (relationship.kind === 'belongsTo') {
+ set(this, name, null);
+ } else if (relationship.kind === 'hasMany') {
+ get(this, name).clear();
+ }
+ }, this);
+ },
+
+ updateRecordArrays: function() {
+ var store = get(this, 'store');
+ if (store) {
+ store.dataWasUpdated(this.constructor, get(this, '_reference'), this);
+ }
+ },
+
+ /**
+ If the adapter did not return a hash in response to a commit,
+ merge the changed attributes and relationships into the existing
+ saved data.
+ */
+ adapterDidCommit: function() {
+ var attributes = get(this, 'data').attributes;
+
+ get(this.constructor, 'attributes').forEach(function(name, meta) {
+ attributes[name] = get(this, name);
+ }, this);
+
+ this.send('didCommit');
+ this.updateRecordArraysLater();
+ },
+
+ adapterDidDirty: function() {
+ this.send('becomeDirty');
+ this.updateRecordArraysLater();
+ },
+
+ dataDidChange: Ember.observer(function() {
+ var relationships = get(this.constructor, 'relationshipsByName');
+
+ this.updateRecordArraysLater();
+
+ relationships.forEach(function(name, relationship) {
+ if (relationship.kind === 'hasMany') {
+ this.hasManyDidChange(relationship.key);
+ }
+ }, this);
+
+ this.send('finishedMaterializing');
+ }, 'data'),
+
+ hasManyDidChange: function(key) {
+ var cachedValue = this.cacheFor(key);
+
+ if (cachedValue) {
+ var type = get(this.constructor, 'relationshipsByName').get(key).type;
+ var store = get(this, 'store');
+ var ids = this._data.hasMany[key] || [];
+
+ var references = map(ids, function(id) {
+ // if it was already a reference, return the reference
+ if (typeof id === 'object') { return id; }
+ return store.referenceForId(type, id);
+ });
+
+ set(cachedValue, 'content', Ember.A(references));
+ }
+ },
+
+ updateRecordArraysLater: function() {
+ Ember.run.once(this, this.updateRecordArrays);
+ },
+
+ setupData: function(prematerialized) {
+ this._data = {
+ attributes: {},
+ belongsTo: {},
+ hasMany: {},
+ id: null
+ };
+ },
+
+ materializeId: function(id) {
+ set(this, 'id', id);
+ },
+
+ materializeAttributes: function(attributes) {
+ Ember.assert("Must pass a hash of attributes to materializeAttributes", !!attributes);
+ this._data.attributes = attributes;
+ },
+
+ materializeAttribute: function(name, value) {
+ this._data.attributes[name] = value;
+ },
+
+ materializeHasMany: function(name, ids) {
+ this._data.hasMany[name] = ids;
+ },
+
+ materializeBelongsTo: function(name, id) {
+ this._data.belongsTo[name] = id;
+ },
+
+ rollback: function() {
+ this._setup();
+ this.send('becameClean');
+
+ this.suspendRelationshipObservers(function() {
+ this.notifyPropertyChange('data');
+ });
+ },
+
+ toStringExtension: function() {
+ return get(this, 'id');
+ },
+
+ /**
+ @private
+
+ The goal of this method is to temporarily disable specific observers
+ that take action in response to application changes.
+
+ This allows the system to make changes (such as materialization and
+ rollback) that should not trigger secondary behavior (such as setting an
+ inverse relationship or marking records as dirty).
+
+ The specific implementation will likely change as Ember proper provides
+ better infrastructure for suspending groups of observers, and if Array
+ observation becomes more unified with regular observers.
+ */
+ suspendRelationshipObservers: function(callback, binding) {
+ var observers = get(this.constructor, 'relationshipNames').belongsTo;
+ var self = this;
+
+ try {
+ this._suspendedRelationships = true;
+ Ember._suspendObservers(self, observers, null, 'belongsToDidChange', function() {
+ Ember._suspendBeforeObservers(self, observers, null, 'belongsToWillChange', function() {
+ callback.call(binding || self);
+ });
+ });
+ } finally {
+ this._suspendedRelationships = false;
+ }
+ },
+
+ becameInFlight: function() {
+ },
+
+ // FOR USE BY THE BASIC ADAPTER
+
+ save: function() {
+ this.get('store').scheduleSave(this);
+ },
+
+ // FOR USE DURING COMMIT PROCESS
+
+ adapterDidUpdateAttribute: function(attributeName, value) {
+
+ // If a value is passed in, update the internal attributes and clear
+ // the attribute cache so it picks up the new value. Otherwise,
+ // collapse the current value into the internal attributes because
+ // the adapter has acknowledged it.
+ if (value !== undefined) {
+ get(this, 'data.attributes')[attributeName] = value;
+ this.notifyPropertyChange(attributeName);
+ } else {
+ value = get(this, attributeName);
+ get(this, 'data.attributes')[attributeName] = value;
+ }
+
+ this.updateRecordArraysLater();
+ },
+
+ _reference: Ember.computed(function() {
+ return get(this, 'store').referenceForClientId(get(this, 'clientId'));
+ }),
+
+ adapterDidInvalidate: function(errors) {
+ this.send('becameInvalid', errors);
+ },
+
+ adapterDidError: function() {
+ this.send('becameError');
+ },
+
+ /**
+ @private
+
+ Override the default event firing from Ember.Evented to
+ also call methods with the given name.
+ */
+ trigger: function(name) {
+ Ember.tryInvoke(this, name, [].slice.call(arguments, 1));
+ this._super.apply(this, arguments);
+ }
+});
+
+// Helper function to generate store aliases.
+// This returns a function that invokes the named alias
+// on the default store, but injects the class as the
+// first parameter.
+var storeAlias = function(methodName) {
+ return function() {
+ var store = get(DS, 'defaultStore'),
+ args = [].slice.call(arguments);
+
+ args.unshift(this);
+ Ember.assert("Your application does not have a 'Store' property defined. Attempts to call '" + methodName + "' on model classes will fail. Please provide one as with 'YourAppName.Store = DS.Store.extend()'", !!store);
+ return store[methodName].apply(store, args);
+ };
+};
+
+DS.Model.reopenClass({
+ isLoaded: storeAlias('recordIsLoaded'),
+ find: storeAlias('find'),
+ all: storeAlias('all'),
+ query: storeAlias('findQuery'),
+ filter: storeAlias('filter'),
+
+ _create: DS.Model.create,
+
+ 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.");
+ },
+
+ createRecord: storeAlias('createRecord')
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get;
+DS.Model.reopenClass({
+ attributes: Ember.computed(function() {
+ var map = Ember.Map.create();
+
+ this.eachComputedProperty(function(name, meta) {
+ if (meta.isAttribute) {
+ Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('<type>')` from " + this.toString(), name !== 'id');
+
+ meta.name = name;
+ map.set(name, meta);
+ }
+ });
+
+ return map;
+ })
+});
+
+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) {
+ get(this.constructor, 'attributes').forEach(function(name, meta) {
+ callback.call(binding, name, meta);
+ }, binding);
+ },
+
+ attributeWillChange: Ember.beforeObserver(function(record, key) {
+ var reference = get(record, '_reference'),
+ store = get(record, 'store');
+
+ record.send('willSetProperty', { reference: reference, store: store, name: key });
+ }),
+
+ attributeDidChange: Ember.observer(function(record, key) {
+ record.send('didSetProperty', { name: key });
+ })
+});
+
+function getAttr(record, options, key) {
+ var attributes = get(record, 'data').attributes;
+ var value = attributes[key];
+
+ if (value === undefined) {
+ value = options.defaultValue;
+ }
+
+ return value;
+}
+
+DS.attr = function(type, options) {
+ options = options || {};
+
+ var meta = {
+ type: type,
+ isAttribute: true,
+ options: options
+ };
+
+ return Ember.computed(function(key, value, oldValue) {
+ if (arguments.length > 1) {
+ Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('<type>')` from " + this.constructor.toString(), key !== 'id');
+ } else {
+ value = getAttr(this, options, key);
+ }
+
+ return value;
+ // `data` is never set directly. However, it may be
+ // invalidated from the state manager's setData
+ // event.
+ }).property('data').meta(meta);
+};
+
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(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 || {};
+
+ var meta = { type: type, isRelationship: true, options: options, kind: 'belongsTo' };
+
+ return Ember.computed(function(key, value) {
+ if (arguments.length === 2) {
+ return value === undefined ? null : value;
+ }
+
+ var data = get(this, 'data').belongsTo,
+ store = get(this, 'store'), id;
+
+ if (typeof type === 'string') {
+ type = get(this, type, false) || get(Ember.lookup, type);
+ }
+
+ id = data[key];
+
+ if(!id) {
+ return null;
+ } else if (typeof id === 'object') {
+ return store.recordForReference(id);
+ } else {
+ return store.find(type, id);
+ }
+ }).property('data').meta(meta);
+};
+
+/**
+ 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;
+ }
+ }
+ }),
+
+ /** @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];
+ })
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set;
+var hasRelationship = function(type, options) {
+ options = options || {};
+
+ var meta = { type: type, isRelationship: true, options: options, kind: 'hasMany' };
+
+ return Ember.computed(function(key, value) {
+ var data = get(this, 'data').hasMany,
+ store = get(this, 'store'),
+ ids, relationship;
+
+ if (typeof type === 'string') {
+ type = get(this, type, false) || get(Ember.lookup, type);
+ }
+
+ ids = data[key];
+ relationship = store.findMany(type, ids, this, meta);
+ set(relationship, 'owner', this);
+ set(relationship, 'name', key);
+
+ return relationship;
+ }).property().meta(meta);
+};
+
+DS.hasMany = function(type, options) {
+ Ember.assert("The type passed to DS.hasMany must be defined", !!type);
+ return hasRelationship(type, options);
+};
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set;
+
+/**
+ @private
+
+ This file defines several extensions to the base `DS.Model` class that
+ add support for one-to-many relationships.
+*/
+
+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) {
+
+ // If it is, get the metadata for the relationship. This is
+ // populated by the `DS.belongsTo` helper when it is creating
+ // the computed property.
+ var meta = value.meta();
+
+ if (meta.isRelationship && meta.kind === 'belongsTo') {
+ Ember.addObserver(proto, key, null, 'belongsToDidChange');
+ Ember.addBeforeObserver(proto, key, null, 'belongsToWillChange');
+ }
+
+ if (meta.isAttribute) {
+ Ember.addObserver(proto, key, null, 'attributeDidChange');
+ Ember.addBeforeObserver(proto, key, null, 'attributeWillChange');
+ }
+
+ meta.parentType = proto.constructor;
+ }
+ }
+});
+
+/**
+ These DS.Model extensions add class methods that provide relationship
+ introspection abilities about relationships.
+
+ A note about the computed properties contained here:
+
+ **These properties are effectively sealed once called for the first time.**
+ To avoid repeatedly doing expensive iteration over a model's fields, these
+ values are computed once and then cached for the remainder of the runtime of
+ your application.
+
+ If your application needs to modify a class after its initial definition
+ (for example, using `reopen()` to add additional attributes), make sure you
+ do it before using your model with the store, which uses these properties
+ extensively.
+*/
+
+DS.Model.reopenClass({
+ /**
+ For a given relationship name, returns the model type of the relationship.
+
+ For example, if you define a model like this:
+
+ App.Post = DS.Model.extend({
+ comments: DS.hasMany(App.Comment)
+ });
+
+ Calling `App.Post.typeForRelationship('comments')` will return `App.Comment`.
+
+ @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;
+ },
+
+ /**
+ The model's relationships as a map, keyed on the type of the
+ relationship. The value of each entry is an array containing a descriptor
+ for each relationship with that type, describing the name of the relationship
+ as well as the type.
+
+ For example, given the following model definition:
+
+ App.Blog = DS.Model.extend({
+ users: DS.hasMany(App.User),
+ owner: DS.belongsTo(App.User),
+ posts: DS.hasMany(App.Post)
+ });
+
+ This computed property would return a map describing these
+ relationships, like this:
+
+ var relationships = Ember.get(App.Blog, 'relationships');
+ associatons.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 []; }
+ });
+
+ // Loop through each computed property on the class
+ this.eachComputedProperty(function(name, meta) {
+
+ // If the computed property is a relationship, add
+ // it to the map.
+ if (meta.isRelationship) {
+ if (typeof meta.type === 'string') {
+ meta.type = Ember.get(Ember.lookup, meta.type);
+ }
+
+ var relationshipsForType = map.get(meta.type);
+
+ relationshipsForType.push({ name: name, kind: meta.kind });
+ }
+ });
+
+ return map;
+ }),
+
+ /**
+ A hash containing lists of the model's relationships, grouped
+ by the relationship kind. For example, given a model with this
+ definition:
+
+ App.Blog = DS.Model.extend({
+ users: DS.hasMany(App.User),
+ owner: DS.belongsTo(App.User),
+
+ posts: DS.hasMany(App.Post)
+ });
+
+ This property would contain the following:
+
+ var relationshipNames = Ember.get(App.Blog, 'relationshipNames');
+ relationshipNames.hasMany;
+ //=> ['users', 'posts']
+ relationshipNames.belongsTo;
+ //=> ['owner']
+
+ @type Object
+ @readOnly
+ */
+ relationshipNames: Ember.computed(function() {
+ var names = { hasMany: [], belongsTo: [] };
+
+ this.eachComputedProperty(function(name, meta) {
+ if (meta.isRelationship) {
+ names[meta.kind].push(name);
+ }
+ });
+
+ return names;
+ }),
+
+ /**
+ An array of types directly related to a model. Each type will be
+ included once, regardless of the number of relationships it has with
+ the model.
+
+ For example, given a model with this definition:
+
+ App.Blog = DS.Model.extend({
+ users: DS.hasMany(App.User),
+ owner: DS.belongsTo(App.User),
+ posts: DS.hasMany(App.Post)
+ });
+
+ This property would contain the following:
+
+ var relatedTypes = Ember.get(App.Blog, 'relatedTypes');
+ //=> [ App.User, App.Post ]
+
+ @type Ember.Array
+ @readOnly
+ */
+ relatedTypes: Ember.computed(function() {
+ var type,
+ types = Ember.A([]);
+
+ // Loop through each computed property on the class,
+ // and create an array of the unique types involved
+ // in relationships
+ this.eachComputedProperty(function(name, meta) {
+ if (meta.isRelationship) {
+ type = meta.type;
+
+ if (typeof type === 'string') {
+ type = get(this, type, false) || get(Ember.lookup, type);
+ }
+
+ if (!types.contains(type)) {
+ types.push(type);
+ }
+ }
+ });
+
+ return types;
+ }),
+
+ /**
+ A map whose keys are the relationships of a model and whose values are
+ relationship descriptors.
+
+ For example, given a model with this
+ definition:
+
+ App.Blog = DS.Model.extend({
+ users: DS.hasMany(App.User),
+ owner: DS.belongsTo(App.User),
+
+ posts: DS.hasMany(App.Post)
+ });
+
+ This property would contain the following:
+
+ var relationshipsByName = Ember.get(App.Blog, 'relationshipsByName');
+ relationshipsByName.get('users');
+ //=> { key: 'users', kind: 'hasMany', type: App.User }
+ relationshipsByName.get('owner');
+ //=> { key: 'owner', kind: 'belongsTo', type: App.User }
+
+ @type Ember.Map
+ @readOnly
+ */
+ relationshipsByName: Ember.computed(function() {
+ var map = Ember.Map.create(), type;
+
+ this.eachComputedProperty(function(name, meta) {
+ if (meta.isRelationship) {
+ meta.key = name;
+ type = meta.type;
+
+ if (typeof type === 'string') {
+ type = get(this, type, false) || get(Ember.lookup, type);
+ meta.type = type;
+ }
+
+ map.set(name, meta);
+ }
+ });
+
+ return map;
+ }),
+
+ /**
+ A map whose keys are the fields of the model and whose values are strings
+ describing the kind of the field. A model's fields are the union of all of its
+ attributes and relationships.
+
+ For example:
+
+ App.Blog = DS.Model.extend({
+ users: DS.hasMany(App.User),
+ owner: DS.belongsTo(App.User),
+
+ posts: DS.hasMany(App.Post),
+
+ title: DS.attr('string')
+ });
+
+ var fields = Ember.get(App.Blog, 'fields');
+ fields.forEach(function(field, kind) {
+ console.log(field, kind);
+ });
+
+ // prints:
+ // users, hasMany
+ // owner, belongsTo
+ // posts, hasMany
+ // title, attribute
+
+ @type Ember.Map
+ @readOnly
+ */
+ fields: Ember.computed(function() {
+ var map = Ember.Map.create(), type;
+
+ this.eachComputedProperty(function(name, meta) {
+ if (meta.isRelationship) {
+ map.set(name, meta.kind);
+ } else if (meta.isAttribute) {
+ map.set(name, 'attribute');
+ }
+ });
+
+ return map;
+ }),
+
+ /**
+ Given a callback, iterates over each of the relationships in the model,
+ invoking the callback with the name of each relationship and its relationship
+ descriptor.
+
+ @param {Function} callback the callback to invoke
+ @param {any} binding the value to which the callback's `this` should be bound
+ */
+ eachRelationship: function(callback, binding) {
+ get(this, 'relationshipsByName').forEach(function(name, relationship) {
+ callback.call(binding, name, relationship);
+ });
+ },
+
+ /**
+ Given a callback, iterates over each of the types related to a model,
+ invoking the callback with the related type's class. Each type will be
+ returned just once, regardless of how many different relationships it has
+ with a model.
+
+ @param {Function} callback the callback to invoke
+ @param {any} binding the value to which the callback's `this` should be bound
+ */
+ eachRelatedType: function(callback, binding) {
+ get(this, 'relatedTypes').forEach(function(type) {
+ callback.call(binding, type);
+ });
+ }
+});
+
+DS.Model.reopen({
+ /**
+ Given a callback, iterates over each of the relationships in the model,
+ invoking the callback with the name of each relationship and its relationship
+ descriptor.
+
+ @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);
+ }
+});
+
+/**
+ @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.
+
+ 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.
+
+ For example, imagine the following two associated models:
+
+ App.Post = DS.Model.extend({
+ comments: DS.hasMany('App.Comment')
+ });
+
+ App.Comment = DS.Model.extend({
+ post: DS.belongsTo('App.Post')
+ });
+
+ If the `post` property of a `Comment` was modified, Ember Data would invoke
+ this helper like this:
+
+ DS._inverseNameFor(App.Comment, App.Post, 'hasMany');
+ //=> 'comments'
+
+ 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];
+};
+
+/**
+ @private
+
+ Given a model and a relationship name, returns the model type of
+ the named relationship.
+
+ App.Post = DS.Model.extend({
+ comments: DS.hasMany('App.Comment')
+ });
+
+ DS._inverseTypeFor(App.Post, 'comments');
+ //=> App.Comment
+ @param {DS.Model class} modelType
+ @param {String} relationshipName
+ @return {DS.Model class}
+*/
+DS._inverseTypeFor = function(modelType, relationshipName) {
+ var relationships = get(modelType, 'relationshipsByName'),
+ relationship = relationships.get(relationshipName);
+
+ if (relationship) { return relationship.type; }
+};
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set;
+var forEach = Ember.EnumerableUtils.forEach;
+
+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;
+};
+
+DS.RelationshipChangeAdd = function(options){
+ DS.RelationshipChange.call(this, options);
+};
+
+DS.RelationshipChangeRemove = function(options){
+ DS.RelationshipChange.call(this, options);
+};
+
+/** @private */
+DS.RelationshipChange.create = function(options) {
+ return new DS.RelationshipChange(options);
+};
+
+/** @private */
+DS.RelationshipChangeAdd.create = function(options) {
+ return new DS.RelationshipChangeAdd(options);
+};
+
+/** @private */
+DS.RelationshipChangeRemove.create = function(options) {
+ return new DS.RelationshipChangeRemove(options);
+};
+
+DS.OneToManyChange = {};
+DS.OneToNoneChange = {};
+DS.ManyToNoneChange = {};
+DS.OneToOneChange = {};
+DS.ManyToManyChange = {};
+
+DS.RelationshipChange._createChange = function(options){
+ if(options.changeType === "add"){
+ return DS.RelationshipChangeAdd.create(options);
+ }
+ if(options.changeType === "remove"){
+ return DS.RelationshipChangeRemove.create(options);
+ }
+};
+
+
+DS.RelationshipChange.determineRelationshipType = function(recordType, knownSide){
+ var knownKey = knownSide.key, key, 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";
+ }
+ }
+
+};
+
+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);
+ }
+};
+
+/** @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;
+};
+
+/** @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) {
+ // Get the type of the child based on the child's client ID
+ var childType = childReference.type, key;
+
+ // If the name of the belongsTo side of the relationship is specified,
+ // use that
+ // If the type of the parent is specified, look it up on the child's type
+ // definition.
+ 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
+ });
+
+ store.addRelationshipChangeFor(childReference, key, parentReference, null, change);
+
+
+ return change;
+};
+
+/** @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 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 change = DS.RelationshipChange._createChange({
+ parentReference: parentReference,
+ childReference: childReference,
+ firstRecordReference: childReference,
+ secondRecordReference: parentReference,
+ firstRecordKind: "belongsTo",
+ secondRecordKind: "belongsTo",
+ store: store,
+ changeType: options.changeType,
+ firstRecordName: key
+ });
+
+ store.addRelationshipChangeFor(childReference, key, parentReference, null, change);
+
+
+ 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();
+ }
+ }
+};
+
+/** @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;
+
+ // 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 change = DS.RelationshipChange._createChange({
+ parentReference: parentReference,
+ childReference: childReference,
+ firstRecordReference: childReference,
+ secondRecordReference: parentReference,
+ firstRecordKind: "belongsTo",
+ secondRecordKind: "hasMany",
+ store: store,
+ changeType: options.changeType,
+ firstRecordName: key
+ });
+
+ store.addRelationshipChangeFor(childReference, key, parentReference, null, change);
+
+
+ return change;
+};
+
+
+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
+ });
+ 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;
+ });
+};
+
+DS.RelationshipChange.prototype = {
+
+ getSecondRecordName: function() {
+ var name = this.secondRecordName, store = this.store, parent;
+
+ if (!name) {
+ parent = this.secondRecordReference;
+ if (!parent) { return; }
+
+ var childType = this.firstRecordReference.type;
+ var inverseType = DS._inverseTypeFor(childType, this.firstRecordName);
+ name = inverseHasManyName(inverseType, childType, this.firstRecordName);
+ this.secondRecordName = name;
+ }
+
+ return name;
+ },
+
+ /**
+ Get the name of the relationship on the belongsTo side.
+
+ @returns {String}
+ */
+ getFirstRecordName: function() {
+ var name = this.firstRecordName, store = this.store, parent, child;
+
+ if (!name) {
+ parent = this.secondRecordReference;
+ child = this.firstRecordReference;
+ if (!(child && parent)) { return; }
+
+ name = DS._inverseRelationshipFor(child.type, parent.type).name;
+
+ this.firstRecordName = name;
+ }
+
+ return name;
+ },
+
+ /** @private */
+ destroy: function() {
+ var childReference = this.childReference,
+ belongsToName = this.getFirstRecordName(),
+ hasManyName = this.getSecondRecordName(),
+ store = this.store,
+ child, oldParent, newParent, lastParent, transaction;
+
+ store.removeRelationshipChangeFor(childReference, belongsToName, this.parentReference, hasManyName, this.changeType);
+
+ if (transaction = this.transaction) {
+ transaction.relationshipBecameClean(this);
+ }
+ },
+
+ /** @private */
+ getByReference: function(reference) {
+ var store = this.store;
+
+ // return null or undefined if the original reference was null or undefined
+ if (!reference) { return reference; }
+
+ if (store.recordIsMaterialized(reference)) {
+ return store.recordForReference(reference);
+ }
+ },
+
+ 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 = this.store.ensureSameTransaction([child, parentRecord]);
+
+ this.transaction = transaction;
+ return transaction;
+ },
+
+ callChangeEvents: function(){
+ var hasManyName = this.getSecondRecordName(),
+ belongsToName = this.getFirstRecordName(),
+ child = this.getFirstRecord(),
+ parentRecord = this.getSecondRecord();
+
+ var dirtySet = new Ember.OrderedSet();
+
+ // 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);
+ }
+
+ if (child) {
+ this.store.recordBelongsToDidChange(dirtySet, child, this);
+ }
+
+ dirtySet.forEach(function(record) {
+ record.adapterDidDirty();
+ });
+ },
+
+ 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();
+ }
+ });
+ }
+};
+
+DS.RelationshipChangeAdd.prototype = Ember.create(DS.RelationshipChange.create({}));
+DS.RelationshipChangeRemove.prototype = Ember.create(DS.RelationshipChange.create({}));
+
+DS.RelationshipChangeAdd.prototype.changeType = "add";
+DS.RelationshipChangeAdd.prototype.sync = function() {
+ var secondRecordName = this.getSecondRecordName(),
+ firstRecordName = this.getFirstRecordName(),
+ firstRecord = this.getFirstRecord(),
+ secondRecord = this.getSecondRecord();
+
+ //Ember.assert("You specified a hasMany (" + hasManyName + ") on " + (!belongsToName && (newParent || oldParent || this.lastParent).constructor) + " but did not specify an inverse belongsTo on " + child.constructor, belongsToName);
+ //Ember.assert("You specified a belongsTo (" + belongsToName + ") on " + child.constructor + " but did not specify an inverse hasMany on " + (!hasManyName && (newParent || oldParent || this.lastParentRecord).constructor), hasManyName);
+
+ var transaction = this.ensureSameTransaction();
+ transaction.relationshipBecameDirty(this);
+
+ this.callChangeEvents();
+
+ if (secondRecord && firstRecord) {
+ if(this.secondRecordKind === "belongsTo"){
+ secondRecord.suspendRelationshipObservers(function(){
+ set(secondRecord, secondRecordName, firstRecord);
+ });
+
+ }
+ 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();
+
+ //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(firstRecord, secondRecord, secondRecordName, firstRecordName);
+ transaction.relationshipBecameDirty(this);
+
+ this.callChangeEvents();
+
+ 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);
+ });
+ }
+ }
+
+ 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);
+ });
+ }
+ }
+
+ this.coalesce();
+};
+
+function inverseBelongsToName(parentType, childType, hasManyName) {
+ // Get the options passed to the parent's DS.hasMany()
+ var options = parentType.metaForProperty(hasManyName).options;
+ var belongsToName;
+
+ if (belongsToName = options.inverse) {
+ return belongsToName;
+ }
+
+ return DS._inverseRelationshipFor(childType, parentType).name;
+}
+
+function inverseHasManyName(parentType, childType, belongsToName) {
+ var options = childType.metaForProperty(belongsToName).options;
+ var hasManyName;
+
+ if (hasManyName = options.inverse) {
+ return hasManyName;
+ }
+
+ return DS._inverseRelationshipFor(parentType, childType).name;
+}
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(function() {
+var set = Ember.set;
+
+/**
+ This code registers an injection for Ember.Application.
+
+ If an Ember.js developer defines a subclass of DS.Store on their application,
+ this code will automatically instantiate it and make it available on the
+ router.
+
+ Additionally, after an application's controllers have been injected, they will
+ each have the store made available to them.
+
+ For example, imagine an Ember.js application with the following classes:
+
+ App.Store = DS.Store.extend({
+ adapter: 'App.MyCustomAdapter'
+ });
+
+ App.PostsController = Ember.ArrayController.extend({
+ // ...
+ });
+
+ When the application is initialized, `App.Store` will automatically be
+ instantiated, and the instance of `App.PostsController` will have its `store`
+ property set to that instance.
+
+ Note that this code will only be run if the `ember-application` package is
+ loaded. If Ember Data is being used in an environment other than a
+ typical application (e.g., node.js where only `ember-runtime` is available),
+ this code will be ignored.
+*/
+
+Ember.onLoad('Ember.Application', function(Application) {
+ if (Application.registerInjection) {
+ Application.registerInjection({
+ name: "store",
+ before: "controllers",
+
+ // If a store subclass is defined, like App.Store,
+ // instantiate it and inject it into the router.
+ injection: function(app, stateManager, property) {
+ if (!stateManager) { return; }
+ if (property === 'Store') {
+ set(stateManager, 'store', app[property].create());
+ }
+ }
+ });
+
+ Application.registerInjection({
+ name: "giveStoreToControllers",
+ after: ['store','controllers'],
+
+ // For each controller, set its `store` property
+ // to the DS.Store instance we created above.
+ injection: function(app, stateManager, property) {
+ if (!stateManager) { return; }
+ if (/^[A-Z].*Controller$/.test(property)) {
+ var controllerName = property.charAt(0).toLowerCase() + property.substr(1);
+ var store = stateManager.get('store');
+ var controller = stateManager.get(controllerName);
+ if(!controller) { return; }
+
+ controller.set('store', store);
+ }
+ }
+ });
+ } else if (Application.initializer) {
+ Application.initializer({
+ name: "store",
+
+ initialize: function(container, application) {
+ application.register('store:main', application.Store);
+
+ // Eagerly generate the store so defaultStore is populated.
+ // TODO: Do this in a finisher hook
+ container.lookup('store:main');
+ }
+ });
+
+ Application.initializer({
+ name: "injectStore",
+
+ initialize: function(container, application) {
+ application.inject('controller', 'store', 'store:main');
+ application.inject('route', 'store', 'store:main');
+ }
+ });
+ }
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set, map = Ember.ArrayPolyfills.map, isNone = Ember.isNone;
+
+function mustImplement(name) {
+ return function() {
+ throw new Ember.Error("Your serializer " + this.toString() + " does not implement the required method " + name);
+ };
+}
+
+/**
+ A serializer is responsible for serializing and deserializing a group of
+ records.
+
+ `DS.Serializer` is an abstract base class designed to help you build a
+ serializer that can read to and write from any serialized form. While most
+ applications will use `DS.JSONSerializer`, which reads and writes JSON, the
+ serializer architecture allows your adapter to transmit things like XML,
+ strings, or custom binary data.
+
+ Typically, your application's `DS.Adapter` is responsible for both creating a
+ serializer as well as calling the appropriate methods when it needs to
+ materialize data or serialize a record.
+
+ The serializer API is designed as a series of layered hooks that you can
+ override to customize any of the individual steps of serialization and
+ deserialization.
+
+ The hooks are organized by the three responsibilities of the serializer:
+
+ 1. Determining naming conventions
+ 2. Serializing records into a serialized form
+ 3. Deserializing records from a serialized form
+
+ Because Ember Data lazily materializes records, the deserialization
+ step, and therefore the hooks you implement, are split into two phases:
+
+ 1. Extraction, where the serialized forms for multiple records are
+ extracted from a single payload. The IDs of each record are also
+ extracted for indexing.
+ 2. Materialization, where a newly-created record has its attributes
+ and relationships initialized based on the serialized form loaded
+ by the adapter.
+
+ Additionally, a serializer can convert values from their JavaScript
+ versions into their serialized versions via a declarative API.
+
+ ## Naming Conventions
+
+ One of the most common uses of the serializer is to map attribute names
+ from the serialized form to your `DS.Model`. For example, in your model,
+ you may have an attribute called `firstName`:
+
+ ```javascript
+ App.Person = DS.Model.extend({
+ firstName: DS.attr('string')
+ });
+ ```
+
+ However, because the web API your adapter is communicating with is
+ legacy, it calls this attribute `FIRST_NAME`.
+
+ You can determine the attribute name used in the serialized form
+ by implementing `keyForAttributeName`:
+
+ ```javascript
+ keyForAttributeName: function(type, name) {
+ return name.underscore.toUpperCase();
+ }
+ ```
+
+ If your attribute names are not predictable, you can re-map them
+ one-by-one using the adapter's `map` API:
+
+ ```javascript
+ App.Adapter.map('App.Person', {
+ firstName: { key: '*API_USER_FIRST_NAME*' }
+ });
+ ```
+
+ This API will also work for relationships and primary keys. For
+ example:
+
+ ```javascript
+ App.Adapter.map('App.Person', {
+ primaryKey: '_id'
+ });
+ ```
+
+ ## Serialization
+
+ During the serialization process, a record or records are converted
+ from Ember.js objects into their serialized form.
+
+ These methods are designed in layers, like a delicious 7-layer
+ cake (but with fewer layers).
+
+ The main entry point for serialization is the `serialize`
+ method, which takes the record and options.
+
+ The `serialize` method is responsible for:
+
+ * turning the record's attributes (`DS.attr`) into
+ attributes on the JSON object.
+ * optionally adding the record's ID onto the hash
+ * adding relationships (`DS.hasMany` and `DS.belongsTo`)
+ to the JSON object.
+
+ Depending on the backend, the serializer can choose
+ whether to include the `hasMany` or `belongsTo`
+ relationships on the JSON hash.
+
+ For very custom serialization, you can implement your
+ own `serialize` method. In general, however, you will want
+ to override the hooks described below.
+
+ ### Adding the ID
+
+ The default `serialize` will optionally call your serializer's
+ `addId` method with the JSON hash it is creating, the
+ record's type, and the record's ID. The `serialize` method
+ will not call `addId` if the record's ID is undefined.
+
+ Your adapter must specifically request ID inclusion by
+ passing `{ includeId: true }` as an option to `serialize`.
+
+ NOTE: You may not want to include the ID when updating an
+ existing record, because your server will likely disallow
+ changing an ID after it is created, and the PUT request
+ itself will include the record's identification.
+
+ By default, `addId` will:
+
+ 1. Get the primary key name for the record by calling
+ the serializer's `primaryKey` with the record's type.
+ Unless you override the `primaryKey` method, this
+ will be `'id'`.
+ 2. Assign the record's ID to the primary key in the
+ JSON hash being built.
+
+ If your backend expects a JSON object with the primary
+ key at the root, you can just override the `primaryKey`
+ method on your serializer subclass.
+
+ Otherwise, you can override the `addId` method for
+ more specialized handling.
+
+ ### Adding Attributes
+
+ By default, the serializer's `serialize` method will call
+ `addAttributes` with the JSON object it is creating
+ and the record to serialize.
+
+ The `addAttributes` method will then call `addAttribute`
+ in turn, with the JSON object, the record to serialize,
+ the attribute's name and its type.
+
+ Finally, the `addAttribute` method will serialize the
+ attribute:
+
+ 1. It will call `keyForAttributeName` to determine
+ the key to use in the JSON hash.
+ 2. It will get the value from the record.
+ 3. It will call `serializeValue` with the attribute's
+ value and attribute type to convert it into a
+ JSON-compatible value. For example, it will convert a
+ Date into a String.
+
+ If your backend expects a JSON object with attributes as
+ keys at the root, you can just override the `serializeValue`
+ and `keyForAttributeName` methods in your serializer
+ subclass and let the base class do the heavy lifting.
+
+ If you need something more specialized, you can probably
+ override `addAttribute` and let the default `addAttributes`
+ handle the nitty gritty.
+
+ ### Adding Relationships
+
+ By default, `serialize` will call your serializer's
+ `addRelationships` method with the JSON object that is
+ being built and the record being serialized. The default
+ implementation of this method is to loop over all of the
+ relationships defined on your record type and:
+
+ * If the relationship is a `DS.hasMany` relationship,
+ call `addHasMany` with the JSON object, the record
+ and a description of the relationship.
+ * If the relationship is a `DS.belongsTo` relationship,
+ call `addBelongsTo` with the JSON object, the record
+ and a description of the relationship.
+
+ The relationship description has the following keys:
+
+ * `type`: the class of the associated information (the
+ first parameter to `DS.hasMany` or `DS.belongsTo`)
+ * `kind`: either `hasMany` or `belongsTo`
+
+ The relationship description may get additional
+ information in the future if more capabilities or
+ relationship types are added. However, it will
+ remain backwards-compatible, so the mere existence
+ of new features should not break existing adapters.
+*/
+DS.Serializer = Ember.Object.extend({
+ init: function() {
+ this.mappings = Ember.Map.create();
+ this.configurations = Ember.Map.create();
+ this.globalConfigurations = {};
+ },
+
+ extract: mustImplement('extract'),
+ extractMany: mustImplement('extractMany'),
+
+ extractRecordRepresentation: function(loader, type, json, shouldSideload) {
+ var mapping = this.mappingForType(type);
+ var embeddedData, prematerialized = {}, reference;
+
+ if (shouldSideload) {
+ reference = loader.sideload(type, json);
+ } else {
+ reference = loader.load(type, json);
+ }
+
+ this.eachEmbeddedHasMany(type, function(name, relationship) {
+ var embeddedData = json[this.keyFor(relationship)];
+ if (!isNone(embeddedData)) {
+ this.extractEmbeddedHasMany(loader, relationship, embeddedData, reference, prematerialized);
+ }
+ }, this);
+
+ this.eachEmbeddedBelongsTo(type, function(name, relationship) {
+ var embeddedData = json[this.keyFor(relationship)];
+ if (!isNone(embeddedData)) {
+ this.extractEmbeddedBelongsTo(loader, relationship, embeddedData, reference, prematerialized);
+ }
+ }, this);
+
+ loader.prematerialize(reference, prematerialized);
+
+ return reference;
+ },
+
+ extractEmbeddedHasMany: function(loader, relationship, array, parent, prematerialized) {
+ var references = map.call(array, function(item) {
+ if (!item) { return; }
+
+ var reference = this.extractRecordRepresentation(loader, relationship.type, item, true);
+
+ // If the embedded record should also be saved back when serializing the parent,
+ // make sure we set its parent since it will not have an ID.
+ var embeddedType = this.embeddedType(parent.type, relationship.key);
+ if (embeddedType === 'always') {
+ reference.parent = parent;
+ }
+
+ return reference;
+ }, this);
+
+ prematerialized[relationship.key] = references;
+ },
+
+ extractEmbeddedBelongsTo: function(loader, relationship, data, parent, prematerialized) {
+ var reference = this.extractRecordRepresentation(loader, relationship.type, data, true);
+ prematerialized[relationship.key] = reference;
+
+ // If the embedded record should also be saved back when serializing the parent,
+ // make sure we set its parent since it will not have an ID.
+ var embeddedType = this.embeddedType(parent.type, relationship.key);
+ if (embeddedType === 'always') {
+ reference.parent = parent;
+ }
+ },
+
+ //.......................
+ //. SERIALIZATION HOOKS
+ //.......................
+
+ /**
+ The main entry point for serializing a record. While you can consider this
+ a hook that can be overridden in your serializer, you will have to manually
+ handle serialization. For most cases, there are more granular hooks that you
+ can override.
+
+ If overriding this method, these are the responsibilities that you will need
+ to implement yourself:
+
+ * If the option hash contains `includeId`, add the record's ID to the serialized form.
+ By default, `serialize` calls `addId` if appropriate.
+ * Add the record's attributes to the serialized form. By default, `serialize` calls
+ `addAttributes`.
+ * Add the record's relationships to the serialized form. By default, `serialize` calls
+ `addRelationships`.
+
+ @param {DS.Model} record the record to serialize
+ @param {Object} [options] a hash of options
+ @returns {any} the serialized form of the record
+ */
+ serialize: function(record, options) {
+ options = options || {};
+
+ var serialized = this.createSerializedForm(), id;
+
+ if (options.includeId) {
+ if (id = get(record, 'id')) {
+ this._addId(serialized, record.constructor, id);
+ }
+ }
+
+ this.addAttributes(serialized, record);
+ this.addRelationships(serialized, record);
+
+ return serialized;
+ },
+
+ /**
+ @private
+
+ Given an attribute type and value, convert the value into the
+ serialized form using the transform registered for that type.
+
+ @param {any} value the value to convert to the serialized form
+ @param {String} attributeType the registered type (e.g. `string`
+ or `boolean`)
+ @returns {any} the serialized form of the value
+ */
+ serializeValue: function(value, attributeType) {
+ var transform = this.transforms ? this.transforms[attributeType] : null;
+
+ Ember.assert("You tried to use an attribute type (" + attributeType + ") that has not been registered", transform);
+ return transform.serialize(value);
+ },
+
+ /**
+ A hook you can use to normalize IDs before adding them to the
+ serialized representation.
+
+ Because the store coerces all IDs to strings for consistency,
+ this is the opportunity for the serializer to, for example,
+ convert numerical IDs back into number form.
+
+ @param {String} id the id from the record
+ @returns {any} the serialized representation of the id
+ */
+ serializeId: function(id) {
+ if (isNaN(id)) { return id; }
+ return +id;
+ },
+
+ /**
+ A hook you can use to change how attributes are added to the serialized
+ representation of a record.
+
+ By default, `addAttributes` simply loops over all of the attributes of the
+ passed record, maps the attribute name to the key for the serialized form,
+ and invokes any registered transforms on the value. It then invokes the
+ more granular `addAttribute` with the key and transformed value.
+
+ Since you can override `keyForAttributeName`, `addAttribute`, and register
+ custom tranforms, you should rarely need to override this hook.
+
+ @param {any} data the serialized representation that is being built
+ @param {DS.Model} record the record to serialize
+ */
+ addAttributes: function(data, record) {
+ record.eachAttribute(function(name, attribute) {
+ this._addAttribute(data, record, name, attribute.type);
+ }, this);
+ },
+
+ /**
+ A hook you can use to customize how the key/value pair is added to
+ the serialized data.
+
+ @param {any} serialized the serialized form being built
+ @param {String} key the key to add to the serialized data
+ @param {any} value the value to add to the serialized data
+ */
+ addAttribute: Ember.K,
+
+ /**
+ A hook you can use to customize how the record's id is added to
+ the serialized data.
+
+ The `addId` hook is called with:
+
+ * the serialized representation being built
+ * the resolved primary key (taking configurations and the
+ `primaryKey` hook into consideration)
+ * the serialized id (after calling the `serializeId` hook)
+
+ @param {any} data the serialized representation that is being built
+ @param {String} key the resolved primary key
+ @param {id} id the serialized id
+ */
+ addId: Ember.K,
+
+ /**
+ A hook you can use to change how relationships are added to the serialized
+ representation of a record.
+
+ By default, `addAttributes` loops over all of the relationships of the
+ passed record, maps the relationship names to the key for the serialized form,
+ and then invokes the public `addBelongsTo` and `addHasMany` hooks.
+
+ Since you can override `keyForBelongsTo`, `keyForHasMany`, `addBelongsTo`,
+ `addHasMany`, and register mappings, you should rarely need to override this
+ hook.
+
+ @param {any} data the serialized representation that is being built
+ @param {DS.Model} record the record to serialize
+ */
+ addRelationships: function(data, record) {
+ record.eachRelationship(function(name, relationship) {
+ if (relationship.kind === 'belongsTo') {
+ this._addBelongsTo(data, record, name, relationship);
+ } else if (relationship.kind === 'hasMany') {
+ this._addHasMany(data, record, name, relationship);
+ }
+ }, this);
+ },
+
+ /**
+ A hook you can use to add a `belongsTo` relationship to the
+ serialized representation.
+
+ The specifics of this hook are very adapter-specific, so there
+ is no default implementation. You can see `DS.JSONSerializer`
+ for an example of an implementation of the `addBelongsTo` hook.
+
+ The `belongsTo` relationship object has the following properties:
+
+ * **type** a subclass of DS.Model that is the type of the
+ relationship. This is the first parameter to DS.belongsTo
+ * **options** the options passed to the call to DS.belongsTo
+ * **kind** always `belongsTo`
+
+ Additional properties may be added in the future.
+
+ @param {any} data the serialized representation that is being built
+ @param {DS.Model} record the record to serialize
+ @param {String} key the key for the serialized object
+ @param {Object} relationship an object representing the relationship
+ */
+ addBelongsTo: Ember.K,
+
+ /**
+ A hook you can use to add a `hasMany` relationship to the
+ serialized representation.
+
+ The specifics of this hook are very adapter-specific, so there
+ is no default implementation. You may not need to implement this,
+ for example, if your backend only expects relationships on the
+ child of a one to many relationship.
+
+ The `hasMany` relationship object has the following properties:
+
+ * **type** a subclass of DS.Model that is the type of the
+ relationship. This is the first parameter to DS.hasMany
+ * **options** the options passed to the call to DS.hasMany
+ * **kind** always `hasMany`
+
+ Additional properties may be added in the future.
+
+ @param {any} data the serialized representation that is being built
+ @param {DS.Model} record the record to serialize
+ @param {String} key the key for the serialized object
+ @param {Object} relationship an object representing the relationship
+ */
+ addHasMany: Ember.K,
+
+ /**
+ NAMING CONVENTIONS
+
+ The most commonly overridden APIs of the serializer are
+ the naming convention methods:
+
+ * `keyForAttributeName`: converts a camelized attribute name
+ into a key in the adapter-provided data hash. For example,
+ if the model's attribute name was `firstName`, and the
+ server used underscored names, you would return `first_name`.
+ * `primaryKey`: returns the key that should be used to
+ extract the id from the adapter-provided data hash. It is
+ also used when serializing a record.
+ */
+
+ /**
+ A hook you can use in your serializer subclass to customize
+ how an unmapped attribute name is converted into a key.
+
+ By default, this method returns the `name` parameter.
+
+ For example, if the attribute names in your JSON are underscored,
+ you will want to convert them into JavaScript conventional
+ camelcase:
+
+ ```javascript
+ App.MySerializer = DS.Serializer.extend({
+ // ...
+
+ keyForAttributeName: function(type, name) {
+ return name.camelize();
+ }
+ });
+ ```
+
+ @param {DS.Model subclass} type the type of the record with
+ the attribute name `name`
+ @param {String} name the attribute name to convert into a key
+
+ @returns {String} the key
+ */
+ keyForAttributeName: function(type, name) {
+ return name;
+ },
+
+ /**
+ A hook you can use in your serializer to specify a conventional
+ primary key.
+
+ By default, this method will return the string `id`.
+
+ In general, you should not override this hook to specify a special
+ primary key for an individual type; use `configure` instead.
+
+ For example, if your primary key is always `__id__`:
+
+ ```javascript
+ App.MySerializer = DS.Serializer.extend({
+ // ...
+ primaryKey: function(type) {
+ return '__id__';
+ }
+ });
+ ```
+
+ In another example, if the primary key always includes the
+ underscored version of the type before the string `id`:
+
+ ```javascript
+ App.MySerializer = DS.Serializer.extend({
+ // ...
+ primaryKey: function(type) {
+ // If the type is `BlogPost`, this will return
+ // `blog_post_id`.
+ var typeString = type.toString().split(".")[1].underscore();
+ return typeString + "_id";
+ }
+ });
+ ```
+
+ @param {DS.Model subclass} type
+ @returns {String} the primary key for the type
+ */
+ primaryKey: function(type) {
+ return "id";
+ },
+
+ /**
+ A hook you can use in your serializer subclass to customize
+ how an unmapped `belongsTo` relationship is converted into
+ a key.
+
+ By default, this method calls `keyForAttributeName`, so if
+ your naming convention is uniform across attributes and
+ relationships, you can use the default here and override
+ just `keyForAttributeName` as needed.
+
+ For example, if the `belongsTo` names in your JSON always
+ begin with `BT_` (e.g. `BT_posts`), you can strip out the
+ `BT_` prefix:"
+
+ ```javascript
+ App.MySerializer = DS.Serializer.extend({
+ // ...
+ keyForBelongsTo: function(type, name) {
+ return name.match(/^BT_(.*)$/)[1].camelize();
+ }
+ });
+ ```
+
+ @param {DS.Model subclass} type the type of the record with
+ the `belongsTo` relationship.
+ @param {String} name the relationship name to convert into a key
+
+ @returns {String} the key
+ */
+ keyForBelongsTo: function(type, name) {
+ return this.keyForAttributeName(type, name);
+ },
+
+ /**
+ A hook you can use in your serializer subclass to customize
+ how an unmapped `hasMany` relationship is converted into
+ a key.
+
+ By default, this method calls `keyForAttributeName`, so if
+ your naming convention is uniform across attributes and
+ relationships, you can use the default here and override
+ just `keyForAttributeName` as needed.
+
+ For example, if the `hasMany` names in your JSON always
+ begin with the "table name" for the current type (e.g.
+ `post_comments`), you can strip out the prefix:"
+
+ ```javascript
+ App.MySerializer = DS.Serializer.extend({
+ // ...
+ keyForHasMany: function(type, name) {
+ // if your App.BlogPost has many App.BlogComment, the key from
+ // the server would look like: `blog_post_blog_comments`
+ //
+ // 1. Convert the type into a string and underscore the
+ // second part (App.BlogPost -> blog_post)
+ // 2. Extract the part after `blog_post_` (`blog_comments`)
+ // 3. Underscore it, to become `blogComments`
+ var typeString = type.toString().split(".")[1].underscore();
+ return name.match(new RegExp("^" + typeString + "_(.*)$"))[1].camelize();
+ }
+ });
+ ```
+
+ @param {DS.Model subclass} type the type of the record with
+ the `belongsTo` relationship.
+ @param {String} name the relationship name to convert into a key
+
+ @returns {String} the key
+ */
+ keyForHasMany: function(type, name) {
+ return this.keyForAttributeName(type, name);
+ },
+
+ //.........................
+ //. MATERIALIZATION HOOKS
+ //.........................
+
+ materialize: function(record, serialized, prematerialized) {
+ var id;
+ if (Ember.isNone(get(record, 'id'))) {
+ if (prematerialized && prematerialized.hasOwnProperty('id')) {
+ id = prematerialized.id;
+ } else {
+ id = this.extractId(record.constructor, serialized);
+ }
+ record.materializeId(id);
+ }
+
+ this.materializeAttributes(record, serialized, prematerialized);
+ this.materializeRelationships(record, serialized, prematerialized);
+ },
+
+ deserializeValue: function(value, attributeType) {
+ var transform = this.transforms ? this.transforms[attributeType] : null;
+
+ Ember.assert("You tried to use a attribute type (" + attributeType + ") that has not been registered", transform);
+ return transform.deserialize(value);
+ },
+
+ materializeAttributes: function(record, serialized, prematerialized) {
+ record.eachAttribute(function(name, attribute) {
+ if (prematerialized && prematerialized.hasOwnProperty(name)) {
+ record.materializeAttribute(name, prematerialized[name]);
+ } else {
+ this.materializeAttribute(record, serialized, name, attribute.type);
+ }
+ }, this);
+ },
+
+ materializeAttribute: function(record, serialized, attributeName, attributeType) {
+ var value = this.extractAttribute(record.constructor, serialized, attributeName);
+ value = this.deserializeValue(value, attributeType);
+
+ record.materializeAttribute(attributeName, value);
+ },
+
+ materializeRelationships: function(record, hash, prematerialized) {
+ record.eachRelationship(function(name, relationship) {
+ if (relationship.kind === 'hasMany') {
+ if (prematerialized && prematerialized.hasOwnProperty(name)) {
+ record.materializeHasMany(name, prematerialized[name]);
+ } else {
+ this.materializeHasMany(name, record, hash, relationship, prematerialized);
+ }
+ } else if (relationship.kind === 'belongsTo') {
+ if (prematerialized && prematerialized.hasOwnProperty(name)) {
+ record.materializeBelongsTo(name, prematerialized[name]);
+ } else {
+ this.materializeBelongsTo(name, record, hash, relationship, prematerialized);
+ }
+ }
+ }, this);
+ },
+
+ materializeHasMany: function(name, record, hash, relationship) {
+ var key = this._keyForHasMany(record.constructor, relationship.key);
+ record.materializeHasMany(name, this.extractHasMany(record.constructor, hash, key));
+ },
+
+ materializeBelongsTo: function(name, record, hash, relationship) {
+ var key = this._keyForBelongsTo(record.constructor, relationship.key);
+ record.materializeBelongsTo(name, this.extractBelongsTo(record.constructor, hash, key));
+ },
+
+ _extractEmbeddedRelationship: function(type, hash, name, relationshipType) {
+ var key = this['_keyFor' + relationshipType](type, name);
+
+ if (this.embeddedType(type, name)) {
+ return this['extractEmbedded' + relationshipType](type, hash, key);
+ }
+ },
+
+ _extractEmbeddedBelongsTo: function(type, hash, name) {
+ return this._extractEmbeddedRelationship(type, hash, name, 'BelongsTo');
+ },
+
+ _extractEmbeddedHasMany: function(type, hash, name) {
+ return this._extractEmbeddedRelationship(type, hash, name, 'HasMany');
+ },
+
+ /**
+ @private
+
+ This method is called to get the primary key for a given
+ type.
+
+ If a primary key configuration exists for this type, this
+ method will return the configured value. Otherwise, it will
+ call the public `primaryKey` hook.
+
+ @param {DS.Model subclass} type
+ @returns {String} the primary key for the type
+ */
+ _primaryKey: function(type) {
+ var config = this.configurationForType(type),
+ primaryKey = config && config.primaryKey;
+
+ if (primaryKey) {
+ return primaryKey;
+ } else {
+ return this.primaryKey(type);
+ }
+ },
+
+ /**
+ @private
+
+ This method looks up the key for the attribute name and transforms the
+ attribute's value using registered transforms.
+
+ Specifically:
+
+ 1. Look up the key for the attribute name. If available, this will use
+ any registered mappings. Otherwise, it will invoke the public
+ `keyForAttributeName` hook.
+ 2. Get the value from the record using the `attributeName`.
+ 3. Transform the value using registered transforms for the `attributeType`.
+ 4. Invoke the public `addAttribute` hook with the hash, key, and
+ transformed value.
+
+ @param {any} data the serialized representation being built
+ @param {DS.Model} record the record to serialize
+ @param {String} attributeName the name of the attribute on the record
+ @param {String} attributeType the type of the attribute (e.g. `string`
+ or `boolean`)
+ */
+ _addAttribute: function(data, record, attributeName, attributeType) {
+ var key = this._keyForAttributeName(record.constructor, attributeName);
+ var value = get(record, attributeName);
+
+ this.addAttribute(data, key, this.serializeValue(value, attributeType));
+ },
+
+ /**
+ @private
+
+ This method looks up the primary key for the `type` and invokes
+ `serializeId` on the `id`.
+
+ It then invokes the public `addId` hook with the primary key and
+ the serialized id.
+
+ @param {any} data the serialized representation that is being built
+ @param {Ember.Model subclass} type
+ @param {any} id the materialized id from the record
+ */
+ _addId: function(hash, type, id) {
+ var primaryKey = this._primaryKey(type);
+
+ this.addId(hash, primaryKey, this.serializeId(id));
+ },
+
+ /**
+ @private
+
+ This method is called to get a key used in the data from
+ an attribute name. It first checks for any mappings before
+ calling the public hook `keyForAttributeName`.
+
+ @param {DS.Model subclass} type the type of the record with
+ the attribute name `name`
+ @param {String} name the attribute name to convert into a key
+
+ @returns {String} the key
+ */
+ _keyForAttributeName: function(type, name) {
+ return this._keyFromMappingOrHook('keyForAttributeName', type, name);
+ },
+
+ /**
+ @private
+
+ This method is called to get a key used in the data from
+ a belongsTo relationship. It first checks for any mappings before
+ calling the public hook `keyForBelongsTo`.
+
+ @param {DS.Model subclass} type the type of the record with
+ the `belongsTo` relationship.
+ @param {String} name the relationship name to convert into a key
+
+ @returns {String} the key
+ */
+ _keyForBelongsTo: function(type, name) {
+ return this._keyFromMappingOrHook('keyForBelongsTo', type, name);
+ },
+
+ keyFor: function(description) {
+ var type = description.parentType,
+ name = description.key;
+
+ switch (description.kind) {
+ case 'belongsTo':
+ return this._keyForBelongsTo(type, name);
+ case 'hasMany':
+ return this._keyForHasMany(type, name);
+ }
+ },
+
+ /**
+ @private
+
+ This method is called to get a key used in the data from
+ a hasMany relationship. It first checks for any mappings before
+ calling the public hook `keyForHasMany`.
+
+ @param {DS.Model subclass} type the type of the record with
+ the `hasMany` relationship.
+ @param {String} name the relationship name to convert into a key
+
+ @returns {String} the key
+ */
+ _keyForHasMany: function(type, name) {
+ return this._keyFromMappingOrHook('keyForHasMany', type, name);
+ },
+ /**
+ @private
+
+ This method converts the relationship name to a key for serialization,
+ and then invokes the public `addBelongsTo` hook.
+
+ @param {any} data the serialized representation that is being built
+ @param {DS.Model} record the record to serialize
+ @param {String} name the relationship name
+ @param {Object} relationship an object representing the relationship
+ */
+ _addBelongsTo: function(data, record, name, relationship) {
+ var key = this._keyForBelongsTo(record.constructor, name);
+ this.addBelongsTo(data, record, key, relationship);
+ },
+
+ /**
+ @private
+
+ This method converts the relationship name to a key for serialization,
+ and then invokes the public `addHasMany` hook.
+
+ @param {any} data the serialized representation that is being built
+ @param {DS.Model} record the record to serialize
+ @param {String} name the relationship name
+ @param {Object} relationship an object representing the relationship
+ */
+ _addHasMany: function(data, record, name, relationship) {
+ var key = this._keyForHasMany(record.constructor, name);
+ this.addHasMany(data, record, key, relationship);
+ },
+
+ /**
+ @private
+
+ An internal method that handles checking whether a mapping
+ exists for a particular attribute or relationship name before
+ calling the public hooks.
+
+ If a mapping is found, and the mapping has a key defined,
+ use that instead of invoking the hook.
+
+ @param {String} publicMethod the public hook to invoke if
+ a mapping is not found (e.g. `keyForAttributeName`)
+ @param {DS.Model subclass} type the type of the record with
+ the attribute or relationship name.
+ @param {String} name the attribute or relationship name to
+ convert into a key
+ */
+ _keyFromMappingOrHook: function(publicMethod, type, name) {
+ var key = this.mappingOption(type, name, 'key');
+
+ if (key) {
+ return key;
+ } else {
+ return this[publicMethod](type, name);
+ }
+ },
+
+ /**
+ TRANSFORMS
+ */
+
+ registerTransform: function(type, transform) {
+ this.transforms[type] = transform;
+ },
+
+ registerEnumTransform: function(type, objects) {
+ var transform = {
+ deserialize: function(deserialized) {
+ return Ember.A(objects).objectAt(deserialized);
+ },
+ serialize: function(serialized) {
+ return Ember.EnumerableUtils.indexOf(objects, serialized);
+ },
+ values: objects
+ };
+ this.registerTransform(type, transform);
+ },
+
+ /**
+ MAPPING CONVENIENCE
+ */
+
+ map: function(type, mappings) {
+ this.mappings.set(type, mappings);
+ },
+
+ configure: function(type, configuration) {
+ if (type && !configuration) {
+ Ember.merge(this.globalConfigurations, type);
+ return;
+ }
+
+ var config = Ember.create(this.globalConfigurations);
+ Ember.merge(config, configuration);
+
+ this.configurations.set(type, config);
+ },
+
+ mappingForType: function(type) {
+ this._reifyMappings();
+ return this.mappings.get(type) || {};
+ },
+
+ configurationForType: function(type) {
+ this._reifyConfigurations();
+ return this.configurations.get(type) || this.globalConfigurations;
+ },
+
+ _reifyMappings: function() {
+ if (this._didReifyMappings) { return; }
+
+ var mappings = this.mappings,
+ reifiedMappings = Ember.Map.create();
+
+ mappings.forEach(function(key, mapping) {
+ if (typeof key === 'string') {
+ var type = Ember.get(Ember.lookup, key);
+ Ember.assert("Could not find model at path " + key, type);
+
+ reifiedMappings.set(type, mapping);
+ } else {
+ reifiedMappings.set(key, mapping);
+ }
+ });
+
+ this.mappings = reifiedMappings;
+
+ this._didReifyMappings = true;
+ },
+
+ _reifyConfigurations: function() {
+ if (this._didReifyConfigurations) { return; }
+
+ var configurations = this.configurations,
+ reifiedConfigurations = Ember.Map.create();
+
+ configurations.forEach(function(key, mapping) {
+ if (typeof key === 'string' && key !== 'plurals') {
+ var type = Ember.get(Ember.lookup, key);
+ Ember.assert("Could not find model at path " + key, type);
+
+ reifiedConfigurations.set(type, mapping);
+ } else {
+ reifiedConfigurations.set(key, mapping);
+ }
+ });
+
+ this.configurations = reifiedConfigurations;
+
+ this._didReifyConfigurations = true;
+ },
+
+ mappingOption: function(type, name, option) {
+ var mapping = this.mappingForType(type)[name];
+
+ return mapping && mapping[option];
+ },
+
+ configOption: function(type, option) {
+ var config = this.configurationForType(type);
+
+ return config[option];
+ },
+
+ // EMBEDDED HELPERS
+
+ embeddedType: function(type, name) {
+ return this.mappingOption(type, name, 'embedded');
+ },
+
+ eachEmbeddedRecord: function(record, callback, binding) {
+ this.eachEmbeddedBelongsToRecord(record, callback, binding);
+ this.eachEmbeddedHasManyRecord(record, callback, binding);
+ },
+
+ eachEmbeddedBelongsToRecord: function(record, callback, binding) {
+ var type = record.constructor;
+
+ this.eachEmbeddedBelongsTo(record.constructor, function(name, relationship, embeddedType) {
+ var embeddedRecord = get(record, name);
+ if (embeddedRecord) { callback.call(binding, embeddedRecord, embeddedType); }
+ });
+ },
+
+ eachEmbeddedHasManyRecord: function(record, callback, binding) {
+ var type = record.constructor;
+
+ this.eachEmbeddedHasMany(record.constructor, function(name, relationship, embeddedType) {
+ var array = get(record, name);
+ for (var i=0, l=get(array, 'length'); i<l; i++) {
+ callback.call(binding, array.objectAt(i), embeddedType);
+ }
+ });
+ },
+
+ eachEmbeddedHasMany: function(type, callback, binding) {
+ this.eachEmbeddedRelationship(type, 'hasMany', callback, binding);
+ },
+
+ eachEmbeddedBelongsTo: function(type, callback, binding) {
+ this.eachEmbeddedRelationship(type, 'belongsTo', callback, binding);
+ },
+
+ eachEmbeddedRelationship: function(type, kind, callback, binding) {
+ type.eachRelationship(function(name, relationship) {
+ var embeddedType = this.embeddedType(type, name);
+
+ if (embeddedType) {
+ if (relationship.kind === kind) {
+ callback.call(binding, name, relationship, embeddedType);
+ }
+ }
+ }, this);
+ }
+});
+
+
+})();
+
+
+
+(function() {
+var none = Ember.isNone;
+
+/**
+ DS.Transforms is a hash of transforms used by DS.Serializer.
+*/
+DS.JSONTransforms = {
+ string: {
+ deserialize: function(serialized) {
+ return none(serialized) ? null : String(serialized);
+ },
+
+ serialize: function(deserialized) {
+ return none(deserialized) ? null : String(deserialized);
+ }
+ },
+
+ number: {
+ deserialize: function(serialized) {
+ return none(serialized) ? null : Number(serialized);
+ },
+
+ serialize: function(deserialized) {
+ return none(deserialized) ? null : Number(deserialized);
+ }
+ },
+
+ // Handles the following boolean inputs:
+ // "TrUe", "t", "f", "FALSE", 0, (non-zero), or boolean true/false
+ 'boolean': {
+ deserialize: function(serialized) {
+ var type = typeof serialized;
+
+ if (type === "boolean") {
+ return serialized;
+ } else if (type === "string") {
+ return serialized.match(/^true$|^t$|^1$/i) !== null;
+ } else if (type === "number") {
+ return serialized === 1;
+ } else {
+ return false;
+ }
+ },
+
+ serialize: function(deserialized) {
+ return Boolean(deserialized);
+ }
+ },
+
+ date: {
+ deserialize: function(serialized) {
+ var type = typeof serialized;
+ var date = null;
+
+ if (type === "string" || type === "number") {
+ // this is a fix for Safari 5.1.5 on Mac which does not accept timestamps as yyyy-mm-dd
+ if (type === "string" && serialized.search(/^\d{4}-\d{2}-\d{2}$/) !== -1) {
+ serialized += "T00:00:00Z";
+ }
+
+ date = new Date(serialized);
+
+ // this is a fix for IE8 which does not accept timestamps in ISO 8601 format
+ if (type === "string" && isNaN(date)) {
+ date = new Date(Date.parse(serialized.replace(/\-/ig, '/').replace(/Z$/, '').split('.')[0]));
+ }
+
+ return date;
+ } else if (serialized === null || serialized === undefined) {
+ // if the value is not present in the data,
+ // return undefined, not null.
+ return serialized;
+ } else {
+ return null;
+ }
+ },
+
+ serialize: function(date) {
+ if (date instanceof Date) {
+ var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+ var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+
+ var pad = function(num) {
+ return num < 10 ? "0"+num : ""+num;
+ };
+
+ var utcYear = date.getUTCFullYear(),
+ utcMonth = date.getUTCMonth(),
+ utcDayOfMonth = date.getUTCDate(),
+ utcDay = date.getUTCDay(),
+ utcHours = date.getUTCHours(),
+ utcMinutes = date.getUTCMinutes(),
+ utcSeconds = date.getUTCSeconds();
+
+
+ var dayOfWeek = days[utcDay];
+ var dayOfMonth = pad(utcDayOfMonth);
+ var month = months[utcMonth];
+
+ return dayOfWeek + ", " + dayOfMonth + " " + month + " " + utcYear + " " +
+ pad(utcHours) + ":" + pad(utcMinutes) + ":" + pad(utcSeconds) + " GMT";
+ } else if (date === undefined) {
+ return undefined;
+ } else {
+ return null;
+ }
+ }
+ }
+};
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set;
+
+var generatedId = 0;
+
+DS.JSONSerializer = DS.Serializer.extend({
+ init: function() {
+ this._super();
+
+ if (!get(this, 'transforms')) {
+ this.set('transforms', DS.JSONTransforms);
+ }
+
+ this.sideloadMapping = Ember.Map.create();
+
+ this.configure({
+ meta: 'meta',
+ since: 'since'
+ });
+ },
+
+ configure: function(type, configuration) {
+ if (type && !configuration) {
+ return this._super(type);
+ }
+
+ var sideloadAs = configuration.sideloadAs;
+
+ if (sideloadAs) {
+ this.sideloadMapping.set(sideloadAs, type);
+
+ // Set a flag indicating that mappings may need to be normalized
+ // (i.e. converted from strings -> 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;
+
+ delete configuration.sideloadAs;
+ }
+
+ this._super.apply(this, arguments);
+ },
+
+ addId: function(data, key, id) {
+ data[key] = id;
+ },
+
+ /**
+ A hook you can use to customize how the key/value pair is added to
+ the serialized data.
+
+ @param {any} hash the JSON hash being built
+ @param {String} key the key to add to the serialized data
+ @param {any} value the value to add to the serialized data
+ */
+ addAttribute: function(hash, key, value) {
+ 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];
+ },
+
+ extractId: function(type, hash) {
+ var primaryKey = this._primaryKey(type);
+
+ if (hash.hasOwnProperty(primaryKey)) {
+ // Ensure that we coerce IDs to strings so that record
+ // IDs remain consistent between application runs; especially
+ // if the ID is serialized and later deserialized from the URL,
+ // when type information will have been lost.
+ return hash[primaryKey]+'';
+ } else {
+ return null;
+ }
+ },
+
+ extractHasMany: function(type, hash, key) {
+ return hash[key];
+ },
+
+ extractBelongsTo: function(type, hash, key) {
+ return hash[key];
+ },
+
+ addBelongsTo: function(hash, record, key, relationship) {
+ var type = record.constructor,
+ name = relationship.key,
+ value = null,
+ embeddedChild;
+
+ if (this.embeddedType(type, name)) {
+ if (embeddedChild = get(record, name)) {
+ value = this.serialize(embeddedChild, { includeId: true });
+ }
+
+ hash[key] = value;
+ } else {
+ var id = get(record, relationship.key+'.id');
+ if (!Ember.isNone(id)) { hash[key] = id; }
+ }
+ },
+
+ /**
+ Adds a has-many relationship to the JSON hash being built.
+
+ The default REST semantics are to only add a has-many relationship if it
+ is embedded. If the relationship was initially loaded by ID, we assume that
+ that was done as a performance optimization, and that changes to the
+ has-many should be saved as foreign key changes on the child's belongs-to
+ relationship.
+
+ @param {Object} hash the JSON being built
+ @param {DS.Model} record the record being serialized
+ @param {String} key the JSON key into which the serialized relationship
+ 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 = [],
+ manyArray, embeddedType;
+
+ // If the has-many is not embedded, there is nothing to do.
+ embeddedType = this.embeddedType(type, name);
+ if (embeddedType !== 'always') { return; }
+
+ // Get the DS.ManyArray for the relationship off the record
+ manyArray = get(record, name);
+
+ // Build up the array of serialized records
+ manyArray.forEach(function (record) {
+ serializedHasMany.push(this.serialize(record, { includeId: true }));
+ }, this);
+
+ // Set the appropriate property of the serialized JSON to the
+ // array of serialized embedded records
+ hash[key] = serializedHasMany;
+ },
+
+ // EXTRACTION
+
+ extract: function(loader, json, type, record) {
+ var root = this.rootForType(type);
+
+ this.sideload(loader, type, json, root);
+ this.extractMeta(loader, type, json);
+
+ if (json[root]) {
+ if (record) { loader.updateId(record, json[root]); }
+ this.extractRecordRepresentation(loader, type, json[root]);
+ }
+ },
+
+ extractMany: function(loader, json, type, records) {
+ var root = this.rootForType(type);
+ root = this.pluralize(root);
+
+ this.sideload(loader, type, json, root);
+ this.extractMeta(loader, type, json);
+
+ if (json[root]) {
+ var objects = json[root], references = [];
+ if (records) { records = records.toArray(); }
+
+ for (var i = 0; i < objects.length; i++) {
+ if (records) { loader.updateId(records[i], objects[i]); }
+ var reference = this.extractRecordRepresentation(loader, type, objects[i]);
+ references.push(reference);
+ }
+
+ loader.populateArray(references);
+ }
+ },
+
+ extractMeta: function(loader, type, json) {
+ var meta = json[this.configOption(type, 'meta')], since;
+ if (!meta) { return; }
+
+ if (since = meta[this.configOption(type, 'since')]) {
+ loader.sinceForType(type, since);
+ }
+ },
+
+ /**
+ @private
+
+ Iterates over the `json` payload and attempts to load any data
+ included alongside `root`.
+
+ The keys expected for sideloaded data are based upon the types related
+ to the root model. Recursion is used to ensure that types related to
+ related types can be loaded as well. Any custom keys specified by
+ `sideloadAs` mappings will also be respected.
+
+ @param {DS.Store subclass} loader
+ @param {DS.Model subclass} type
+ @param {Object} json
+ @param {String} root
+ */
+ 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')) {
+ 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);
+
+ 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
+
+ Configures possible sideload mappings for the types related to a
+ particular model. This recursive method ensures that sideloading
+ works for related models as well.
+
+ @param {DS.Model subclass} type
+ @param {Ember.A} configured an array of types that have already been configured
+ */
+ configureSideloadMappingForType: function(type, configured) {
+ if (!configured) {configured = Ember.A([]);}
+ configured.pushObject(type);
+
+ type.eachRelatedType(function(relatedType) {
+ if (!configured.contains(relatedType)) {
+ var root = this.sideloadMappingForType(relatedType);
+ if (!root) {
+ root = this.defaultSideloadRootForType(relatedType);
+ this.sideloadMapping.set(root, relatedType);
+ }
+ this.configureSideloadMappingForType(relatedType, configured);
+ }
+ }, this);
+ },
+
+ loadValue: function(loader, type, value) {
+ if (value instanceof Array) {
+ for (var i=0; i < value.length; i++) {
+ loader.sideload(type, value[i]);
+ }
+ } else {
+ loader.sideload(type, value);
+ }
+ },
+
+ // HELPERS
+
+ // 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";
+ },
+
+ // 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;
+ }
+ },
+
+ /**
+ @private
+
+ Determines the singular root name for a particular type.
+
+ This is an underscored, lowercase version of the model name.
+ For example, the type `App.UserGroup` will have the root
+ `user_group`.
+
+ @param {DS.Model subclass} type
+ @returns {String} name of the root element
+ */
+ rootForType: function(type) {
+ var typeString = type.toString();
+
+ Ember.assert("Your model must not be anonymous. It was " + type, typeString.charAt(0) !== '(');
+
+ // use the last part of the name as the URL
+ var parts = typeString.split(".");
+ var name = parts[parts.length - 1];
+ 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
+ */
+ defaultSideloadRootForType: function(type) {
+ return this.pluralize(this.rootForType(type));
+ }
+});
+
+})();
+
+
+
+(function() {
+function loaderFor(store) {
+ return {
+ load: function(type, data, prematerialized) {
+ return store.load(type, data, prematerialized);
+ },
+
+ loadMany: function(type, array) {
+ return store.loadMany(type, array);
+ },
+
+ updateId: function(record, data) {
+ return store.updateId(record, data);
+ },
+
+ populateArray: Ember.K,
+
+ sideload: function(type, data) {
+ return store.load(type, data);
+ },
+
+ sideloadMany: function(type, array) {
+ return store.loadMany(type, array);
+ },
+
+ prematerialize: function(reference, prematerialized) {
+ store.prematerialize(reference, prematerialized);
+ },
+
+ sinceForType: function(type, since) {
+ store.sinceForType(type, since);
+ }
+ };
+}
+
+DS.loaderFor = loaderFor;
+
+/**
+ An adapter is an object that receives requests from a store and
+ translates them into the appropriate action to take against your
+ persistence layer. The persistence layer is usually an HTTP API, but may
+ be anything, such as the browser's local storage.
+
+ ### Creating an Adapter
+
+ First, create a new subclass of `DS.Adapter`:
+
+ App.MyAdapter = DS.Adapter.extend({
+ // ...your code here
+ });
+
+ To tell your store which adapter to use, set its `adapter` property:
+
+ App.store = DS.Store.create({
+ revision: 3,
+ adapter: App.MyAdapter.create()
+ });
+
+ `DS.Adapter` is an abstract base class that you should override in your
+ application to customize it for your backend. The minimum set of methods
+ that you should implement is:
+
+ * `find()`
+ * `createRecord()`
+ * `updateRecord()`
+ * `deleteRecord()`
+
+ 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;
+
+DS.Adapter = Ember.Object.extend(DS._Mappable, {
+
+ init: function() {
+ var serializer = get(this, 'serializer');
+
+ if (Ember.Object.detect(serializer)) {
+ serializer = serializer.create();
+ set(this, 'serializer', serializer);
+ }
+
+ this._attributesMap = this.createInstanceMapFor('attributes');
+ this._configurationsMap = this.createInstanceMapFor('configurations');
+
+ this._outstandingOperations = new Ember.MapWithDefault({
+ defaultValue: function() { return 0; }
+ });
+
+ this._dependencies = new Ember.MapWithDefault({
+ defaultValue: function() { return new Ember.OrderedSet(); }
+ });
+
+ this.registerSerializerTransforms(this.constructor, serializer, {});
+ this.registerSerializerMappings(serializer);
+ },
+
+ /**
+ Loads a payload for a record into the store.
+
+ This method asks the serializer to break the payload into
+ constituent parts, and then loads them into the store. For example,
+ if you have a payload that contains embedded records, they will be
+ extracted by the serializer and loaded into the store.
+
+ For example:
+
+ ```javascript
+ 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`.
+
+ @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);
+ },
+
+ /**
+ Acknowledges that the adapter has finished creating a record.
+
+ Your adapter should call this method from `createRecord` when
+ it has saved a new record to its persistent storage and 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.
+
+ 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.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} record
+ @param {any} payload
+ */
+ didCreateRecord: function(store, type, record, payload) {
+ store.didSaveRecord(record);
+
+ if (payload) {
+ var loader = DS.loaderFor(store);
+
+ loader.load = function(type, data, prematerialized) {
+ store.updateId(record, data);
+ return store.load(type, data, prematerialized);
+ };
+
+ get(this, 'serializer').extract(loader, payload, type);
+ }
+ },
+
+ /**
+ 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
+ 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.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} record
+ @param {any} payload
+ */
+ didCreateRecords: function(store, type, records, payload) {
+ records.forEach(function(record) {
+ store.didSaveRecord(record);
+ }, this);
+
+ if (payload) {
+ var loader = DS.loaderFor(store);
+ get(this, 'serializer').extractMany(loader, payload, type, records);
+ }
+ },
+
+ /**
+ @private
+
+ Acknowledges that the adapter has finished updating or deleting a record.
+
+ Your adapter should call this method from `updateRecord` or `deleteRecord`
+ when it has updated or deleted a record to its persistent storage and
+ received an acknowledgement.
+
+ If the persistent storage returns a new payload in response to the
+ update or delete, and you want to update the existing record with the
+ new information, pass the payload as the fourth parameter.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} record
+ @param {any} payload
+ */
+ didSaveRecord: function(store, type, record, payload) {
+ store.didSaveRecord(record);
+
+ var serializer = get(this, 'serializer'),
+ mappings = serializer.mappingForType(type);
+
+ serializer.eachEmbeddedRecord(record, function(embeddedRecord, embeddedType) {
+ if (embeddedType === 'load') { return; }
+
+ this.didSaveRecord(store, embeddedRecord.constructor, embeddedRecord);
+ }, this);
+
+ if (payload) {
+ var loader = DS.loaderFor(store);
+ serializer.extract(loader, payload, type);
+ }
+ },
+
+ /**
+ Acknowledges that the adapter has finished updating a record.
+
+ Your adapter should call this method from `updateRecord` when it
+ has updated a record to its persistent storage and received an
+ acknowledgement.
+
+ If the persistent storage returns a new payload in response to the
+ update, pass the payload as the fourth parameter.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} record
+ @param {any} payload
+ */
+ didUpdateRecord: function() {
+ this.didSaveRecord.apply(this, arguments);
+ },
+
+ /**
+ Acknowledges that the adapter has finished deleting a record.
+
+ Your adapter should call this method from `deleteRecord` when it
+ has deleted a record from its persistent storage and received an
+ acknowledgement.
+
+ If the persistent storage returns a new payload in response to the
+ deletion, pass the payload as the fourth parameter.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} record
+ @param {any} payload
+ */
+ didDeleteRecord: function() {
+ this.didSaveRecord.apply(this, arguments);
+ },
+
+ /**
+ Acknowledges that the adapter has finished updating or deleting
+ multiple records.
+
+ Your adapter should call this method from its `updateRecords` or
+ `deleteRecords` when it has updated or deleted multiple records
+ to its persistent storage and received an acknowledgement.
+
+ If the persistent storage returns a new payload in response to the
+ creation, pass the payload as the fourth parameter.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} records
+ @param {any} payload
+ */
+ didSaveRecords: function(store, type, records, payload) {
+ records.forEach(function(record) {
+ store.didSaveRecord(record);
+ }, this);
+
+ if (payload) {
+ var loader = DS.loaderFor(store);
+ get(this, 'serializer').extractMany(loader, payload, type);
+ }
+ },
+
+ /**
+ Acknowledges that the adapter has finished updating multiple records.
+
+ Your adapter should call this method from its `updateRecords` when
+ it has updated multiple records to its persistent storage and
+ received an acknowledgement.
+
+ If the persistent storage returns a new payload in response to the
+ update, pass the payload as the fourth parameter.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} records
+ @param {any} payload
+ */
+ didUpdateRecords: function() {
+ this.didSaveRecords.apply(this, arguments);
+ },
+
+ /**
+ Acknowledges that the adapter has finished updating multiple records.
+
+ Your adapter should call this method from its `deleteRecords` when
+ it has deleted multiple records to its persistent storage and
+ received an acknowledgement.
+
+ If the persistent storage returns a new payload in response to the
+ deletion, pass the payload as the fourth parameter.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} records
+ @param {any} payload
+ */
+ didDeleteRecords: function() {
+ this.didSaveRecords.apply(this, arguments);
+ },
+
+ /**
+ Loads the response to a request for a record by ID.
+
+ Your adapter should call this method from its `find` method
+ with the response from the backend.
+
+ You should pass the same ID to this method that was given
+ to your find method so that the store knows which record
+ to associate the new data with.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {any} payload
+ @param {String} id
+ */
+ didFindRecord: function(store, type, payload, id) {
+ var loader = DS.loaderFor(store);
+
+ loader.load = function(type, data, prematerialized) {
+ prematerialized = prematerialized || {};
+ prematerialized.id = id;
+
+ return store.load(type, data, prematerialized);
+ };
+
+ get(this, 'serializer').extract(loader, payload, type);
+ },
+
+ /**
+ Loads the response to a request for all records by type.
+
+ You adapter should call this method from its `findAll`
+ method with the response from the backend.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {any} payload
+ */
+ didFindAll: function(store, type, payload) {
+ var loader = DS.loaderFor(store),
+ serializer = get(this, 'serializer');
+
+ store.didUpdateAll(type);
+
+ serializer.extractMany(loader, payload, type);
+ },
+
+ /**
+ Loads the response to a request for records by query.
+
+ Your adapter should call this method from its `findQuery`
+ method with the response from the backend.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {any} payload
+ @param {DS.AdapterPopulatedRecordArray} recordArray
+ */
+ didFindQuery: function(store, type, payload, recordArray) {
+ var loader = DS.loaderFor(store);
+
+ loader.populateArray = function(data) {
+ recordArray.load(data);
+ };
+
+ get(this, 'serializer').extractMany(loader, payload, type);
+ },
+
+ /**
+ Loads the response to a request for many records by ID.
+
+ You adapter should call this method from its `findMany`
+ method with the response from the backend.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {any} payload
+ */
+ didFindMany: function(store, type, payload) {
+ var loader = DS.loaderFor(store);
+
+ get(this, 'serializer').extractMany(loader, payload, type);
+ },
+
+ /**
+ Notifies the store that a request to the backend returned
+ an error.
+
+ Your adapter should call this method to indicate that the
+ backend returned an error for a request.
+
+ @param {DS.Store} store
+ @param {subclass of DS.Model} type
+ @param {DS.Model} record
+ */
+ didError: function(store, type, record) {
+ store.recordWasError(record);
+ },
+
+ dirtyRecordsForAttributeChange: function(dirtySet, record, attributeName, newValue, oldValue) {
+ if (newValue !== oldValue) {
+ // If this record is embedded, add its parent
+ // to the dirty set.
+ this.dirtyRecordsForRecordChange(dirtySet, record);
+ }
+ },
+
+ dirtyRecordsForRecordChange: function(dirtySet, record) {
+ dirtySet.add(record);
+ },
+
+ dirtyRecordsForBelongsToChange: function(dirtySet, child) {
+ this.dirtyRecordsForRecordChange(dirtySet, child);
+ },
+
+ dirtyRecordsForHasManyChange: function(dirtySet, parent) {
+ this.dirtyRecordsForRecordChange(dirtySet, parent);
+ },
+
+ /**
+ @private
+
+ This method recursively climbs the superclass hierarchy and
+ registers any class-registered transforms on the adapter's
+ serializer.
+
+ Once it registers a transform for a given type, it ignores
+ subsequent transforms for the same attribute type.
+
+ @param {Class} klass the DS.Adapter subclass to extract the
+ transforms from
+ @param {DS.Serializer} serializer the serializer to register
+ the transforms onto
+ @param {Object} seen a hash of attributes already seen
+ */
+ registerSerializerTransforms: function(klass, serializer, seen) {
+ var transforms = klass._registeredTransforms, superclass, prop;
+
+ for (prop in transforms) {
+ if (!transforms.hasOwnProperty(prop) || prop in seen) { continue; }
+ seen[prop] = true;
+
+ serializer.registerTransform(prop, transforms[prop]);
+ }
+
+ if (superclass = klass.superclass) {
+ this.registerSerializerTransforms(superclass, serializer, seen);
+ }
+ },
+
+ /**
+ @private
+
+ This method recursively climbs the superclass hierarchy and
+ registers any class-registered mappings on the adapter's
+ serializer.
+
+ @param {Class} klass the DS.Adapter subclass to extract the
+ transforms from
+ @param {DS.Serializer} serializer the serializer to register the
+ mappings onto
+ */
+ registerSerializerMappings: function(serializer) {
+ var mappings = this._attributesMap,
+ configurations = this._configurationsMap;
+
+ mappings.forEach(serializer.map, serializer);
+ configurations.forEach(serializer.configure, serializer);
+ },
+
+ /**
+ The `find()` method is invoked when the store is asked for a record that
+ has not previously been loaded. In response to `find()` being called, you
+ should query your persistence layer for a record with the given ID. Once
+ found, you can asynchronously call the store's `load()` method to load
+ the record.
+
+ Here is an example `find` implementation:
+
+ 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);
+ });
+ }
+ */
+ find: null,
+
+ serializer: DS.JSONSerializer,
+
+ registerTransform: function(attributeType, transform) {
+ get(this, 'serializer').registerTransform(attributeType, transform);
+ },
+
+ /**
+ 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.
+
+ Eg: Say you want to utilize "low","medium","high" text strings
+ 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']);
+
+ You would then refer to the 'priority' DS.attr in your model:
+ 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} }
+
+ @param {String} type of the transform
+ @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) {
+ get(this, 'serializer').registerEnumTransform(attributeType, objects);
+ },
+
+ /**
+ If the globally unique IDs for your records should be generated on the client,
+ implement the `generateIdForRecord()` method. This method will be invoked
+ each time you create a new record, and the value returned from it will be
+ assigned to the record's `primaryKey`.
+
+ Most traditional REST-like HTTP APIs will not use this method. Instead, the ID
+ of the record will be set by the server, and your adapter will update the store
+ with the new ID when it calls `didCreateRecord()`. Only implement this method if
+ you intend to generate record IDs on the client-side.
+
+ The `generateIdForRecord()` method will be invoked with the requesting store as
+ the first parameter and the newly created record as the second parameter:
+
+ generateIdForRecord: function(store, record) {
+ var uuid = App.generateUUIDWithStatisticallyLowOddsOfCollision();
+ return uuid;
+ }
+ */
+ generateIdForRecord: null,
+
+ materialize: function(record, data, prematerialized) {
+ get(this, 'serializer').materialize(record, data, prematerialized);
+ },
+
+ serialize: function(record, options) {
+ return get(this, 'serializer').serialize(record, options);
+ },
+
+ extractId: function(type, data) {
+ return get(this, 'serializer').extractId(type, data);
+ },
+
+ groupByType: function(enumerable) {
+ var map = Ember.MapWithDefault.create({
+ defaultValue: function() { return Ember.OrderedSet.create(); }
+ });
+
+ enumerable.forEach(function(item) {
+ map.get(item.constructor).add(item);
+ });
+
+ return map;
+ },
+
+ commit: function(store, commitDetails) {
+ this.save(store, commitDetails);
+ },
+
+ save: function(store, commitDetails) {
+ var adapter = this;
+
+ function filter(records) {
+ var filteredSet = Ember.OrderedSet.create();
+
+ records.forEach(function(record) {
+ if (adapter.shouldSave(record)) {
+ filteredSet.add(record);
+ }
+ });
+
+ return filteredSet;
+ }
+
+ this.groupByType(commitDetails.created).forEach(function(type, set) {
+ this.createRecords(store, type, filter(set));
+ }, this);
+
+ this.groupByType(commitDetails.updated).forEach(function(type, set) {
+ this.updateRecords(store, type, filter(set));
+ }, this);
+
+ this.groupByType(commitDetails.deleted).forEach(function(type, set) {
+ this.deleteRecords(store, type, filter(set));
+ }, this);
+ },
+
+ shouldSave: Ember.K,
+
+ createRecords: function(store, type, records) {
+ records.forEach(function(record) {
+ this.createRecord(store, type, record);
+ }, this);
+ },
+
+ updateRecords: function(store, type, records) {
+ records.forEach(function(record) {
+ this.updateRecord(store, type, record);
+ }, this);
+ },
+
+ deleteRecords: function(store, type, records) {
+ records.forEach(function(record) {
+ this.deleteRecord(store, type, record);
+ }, this);
+ },
+
+ findMany: function(store, type, ids) {
+ ids.forEach(function(id) {
+ this.find(store, type, id);
+ }, this);
+ }
+});
+
+DS.Adapter.reopenClass({
+ registerTransform: function(attributeType, transform) {
+ var registeredTransforms = this._registeredTransforms || {};
+
+ registeredTransforms[attributeType] = transform;
+
+ this._registeredTransforms = registeredTransforms;
+ },
+
+ map: DS._Mappable.generateMapFunctionFor('attributes', function(key, newValue, map) {
+ var existingValue = map.get(key);
+
+ merge(existingValue, newValue);
+ }),
+
+ configure: DS._Mappable.generateMapFunctionFor('configurations', function(key, newValue, map) {
+ var existingValue = map.get(key);
+
+ // If a mapping configuration is provided, peel it off and apply it
+ // using the DS.Adapter.map API.
+ var mappings = newValue && newValue.mappings;
+ if (mappings) {
+ this.map(key, mappings);
+ delete newValue.mappings;
+ }
+
+ merge(existingValue, newValue);
+ }),
+
+ resolveMapConflict: function(oldValue, newValue, mappingsKey) {
+ merge(newValue, oldValue);
+
+ return newValue;
+ }
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get, set = Ember.set;
+
+DS.FixtureSerializer = DS.Serializer.extend({
+ deserializeValue: function(value, attributeType) {
+ return value;
+ },
+
+ serializeValue: function(value, attributeType) {
+ return value;
+ },
+
+ addId: function(data, key, id) {
+ data[key] = id;
+ },
+
+ addAttribute: function(hash, key, value) {
+ hash[key] = value;
+ },
+
+ addBelongsTo: function(hash, record, key, relationship) {
+ var id = get(record, relationship.key+'.id');
+ if (!Ember.isNone(id)) { hash[key] = id; }
+ },
+
+ 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 {};
+ },
+
+ extract: function(loader, fixture, type, record) {
+ if (record) { loader.updateId(record, fixture); }
+ this.extractRecordRepresentation(loader, type, fixture);
+ },
+
+ extractMany: function(loader, fixtures, type, records) {
+ var objects = fixtures, references = [];
+ if (records) { records = records.toArray(); }
+
+ for (var i = 0; i < objects.length; i++) {
+ if (records) { loader.updateId(records[i], objects[i]); }
+ var reference = this.extractRecordRepresentation(loader, type, objects[i]);
+ references.push(reference);
+ }
+
+ loader.populateArray(references);
+ },
+
+ extractId: function(type, hash) {
+ var primaryKey = this._primaryKey(type);
+
+ if (hash.hasOwnProperty(primaryKey)) {
+ // Ensure that we coerce IDs to strings so that record
+ // IDs remain consistent between application runs; especially
+ // if the ID is serialized and later deserialized from the URL,
+ // when type information will have been lost.
+ return hash[primaryKey]+'';
+ } else {
+ return null;
+ }
+ },
+
+ extractAttribute: function(type, hash, attributeName) {
+ var key = this._keyForAttributeName(type, attributeName);
+ return hash[key];
+ },
+
+ extractHasMany: function(type, hash, key) {
+ return hash[key];
+ },
+
+ extractBelongsTo: function(type, hash, key) {
+ return hash[key];
+ }
+});
+
+})();
+
+
+
+(function() {
+var get = Ember.get, fmt = Ember.String.fmt,
+ dump = Ember.get(window, 'JSON.stringify') || function(object) { return object.toString(); };
+
+/**
+ `DS.FixtureAdapter` is an adapter that loads records from memory.
+ Its primarily used for development and testing. You can also use
+ `DS.FixtureAdapter` while working on the API but are not ready to
+ integrate yet. It is a fully functioning adapter. All CRUD methods
+ are implemented. You can also implement query logic that a remote
+ system would do. Its possible to do develop your entire application
+ with `DS.FixtureAdapter`.
+
+*/
+DS.FixtureAdapter = DS.Adapter.extend({
+
+ simulateRemoteResponse: true,
+
+ latency: 50,
+
+ serializer: DS.FixtureSerializer,
+
+ /*
+ Implement this method in order to provide data associated with a type
+ */
+ fixturesForType: function(type) {
+ 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)]));
+ }
+ fixture.id = fixture.id + '';
+ return fixture;
+ });
+ }
+ return null;
+ },
+
+ /*
+ Implement this method in order to query fixtures data
+ */
+ queryFixtures: function(fixtures, query, type) {
+ Ember.assert('Not implemented: You must override the DS.FixtureAdapter::queryFixtures method to support querying the fixture store.');
+ },
+
+ updateFixtures: function(type, fixture) {
+ if(!type.FIXTURES) {
+ type.FIXTURES = [];
+ }
+
+ var fixtures = type.FIXTURES;
+
+ this.deleteLoadedFixture(type, fixture);
+
+ fixtures.push(fixture);
+ },
+
+ /*
+ Implement this method in order to provide provide json for CRUD methods
+ */
+ mockJSON: function(type, record) {
+ return this.serialize(record, { includeId: true });
+ },
+
+ /*
+ Adapter methods
+ */
+ generateIdForRecord: function(store, record) {
+ return Ember.guidFor(record);
+ },
+
+ find: function(store, type, id) {
+ var fixtures = this.fixturesForType(type),
+ fixture;
+
+ Ember.warn("Unable to find fixtures for model type " + type.toString(), fixtures);
+
+ if (fixtures) {
+ fixture = Ember.A(fixtures).findProperty('id', id);
+ }
+
+ if (fixture) {
+ this.simulateRemoteCall(function() {
+ this.didFindRecord(store, type, fixture, id);
+ }, this);
+ }
+ },
+
+ findMany: function(store, type, ids) {
+ var fixtures = this.fixturesForType(type);
+
+ Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures);
+
+ if (fixtures) {
+ fixtures = fixtures.filter(function(item) {
+ return ids.indexOf(item.id) !== -1;
+ });
+ }
+
+ if (fixtures) {
+ this.simulateRemoteCall(function() {
+ this.didFindMany(store, type, fixtures);
+ }, this);
+ }
+ },
+
+ findAll: function(store, type) {
+ var fixtures = this.fixturesForType(type);
+
+ Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures);
+
+ this.simulateRemoteCall(function() {
+ this.didFindAll(store, type, fixtures);
+ }, this);
+ },
+
+ findQuery: function(store, type, query, array) {
+ var fixtures = this.fixturesForType(type);
+
+ Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures);
+
+ fixtures = this.queryFixtures(fixtures, query, type);
+
+ if (fixtures) {
+ this.simulateRemoteCall(function() {
+ this.didFindQuery(store, type, fixtures, array);
+ }, this);
+ }
+ },
+
+ createRecord: function(store, type, record) {
+ var fixture = this.mockJSON(type, record);
+
+ this.updateFixtures(type, fixture);
+
+ this.simulateRemoteCall(function() {
+ this.didCreateRecord(store, type, record, fixture);
+ }, this);
+ },
+
+ updateRecord: function(store, type, record) {
+ var fixture = this.mockJSON(type, record);
+
+ this.updateFixtures(type, fixture);
+
+ this.simulateRemoteCall(function() {
+ this.didUpdateRecord(store, type, record, fixture);
+ }, this);
+ },
+
+ deleteRecord: function(store, type, record) {
+ var fixture = this.mockJSON(type, record);
+
+ this.deleteLoadedFixture(type, fixture);
+
+ this.simulateRemoteCall(function() {
+ this.didDeleteRecord(store, type, record);
+ }, this);
+ },
+
+ /*
+ @private
+ */
+ deleteLoadedFixture: function(type, record) {
+ var id = this.extractId(type, record);
+
+ var existingFixture = this.findExistingFixture(type, record);
+
+ if(existingFixture) {
+ var index = type.FIXTURES.indexOf(existingFixture);
+ type.FIXTURES.splice(index, 1);
+ return true;
+ }
+ },
+
+ findExistingFixture: function(type, record) {
+ var fixtures = this.fixturesForType(type);
+ var id = this.extractId(type, record);
+
+ return this.findFixtureById(fixtures, id);
+ },
+
+ findFixtureById: function(fixtures, id) {
+ var adapter = this;
+
+ return Ember.A(fixtures).find(function(r) {
+ if(''+get(r, 'id') === ''+id) {
+ return true;
+ } else {
+ return false;
+ }
+ });
+ },
+
+ simulateRemoteCall: function(callback, context) {
+ if (get(this, 'simulateRemoteResponse')) {
+ // Schedule with setTimeout
+ Ember.run.later(context, callback, get(this, 'latency'));
+ } else {
+ // Asynchronous, but at the of the runloop with zero latency
+ Ember.run.once(context, callback);
+ }
+ }
+});
+
+})();
+
+
+
+(function() {
+DS.RESTSerializer = DS.JSONSerializer.extend({
+ keyForAttributeName: function(type, name) {
+ return Ember.String.decamelize(name);
+ },
+
+ keyForBelongsTo: function(type, name) {
+ var key = this.keyForAttributeName(type, name);
+
+ if (this.embeddedType(type, name)) {
+ return key;
+ }
+
+ return key + "_id";
+ },
+
+ keyForHasMany: function(type, name) {
+ var key = this.keyForAttributeName(type, name);
+
+ if (this.embeddedType(type, name)) {
+ return key;
+ }
+
+ return this.singularize(key) + "_ids";
+ }
+});
+
+})();
+
+
+
+(function() {
+/*global jQuery*/
+
+var get = Ember.get, set = Ember.set, merge = Ember.merge;
+
+/**
+ The REST adapter allows your store to communicate with an HTTP server by
+ transmitting JSON via XHR. Most Ember.js apps that consume a JSON API
+ should use the REST adapter.
+
+ This adapter is designed around the idea that the JSON exchanged with
+ the server should be conventional.
+
+ ## JSON Structure
+
+ The REST adapter expects the JSON returned from your server to follow
+ these conventions.
+
+ ### Object Root
+
+ The JSON payload should be an object that contains the record inside a
+ root property. For example, in response to a `GET` request for
+ `/posts/1`, the JSON should look like this:
+
+ ```js
+ {
+ "post": {
+ title: "I'm Running to Reform the W3C's Tag",
+ author: "Yehuda Katz"
+ }
+ }
+ ```
+
+ ### Conventional Names
+
+ Attribute names in your JSON payload should be the underscored versions of
+ the attributes in your Ember.js models.
+
+ For example, if you have a `Person` model:
+
+ ```js
+ App.Person = DS.Model.extend({
+ firstName: DS.attr('string'),
+ lastName: DS.attr('string'),
+ occupation: DS.attr('string')
+ });
+ ```
+
+ The JSON returned should look like this:
+
+ ```js
+ {
+ "person": {
+ "first_name": "Barack",
+ "last_name": "Obama",
+ "occupation": "President"
+ }
+ }
+ ```
+*/
+DS.RESTAdapter = DS.Adapter.extend({
+ bulkCommit: false,
+ since: 'since',
+
+ serializer: DS.RESTSerializer,
+
+ init: function() {
+ this._super.apply(this, arguments);
+ },
+
+ shouldSave: function(record) {
+ var reference = get(record, '_reference');
+
+ 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);
+ },
+
+ dirtyRecordsForHasManyChange: function(dirtySet, record, relationship) {
+ var embeddedType = get(this, 'serializer').embeddedType(record.constructor, relationship.secondRecordName);
+
+ if (embeddedType === 'always') {
+ relationship.childReference.parent = relationship.parentReference;
+ this._dirtyTree(dirtySet, record);
+ }
+ },
+
+ _dirtyTree: function(dirtySet, record) {
+ dirtySet.add(record);
+
+ get(this, 'serializer').eachEmbeddedRecord(record, function(embeddedRecord, embeddedType) {
+ if (embeddedType !== 'always') { return; }
+ if (dirtySet.has(embeddedRecord)) { return; }
+ this._dirtyTree(dirtySet, embeddedRecord);
+ }, this);
+
+ var reference = record.get('_reference');
+
+ if (reference.parent) {
+ var store = get(record, 'store');
+ var parent = store.recordForReference(reference.parent);
+ this._dirtyTree(dirtySet, parent);
+ }
+ },
+
+ createRecords: function(store, type, records) {
+ if (get(this, 'bulkCommit') === false) {
+ return this._super(store, type, records);
+ }
+
+ var root = this.rootForType(type),
+ plural = this.pluralize(root);
+
+ var data = {};
+ data[plural] = [];
+ records.forEach(function(record) {
+ 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);
+ });
+ }
+ });
+ },
+
+ updateRecord: function(store, type, record) {
+ var id = get(record, 'id');
+ var root = this.rootForType(type);
+
+ var 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);
+ }
+ });
+ },
+
+ updateRecords: function(store, type, records) {
+ if (get(this, 'bulkCommit') === false) {
+ return this._super(store, type, records);
+ }
+
+ var root = this.rootForType(type),
+ plural = this.pluralize(root);
+
+ 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);
+ });
+ }
+ });
+ },
+
+ deleteRecord: function(store, type, record) {
+ var id = get(record, 'id');
+ var root = this.rootForType(type);
+
+ this.ajax(this.buildURL(root, id), "DELETE", {
+ context: this,
+ success: function(json) {
+ Ember.run(this, function(){
+ this.didSaveRecord(store, type, record, json);
+ });
+ }
+ });
+ },
+
+ deleteRecords: function(store, type, records) {
+ if (get(this, 'bulkCommit') === false) {
+ return this._super(store, type, records);
+ }
+
+ var root = this.rootForType(type),
+ plural = this.pluralize(root),
+ serializer = get(this, 'serializer');
+
+ 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);
+ });
+ }
+ });
+ },
+
+ find: function(store, type, id) {
+ var root = this.rootForType(type);
+
+ this.ajax(this.buildURL(root, id), "GET", {
+ success: function(json) {
+ Ember.run(this, function(){
+ this.didFindRecord(store, type, json, id);
+ });
+ }
+ });
+ },
+
+ findAll: function(store, type, since) {
+ var root = this.rootForType(type);
+
+ this.ajax(this.buildURL(root), "GET", {
+ data: this.sinceQuery(since),
+ success: function(json) {
+ Ember.run(this, function(){
+ this.didFindAll(store, type, json);
+ });
+ }
+ });
+ },
+
+ findQuery: function(store, type, query, recordArray) {
+ var root = this.rootForType(type);
+
+ this.ajax(this.buildURL(root), "GET", {
+ data: query,
+ success: function(json) {
+ Ember.run(this, function(){
+ this.didFindQuery(store, type, json, recordArray);
+ });
+ }
+ });
+ },
+
+ findMany: function(store, type, ids, owner) {
+ var root = this.rootForType(type);
+ 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);
+ });
+ }
+ });
+ },
+
+ /**
+ @private
+
+ This method serializes a list of IDs using `serializeId`
+
+ @returns {Array} an array of serialized IDs
+ */
+ serializeIds: function(ids) {
+ var serializer = get(this, 'serializer');
+
+ return Ember.EnumerableUtils.map(ids, function(id) {
+ return serializer.serializeId(id);
+ });
+ },
+
+ didError: function(store, type, record, xhr) {
+ if (xhr.status === 422) {
+ var data = JSON.parse(xhr.responseText);
+ store.recordWasInvalid(record, data['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;
+
+ if (hash.data && type !== 'GET') {
+ hash.data = JSON.stringify(hash.data);
+ }
+
+ jQuery.ajax(hash);
+ },
+
+ url: "",
+
+ rootForType: function(type) {
+ var serializer = get(this, 'serializer');
+ return serializer.rootForType(type);
+ },
+
+ pluralize: function(string) {
+ var serializer = get(this, 'serializer');
+ return serializer.pluralize(string);
+ },
+
+ buildURL: function(record, suffix) {
+ var url = [this.url];
+
+ Ember.assert("Namespace URL (" + this.namespace + ") must not start with slash", !this.namespace || this.namespace.toString().charAt(0) !== "/");
+ 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) {
+ url.push(this.namespace);
+ }
+
+ url.push(this.pluralize(record));
+ if (suffix !== undefined) {
+ url.push(suffix);
+ }
+
+ return url.join("/");
+ },
+
+ sinceQuery: function(since) {
+ var query = {};
+ query[get(this, 'since')] = since;
+ return since ? query : null;
+ }
+});
+
+
+})();
+
+
+
+(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 CreateProcessor(record, store, type) {
+ this.record = record;
+ ObjectProcessor.call(this, record.toJSON(), type, store);
+}
+
+CreateProcessor.prototype = Ember.create(ObjectProcessor.prototype);
+
+CreateProcessor.prototype.save = function() {};
+
+function createProcessorFactory(store, type) {
+ return function(record) {
+ return new CreateProcessor(record, store, type);
+ };
+}
+
+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, createProcessorFactory(store, type));
+ }
+});
+
+DS.registerTransforms = function(kind, object) {
+ registeredTransforms[kind] = object;
+};
+
+DS.clearTransforms = function() {
+ registeredTransforms = {};
+};
+
+DS.clearTransforms();
+
+})();
+
+
+
+(function() {
+
+})();
+
+
+
+(function() {
+//Copyright (C) 2011 by Living Social, Inc.
+
+//Permission is hereby granted, free of charge, to any person obtaining a copy of
+//this software and associated documentation files (the "Software"), to deal in
+//the Software without restriction, including without limitation the rights to
+//use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+//of the Software, and to permit persons to whom the Software is furnished to do
+//so, subject to the following conditions:
+
+//The above copyright notice and this permission notice shall be included in all
+//copies or substantial portions of the Software.
+
+//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+//SOFTWARE.
+
+})();
+
diff --git a/public/js/models.js b/public/js/models.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/public/js/models.js
diff --git a/public/js/old-app.js b/public/js/old-app.js
new file mode 100644
index 0000000..7098e43
--- /dev/null
+++ b/public/js/old-app.js
@@ -0,0 +1,58 @@
+App = Ember.Application.create({
+ LOG_TRANSITIONS: true
+});
+
+// App.Router.map(function() {
+// this.resource("switchboards", { path: "/switchboards" });
+// });
+
+// App.TablesRoute = Ember.Route.extend({
+// model: function() {
+// return ;
+// }
+// })
+
+// DS.RESTAdapter.configure("plurals", {
+// switchboard_entry: "switchboard_entries"
+// });
+
+
+App.Store = DS.Store.extend({
+ adapter: 'DS.FixtureAdapter',
+ revision: 11
+});
+
+App.Switchboard = DS.Model.extend({
+ switchboard_entrys: DS.hasMany('App.SwitchboardEntry'),
+ name: DS.attr('string')
+});
+
+App.SwitchboardEntry = DS.Model.extend({
+ name: DS.attr('string')
+});
+
+App.Router.map(function() {
+ this.resource('switchboard', { path: '/users/:user_id/switchboards/:switchboard_id' });
+});
+
+App.SwitchboardsRoute = Ember.Route.extend({
+ model: function(params) {
+ return App.Switchboard.find(params.switchboard_id);
+ }
+});
+
+App.Switchboard.FIXTURES = [{
+ id: 1,
+ name: 'Erstes Ember Test Switchboard'
+}, {
+ id: 2,
+ name: 'Zweites Switchboard'
+}]
+
+
+// var switchboard_view = Ember.View.create({
+// templateName: 'switchboard',
+// name: "test"
+// });
+
+// switchboard_view.appendTo('#xxxyyy');