From 595e5f9d32c9dda8f7b6c0dd5e7e4fba4693eca4 Mon Sep 17 00:00:00 2001 From: Stefan Wintermeyer Date: Wed, 20 Mar 2013 17:08:14 +0100 Subject: basic Ember.js setup --- public/js/app.js | 16 + public/js/libs/ember-data.js | 7793 +++++++++++ public/js/libs/ember.js | 26839 +++++++++++++++++++++++++++++++++++++ public/js/libs/handlebars.js | 2201 +++ public/js/libs/new-ember-data.js | 8431 ++++++++++++ public/js/models.js | 0 public/js/old-app.js | 58 + 7 files changed, 45338 insertions(+) create mode 100644 public/js/app.js create mode 100644 public/js/libs/ember-data.js create mode 100644 public/js/libs/ember.js create mode 100644 public/js/libs/handlebars.js create mode 100644 public/js/libs/new-ember-data.js create mode 100644 public/js/models.js create mode 100644 public/js/old-app.js (limited to 'public') 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 "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('')` 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('')` 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 '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.\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= 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 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= 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= 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 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 '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;idx1) 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) 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=0 && addAmt>=0 && get(this, 'hasEnumerableObservers')) { + adding = []; + lim = startIdx+addAmt; + for(var idx=startIdx;idx 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() //=> "" + + 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(); // #=> "" + + @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=0 && idx=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=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=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}} +
  • {{title}} ({{titleLength}} characters)
  • + {{/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 ­ +var needsShy = (function(){ + var testEl = document.createElement('div'); + testEl.innerHTML = "
    "; + testEl.firstChild.innerHTML = ""; + return testEl.firstChild.innerHTML === ''; +})(); + +// IE 8 (and likely earlier) likes to move whitespace preceeding +// a script tag to appear after it. This means that we can +// accidentally remove whitespace when updating a morph. +var movesWhitespace = (function() { + var testEl = document.createElement('div'); + testEl.innerHTML = "Test: Value"; + return testEl.childNodes[0].nodeValue === 'Test:' && + testEl.childNodes[2].nodeValue === ' Value'; +})(); + +// Use this to find children by ID instead of using jQuery +var findChildById = function(element, id) { + if (element.getAttribute('id') === id) { return element; } + + var len = element.childNodes.length, idx, node, found; + for (idx=0; idx 0) { + var len = matches.length, idx; + for (idx=0; idxTest'); + canSet = el.options.length === 1; + } + + 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 = ''; + + 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(''); } + }, + + 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 = { + "<": "<", + ">": ">", + '"': """, + "'": "'", + "`": "`" + }; + + var badChars = /&(?!\w+;)|[<>"'`]/g; + var possible = /[&<>"'`]/; + + var escapeChar = function(chr) { + return escape[chr] || "&"; + }; + + 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 + + ``` + + ## HTML `class` Attribute + + The HTML `class` attribute of a view's tag can be set by providing a + `classNames` property that is set to an array of strings: + + ```javascript + MyView = Ember.View.extend({ + classNames: ['my-class', 'my-other-class'] + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + `class` attribute values can also be set by providing a `classNameBindings` + property set to an array of properties names for the view. The return value + of these properties will be added as part of the value for the view's `class` + attribute. These properties can be computed properties: + + ```javascript + MyView = Ember.View.extend({ + classNameBindings: ['propertyA', 'propertyB'], + propertyA: 'from-a', + propertyB: function(){ + if(someLogic){ return 'from-b'; } + }.property() + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + If the value of a class name binding returns a boolean the property name + itself will be used as the class name if the property is true. The class name + will not be added if the value is `false` or `undefined`. + + ```javascript + MyView = Ember.View.extend({ + classNameBindings: ['hovered'], + hovered: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + When using boolean class name bindings you can supply a string value other + than the property name for use as the `class` HTML attribute by appending the + preferred value after a ":" character when defining the binding: + + ```javascript + MyView = Ember.View.extend({ + classNameBindings: ['awesome:so-very-cool'], + awesome: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + Boolean value class name bindings whose property names are in a + camelCase-style format will be converted to a dasherized format: + + ```javascript + MyView = Ember.View.extend({ + classNameBindings: ['isUrgent'], + isUrgent: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + Class name bindings can also refer to object values that are found by + traversing a path relative to the view itself: + + ```javascript + MyView = Ember.View.extend({ + classNameBindings: ['messages.empty'] + messages: Ember.Object.create({ + empty: true + }) + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + If you want to add a class name for a property which evaluates to true and + and a different class name if it evaluates to false, you can pass a binding + like this: + + ```javascript + // Applies 'enabled' class when isEnabled is true and 'disabled' when isEnabled is false + Ember.View.create({ + classNameBindings: ['isEnabled:enabled:disabled'] + isEnabled: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + When isEnabled is `false`, the resulting HTML reprensentation looks like + this: + + ```html +
    + ``` + + This syntax offers the convenience to add a class if a property is `false`: + + ```javascript + // Applies no class when isEnabled is true and class 'disabled' when isEnabled is false + Ember.View.create({ + classNameBindings: ['isEnabled::disabled'] + isEnabled: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    + ``` + + When the `isEnabled` property on the view is set to `false`, it will result + in view instances with an HTML representation of: + + ```html +
    + ``` + + Updates to the the value of a class name binding will result in automatic + update of the HTML `class` attribute in the view's rendered HTML + representation. If the value becomes `false` or `undefined` the class name + will be removed. + + Both `classNames` and `classNameBindings` are concatenated properties. See + `Ember.Object` documentation for more information about concatenated + properties. + + ## HTML Attributes + + The HTML attribute section of a view's tag can be set by providing an + `attributeBindings` property set to an array of property names on the view. + The return value of these properties will be used as the value of the view's + HTML associated attribute: + + ```javascript + AnchorView = Ember.View.extend({ + tagName: 'a', + attributeBindings: ['href'], + href: 'http://google.com' + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html + + ``` + + If the return value of an `attributeBindings` monitored property is a boolean + the property will follow HTML's pattern of repeating the attribute's name as + its value: + + ```javascript + MyTextInput = Ember.View.extend({ + tagName: 'input', + attributeBindings: ['disabled'], + disabled: true + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html + + ``` + + `attributeBindings` can refer to computed properties: + + ```javascript + MyTextInput = Ember.View.extend({ + tagName: 'input', + attributeBindings: ['disabled'], + disabled: function(){ + if (someLogic) { + return true; + } else { + return false; + } + }.property() + }); + ``` + + Updates to the the property of an attribute binding will result in automatic + update of the HTML attribute in the view's rendered HTML representation. + + `attributeBindings` is a concatenated property. See `Ember.Object` + documentation for more information about concatenated properties. + + ## Templates + + The HTML contents of a view's rendered representation are determined by its + template. Templates can be any function that accepts an optional context + parameter and returns a string of HTML that will be inserted within the + view's tag. Most typically in Ember this function will be a compiled + `Ember.Handlebars` template. + + ```javascript + AView = Ember.View.extend({ + template: Ember.Handlebars.compile('I am the template') + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    I am the template
    + ``` + + Within an Ember application is more common to define a Handlebars templates as + part of a page: + + ```html + + ``` + + And associate it by name using a view's `templateName` property: + + ```javascript + AView = Ember.View.extend({ + templateName: 'some-template' + }); + ``` + + Using a value for `templateName` that does not have a Handlebars template + with a matching `data-template-name` attribute will throw an error. + + 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 +
    I was the default
    + ``` + + If a `template` or `templateName` is provided it will take precedence over + `defaultTemplate`: + + ```javascript + AView = Ember.View.extend({ + defaultTemplate: Ember.Handlebars.compile('I was the default') + }); + + aView = AView.create({ + template: Ember.Handlebars.compile('I was the template, not default') + }); + ``` + + Will result in the following HTML representation when rendered: + + ```html +
    I was the template, not default
    + ``` + + ## View Context + + The default context of the compiled template is the view's controller: + + ```javascript + AView = Ember.View.extend({ + template: Ember.Handlebars.compile('Hello {{excitedGreeting}}') + }); + + aController = Ember.Object.create({ + firstName: 'Barry', + excitedGreeting: function(){ + return this.get("content.firstName") + "!!!" + }.property() + }); + + aView = AView.create({ + controller: aController, + }); + ``` + + Will result in an HTML representation of: + + ```html +
    Hello Barry!!!
    + ``` + + A context can also be explicitly supplied through the view's `context` + property. If the view has neither `context` nor `controller` properties, the + `parentView`'s context will be used. + + ## Layouts + + Views can have a secondary template that wraps their main template. Like + primary templates, layouts can be any function that accepts an optional + context parameter and returns a string of HTML that will be inserted inside + view's tag. Views whose HTML element is self closing (e.g. ``) + cannot have a layout and this property will be ignored. + + Most typically in Ember a layout will be a compiled `Ember.Handlebars` + template. + + A view's layout can be set directly with the `layout` property or reference + an existing Handlebars template by name with the `layoutName` property. + + A template used as a layout must contain a single use of the Handlebars + `{{yield}}` helper. The HTML contents of a view's rendered `template` will be + inserted at this location: + + ```javascript + AViewWithLayout = Ember.View.extend({ + layout: Ember.Handlebars.compile("
    {{yield}}
    ") + template: Ember.Handlebars.compile("I got wrapped"), + }); + ``` + + Will result in view instances with an HTML representation of: + + ```html +
    +
    + I got wrapped +
    +
    + ``` + + See `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` 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
    + 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
    + 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 `` the following code + + ```javascript + aContainer = Ember.ContainerView.create({ + classNames: ['the-container'], + childViews: ['aView', 'bView'], + aView: Ember.View.create({ + template: Ember.Handlebars.compile("A") + }), + bView: Ember.View.create({ + template: Ember.Handlebars.compile("B") + }) + }); + + aContainer.appendTo('body'); + ``` + + Results in the HTML + + ```html +
    +
    A
    +
    B
    +
    + ``` + + Removing a view + + ```javascript + aContainer.toArray(); // [aContainer.aView, aContainer.bView] + aContainer.removeObject(aContainer.get('bView')); + aContainer.toArray(); // [aContainer.aView] + ``` + + Will result in the following HTML + + ```html +
    +
    A
    +
    + ``` + + Similarly, adding a child view is accomplished by adding `Ember.View` instances to the + container view. + + Given an empty `` the following code + + ```javascript + aContainer = Ember.ContainerView.create({ + classNames: ['the-container'], + childViews: ['aView', 'bView'], + aView: Ember.View.create({ + template: Ember.Handlebars.compile("A") + }), + bView: Ember.View.create({ + template: Ember.Handlebars.compile("B") + }) + }); + + aContainer.appendTo('body'); + ``` + + Results in the HTML + + ```html +
    +
    A
    +
    B
    +
    + ``` + + Adding a view + + ```javascript + AnotherViewClass = Ember.View.extend({ + template: Ember.Handlebars.compile("Another view") + }); + + aContainer.toArray(); // [aContainer.aView, aContainer.bView] + aContainer.pushObject(AnotherViewClass.create()); + aContainer.toArray(); // [aContainer.aView, aContainer.bView, ] + ``` + + Will result in the following HTML + + ```html +
    +
    A
    +
    B
    +
    Another view
    +
    + ``` + + ## Templates and Layout + + A `template`, `templateName`, `defaultTemplate`, `layout`, `layoutName` or + `defaultLayout` property on a container view will not result in the template + or layout being rendered. The HTML contents of a `Ember.ContainerView`'s DOM + representation will only be the rendered HTML of its child views. + + ## Binding a View to Display + + If you would like to display a single view in your ContainerView, you can set + its `currentView` property. When the `currentView` property is set to a view + instance, it will be added to the ContainerView. If the `currentView` property + is later changed to a different view, the new view will replace the old view. + If `currentView` is set to `null`, the last `currentView` will be removed. + + This functionality is useful for cases where you want to bind the display of + a ContainerView to a controller or state manager. For example, you can bind + the `currentView` of a container to a controller like this: + + ```javascript + App.appController = Ember.Object.create({ + view: Ember.View.create({ + templateName: 'person_template' + }) + }); + ``` + + ```handlebars + {{view Ember.ContainerView currentViewBinding="App.appController.view"}} + ``` + + @class ContainerView + @namespace Ember + @extends Ember.View +*/ +Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { + states: states, + + init: function() { + this._super(); + + var childViews = get(this, 'childViews'); + + // redefine view's childViews property that was obliterated + Ember.defineProperty(this, 'childViews', Ember.View.childViewsProperty); + + var _childViews = this._childViews; + + forEach(childViews, function(viewName, idx) { + var view; + + if ('string' === typeof viewName) { + view = get(this, viewName); + view = this.createChildView(view); + set(this, viewName, view); + } else { + view = this.createChildView(viewName); + } + + _childViews[idx] = view; + }, this); + + var currentView = get(this, 'currentView'); + if (currentView) { + _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` and the following code: + + ```javascript + someItemsView = Ember.CollectionView.create({ + classNames: ['a-collection'], + content: ['A','B','C'], + itemViewClass: Ember.View.extend({ + template: Ember.Handlebars.compile("the letter: {{view.content}}") + }) + }); + + someItemsView.appendTo('body'); + ``` + + Will result in the following HTML structure + + ```html +
    +
    the letter: A
    +
    the letter: B
    +
    the letter: C
    +
    + ``` + + ## Automatic matching of parent/child tagNames + + Setting the `tagName` property of a `CollectionView` to any of + "ul", "ol", "table", "thead", "tbody", "tfoot", "tr", or "select" will result + in the item views receiving an appropriately matched `tagName` property. + + Given an empty `` and the following code: + + ```javascript + anUndorderedListView = Ember.CollectionView.create({ + tagName: 'ul', + content: ['A','B','C'], + itemViewClass: Ember.View.extend({ + template: Ember.Handlebars.compile("the letter: {{view.content}}") + }) + }); + + anUndorderedListView.appendTo('body'); + ``` + + Will result in the following HTML structure + + ```html +
      +
    • the letter: A
    • +
    • the letter: B
    • +
    • the letter: C
    • +
    + ``` + + Additional `tagName` pairs can be provided by adding to + `Ember.CollectionView.CONTAINER_MAP ` + + ```javascript + Ember.CollectionView.CONTAINER_MAP['article'] = 'section' + ``` + + ## Programatic creation of child views + + For cases where additional customization beyond the use of a single + `itemViewClass` or `tagName` matching is required CollectionView's + `createChildView` method can be overidden: + + ```javascript + CustomCollectionView = Ember.CollectionView.extend({ + createChildView: function(viewClass, attrs) { + if (attrs.content.kind == 'album') { + viewClass = App.AlbumView; + } else { + viewClass = App.SongView; + } + this._super(viewClass, attrs); + } + }); + ``` + + ## Empty View + + You can provide an `Ember.View` subclass to the `Ember.CollectionView` + instance as its `emptyView` property. If the `content` property of a + `CollectionView` is set to `null` or an empty array, an instance of this view + will be the `CollectionView`s only child. + + ```javascript + aListWithNothing = Ember.CollectionView.create({ + classNames: ['nothing'] + content: null, + emptyView: Ember.View.extend({ + template: Ember.Handlebars.compile("The collection is empty") + }) + }); + + aListWithNothing.appendTo('body'); + ``` + + Will result in the following HTML structure + + ```html +
    +
    + The collection is empty +
    +
    + ``` + + ## Adding and Removing items + + The `childViews` property of a `CollectionView` should not be directly + manipulated. Instead, add, remove, replace items from its `content` property. + This will trigger appropriate changes to its rendered HTML. + + ## 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 ­ + needsShy = (function(){ + var testEl = document.createElement('div'); + testEl.innerHTML = "
    "; + testEl.firstChild.innerHTML = ""; + return testEl.firstChild.innerHTML === ''; + })(), + + + // IE 8 (and likely earlier) likes to move whitespace preceeding + // a script tag to appear after it. This means that we can + // accidentally remove whitespace when updating a morph. + movesWhitespace = (function() { + var testEl = document.createElement('div'); + testEl.innerHTML = "Test: Value"; + return testEl.childNodes[0].nodeValue === 'Test:' && + testEl.childNodes[2].nodeValue === ' Value'; + })(); + + // Constructor that supports either Metamorph('foo') or new + // Metamorph('foo'); + // + // Takes a string of HTML as the argument. + + var Metamorph = function(html) { + var self; + + if (this instanceof Metamorph) { + self = this; + } else { + self = new K(); + } + + self.innerHTML = html; + var myGuid = 'metamorph-'+(guid++); + self.start = myGuid + '-start'; + self.end = myGuid + '-end'; + + return self; + }; + + K.prototype = Metamorph.prototype; + + var rangeFor, htmlFunc, removeFunc, outerHTMLFunc, appendToFunc, afterFunc, prependFunc, startTagFunc, endTagFunc; + + outerHTMLFunc = function() { + return this.startTag() + this.innerHTML + this.endTag(); + }; + + startTagFunc = function() { + /* + * We replace chevron by its hex code in order to prevent escaping problems. + * Check this thread for more explaination: + * http://stackoverflow.com/questions/8231048/why-use-x3c-instead-of-when-generating-html-from-javascript + */ + return "hi"; + * div.firstChild.firstChild.tagName //=> "" + * + * If our script markers are inside such a node, we need to find that + * node and use *it* as the marker. + **/ + var realNode = function(start) { + while (start.parentNode.tagName === "") { + start = start.parentNode; + } + + return start; + }; + + /** + * When automatically adding a tbody, Internet Explorer inserts the + * tbody immediately before the first . Other browsers create it + * before the first node, no matter what. + * + * This means the the following code: + * + * div = document.createElement("div"); + * div.innerHTML = "
    hi
    + * + * Generates the following DOM in IE: + * + * + div + * + table + * - script id='first' + * + tbody + * + tr + * + td + * - "hi" + * - script id='last' + * + * Which means that the two script tags, even though they were + * inserted at the same point in the hierarchy in the original + * HTML, now have different parents. + * + * This code reparents the first script tag by making it the tbody's + * first child. + **/ + var fixParentage = function(start, end) { + if (start.parentNode !== end.parentNode) { + end.parentNode.insertBefore(start, end.parentNode.firstChild); + } + }; + + htmlFunc = function(html, outerToo) { + // get the real starting node. see realNode for details. + var start = realNode(document.getElementById(this.start)); + var end = document.getElementById(this.end); + var parentNode = end.parentNode; + var node, nextSibling, last; + + // make sure that the start and end nodes share the same + // parent. If not, fix it. + fixParentage(start, end); + + // remove all of the nodes after the starting placeholder and + // before the ending placeholder. + node = start.nextSibling; + while (node) { + nextSibling = node.nextSibling; + last = node === end; + + // if this is the last node, and we want to remove it as well, + // set the `end` node to the next sibling. This is because + // for the rest of the function, we insert the new nodes + // before the end (note that insertBefore(node, null) is + // the same as appendChild(node)). + // + // if we do not want to remove it, just break. + if (last) { + if (outerToo) { end = node.nextSibling; } else { break; } + } + + node.parentNode.removeChild(node); + + // if this is the last node and we didn't break before + // (because we wanted to remove the outer nodes), break + // now. + if (last) { break; } + + node = nextSibling; + } + + // get the first node for the HTML string, even in cases like + // tables and lists where a simple innerHTML on a div would + // swallow some of the content. + node = firstNodeFor(start.parentNode, html); + + // copy the nodes for the HTML between the starting and ending + // placeholder. + while (node) { + nextSibling = node.nextSibling; + parentNode.insertBefore(node, end); + node = nextSibling; + } + }; + + // remove the nodes in the DOM representing this metamorph. + // + // this includes the starting and ending placeholders. + removeFunc = function() { + var start = realNode(document.getElementById(this.start)); + var end = document.getElementById(this.end); + + this.html(''); + start.parentNode.removeChild(start); + end.parentNode.removeChild(end); + }; + + appendToFunc = function(parentNode) { + var node = firstNodeFor(parentNode, this.outerHTML()); + var nextSibling; + + while (node) { + nextSibling = node.nextSibling; + parentNode.appendChild(node); + node = nextSibling; + } + }; + + afterFunc = function(html) { + // get the real starting node. see realNode for details. + var end = document.getElementById(this.end); + var insertBefore = end.nextSibling; + var parentNode = end.parentNode; + var nextSibling; + var node; + + // get the first node for the HTML string, even in cases like + // tables and lists where a simple innerHTML on a div would + // swallow some of the content. + node = firstNodeFor(parentNode, html); + + // copy the nodes for the HTML between the starting and ending + // placeholder. + while (node) { + nextSibling = node.nextSibling; + parentNode.insertBefore(node, insertBefore); + node = nextSibling; + } + }; + + prependFunc = function(html) { + var start = document.getElementById(this.start); + var parentNode = start.parentNode; + var nextSibling; + var node; + + node = firstNodeFor(parentNode, html); + var insertBefore = start.nextSibling; + + while (node) { + nextSibling = node.nextSibling; + parentNode.insertBefore(node, insertBefore); + node = nextSibling; + } + }; + } + + Metamorph.prototype.html = function(html) { + this.checkRemoved(); + if (html === undefined) { return this.innerHTML; } + + htmlFunc.call(this, html); + + this.innerHTML = html; + }; + + Metamorph.prototype.replaceWith = function(html) { + this.checkRemoved(); + htmlFunc.call(this, html, true); + }; + + Metamorph.prototype.remove = removeFunc; + Metamorph.prototype.outerHTML = outerHTMLFunc; + Metamorph.prototype.appendTo = appendToFunc; + Metamorph.prototype.after = afterFunc; + Metamorph.prototype.prepend = prependFunc; + Metamorph.prototype.startTag = startTagFunc; + Metamorph.prototype.endTag = endTagFunc; + + Metamorph.prototype.isRemoved = function() { + var before = document.getElementById(this.start); + var after = document.getElementById(this.end); + + return !before || !after; + }; + + Metamorph.prototype.checkRemoved = function() { + if (this.isRemoved()) { + throw new Error("Cannot perform operations on a Metamorph that is not in the DOM."); + } + }; + + return Metamorph; + }); + +})(); + +(function() { +/** +@module ember +@submodule ember-handlebars +*/ + +// 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 + ``` + + The above handlebars template will fill the ``'s `src` attribute will + the value of the property referenced with `"imageUrl"` and its `alt` + attribute with the value of the property referenced with `"imageTitle"`. + + If the rendering context of this template is the following object: + + ```javascript + { + imageUrl: 'http://lolcats.info/haz-a-funny', + imageTitle: 'A humorous image of a cat' + } + ``` + + The resulting HTML output will be: + + ```html + A humorous image of a cat + ``` + + `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 + 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 + + ``` + + A boolean return value will insert a specified class name if the property + returns `true` and remove the class name if the property returns `false`. + + A class name is provided via the syntax + `somePropertyName:class-name-if-true`. + + ```javascript + AView = Ember.View.extend({ + someBool: true + }) + ``` + + ```handlebars + + ``` + + Result in the following rendered output: + + ```html + + ``` + + An additional section of the binding can be provided if you want to + replace the existing class instead of removing it when the boolean + value changes: + + ```handlebars + + ``` + + A hard-coded value can be used by prepending `:` to the desired + class name: `:class-name-to-always-apply`. + + ```handlebars + + ``` + + Results in the following rendered output: + + ```html + + ``` + + All three strategies - string return value, boolean return value, and + hard-coded value – can be combined in a single declaration: + + ```handlebars + + ``` + + @method 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 `` and the following template: + + ```handlebars + A span: + {{#view tagName="span"}} + hello. + {{/view}} + ``` + + Will result in HTML structure: + + ```html + + + +
    + A span: + + Hello. + +
    + + ``` + + ### `parentView` setting + + The `parentView` property of the new `Ember.View` instance created through + `{{view}}` will be set to the `Ember.View` instance of the template where + `{{view}}` was called. + + ```javascript + aView = Ember.View.create({ + template: Ember.Handlebars.compile("{{#view}} my parent: {{parentView.elementId}} {{/view}}") + }); + + aView.appendTo('body'); + ``` + + Will result in HTML structure: + + ```html +
    +
    + my parent: ember1 +
    +
    + ``` + + ### 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 +
    + + hello. + +
    + ``` + + The HTML `class` attribute can be set on the `{{view}}`'s resulting element + with the `class` or `classNameBindings` options. The `class` option will + directly set the CSS `class` attribute and will not be passed to + `Ember.View.create`. `classNameBindings` will be passed to `create` and use + `Ember.View`'s class name binding functionality: + + ```handlebars + {{#view tagName="span" class="a-custom-class"}} + hello. + {{/view}} + ``` + + Results in the following HTML structure: + + ```html +
    + + hello. + +
    + ``` + + ### 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 +
    +
    + hi +
    +
    + ``` + + ### 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 `` 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 +
    +
    Hi Dave
    +
    Hi Mary
    +
    Hi Sara
    +
    + ``` + + ### 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 +
    +
    Greetings Dave
    +
    Greetings Mary
    +
    Greetings Sara
    +
    + ``` + + ### 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 +
    +

    Howdy Dave

    +

    Howdy Mary

    +

    Howdy Sara

    +
    + ``` + + @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 +
    {{unbound somePropertyThatDoesntChange}}
    + ``` + + `unbound` can also be used in conjunction with a bound helper to + render it in its unbound form: + + ```handlebars +
    {{unbound helperName somePropertyThatDoesntChange}}
    + ``` + + @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}} +

    Sorry, nobody is available for this task.

    + {{/each}} + ``` + ### Specifying a View class for items + If you provide an `itemViewClass` option that references a view class + with its own `template` you can omit the block. + + 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 +
    +
    Greetings Dave
    +
    Greetings Mary
    +
    Greetings Sara
    +
    + ``` + + ### 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 + + ``` + + ```html + + ``` + + This helper looks for templates in the global `Ember.TEMPLATES` hash. If you + add ` + ``` + + 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 +
    +
    + click me +
    +
    + ``` + + 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 + + ``` + + 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 + + ``` + + 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 + + ``` + + 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 + + ``` + + 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 + + ``` + + 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 + + ``` + + 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(, , ) + 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` 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: 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`, an attempt to + // transition to `comments.show` will match ``. + // + // 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, `` 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 + // `[, ]` + // * exitStates: a list of all of the states from the + // `resolveState` to the `currentState`. In the above + // example, `exitStates` would have + // `[`, `]`. + 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: `` + // * enterStates: `[, , ]` + // * exitStates: `[, , ]` + // + // The goal of this code is to remove the common states, so we + // have: + // + // * resolveState: `` + // * enterStates: `[]` + // * exitStates: `[]` + // + // 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 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 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 = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + "`": "`" + }; + + var badChars = /[&<>"'`]/g; + var possible = /[&<>"'`]/; + + var escapeChar = function(chr) { + return escape[chr] || "&"; + }; + + 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 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 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 "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('')` 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('')` 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 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 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'); -- cgit v1.2.3 From 877364c24ef9c7954f0e193456bb3f2d39169977 Mon Sep 17 00:00:00 2001 From: Stefan Wintermeyer Date: Mon, 25 Mar 2013 10:26:49 +0100 Subject: First try --- public/js/app.js | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) (limited to 'public') diff --git a/public/js/app.js b/public/js/app.js index 238f29e..97ed43e 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -3,14 +3,69 @@ App = Ember.Application.create({ rootElement: '#container' }); +// Router +App.Router.map(function() { + this.resource('switchboards', function() { + this.resource('switchboard', { path: ':switchboard_id' }); + }); +}); App.ApplicationRoute = Ember.Route.extend({ setupController: function(controller) { // `controller` is the instance of ApplicationController - controller.set('title', "Hello world!"); + controller.set('title', "Hello world! Switchboard #" + switchboard_id); + } +}); + +App.SwitchboardsRoute = Ember.Route.extend({ + model: function() { + return App.Switchboard.find(); } }); +App.IndexRoute = Ember.Route.extend({ + redirect: function() { + this.transitionTo('switchboard', App.Switchboard.find(switchboard_id)); + } +}); + +// Controller App.ApplicationController = Ember.Controller.extend({ appName: 'My First Example' -}); \ No newline at end of file +}); + +App.SwitchboardsController = Ember.ArrayController.extend({ + // switchboardEntrys: table.get('tab.tabItems') +}) + +// Models +App.Store = DS.Store.extend({ + revision: 11 +}); + +DS.RESTAdapter.configure("plurals", { + switchboard_entry: "switchboard_entries" +}); + +App.Switchboard = DS.Model.extend({ + switchboardEntrys: DS.hasMany('App.SwitchboardEntry'), + name: DS.attr('string'), + didLoad: function() { + console.log('Switchboard model loaded') + } +}); + + + +App.SwitchboardEntry = DS.Model.extend({ + switchboard: DS.belongsTo('App.Switchboard'), + name: DS.attr('string'), + didLoad: function() { + console.log('SwitchboardEntry model loaded') + } +}); + +// // Views +// App.SwitchboardView = Ember.View.extend({ +// templateName: 'switchboard' +// }); \ No newline at end of file -- cgit v1.2.3 From 64653a9149eca977c16233abb0a472730b94a464 Mon Sep 17 00:00:00 2001 From: Stefan Wintermeyer Date: Tue, 26 Mar 2013 16:55:08 +0100 Subject: Store reload interval in the Switchboard table. --- public/js/app.js | 58 ++++++++++++++++------------------------------ public/js/old2-app.js | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 38 deletions(-) create mode 100644 public/js/old2-app.js (limited to 'public') diff --git a/public/js/app.js b/public/js/app.js index 97ed43e..87a4ba6 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -1,42 +1,31 @@ App = Ember.Application.create({ LOG_TRANSITIONS: true, - rootElement: '#container' + rootElement: '#emberjs-container', + + // Reload the switchboard every x milliseconds + // if reload_interval != 0 + ready: function() { + if (reload_interval != 0) { + var switchboard = App.Switchboard.find(switchboard_id); + setInterval(function() { + switchboard.reload(); + }, reload_interval); + } + } }); // Router App.Router.map(function() { - this.resource('switchboards', function() { - this.resource('switchboard', { path: ':switchboard_id' }); - }); -}); - -App.ApplicationRoute = Ember.Route.extend({ - setupController: function(controller) { - // `controller` is the instance of ApplicationController - controller.set('title', "Hello world! Switchboard #" + switchboard_id); - } + this.resource('switchboard', { path: '/' }); }); -App.SwitchboardsRoute = Ember.Route.extend({ +App.SwitchboardRoute = Ember.Route.extend({ model: function() { - return App.Switchboard.find(); + return App.Switchboard.find(switchboard_id); } }); -App.IndexRoute = Ember.Route.extend({ - redirect: function() { - this.transitionTo('switchboard', App.Switchboard.find(switchboard_id)); - } -}); - // Controller -App.ApplicationController = Ember.Controller.extend({ - appName: 'My First Example' -}); - -App.SwitchboardsController = Ember.ArrayController.extend({ - // switchboardEntrys: table.get('tab.tabItems') -}) // Models App.Store = DS.Store.extend({ @@ -50,22 +39,15 @@ DS.RESTAdapter.configure("plurals", { App.Switchboard = DS.Model.extend({ switchboardEntrys: DS.hasMany('App.SwitchboardEntry'), name: DS.attr('string'), - didLoad: function() { - console.log('Switchboard model loaded') - } }); - - App.SwitchboardEntry = DS.Model.extend({ switchboard: DS.belongsTo('App.Switchboard'), + switchboard: DS.belongsTo('App.SipAccount'), name: DS.attr('string'), - didLoad: function() { - console.log('SwitchboardEntry model loaded') - } }); -// // Views -// App.SwitchboardView = Ember.View.extend({ -// templateName: 'switchboard' -// }); \ No newline at end of file +App.SipAccount = DS.Model.extend({ + switchboardEntrys: DS.hasMany('App.SwitchboardEntry'), + callerName: DS.attr('string'), +}); \ No newline at end of file diff --git a/public/js/old2-app.js b/public/js/old2-app.js new file mode 100644 index 0000000..4ee9ac9 --- /dev/null +++ b/public/js/old2-app.js @@ -0,0 +1,64 @@ +App = Ember.Application.create({ + LOG_TRANSITIONS: true, + rootElement: '#emberjs-container' +}); + +// Router +App.Router.map(function() { + this.resource('switchboards', function() { + this.resource('switchboard', { path: ':switchboard_id' }); + }); +}); + +App.ApplicationRoute = Ember.Route.extend({ + setupController: function(controller) { + // `controller` is the instance of ApplicationController + controller.set('title', "Hello world! Switchboard #" + switchboard_id); + } +}); + +App.SwitchboardsRoute = Ember.Route.extend({ + model: function() { + return App.Switchboard.find(); + } +}); + +App.IndexRoute = Ember.Route.extend({ + redirect: function() { + this.transitionTo('switchboard', App.Switchboard.find(switchboard_id)); + } +}); + +// Controller +App.ApplicationController = Ember.Controller.extend({ + appName: 'My First Example' +}); + +App.SwitchboardsController = Ember.ArrayController.extend({ + // switchboardEntrys: table.get('tab.tabItems') +}) + +// Models +App.Store = DS.Store.extend({ + revision: 11 +}); + +DS.RESTAdapter.configure("plurals", { + switchboard_entry: "switchboard_entries" +}); + +App.Switchboard = DS.Model.extend({ + switchboardEntrys: DS.hasMany('App.SwitchboardEntry'), + name: DS.attr('string'), + didLoad: function() { + console.log('Switchboard model loaded') + } +}); + +App.SwitchboardEntry = DS.Model.extend({ + switchboard: DS.belongsTo('App.Switchboard'), + name: DS.attr('string'), + didLoad: function() { + console.log('SwitchboardEntry model loaded') + } +}); -- cgit v1.2.3 From cea2cc3c1cc1e48fc4600c698d52dfda2bde4505 Mon Sep 17 00:00:00 2001 From: Stefan Wintermeyer Date: Wed, 3 Apr 2013 22:08:19 +0200 Subject: Massive changes to the switchboard. --- public/js/app.js | 45 +- public/js/libs/ember-data.js | 883 +++++++-- public/js/libs/ember.js | 4311 +++++++++++++++++++++++++----------------- 3 files changed, 3422 insertions(+), 1817 deletions(-) (limited to 'public') diff --git a/public/js/app.js b/public/js/app.js index 87a4ba6..4bc8cc1 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -9,6 +9,11 @@ App = Ember.Application.create({ var switchboard = App.Switchboard.find(switchboard_id); setInterval(function() { switchboard.reload(); + + // var switchboard_entries = App.SwitchboardEntry.find(); + // switchboard_entries.forEach(function(switchboard_entry) { + // switchboard_entry.reload(); + // }); }, reload_interval); } } @@ -29,25 +34,57 @@ App.SwitchboardRoute = Ember.Route.extend({ // Models App.Store = DS.Store.extend({ - revision: 11 + revision: 12 }); DS.RESTAdapter.configure("plurals", { switchboard_entry: "switchboard_entries" }); +DS.RESTAdapter.reopen({ + namespace: 'api/v1' +}); + App.Switchboard = DS.Model.extend({ switchboardEntrys: DS.hasMany('App.SwitchboardEntry'), - name: DS.attr('string'), + name: DS.attr('string') }); App.SwitchboardEntry = DS.Model.extend({ switchboard: DS.belongsTo('App.Switchboard'), - switchboard: DS.belongsTo('App.SipAccount'), + sipAccount: DS.belongsTo('App.SipAccount'), name: DS.attr('string'), + path_to_user: DS.attr('string'), + avatar_src: DS.attr('string'), + callstate: DS.attr('string') }); +App.Adapter = DS.RESTAdapter.extend(); + +App.store = App.Store.create({ + adapter: App.Adapter.create() +}); + +App.store.adapter.serializer.configure(App.SwitchboardEntry, { sideloadAs: 'switchboard_entries' }); + App.SipAccount = DS.Model.extend({ switchboardEntrys: DS.hasMany('App.SwitchboardEntry'), + phoneNumbers: DS.hasMany('App.PhoneNumber'), callerName: DS.attr('string'), -}); \ No newline at end of file + authName: DS.attr('string') +}); + +App.PhoneNumber = DS.Model.extend({ + name: DS.attr('string'), + number: DS.attr('string') +}); + +App.store.adapter.serializer.configure(App.PhoneNumber, { sideloadAs: 'phone_numbers' }); + +Ember.Handlebars.registerBoundHelper('avatar_img', function(value) { + return new Handlebars.SafeString('Avatar image'); +}); + +Ember.Handlebars.registerBoundHelper('show_callstate', function(value) { + return new Handlebars.SafeString('' + value + ''); +}); diff --git a/public/js/libs/ember-data.js b/public/js/libs/ember-data.js index 6db1c0f..00a6e25 100644 --- a/public/js/libs/ember-data.js +++ b/public/js/libs/ember-data.js @@ -1,7 +1,10 @@ +// Last commit: 5fd6d65 (2013-03-28 01:13:50 +0100) + + (function() { window.DS = Ember.Namespace.create({ - // this one goes to 11 - CURRENT_API_REVISION: 11 + // this one goes past 11 + CURRENT_API_REVISION: 12 }); })(); @@ -746,7 +749,7 @@ var classify = Ember.String.classify, get = Ember.get; 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' } }); + 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. @@ -846,7 +849,7 @@ DS._Mappable = Ember.Mixin.create({ instanceMap.set(transformedKey, newValue); } - }, + } }); @@ -970,6 +973,8 @@ DS.Store = Ember.Object.extend(DS._Mappable, { // `isLoaded` and fires a `didLoad` event. this.loadingRecordArrays = {}; + this._recordsToSave = Ember.OrderedSet.create(); + set(this, 'defaultTransaction', this.transaction()); }, @@ -1542,7 +1547,7 @@ DS.Store = Ember.Object.extend(DS._Mappable, { 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); } + else if (ids !== undefined) { throw fmt("Adapter is either null or does not implement `findHasMany` method", this); } return this.createManyArray(type, Ember.A()); } @@ -1780,6 +1785,39 @@ DS.Store = Ember.Object.extend(DS._Mappable, { } }, + // ................. + // . 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 . // .............. @@ -2268,7 +2306,6 @@ DS.Store = Ember.Object.extend(DS._Mappable, { } var content = get(array, 'content'); - var alreadyInArray = content.indexOf(clientId) !== -1; var recordArrays = this.recordArraysForClientId(clientId); var reference = this.referenceForClientId(clientId); @@ -2413,7 +2450,6 @@ DS.Store = Ember.Object.extend(DS._Mappable, { if (prematerialized && prematerialized.id) { id = prematerialized.id; } else if (id === undefined) { - var adapter = this.adapterForType(type); id = this.preprocessData(type, data); } @@ -2470,7 +2506,10 @@ DS.Store = Ember.Object.extend(DS._Mappable, { // TODO (tomdale) this assumes that loadHasMany *always* means // that the records for the provided IDs are loaded. - if (relationship) { set(relationship, 'isLoaded', true); } + if (relationship) { + set(relationship, 'isLoaded', true); + relationship.trigger('didLoad'); + } }, /** @private @@ -2492,6 +2531,8 @@ DS.Store = Ember.Object.extend(DS._Mappable, { 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; @@ -2518,7 +2559,7 @@ DS.Store = Ember.Object.extend(DS._Mappable, { this.recordCache[clientId] = record = type._create({ store: this, - clientId: clientId, + clientId: clientId }); set(record, 'id', id); @@ -2546,12 +2587,10 @@ DS.Store = Ember.Object.extend(DS._Mappable, { if (id) { delete typeMap.idToCid[id]; } }, - destroy: function() { + willDestroy: function() { if (get(DS, 'defaultStore') === this) { set(DS, 'defaultStore', null); } - - return this._super(); }, // ........................ @@ -2722,7 +2761,7 @@ DS.Store.reopenClass({ (function() { -var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, +var get = Ember.get, set = Ember.set, once = Ember.run.once, arrayMap = Ember.ArrayPolyfills.map; /** @@ -2883,14 +2922,6 @@ var stateProperty = Ember.computed(function(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; } @@ -2917,12 +2948,6 @@ var didSetProperty = function(manager, context) { 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, @@ -3267,7 +3292,7 @@ var states = { exit: function(manager) { var record = get(manager, 'record'); - Ember.run.once(function() { + once(function() { record.trigger('didLoad'); }); } @@ -3503,11 +3528,11 @@ DS.StateManager = Ember.StateManager.extend({ (function() { var LoadPromise = DS.LoadPromise; // system/mixins/load_promise -var get = Ember.get, set = Ember.set, none = Ember.isNone, map = Ember.EnumerableUtils.map; +var get = Ember.get, set = Ember.set, map = Ember.EnumerableUtils.map; -var retrieveFromCurrentState = Ember.computed(function(key) { +var retrieveFromCurrentState = Ember.computed(function(key, value) { return get(get(this, 'stateManager.currentState'), key); -}).property('stateManager.currentState'); +}).property('stateManager.currentState').readOnly(); DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { isLoaded: retrieveFromCurrentState, @@ -3542,6 +3567,11 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { return store.serialize(this, options); }, + toJSON: function(options) { + var serializer = DS.JSONSerializer.create(); + return serializer.serialize(this, options); + }, + didLoad: Ember.K, didReload: Ember.K, didUpdate: Ember.K, @@ -3634,6 +3664,9 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { clearRelationships: function() { this.eachRelationship(function(name, relationship) { + // if the relationship is unmaterialized, move on + if (this.cacheFor(name) === undefined) { return; } + if (relationship.kind === 'belongsTo') { set(this, name, null); } else if (relationship.kind === 'hasMany') { @@ -3782,6 +3815,12 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { becameInFlight: function() { }, + // FOR USE BY THE BASIC ADAPTER + + save: function() { + this.get('store').scheduleSave(this); + }, + // FOR USE DURING COMMIT PROCESS adapterDidUpdateAttribute: function(attributeName, value) { @@ -3835,6 +3874,7 @@ var storeAlias = function(methodName) { 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); }; }; @@ -3843,6 +3883,7 @@ DS.Model.reopenClass({ isLoaded: storeAlias('recordIsLoaded'), find: storeAlias('find'), all: storeAlias('all'), + query: storeAlias('findQuery'), filter: storeAlias('filter'), _create: DS.Model.create, @@ -3941,8 +3982,6 @@ DS.attr = function(type, 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('')` from " + this.constructor.toString(), key !== 'id'); } else { @@ -4063,7 +4102,7 @@ var hasRelationship = function(type, options) { } ids = data[key]; - relationship = store.findMany(type, ids || [], this, meta); + relationship = store.findMany(type, ids, this, meta); set(relationship, 'owner', this); set(relationship, 'name', key); @@ -4180,7 +4219,6 @@ DS.Model.reopenClass({ App.Blog = DS.Model.extend({ users: DS.hasMany(App.User), owner: DS.belongsTo(App.User), - posts: DS.hasMany(App.Post) }); @@ -4256,6 +4294,52 @@ DS.Model.reopenClass({ 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)) { + Ember.assert("Trying to sideload " + name + " on " + this.toString() + " but the type doesn't exist.", !!type); + types.push(type); + } + } + }); + + return types; + }), + /** A map whose keys are the relationships of a model and whose values are relationship descriptors. @@ -4357,6 +4441,21 @@ DS.Model.reopenClass({ 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); + }); } }); @@ -4447,6 +4546,8 @@ 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; @@ -4502,7 +4603,7 @@ DS.RelationshipChange.determineRelationshipType = function(recordType, knownSide 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; @@ -4536,10 +4637,10 @@ DS.RelationshipChange.createChange = function(firstRecordReference, secondRecord return DS.OneToManyChange.createChange(secondRecordReference, firstRecordReference, store, options); } else if (changeType === "oneToNone"){ - return DS.OneToNoneChange.createChange(firstRecordReference, {}, store, options); + return DS.OneToNoneChange.createChange(firstRecordReference, secondRecordReference, store, options); } else if (changeType === "manyToNone"){ - return DS.ManyToNoneChange.createChange(firstRecordReference, {}, store, options); + return DS.ManyToNoneChange.createChange(firstRecordReference, secondRecordReference, store, options); } else if (changeType === "oneToOne"){ return DS.OneToOneChange.createChange(firstRecordReference, secondRecordReference, store, options); @@ -4553,6 +4654,8 @@ DS.RelationshipChange.createChange = function(firstRecordReference, secondRecord 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, @@ -4563,12 +4666,14 @@ DS.OneToNoneChange.createChange = function(childReference, parentReference, stor 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, @@ -4578,14 +4683,14 @@ DS.ManyToNoneChange.createChange = function(childReference, parentReference, sto 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 @@ -4593,6 +4698,8 @@ DS.ManyToManyChange.createChange = function(childReference, parentReference, sto key = options.key; var change = DS.RelationshipChange._createChange({ + parentReference: parentReference, + childReference: childReference, firstRecordReference: childReference, secondRecordReference: parentReference, firstRecordKind: "hasMany", @@ -4612,7 +4719,7 @@ DS.ManyToManyChange.createChange = function(childReference, parentReference, sto 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 @@ -4627,6 +4734,8 @@ DS.OneToOneChange.createChange = function(childReference, parentReference, store } var change = DS.RelationshipChange._createChange({ + parentReference: parentReference, + childReference: childReference, firstRecordReference: childReference, secondRecordReference: parentReference, firstRecordKind: "belongsTo", @@ -4663,7 +4772,7 @@ DS.OneToOneChange.maintainInvariant = function(options, store, childReference, k 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 @@ -4678,6 +4787,8 @@ DS.OneToManyChange.createChange = function(childReference, parentReference, stor } var change = DS.RelationshipChange._createChange({ + parentReference: parentReference, + childReference: childReference, firstRecordReference: childReference, secondRecordReference: parentReference, firstRecordKind: "belongsTo", @@ -4764,13 +4875,13 @@ DS.RelationshipChange.prototype = { /** @private */ destroy: function() { - var childReference = this.firstRecordReference, + var childReference = this.childReference, belongsToName = this.getFirstRecordName(), hasManyName = this.getSecondRecordName(), store = this.store, child, oldParent, newParent, lastParent, transaction; - store.removeRelationshipChangeFor(childReference, belongsToName, this.secondRecordReference, hasManyName, this.changeType); + store.removeRelationshipChangeFor(childReference, belongsToName, this.parentReference, hasManyName, this.changeType); if (transaction = this.transaction) { transaction.relationshipBecameClean(this); @@ -4920,7 +5031,9 @@ DS.RelationshipChangeRemove.prototype.sync = function() { if (secondRecord && firstRecord) { if(this.secondRecordKind === "belongsTo"){ + secondRecord.suspendRelationshipObservers(function(){ set(secondRecord, secondRecordName, null); + }); } else if(this.secondRecordKind === "hasMany"){ secondRecord.suspendRelationshipObservers(function(){ @@ -5050,7 +5163,7 @@ Ember.onLoad('Ember.Application', function(Application) { name: "store", initialize: function(container, application) { - container.register('store', 'main', application.Store); + application.register('store:main', application.Store); // Eagerly generate the store so defaultStore is populated. // TODO: Do this in a finisher hook @@ -5061,9 +5174,9 @@ Ember.onLoad('Ember.Application', function(Application) { Application.initializer({ name: "injectStore", - initialize: function(container) { - container.typeInjection('controller', 'store', 'store:main'); - container.typeInjection('route', 'store', 'store:main'); + initialize: function(container, application) { + application.inject('controller', 'store', 'store:main'); + application.inject('route', 'store', 'store:main'); } }); } @@ -5138,20 +5251,29 @@ function mustImplement(name) { by implementing `keyForAttributeName`: ```javascript - keyForAttributeName: function(type, name) { - return name.underscore.toUpperCase(); - } + 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: + one-by-one using the adapter's `map` API: ```javascript - App.Person.map('App.Person', { + 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 @@ -5608,7 +5730,7 @@ DS.Serializer = Ember.Object.extend({ primaryKey: function(type) { // If the type is `BlogPost`, this will return // `blog_post_id`. - var typeString = type.toString.split(".")[1].underscore(); + var typeString = type.toString().split(".")[1].underscore(); return typeString + "_id"; } }); @@ -5986,10 +6108,10 @@ DS.Serializer = Ember.Object.extend({ registerEnumTransform: function(type, objects) { var transform = { deserialize: function(deserialized) { - return objects.objectAt(deserialized); + return Ember.A(objects).objectAt(deserialized); }, serialize: function(serialized) { - return objects.indexOf(serialized); + return Ember.EnumerableUtils.indexOf(objects, serialized); }, values: objects }; @@ -6240,8 +6362,6 @@ DS.JSONTransforms = { return dayOfWeek + ", " + dayOfMonth + " " + month + " " + utcYear + " " + pad(utcHours) + ":" + pad(utcMinutes) + ":" + pad(utcSeconds) + " GMT"; - } else if (date === undefined) { - return undefined; } else { return null; } @@ -6283,6 +6403,13 @@ DS.JSONSerializer = DS.Serializer.extend({ 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; } @@ -6352,7 +6479,7 @@ DS.JSONSerializer = DS.Serializer.extend({ if (this.embeddedType(type, name)) { if (embeddedChild = get(record, name)) { - value = this.serialize(embeddedChild, { include: true }); + value = this.serialize(embeddedChild, { includeId: true }); } hash[key] = value; @@ -6444,46 +6571,88 @@ DS.JSONSerializer = DS.Serializer.extend({ } }, + /** + @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, mappings, loaded = {}; + var sideloadedType; - loaded[root] = true; + this.normalizeSideloadMappings(); + this.configureSideloadMappingForType(type); for (var prop in json) { - if (!json.hasOwnProperty(prop)) { continue; } - if (prop === root) { continue; } - if (prop === this.configOption(type, 'meta')) { continue; } + if (!json.hasOwnProperty(prop) || + prop === root || + prop === this.configOption(type, 'meta')) { + continue; + } - sideloadedType = type.typeForRelationship(prop); + sideloadedType = this.sideloadMapping.get(prop); + Ember.assert("Your server returned a hash with the key " + prop + + " but you have no mapping for it", + !!sideloadedType); - if (!sideloadedType) { - sideloadedType = this.sideloadMapping.get(prop); + this.loadValue(loader, sideloadedType, json[prop]); + } + }, - if (typeof sideloadedType === 'string') { - sideloadedType = get(Ember.lookup, sideloadedType); - } + /** + @private - Ember.assert("Your server returned a hash with the key " + prop + " but you have no mapping for it", !!sideloadedType); - } + Iterates over all the `sideloadAs` mappings and converts any that are + strings to their equivalent types. - this.sideloadRelationships(loader, sideloadedType, json, prop, loaded); + 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; } }, - sideloadRelationships: function(loader, type, json, prop, loaded) { - loaded[prop] = true; + /** + @private - 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); + 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); - - this.loadValue(loader, type, json[prop]); }, loadValue: function(loader, type, value) { @@ -6505,6 +6674,36 @@ DS.JSONSerializer = DS.Serializer.extend({ 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(); @@ -6514,6 +6713,34 @@ DS.JSONSerializer = DS.Serializer.extend({ 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)); } }); @@ -6685,7 +6912,6 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { if (payload) { var loader = DS.loaderFor(store); - var serializer = get(this, 'serializer'); loader.load = function(type, data, prematerialized) { store.updateId(record, data); @@ -7236,14 +7462,122 @@ DS.Adapter.reopenClass({ (function() { -var get = Ember.get; +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 */ @@ -7252,7 +7586,7 @@ DS.FixtureAdapter = DS.Adapter.extend({ 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)); + throw new Error(fmt('the id property must be defined for fixture %@', [dump(fixture)])); } fixture.id = fixture.id + ''; return fixture; @@ -7265,7 +7599,19 @@ DS.FixtureAdapter = DS.Adapter.extend({ Implement this method in order to query fixtures data */ queryFixtures: function(fixtures, query, type) { - return fixtures; + 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); }, /* @@ -7283,18 +7629,19 @@ DS.FixtureAdapter = DS.Adapter.extend({ }, find: function(store, type, id) { - var fixtures = this.fixturesForType(type); + var fixtures = this.fixturesForType(type), + fixture; - Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); + Ember.warn("Unable to find fixtures for model type " + type.toString(), fixtures); if (fixtures) { - fixtures = fixtures.findProperty('id', id); + fixture = Ember.A(fixtures).findProperty('id', id); } - if (fixtures) { + if (fixture) { this.simulateRemoteCall(function() { - store.load(type, fixtures); - }, store, type); + this.didFindRecord(store, type, fixture, id); + }, this); } }, @@ -7311,8 +7658,8 @@ DS.FixtureAdapter = DS.Adapter.extend({ if (fixtures) { this.simulateRemoteCall(function() { - store.loadMany(type, fixtures); - }, store, type); + this.didFindMany(store, type, fixtures); + }, this); } }, @@ -7322,9 +7669,8 @@ DS.FixtureAdapter = DS.Adapter.extend({ Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); this.simulateRemoteCall(function() { - store.loadMany(type, fixtures); - store.didUpdateAll(type); - }, store, type); + this.didFindAll(store, type, fixtures); + }, this); }, findQuery: function(store, type, query, array) { @@ -7336,43 +7682,82 @@ DS.FixtureAdapter = DS.Adapter.extend({ if (fixtures) { this.simulateRemoteCall(function() { - array.load(fixtures); - }, store, type); + this.didFindQuery(store, type, fixtures, array); + }, this); } }, createRecord: function(store, type, record) { var fixture = this.mockJSON(type, record); - fixture.id = this.generateIdForRecord(store, record); + this.updateFixtures(type, fixture); this.simulateRemoteCall(function() { - store.didSaveRecord(record, fixture); - }, store, type, record); + 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() { - store.didSaveRecord(record, fixture); - }, store, type, record); + 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() { - store.didSaveRecord(record); - }, store, type, record); + this.didDeleteRecord(store, type, record); + }, this); }, /* @private */ - simulateRemoteCall: function(callback, store, type, record) { + 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')) { - setTimeout(callback, get(this, 'latency')); + // Schedule with setTimeout + Ember.run.later(context, callback, get(this, 'latency')); } else { - callback(); + // Asynchronous, but at the of the runloop with zero latency + Ember.run.once(context, callback); } } }); @@ -7395,6 +7780,16 @@ DS.RESTSerializer = DS.JSONSerializer.extend({ } 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"; } }); @@ -7499,12 +7894,25 @@ DS.RESTAdapter = DS.Adapter.extend({ }, 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.dirtyRecordsForRecordChange(dirtySet, embeddedRecord); + this._dirtyTree(dirtySet, embeddedRecord); }, this); var reference = record.get('_reference'); @@ -7512,12 +7920,10 @@ DS.RESTAdapter = DS.Adapter.extend({ if (reference.parent) { var store = get(record, 'store'); var parent = store.recordForReference(reference.parent); - this.dirtyRecordsForRecordChange(dirtySet, parent); + this._dirtyTree(dirtySet, parent); } }, - dirtyRecordsForHasManyChange: Ember.K, - createRecords: function(store, type, records) { if (get(this, 'bulkCommit') === false) { return this._super(store, type, records); @@ -7762,6 +8168,251 @@ DS.RESTAdapter = DS.Adapter.extend({ +(function() { +var camelize = Ember.String.camelize, + capitalize = Ember.String.capitalize, + get = Ember.get, + map = Ember.ArrayPolyfills.map, + registeredTransforms; + +var passthruTransform = { + serialize: function(value) { return value; }, + deserialize: function(value) { return value; } +}; + +var defaultTransforms = { + string: passthruTransform, + boolean: passthruTransform, + number: passthruTransform +}; + +function camelizeKeys(json) { + var value; + + for (var prop in json) { + value = json[prop]; + delete json[prop]; + json[camelize(prop)] = value; + } +} + +function munge(json, callback) { + callback(json); +} + +function applyTransforms(json, type, transformType) { + var transforms = registeredTransforms[transformType]; + + Ember.assert("You are trying to apply the '" + transformType + "' transforms, but you didn't register any transforms with that name", transforms); + + get(type, 'attributes').forEach(function(name, attribute) { + var attributeType = attribute.type, + value = json[name]; + + var transform = transforms[attributeType] || defaultTransforms[attributeType]; + + Ember.assert("Your model specified the '" + attributeType + "' type for the '" + name + "' attribute, but no transform for that type was registered", transform); + + json[name] = transform.deserialize(value); + }); +} + +function ObjectProcessor(json, type, store) { + this.json = json; + this.type = type; + this.store = store; +} + +ObjectProcessor.prototype = { + camelizeKeys: function() { + camelizeKeys(this.json); + return this; + }, + + munge: function(callback) { + munge(this.json, callback); + return this; + }, + + applyTransforms: function(transformType) { + applyTransforms(this.json, this.type, transformType); + return this; + } +}; + +function LoadObjectProcessor() { + ObjectProcessor.apply(this, arguments); +} + +LoadObjectProcessor.prototype = Ember.create(ObjectProcessor.prototype); + +LoadObjectProcessor.prototype.load = function() { + this.store.load(this.type, {}, this.json); +}; + +function loadObjectProcessorFactory(store, type) { + return function(json) { + return new LoadObjectProcessor(json, type, store); + }; +} + +function ArrayProcessor(json, type, array, store) { + this.json = json; + this.type = type; + this.array = array; + this.store = store; +} + +ArrayProcessor.prototype = { + load: function() { + var store = this.store, + type = this.type; + + var references = this.json.map(function(object) { + return store.load(type, {}, object); + }); + + this.array.load(references); + }, + + camelizeKeys: function() { + this.json.forEach(camelizeKeys); + return this; + }, + + munge: function(callback) { + this.json.forEach(function(object) { + munge(object, callback); + }); + return this; + }, + + applyTransforms: function(transformType) { + var type = this.type; + + this.json.forEach(function(object) { + applyTransforms(object, type, transformType); + }); + + return this; + } +}; + +function arrayProcessorFactory(store, type, array) { + return function(json) { + return new ArrayProcessor(json, type, array, store); + }; +} + +var HasManyProcessor = function(json, store, record, relationship) { + this.json = json; + this.store = store; + this.record = record; + this.type = record.constructor; + this.relationship = relationship; +}; + +HasManyProcessor.prototype = Ember.create(ArrayProcessor.prototype); + +HasManyProcessor.prototype.load = function() { + var store = this.store; + var ids = map.call(this.json, function(obj) { return obj.id; }); + + store.loadMany(this.relationship.type, this.json); + store.loadHasMany(this.record, this.relationship.key, ids); +}; + +function hasManyProcessorFactory(store, record, relationship) { + return function(json) { + return new HasManyProcessor(json, store, record, relationship); + }; +} + +function SaveProcessor(record, store, type, includeId) { + this.record = record; + ObjectProcessor.call(this, record.toJSON({ includeId: includeId }), type, store); +} + +SaveProcessor.prototype = Ember.create(ObjectProcessor.prototype); + +SaveProcessor.prototype.save = function(callback) { + callback(this.json); +}; + +function saveProcessorFactory(store, type, includeId) { + return function(record) { + return new SaveProcessor(record, store, type, includeId); + }; +} + +DS.BasicAdapter = DS.Adapter.extend({ + find: function(store, type, id) { + var sync = type.sync; + + Ember.assert("You are trying to use the BasicAdapter to find id '" + id + "' of " + type + " but " + type + ".sync was not found", sync); + Ember.assert("The sync code on " + type + " does not implement find(), but you are trying to find id '" + id + "'.", sync.find); + + sync.find(id, loadObjectProcessorFactory(store, type)); + }, + + findQuery: function(store, type, query, recordArray) { + var sync = type.sync; + + Ember.assert("You are trying to use the BasicAdapter to query " + type + " but " + type + ".sync was not found", sync); + Ember.assert("The sync code on " + type + " does not implement query(), but you are trying to query " + type + ".", sync.query); + + sync.query(query, arrayProcessorFactory(store, type, recordArray)); + }, + + findHasMany: function(store, record, relationship, data) { + var name = capitalize(relationship.key), + sync = record.constructor.sync, + processor = hasManyProcessorFactory(store, record, relationship); + + var options = { + relationship: relationship.key, + data: data + }; + + if (sync['find'+name]) { + sync['find' + name](record, options, processor); + } else if (sync.findHasMany) { + sync.findHasMany(record, options, processor); + } else { + Ember.assert("You are trying to use the BasicAdapter to find the " + relationship.key + " has-many relationship, but " + record.constructor + ".sync did not implement findHasMany or find" + name + ".", false); + } + }, + + createRecord: function(store, type, record) { + var sync = type.sync; + sync.createRecord(record, saveProcessorFactory(store, type)); + }, + + updateRecord: function(store, type, record) { + var sync = type.sync; + sync.updateRecord(record, saveProcessorFactory(store, type, true)); + }, + + deleteRecord: function(store, type, record) { + var sync = type.sync; + sync.deleteRecord(record, saveProcessorFactory(store, type, true)); + } +}); + +DS.registerTransforms = function(kind, object) { + registeredTransforms[kind] = object; +}; + +DS.clearTransforms = function() { + registeredTransforms = {}; +}; + +DS.clearTransforms(); + +})(); + + + (function() { })(); diff --git a/public/js/libs/ember.js b/public/js/libs/ember.js index e566b10..7f44724 100644 --- a/public/js/libs/ember.js +++ b/public/js/libs/ember.js @@ -1,5 +1,5 @@ -// Version: v1.0.0-rc.1 -// Last commit: 8b061b4 (2013-02-15 12:10:22 -0800) +// Version: v1.0.0-rc.2 +// Last commit: 656fa6e (2013-03-29 13:40:38 -0700) (function() { @@ -140,6 +140,7 @@ Ember.deprecate = function(message, test) { @method deprecateFunc @param {String} message A description of the deprecation. @param {Function} func The function to be deprecated. + @return {Function} a new function that wrapped the original function with a deprecation warning */ Ember.deprecateFunc = function(message, func) { return function() { @@ -150,8 +151,8 @@ Ember.deprecateFunc = function(message, func) { })(); -// Version: v1.0.0-rc.1 -// Last commit: 8b061b4 (2013-02-15 12:10:22 -0800) +// Version: v1.0.0-rc.2 +// Last commit: 656fa6e (2013-03-29 13:40:38 -0700) (function() { @@ -211,7 +212,7 @@ var define, requireModule; @class Ember @static - @version 1.0.0-rc.1 + @version 1.0.0-rc.2 */ if ('undefined' === typeof Ember) { @@ -238,10 +239,10 @@ Ember.toString = function() { return "Ember"; }; /** @property VERSION @type String - @default '1.0.0-rc.1' + @default '1.0.0-rc.2' @final */ -Ember.VERSION = '1.0.0-rc.1'; +Ember.VERSION = '1.0.0-rc.2'; /** Standard environmental variables. You can define these in a global `ENV` @@ -297,6 +298,15 @@ Ember.LOG_STACKTRACE_ON_DEPRECATION = (Ember.ENV.LOG_STACKTRACE_ON_DEPRECATION ! */ Ember.SHIM_ES5 = (Ember.ENV.SHIM_ES5 === false) ? false : Ember.EXTEND_PROTOTYPES; +/** + Determines whether Ember logs info about version of used libraries + + @property LOG_VERSION + @type Boolean + @default true +*/ +Ember.LOG_VERSION = (Ember.ENV.LOG_VERSION === false) ? false : true; + /** Empty function. Useful for some operations. @@ -393,12 +403,12 @@ Ember.handleErrors = function(func, context) { // 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); + return func.call(context || this); } catch (error) { Ember.onerror(error); } } else { - return func.apply(context || this); + return func.call(context || this); } }; @@ -407,7 +417,60 @@ Ember.merge = function(original, updates) { if (!updates.hasOwnProperty(prop)) { continue; } original[prop] = updates[prop]; } + return original; +}; + +/** + 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) ; + })(); @@ -651,7 +714,7 @@ Ember.generateGuid = function generateGuid(obj, prefix) { @method guidFor @for Ember - @param obj {Object} any object, string, number, Element, or primitive + @param {Object} obj any object, string, number, Element, or primitive @return {String} the unique guid for this instance. */ Ember.guidFor = function guidFor(obj) { @@ -762,7 +825,7 @@ if (isDefinePropertySimulated) { @param {Object} obj The object to retrieve meta for @param {Boolean} [writable=true] Pass `false` if you do not intend to modify the meta hash, allowing the method to avoid making an unnecessary copy. - @return {Hash} + @return {Object} the meta hash for an object */ Ember.meta = function meta(obj, writable) { @@ -914,7 +977,7 @@ Ember.wrap = function(func, superFunc) { @method isArray @for Ember @param {Object} obj The object to test - @return {Boolean} + @return {Boolean} true if the passed object is an array or Array-like */ Ember.isArray = function(obj) { if (!obj || obj.setInterval) { return false; } @@ -1001,8 +1064,8 @@ var needsFinallyFix = (function() { @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 {Function} tryable The function to run the try callback + @param {Function} finalizer 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 @@ -1051,9 +1114,9 @@ if (needsFinallyFix) { @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 {Function} tryable The function to run the try callback + @param {Function} catchable The function to run the catchable callback + @param {Function} finalizer 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 @@ -1565,8 +1628,8 @@ OrderedSet.prototype = { /** @method forEach - @param {Function} function - @param target + @param {Function} fn + @param self */ forEach: function(fn, self) { // allow mutation during iteration @@ -1752,7 +1815,7 @@ var MapWithDefault = Ember.MapWithDefault = function(options) { @static @param [options] @param {anything} [options.defaultValue] - @return {Ember.MapWithDefault|Ember.Map} If options are passed, returns + @return {Ember.MapWithDefault|Ember.Map} If options are passed, returns `Ember.MapWithDefault` otherwise returns `Ember.Map` */ MapWithDefault.create = function(options) { @@ -1815,7 +1878,7 @@ var FIRST_KEY = /^([^\.\*]+)/; // .......................................................... // GET AND SET // -// If we are on a platform that supports accessors we can get use those. +// If we are on a platform that supports accessors we can use those. // Otherwise simulate accessors by looking up the property directly on the // object. @@ -1826,7 +1889,7 @@ var FIRST_KEY = /^([^\.\*]+)/; 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 '_' + 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 @@ -1888,7 +1951,7 @@ get = function get(obj, keyName) { 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 '_' + 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 @@ -2087,7 +2150,7 @@ Ember.setPath = Ember.deprecateFunc('setPath is deprecated since set now support @method trySet @for Ember @param {Object} obj The object to modify. - @param {String} keyName The property key to set + @param {String} path The property path to set @param {Object} value The value to set */ Ember.trySet = function(root, path, value) { @@ -2119,11 +2182,8 @@ Ember.isGlobalPath = function(path) { @module ember-metal */ -var GUID_KEY = Ember.GUID_KEY, - META_KEY = Ember.META_KEY, - EMPTY_META = Ember.EMPTY_META, +var META_KEY = Ember.META_KEY, metaFor = Ember.meta, - o_create = Ember.create, objectDefineProperty = Ember.platform.defineProperty; var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; @@ -2543,7 +2603,6 @@ var guidFor = Ember.guidFor, // 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 = /[\.\*]/; @@ -3083,7 +3142,7 @@ Ember.finishChains = function(obj) { @param {String} keyName The property key (or path) that will change. @return {void} */ -function propertyWillChange(obj, keyName, value) { +function propertyWillChange(obj, keyName) { var m = metaFor(obj, false), watching = m.watching[keyName] > 0 || keyName === 'length', proto = m.proto, @@ -3191,7 +3250,6 @@ Ember.warn("The CP_DEFAULT_CACHEABLE flag has been removed and computed properti 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, @@ -3227,20 +3285,8 @@ function keysForDep(obj, depsMeta, depKey) { return keys; } -/* return obj[META_KEY].deps */ function metaForDeps(obj, meta) { - var deps = meta.deps; - // If the current object has no dependencies... - if (!deps) { - // initialize the dependencies with a pointer back to - // the current object - deps = meta.deps = {}; - } else if (!meta.hasOwnProperty('deps')) { - // otherwise if the dependencies are inherited from the - // object's superclass, clone the deps - deps = meta.deps = o_create(deps); - } - return deps; + return keysForDep(obj, meta, 'deps'); } function addDependentKeys(desc, obj, keyName, meta) { @@ -3293,8 +3339,10 @@ function removeDependentKeys(desc, obj, keyName, meta) { */ function ComputedProperty(func, opts) { this.func = func; + this._cacheable = (opts && opts.cacheable !== undefined) ? opts.cacheable : true; this._dependentKeys = opts && opts.dependentKeys; + this._readOnly = opts && (opts.readOnly !== undefined || !!opts.readOnly); } Ember.ComputedProperty = ComputedProperty; @@ -3323,6 +3371,7 @@ var ComputedPropertyPrototype = ComputedProperty.prototype; @method cacheable @param {Boolean} aFlag optional set to `false` to disable caching + @return {Ember.ComputedProperty} this @chainable */ ComputedPropertyPrototype.cacheable = function(aFlag) { @@ -3343,12 +3392,36 @@ ComputedPropertyPrototype.cacheable = function(aFlag) { ``` @method volatile + @return {Ember.ComputedProperty} this @chainable */ ComputedPropertyPrototype.volatile = function() { return this.cacheable(false); }; +/** + Call on a computed property to set it into read-only mode. When in this + mode the computed property will throw an error when set. + + ```javascript + MyApp.person = Ember.Object.create({ + guid: function() { + return 'guid-guid-guid'; + }.property().readOnly() + }); + + MyApp.person.set('guid', 'new-guid'); // will throw an exception + ``` + + @method readOnly + @return {Ember.ComputedProperty} this + @chainable +*/ +ComputedPropertyPrototype.readOnly = function(readOnly) { + this._readOnly = readOnly === undefined || !!readOnly; + return this; +}; + /** Sets the dependent keys on this computed property. Pass any number of arguments containing key paths that this computed property depends on. @@ -3366,6 +3439,7 @@ ComputedPropertyPrototype.volatile = function() { @method property @param {String} path* zero or more property paths + @return {Ember.ComputedProperty} this @chainable */ ComputedPropertyPrototype.property = function() { @@ -3473,9 +3547,14 @@ ComputedPropertyPrototype.set = function(obj, keyName, value) { cache = meta.cache, cachedValue, ret; + if (this._readOnly) { + throw new Error('Cannot Set: ' + keyName + ' on: ' + obj.toString() ); + } + this._suspended = obj; try { + if (cacheable && cache.hasOwnProperty(keyName)) { cachedValue = cache[keyName]; hadCachedValue = true; @@ -3566,6 +3645,10 @@ Ember.computed = function(func) { func = a_slice.call(arguments, -1)[0]; } + if ( typeof func !== "function" ) { + throw new Error("Computed Property declared without a property function"); + } + var cp = new ComputedProperty(func); if (args) { @@ -3586,6 +3669,7 @@ Ember.computed = function(func) { @param {Object} obj the object whose property you want to check @param {String} key the name of the property whose cached value you want to return + @return {any} the cached value */ Ember.cacheFor = function cacheFor(obj, key) { var cache = metaFor(obj, false).cache; @@ -3595,53 +3679,264 @@ Ember.cacheFor = function cacheFor(obj, key) { } }; +function getProperties(self, propertyNames) { + var ret = {}; + for(var i = 0; i < propertyNames.length; i++) { + ret[propertyNames[i]] = get(self, propertyNames[i]); + } + return ret; +} + +function registerComputed(name, macro) { + Ember.computed[name] = function(dependentKey) { + var args = a_slice.call(arguments); + return Ember.computed(dependentKey, function() { + return macro.apply(this, args); + }); + }; +} + +function registerComputedWithProperties(name, macro) { + Ember.computed[name] = function() { + var properties = a_slice.call(arguments); + + var computed = Ember.computed(function() { + return macro.apply(this, [getProperties(this, properties)]); + }); + + return computed.property.apply(computed, properties); + }; +} + /** - @method computed.not + @method computed.empty @for Ember @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which negate + the original value for property */ -Ember.computed.not = function(dependentKey) { - return Ember.computed(dependentKey, function(key) { - return !get(this, dependentKey); - }); -}; +registerComputed('empty', function(dependentKey) { + return Ember.isEmpty(get(this, dependentKey)); +}); /** - @method computed.empty + @method computed.notEmpty @for Ember @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which returns true if + original value for property is not empty. */ -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); - }); -}; +registerComputed('notEmpty', function(dependentKey) { + return !Ember.isEmpty(get(this, dependentKey)); +}); + +/** + @method computed.none + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which + rturns true if original value for property is null or undefined. +*/ +registerComputed('none', function(dependentKey) { + return Ember.isNone(get(this, dependentKey)); +}); + +/** + @method computed.not + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which returns + inverse of the original value for property +*/ +registerComputed('not', function(dependentKey) { + return !get(this, dependentKey); +}); /** @method computed.bool @for Ember @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which convert + to boolean the original value for property */ -Ember.computed.bool = function(dependentKey) { - return Ember.computed(dependentKey, function(key) { - return !!get(this, dependentKey); - }); -}; +registerComputed('bool', function(dependentKey) { + return !!get(this, dependentKey); +}); + +/** + @method computed.match + @for Ember + @param {String} dependentKey + @param {RegExp} regexp + @return {Ember.ComputedProperty} computed property which match + the original value for property against a given RegExp +*/ +registerComputed('match', function(dependentKey, regexp) { + var value = get(this, dependentKey); + return typeof value === 'string' ? !!value.match(regexp) : false; +}); + +/** + @method computed.equal + @for Ember + @param {String} dependentKey + @param {String|Number|Object} value + @return {Ember.ComputedProperty} computed property which returns true if + the original value for property is equal to the given value. +*/ +registerComputed('equal', function(dependentKey, value) { + return get(this, dependentKey) === value; +}); + +/** + @method computed.gt + @for Ember + @param {String} dependentKey + @param {Number} value + @return {Ember.ComputedProperty} computed property which returns true if + the original value for property is greater then given value. +*/ +registerComputed('gt', function(dependentKey, value) { + return get(this, dependentKey) > value; +}); + +/** + @method computed.gte + @for Ember + @param {String} dependentKey + @param {Number} value + @return {Ember.ComputedProperty} computed property which returns true if + the original value for property is greater or equal then given value. +*/ +registerComputed('gte', function(dependentKey, value) { + return get(this, dependentKey) >= value; +}); + +/** + @method computed.lt + @for Ember + @param {String} dependentKey + @param {Number} value + @return {Ember.ComputedProperty} computed property which returns true if + the original value for property is less then given value. +*/ +registerComputed('lt', function(dependentKey, value) { + return get(this, dependentKey) < value; +}); + +/** + @method computed.lte + @for Ember + @param {String} dependentKey + @param {Number} value + @return {Ember.ComputedProperty} computed property which returns true if + the original value for property is less or equal then given value. +*/ +registerComputed('lte', function(dependentKey, value) { + return get(this, dependentKey) <= value; +}); + +/** + @method computed.and + @for Ember + @param {String} dependentKey, [dependentKey...] + @return {Ember.ComputedProperty} computed property which peforms + a logical `and` on the values of all the original values for properties. +*/ +registerComputedWithProperties('and', function(properties) { + for (var key in properties) { + if (properties.hasOwnProperty(key) && !properties[key]) { + return false; + } + } + return true; +}); + +/** + @method computed.or + @for Ember + @param {String} dependentKey, [dependentKey...] + @return {Ember.ComputedProperty} computed property which peforms + a logical `or` on the values of all the original values for properties. +*/ +registerComputedWithProperties('or', function(properties) { + for (var key in properties) { + if (properties.hasOwnProperty(key) && properties[key]) { + return true; + } + } + return false; +}); + +/** + @method computed.any + @for Ember + @param {String} dependentKey, [dependentKey...] + @return {Ember.ComputedProperty} computed property which returns + the first trouthy value of given list of properties. +*/ +registerComputedWithProperties('any', function(properties) { + for (var key in properties) { + if (properties.hasOwnProperty(key) && properties[key]) { + return properties[key]; + } + } + return null; +}); + +/** + @method computed.map + @for Ember + @param {String} dependentKey, [dependentKey...] + @return {Ember.ComputedProperty} computed property which maps + values of all passed properties in to an array. +*/ +registerComputedWithProperties('map', function(properties) { + var res = []; + for (var key in properties) { + if (properties.hasOwnProperty(key)) { + if (Ember.isNone(properties[key])) { + res.push(null); + } else { + res.push(properties[key]); + } + } + } + return res; +}); /** @method computed.alias @for Ember @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which creates an + alias to the original value for property. */ Ember.computed.alias = function(dependentKey) { return Ember.computed(dependentKey, function(key, value){ - if (arguments.length === 1) { - return get(this, dependentKey); - } else { + if (arguments.length > 1) { set(this, dependentKey, value); return value; + } else { + return get(this, dependentKey); + } + }); +}; + +/** + @method computed.defaultTo + @for Ember + @param {String} defaultPath + @return {Ember.ComputedProperty} computed property which acts like + a standard getter and setter, but defaults to the value from `defaultPath`. +*/ +Ember.computed.defaultTo = function(defaultPath) { + return Ember.computed(function(key, newValue, cachedValue) { + var result; + if (arguments.length === 1) { + return cachedValue != null ? cachedValue : get(this, defaultPath); } + return newValue != null ? newValue : get(this, defaultPath); }); }; @@ -3656,7 +3951,6 @@ Ember.computed.alias = function(dependentKey) { var o_create = Ember.create, metaFor = Ember.meta, - metaPath = Ember.metaPath, META_KEY = Ember.META_KEY; /* @@ -3757,6 +4051,7 @@ function actionsDiff(obj, eventName, otherActions) { @param {String} eventName @param {Object|Function} targetOrMethod A target object or a function @param {Function|String} method A function or the name of a function to be called on `target` + @param {Boolean} once A flag whether a function should only be called once */ function addListener(obj, eventName, target, method, once) { Ember.assert("You must pass at least an object and event name to Ember.addListener", !!obj && !!eventName); @@ -3942,6 +4237,7 @@ function watchedEvents(obj) { @param obj @param {String} eventName @param {Array} params + @param {Array} actions @return true */ function sendEvent(obj, eventName, params, actions) { @@ -3970,7 +4266,7 @@ function sendEvent(obj, eventName, params, actions) { if (params) { method.apply(target, params); } else { - method.apply(target); + method.call(target); } } return true; @@ -4227,7 +4523,7 @@ Ember.RunLoop = RunLoop; ```javascript Ember.run(function(){ - // code to be execute within a RunLoop + // code to be execute within a RunLoop }); ``` @@ -4243,8 +4539,7 @@ Ember.RunLoop = RunLoop; @return {Object} return value from invoking the passed function. */ Ember.run = function(target, method) { - var loop, - args = arguments; + var args = arguments; run.begin(); function tryable() { @@ -4262,11 +4557,11 @@ 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()`. + a lower-level way to use a RunLoop instead of using `Ember.run()`. ```javascript Ember.run.begin(); - // code to be execute within a RunLoop + // code to be execute within a RunLoop Ember.run.end(); ``` @@ -4284,7 +4579,7 @@ Ember.run.begin = function() { ```javascript Ember.run.begin(); - // code to be execute within a RunLoop + // code to be execute within a RunLoop Ember.run.end(); ``` @@ -4308,9 +4603,9 @@ Ember.run.end = function() { @property queues @type Array - @default ['sync', 'actions', 'destroy', 'timers'] + @default ['sync', 'actions', 'destroy'] */ -Ember.run.queues = ['sync', 'actions', 'destroy', 'timers']; +Ember.run.queues = ['sync', 'actions', 'destroy']; /** Adds the passed target/method and any optional arguments to the named @@ -4323,19 +4618,19 @@ Ember.run.queues = ['sync', 'actions', 'destroy', 'timers']; 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 + // this will be executed in the first RunLoop queue, when bindings are synced console.log("scheduled on sync queue"); }); + Ember.run.schedule('actions', this, function(){ + // this will be executed in the 'actions' queue, after bindings have synced. + console.log("scheduled on actions 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 + // scheduled on actions queue ``` @method schedule @@ -4361,7 +4656,7 @@ function autorun() { // Used by global test teardown Ember.run.hasScheduledTimers = function() { - return !!(scheduledAutorun || scheduledLater || scheduledNext); + return !!(scheduledAutorun || scheduledLater); }; // Used by global test teardown @@ -4374,10 +4669,6 @@ Ember.run.cancelTimers = function () { clearTimeout(scheduledLater); scheduledLater = null; } - if (scheduledNext) { - clearTimeout(scheduledNext); - scheduledNext = null; - } timers = {}; }; @@ -4412,7 +4703,8 @@ Ember.run.autorun = function() { 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. + throughout the app immediately without repainting the UI (which happens + in the later 'render' queue added by the `ember-views` package). ```javascript Ember.run.sync(); @@ -4432,25 +4724,30 @@ Ember.run.sync = function() { var timers = {}; // active timers... -var scheduledLater; +var scheduledLater, scheduledLaterExpires; 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; + run(function() { + var now = (+ new Date()), earliest = -1; + for (var key in timers) { + if (!timers.hasOwnProperty(key)) { continue; } + var timer = timers[key]; + if (timer && timer.expires) { + if (now >= timer.expires) { + delete timers[key]; + invoke(timer.target, timer.method, timer.args, 2); + } else { + if (earliest < 0 || (timer.expires < earliest)) { earliest = timer.expires; } + } } } - } - // schedule next timeout to fire... - if (earliest > 0) { scheduledLater = setTimeout(invokeLaterTimers, earliest-(+ new Date())); } + // schedule next timeout to fire when the earliest timer expires + if (earliest > 0) { + scheduledLater = setTimeout(invokeLaterTimers, earliest - now); + scheduledLaterExpires = earliest; + } + }); } /** @@ -4475,8 +4772,7 @@ function invokeLaterTimers() { 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. + @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. */ @@ -4498,7 +4794,19 @@ Ember.run.later = function(target, method) { timer = { target: target, method: method, expires: expires, args: args }; guid = Ember.guidFor(timer); timers[guid] = timer; - run.once(timers, invokeLaterTimers); + + if(scheduledLater && expires < scheduledLaterExpires) { + // Cancel later timer (then reschedule earlier timer below) + clearTimeout(scheduledLater); + scheduledLater = null; + } + + if (!scheduledLater) { + // Schedule later timers to be run. + scheduledLater = setTimeout(invokeLaterTimers, wait); + scheduledLaterExpires = expires; + } + return guid; }; @@ -4554,6 +4862,21 @@ function scheduleOnce(queue, target, method, args) { }); ``` + Also note that passing an anonymous function to `Ember.run.once` will + not prevent additional calls with an identical anonymous function from + scheduling the items multiple times, e.g.: + + ```javascript + function scheduleIt() { + Ember.run.once(myContext, function() { console.log("Closure"); }); + } + scheduleIt(); + scheduleIt(); + // "Closure" will print twice, even though we're using `Ember.run.once`, + // because the function we pass to it is anonymous and won't match the + // previously scheduled operation. + ``` + @method once @param {Object} [target] target of method to invoke @param {Function|String} method The method to invoke. @@ -4570,29 +4893,55 @@ 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)`. + Schedules an item to run from within a separate run loop, after + control has been returned to the system. This is equivalent to calling + `Ember.run.later` with a wait time of 1ms. ```javascript Ember.run.next(myContext, function(){ - // code to be executed in the next RunLoop, which will be scheduled after the current one + // code to be executed in the next run loop, which will be scheduled after the current one + }); + ``` + + Multiple operations scheduled with `Ember.run.next` will coalesce + into the same later run loop, along with any other operations + scheduled by `Ember.run.later` that expire right around the same + time that `Ember.run.next` operations will fire. + + Note that there are often alternatives to using `Ember.run.next`. + For instance, if you'd like to schedule an operation to happen + after all DOM element operations have completed within the current + run loop, you can make use of the `afterRender` run loop queue (added + by the `ember-views` package, along with the preceding `render` queue + where all the DOM element operations happen). Example: + + ```javascript + App.MyCollectionView = Ember.CollectionView.extend({ + didInsertElement: function() { + Ember.run.scheduleOnce('afterRender', this, 'processChildElements'); + }, + processChildElements: function() { + // ... do something with collectionView's child view + // elements after they've finished rendering, which + // can't be done within the CollectionView's + // `didInsertElement` hook because that gets run + // before the child elements have been added to the DOM. + } }); ``` + One benefit of the above approach compared to using `Ember.run.next` is + that you will be able to perform DOM/CSS operations before unprocessed + elements are rendered to the screen, which may prevent flickering or + other artifacts caused by delaying processing until after rendering. + + The other major benefit to the above approach is that `Ember.run.next` + introduces an element of non-determinism, which can make things much + harder to test, due to its reliance on `setTimeout`; it's much harder + to guarantee the order of scheduled operations when they are scheduled + outside of the current run loop, i.e. with `Ember.run.next`. + @method next @param {Object} [target] target of method to invoke @param {Function|String} method The method to invoke. @@ -4601,20 +4950,10 @@ function invokeNextTimers() { @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; +Ember.run.next = function() { + var args = slice.call(arguments); + args.push(1); // 1 millisecond wait + return run.later.apply(this, args); }; /** @@ -4729,7 +5068,7 @@ Binding.prototype = { `get()` - see that method for more information. @method from - @param {String} propertyPath the property path to connect to + @param {String} path the property path to connect to @return {Ember.Binding} `this` */ from: function(path) { @@ -4747,7 +5086,7 @@ Binding.prototype = { `get()` - see that method for more information. @method to - @param {String|Tuple} propertyPath A property path or tuple + @param {String|Tuple} path A property path or tuple @return {Ember.Binding} `this` */ to: function(path) { @@ -4769,6 +5108,10 @@ Binding.prototype = { return this; }, + /** + @method toString + @return {String} string representation of binding + */ toString: function() { var oneWay = this._oneWay ? '[oneWay]' : ''; return "Ember.Binding<" + guidFor(this) + ">(" + this._from + " -> " + this._to + ")" + oneWay; @@ -5119,7 +5462,6 @@ var Mixin, REQUIRED, Alias, 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; @@ -5488,12 +5830,44 @@ Mixin.finishPartial = finishPartial; Ember.anyUnprocessedMixins = false; /** - @method create - @static - @param arguments* -*/ -Mixin.create = function() { - Ember.anyUnprocessedMixins = true; + Creates an instance of a class. Accepts either no arguments, or an object + containing values to initialize the newly instantiated object with. + + ```javascript + App.Person = Ember.Object.extend({ + helloWorld: function() { + alert("Hi, my name is " + this.get('name')); + } + }); + + var tom = App.Person.create({ + name: 'Tom Dale' + }); + + tom.helloWorld(); // alerts "Hi, my name is Tom Dale". + ``` + + `create` will call the `init` function if defined during + `Ember.AnyObject.extend` + + If no arguments are passed to `create`, it will not set values to the new + instance during initialization: + + ```javascript + var noName = App.Person.create(); + noName.helloWorld(); // alerts undefined + ``` + + NOTE: For performance reasons, you cannot declare methods or computed + properties during `create`. You should instead declare methods and computed + properties when using `extend`. + + @method create + @static + @param arguments* +*/ +Mixin.create = function() { + Ember.anyUnprocessedMixins = true; var M = this; return initMixin(new M(), arguments); }; @@ -6003,35 +6377,35 @@ define("rsvp", } function all(promises) { - var i, results = []; - var allPromise = new Promise(); - var remaining = promises.length; + 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 resolver = function(index) { + return function(value) { + resolve(index, value); + }; + }; - var resolve = function(index, value) { - results[index] = value; - if (--remaining === 0) { - allPromise.resolve(results); - } - }; + var resolve = function(index, value) { + results[index] = value; + if (--remaining === 0) { + allPromise.resolve(results); + } + }; - var reject = function(error) { - allPromise.reject(error); - }; + var reject = function(error) { + allPromise.reject(error); + }; - for (i = 0; i < remaining; i++) { - promises[i].then(resolver(i), reject); - } - return allPromise; + for (i = 0; i < remaining; i++) { + promises[i].then(resolver(i), reject); + } + return allPromise; } EventTarget.mixin(Promise.prototype); @@ -6047,12 +6421,6 @@ 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 = {}; @@ -6127,26 +6495,35 @@ define("container", 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); + Ember.deprecate('register("'+type +'", "'+ name+'") is now deprecated in-favour of register("'+type+':'+name+'");', false); fullName = type + ":" + name; } - this.registry.set(fullName, factory); - this._options.set(fullName, options || {}); + var normalizedName = this.normalize(fullName); + + this.registry.set(normalizedName, factory); + this._options.set(normalizedName, options || {}); }, resolve: function(fullName) { return this.resolver(fullName) || this.registry.get(fullName); }, - lookup: function(fullName) { - if (this.cache.has(fullName)) { + normalize: function(fullName) { + return fullName; + }, + + lookup: function(fullName, options) { + fullName = this.normalize(fullName); + + options = options || {}; + + if (this.cache.has(fullName) && options.singleton !== false) { return this.cache.get(fullName); } @@ -6154,7 +6531,7 @@ define("container", if (!value) { return; } - if (isSingleton(this, fullName)) { + if (isSingleton(this, fullName) && options.singleton !== false) { this.cache.set(fullName, value); } @@ -6272,7 +6649,8 @@ define("container", } function factoryFor(container, fullName) { - return container.resolve(fullName); + var name = container.normalize(fullName); + return container.resolve(name); } function instantiate(container, fullName) { @@ -6358,8 +6736,8 @@ var toString = Object.prototype.toString; | '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 | + | 'class' | An Ember class (created using Ember.Object.extend()) | + | 'instance' | An Ember object instance | | 'error' | An instance of the Error object | | 'object' | A JavaScript object not inheriting from Ember.Object | @@ -6384,7 +6762,7 @@ var toString = Object.prototype.toString; @method typeOf @for Ember - @param item {Object} the item to check + @param {Object} item the item to check @return {String} the type */ Ember.typeOf = function(item) { @@ -6403,57 +6781,6 @@ Ember.typeOf = function(item) { 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: @@ -6618,7 +6945,7 @@ function _copy(obj, deep, seen, copies) { @method copy @for Ember - @param {Object} object The object to clone + @param {Object} obj The object to clone @param {Boolean} deep If true, a deep copy of the object is made @return {Object} The cloned object */ @@ -6747,6 +7074,20 @@ Ember.Error.prototype = Ember.create(Error.prototype); +(function() { +/** + Expose RSVP implementation + + @class RSVP + @namespace Ember + @constructor +*/ +Ember.RSVP = requireModule('rsvp'); + +})(); + + + (function() { /** @module ember @@ -6895,10 +7236,11 @@ Ember.String = { */ dasherize: function(str) { var cache = STRING_DASHERIZE_CACHE, - ret = cache[str]; + hit = cache.hasOwnProperty(str), + ret; - if (ret) { - return ret; + if (hit) { + return cache[str]; } else { ret = Ember.String.decamelize(str).replace(STRING_DASHERIZE_REGEXP,'-'); cache[str] = ret; @@ -6908,13 +7250,14 @@ Ember.String = { }, /** - Returns the lowerCaseCamel form of a string. + Returns the lowerCamelCase form of a string. ```javascript 'innerHTML'.camelize(); // 'innerHTML' 'action_name'.camelize(); // 'actionName' 'css-class-name'.camelize(); // 'cssClassName' 'my favorite items'.camelize(); // 'myFavoriteItems' + 'My Favorite Items'.camelize(); // 'myFavoriteItems' ``` @method camelize @@ -6924,6 +7267,8 @@ Ember.String = { camelize: function(str) { return str.replace(STRING_CAMELIZE_REGEXP, function(match, separator, chr) { return chr ? chr.toUpperCase() : ''; + }).replace(/^([A-Z])/, function(match, separator, chr) { + return match.toLowerCase(); }); }, @@ -6935,7 +7280,7 @@ Ember.String = { 'action_name'.classify(); // 'ActionName' 'css-class-name'.classify(); // 'CssClassName' 'my favorite items'.classify(); // 'MyFavoriteItems' - ``` + ``` @method classify @param {String} str the string to classify @@ -6976,10 +7321,10 @@ Ember.String = { /** Returns the Capitalized form of a string - 'innerHTML'.capitalize() => 'InnerHTML' - 'action_name'.capitalize() => 'Action_name' - 'css-class-name'.capitalize() => 'Css-class-name' - 'my favorite items'.capitalize() => 'My favorite items' + '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 @@ -7323,8 +7668,7 @@ function iter(key, value) { @extends Ember.Mixin @since Ember 0.9 */ -Ember.Enumerable = Ember.Mixin.create( - /** @scope Ember.Enumerable.prototype */ { +Ember.Enumerable = Ember.Mixin.create({ // compatibility isEnumerable: true, @@ -7357,7 +7701,7 @@ Ember.Enumerable = Ember.Mixin.create( @method nextObject @param {Number} index the current index of the iteration - @param {Object} previousObject the value returned by the last call to + @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 @@ -7376,10 +7720,10 @@ Ember.Enumerable = Ember.Mixin.create( ```javascript var arr = ["a", "b", "c"]; - arr.firstObject(); // "a" + arr.get('firstObject'); // "a" var arr = []; - arr.firstObject(); // undefined + arr.get('firstObject'); // undefined ``` @property firstObject @@ -7402,10 +7746,10 @@ Ember.Enumerable = Ember.Mixin.create( ```javascript var arr = ["a", "b", "c"]; - arr.lastObject(); // "c" + arr.get('lastObject'); // "c" var arr = []; - arr.lastObject(); // undefined + arr.get('lastObject'); // undefined ``` @property lastObject @@ -7538,7 +7882,7 @@ Ember.Enumerable = Ember.Mixin.create( @return {Array} The mapped array. */ map: function(callback, target) { - var ret = []; + var ret = Ember.A([]); this.forEach(function(x, idx, i) { ret[idx] = callback.call(target, x, idx,i); }); @@ -7588,7 +7932,7 @@ Ember.Enumerable = Ember.Mixin.create( @return {Array} A filtered array. */ filter: function(callback, target) { - var ret = []; + var ret = Ember.A([]); this.forEach(function(x, idx, i) { if (callback.call(target, x, idx, i)) ret.push(x); }); @@ -7877,7 +8221,7 @@ Ember.Enumerable = Ember.Mixin.create( @return {Array} return values from calling invoke. */ invoke: function(methodName) { - var args, ret = []; + var args, ret = Ember.A([]); if (arguments.length>1) args = a_slice.call(arguments, 1); this.forEach(function(x, idx) { @@ -7898,23 +8242,25 @@ Ember.Enumerable = Ember.Mixin.create( @return {Array} the enumerable as an array. */ toArray: function() { - var ret = []; + var ret = Ember.A([]); this.forEach(function(o, idx) { ret[idx] = o; }); return ret ; }, /** - Returns a copy of the array with all null elements removed. + Returns a copy of the array with all null and undefined elements removed. ```javascript - var arr = ["a", null, "c", null]; + var arr = ["a", null, "c", undefined]; arr.compact(); // ["a", "c"] ``` @method compact - @return {Array} the array without null elements. + @return {Array} the array without null and undefined elements. */ - compact: function() { return this.without(null); }, + compact: function() { + return this.filter(function(value) { return value != null; }); + }, /** Returns a new enumerable that excludes the passed value. The default @@ -7932,7 +8278,7 @@ Ember.Enumerable = Ember.Mixin.create( */ without: function(value) { if (!this.contains(value)) return this; // nothing to do - var ret = [] ; + var ret = Ember.A([]); this.forEach(function(k) { if (k !== value) ret[ret.length] = k; }) ; @@ -7952,7 +8298,7 @@ Ember.Enumerable = Ember.Mixin.create( @return {Ember.Enumerable} */ uniq: function() { - var ret = []; + var ret = Ember.A([]); this.forEach(function(k){ if (a_indexOf(ret, k)<0) ret.push(k); }); @@ -7969,6 +8315,7 @@ Ember.Enumerable = Ember.Mixin.create( @property [] @type Ember.Array + @return this */ '[]': Ember.computed(function(key, value) { return this; @@ -7983,8 +8330,9 @@ Ember.Enumerable = Ember.Mixin.create( mixin. @method addEnumerableObserver - @param target {Object} - @param opts {Hash} + @param {Object} target + @param {Hash} [opts] + @return this */ addEnumerableObserver: function(target, opts) { var willChange = (opts && opts.willChange) || 'enumerableWillChange', @@ -8002,8 +8350,9 @@ Ember.Enumerable = Ember.Mixin.create( Removes a registered enumerable observer. @method removeEnumerableObserver - @param target {Object} - @param [opts] {Hash} + @param {Object} target + @param {Hash} [opts] + @return this */ removeEnumerableObserver: function(target, opts) { var willChange = (opts && opts.willChange) || 'enumerableWillChange', @@ -8082,7 +8431,7 @@ Ember.Enumerable = Ember.Mixin.create( @chainable */ enumerableContentDidChange: function(removing, adding) { - var notify = this.propertyDidChange, removeCnt, addCnt, hasDelta; + var removeCnt, addCnt, hasDelta; if ('number' === typeof removing) removeCnt = removing; else if (removing) removeCnt = get(removing, 'length'); @@ -8120,7 +8469,7 @@ Ember.Enumerable = Ember.Mixin.create( // HELPERS // -var get = Ember.get, set = Ember.set, meta = Ember.meta, map = Ember.EnumerableUtils.map, cacheFor = Ember.cacheFor; +var get = Ember.get, set = Ember.set, map = Ember.EnumerableUtils.map, cacheFor = Ember.cacheFor; function none(obj) { return obj===null || obj===undefined; } @@ -8193,6 +8542,7 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot @method objectAt @param {Number} idx The index of the item to return. + @return {any} item at index or undefined */ objectAt: function(idx) { if ((idx < 0) || (idx>=get(this, 'length'))) return undefined ; @@ -8210,6 +8560,7 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot @method objectsAt @param {Array} indexes An array of indexes of items to return. + @return {Array} */ objectsAt: function(indexes) { var self = this; @@ -8229,6 +8580,7 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot This property overrides the default property defined in `Ember.Enumerable`. @property [] + @return this */ '[]': Ember.computed(function(key, value) { if (value !== undefined) this.replace(0, get(this, 'length'), value) ; @@ -8262,15 +8614,19 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot ``` @method slice - @param beginIndex {Integer} (Optional) index to begin slicing from. - @param endIndex {Integer} (Optional) index to end the slice at. + @param {Integer} beginIndex (Optional) index to begin slicing from. + @param {Integer} endIndex (Optional) index to end the slice at. @return {Array} New array with specified slice */ slice: function(beginIndex, endIndex) { - var ret = []; + var ret = Ember.A([]); var length = get(this, 'length') ; if (none(beginIndex)) beginIndex = 0 ; if (none(endIndex) || (endIndex > length)) endIndex = length ; + + if (beginIndex < 0) beginIndex = length + beginIndex; + if (endIndex < 0) endIndex = length + endIndex; + while(beginIndex < endIndex) { ret[ret.length] = this.objectAt(beginIndex++) ; } @@ -8368,7 +8724,7 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot @method addArrayObserver @param {Object} target The observer object. @param {Hash} opts Optional hash of configuration options including - `willChange`, `didChange`, and a `context` option. + `willChange` and `didChange` option. @return {Ember.Array} receiver */ addArrayObserver: function(target, opts) { @@ -8390,6 +8746,8 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot @method removeArrayObserver @param {Object} target The object observing the array. + @param {Hash} opts Optional hash of configuration options including + `willChange` and `didChange` option. @return {Ember.Array} receiver */ removeArrayObserver: function(target, opts) { @@ -8422,9 +8780,9 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot @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 + @param {Number} removeAmt The number of items that will be removed. If you pass `null` assumes 0 - @param {Number} addAmt The number of items that will be added If you + @param {Number} addAmt The number of items that will be added If you pass `null` assumes 0. @return {Ember.Array} receiver */ @@ -8606,7 +8964,7 @@ Ember.Copyable = Ember.Mixin.create( an exception. @method copy - @param deep {Boolean} if `true`, a deep copy of the object should be made + @param {Boolean} deep if `true`, a deep copy of the object should be made @return {Object} copy of receiver */ copy: Ember.required(Function), @@ -8785,8 +9143,7 @@ var forEach = Ember.EnumerableUtils.forEach; @extends Ember.Mixin @uses Ember.Enumerable */ -Ember.MutableEnumerable = Ember.Mixin.create(Ember.Enumerable, - /** @scope Ember.MutableEnumerable.prototype */ { +Ember.MutableEnumerable = Ember.Mixin.create(Ember.Enumerable, { /** __Required.__ You must implement this method to apply this mixin. @@ -8871,7 +9228,7 @@ var EMPTY = []; // HELPERS // -var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; +var get = Ember.get, set = Ember.set; /** This mixin defines the API for modifying array-like objects. These methods @@ -8898,11 +9255,11 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, passed array. You should also call `this.enumerableContentDidChange()` @method replace - @param {Number} idx Starting index in the array to replace. If + @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 + @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 + @param {Array} objects An array of zero or more objects that should be inserted into the array at *idx* */ replace: Ember.required(), @@ -8941,6 +9298,7 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, @method insertAt @param {Number} idx index of insert the object at. @param {Object} object object to insert + @return this */ insertAt: function(idx, object) { if (idx > get(this, 'length')) throw new Error(OUT_OF_RANGE_EXCEPTION) ; @@ -8994,6 +9352,7 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, @method pushObject @param {anything} obj object to push + @return {any} the same obj passed as param */ pushObject: function(obj) { this.insertAt(get(this, 'length'), obj) ; @@ -9073,6 +9432,7 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, @method unshiftObject @param {anything} obj object to unshift + @return {any} the same obj passed as param */ unshiftObject: function(obj) { this.insertAt(0, obj) ; @@ -9167,7 +9527,7 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, @submodule ember-runtime */ -var get = Ember.get, set = Ember.set, defineProperty = Ember.defineProperty; +var get = Ember.get, set = Ember.set; /** ## Overview @@ -9272,7 +9632,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { not defined upfront. @method get - @param {String} key The property to retrieve + @param {String} keyName The property to retrieve @return {Object} The property value or undefined. */ get: function(keyName) { @@ -9353,7 +9713,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { ``` @method set - @param {String} key The property to set + @param {String} keyName The property to set @param {Object} value The value to set or `null`. @return {Ember.Observable} */ @@ -9430,7 +9790,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { like. @method propertyWillChange - @param {String} key The property key that is about to change. + @param {String} keyName The property key that is about to change. @return {Ember.Observable} */ propertyWillChange: function(keyName){ @@ -9765,6 +10125,16 @@ Ember.TargetActionSupport = Ember.Mixin.create({ // outputs: 'Our person has greeted' ``` + You can also chain multiple event subscriptions: + + ```javascript + person.on('greet', function() { + console.log('Our person has greeted'); + }).one('greet', function() { + console.log('Offer one-time special'); + }).off('event', this, forgetThis); + ``` + @class Evented @namespace Ember @extends Ember.Mixin @@ -9789,9 +10159,11 @@ Ember.Evented = Ember.Mixin.create({ @param {String} name The name of the event @param {Object} [target] The "this" binding for the callback @param {Function} method The callback to execute + @return this */ on: function(name, target, method) { Ember.addListener(this, name, target, method); + return this; }, /** @@ -9807,6 +10179,7 @@ Ember.Evented = Ember.Mixin.create({ @param {String} name The name of the event @param {Object} [target] The "this" binding for the callback @param {Function} method The callback to execute + @return this */ one: function(name, target, method) { if (!method) { @@ -9815,6 +10188,7 @@ Ember.Evented = Ember.Mixin.create({ } Ember.addListener(this, name, target, method, true); + return this; }, /** @@ -9855,9 +10229,11 @@ Ember.Evented = Ember.Mixin.create({ @param {String} name The name of the event @param {Object} target The target of the subscription @param {Function} method The function of the subscription + @return this */ off: function(name, target, method) { Ember.removeListener(this, name, target, method); + return this; }, /** @@ -9888,8 +10264,7 @@ RSVP.async = function(callback, binding) { @submodule ember-runtime */ -var get = Ember.get, - slice = Array.prototype.slice; +var get = Ember.get; /** @class Deferred @@ -9965,7 +10340,6 @@ Ember.Container.set = Ember.set; 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, @@ -10113,6 +10487,37 @@ CoreObject.PrototypeMixin = Mixin.create({ isInstance: true, + /** + An overridable method called when objects are instantiated. By default, + does nothing unless it is overridden during class definition. + + Example: + + ```javascript + App.Person = Ember.Object.extend({ + init: function() { + this._super(); + alert('Name is ' + this.get('name')); + } + }); + + var steve = App.Person.create({ + name: "Steve" + }); + + // alerts 'Name is Steve'. + ``` + + NOTE: If you do override `init` for a framework class like `Ember.View` or + `Ember.ArrayController`, be sure to call `this._super()` in your + `init` declaration! If you don't, Ember may not have an opportunity to + do important setup work, and you'll see strange behavior in your + application. + + ``` + + @method init + */ init: function() {}, /** @@ -10157,14 +10562,14 @@ CoreObject.PrototypeMixin = Mixin.create({ 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. @@ -10181,12 +10586,22 @@ CoreObject.PrototypeMixin = Mixin.create({ concatenatedProperties: null, /** + Destroyed object property flag. + + if this property is `true` the observers and bindings were already + removed by the effect of calling the `destroy()` method. + @property isDestroyed @default false */ isDestroyed: false, /** + Destruction scheduled flag. The `destroy()` method has been called. + + The object stays intact until the end of the run loop at which point + the `isDestroyed` flag is set. + @property isDestroying @default false */ @@ -10263,7 +10678,7 @@ CoreObject.PrototypeMixin = Mixin.create({ } }); teacher = App.Teacher.create() - teacher.toString(); // #=> "" + teacher.toString(); //=> "" @method toString @return {String} string representation @@ -10442,680 +10857,723 @@ Ember.CoreObject = CoreObject; @submodule ember-runtime */ -var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, none = Ember.isNone; - /** - An unordered collection of objects. + `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. - 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. + @class Object + @namespace Ember + @extends Ember.CoreObject + @uses Ember.Observable +*/ +Ember.Object = Ember.CoreObject.extend(Ember.Observable); +Ember.Object.toString = function() { return "Ember.Object"; }; - 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()`. +(function() { +/** +@module ember +@submodule ember-runtime +*/ - ```javascript - // creates a new empty set - var foundNames = new Ember.Set(); +var get = Ember.get, indexOf = Ember.ArrayPolyfills.indexOf; - // creates a set with four names in it. - var names = new Ember.Set(["Charles", "Tom", "Juan", "Alex"]); // :P +/** + 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. - // creates a copy of the names set. - var namesCopy = new Ember.Set(names); + # Example Usage - // same as above. - var anotherNamesCopy = names.copy(); + ```javascript + MyFramework = Ember.Namespace.create({ + VERSION: '1.0.0' + }); ``` - ## Adding/Removing Objects + @class Namespace + @namespace Ember + @extends Ember.Object +*/ +var Namespace = Ember.Namespace = Ember.Object.extend({ + isNamespace: true, - 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. + init: function() { + Ember.Namespace.NAMESPACES.push(this); + Ember.Namespace.PROCESSED = false; + }, - 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. + toString: function() { + var name = get(this, 'name'); + if (name) { return name; } - NOTE: You cannot add/remove `null` or `undefined` to a set. Any attempt to do - so will be ignored. + findNamespaces(); + return this[Ember.GUID_KEY+'_name']; + }, - 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. + nameClasses: function() { + processNamespace([this.toString()], this, {}); + }, - ## Testing for an Object + destroy: function() { + var namespaces = Ember.Namespace.NAMESPACES; + Ember.lookup[this.toString()] = undefined; + namespaces.splice(indexOf.call(namespaces, this), 1); + this._super(); + } +}); - To test for an object's presence in a set you simply call - `Ember.Set#contains()`. +Namespace.reopenClass({ + NAMESPACES: [Ember], + NAMESPACES_BY_ID: {}, + PROCESSED: false, + processAll: processAllNamespaces, + byName: function(name) { + if (!Ember.BOOTED) { + processAllNamespaces(); + } - ## Observing changes + return NAMESPACES_BY_ID[name]; + } +}); - 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. +var NAMESPACES_BY_ID = Namespace.NAMESPACES_BY_ID; - This is often unhelpful. If you are filtering sets of objects, for instance, - it is very inefficient to re-filter all of the items each time the set - changes. It would be better if you could just adjust the filtered set based - on what was changed on the original set. The same issue applies to merging - sets, as well. +var hasOwnProp = ({}).hasOwnProperty, + guidFor = Ember.guidFor; - ## Other Methods +function processNamespace(paths, root, seen) { + var idx = paths.length; - `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. + NAMESPACES_BY_ID[paths.join('.')] = root; - 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. + // 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]; - @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 */ { + // 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; - // .......................................................... - // IMPLEMENT ENUMERABLE APIS - // + // 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('.'); - /** - This property will change as the number of objects in the set changes. + // Support nested namespaces + } else if (obj && obj.isNamespace) { + // Skip aliased namespaces + if (seen[guidFor(obj)]) { continue; } + seen[guidFor(obj)] = true; - @property length - @type number - @default 0 - */ - length: 0, + // Process the child namespace + processNamespace(paths, obj, seen); + } + } - /** - Clears the set. This is useful if you want to reuse an existing set - without having to recreate it. + paths.length = idx; // cut out last item +} - ```javascript - var colors = new Ember.Set(["red", "green", "blue"]); - colors.length; // 3 - colors.clear(); - colors.length; // 0 - ``` +function findNamespaces() { + var Namespace = Ember.Namespace, lookup = Ember.lookup, obj, isNamespace; - @method clear - @return {Ember.Set} An empty Set - */ - clear: function() { - if (this.isFrozen) { throw new Error(Ember.FROZEN_ERROR); } + if (Namespace.PROCESSED) { return; } - var len = get(this, 'length'); - if (len === 0) { return this; } + for (var prop in lookup) { + // These don't raise exceptions but can cause warnings + if (prop === "parent" || prop === "top" || prop === "frameElement") { continue; } - var guid; + // 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; } - this.enumerableContentWillChange(len, 0); - Ember.propertyWillChange(this, 'firstObject'); - Ember.propertyWillChange(this, 'lastObject'); + // 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; + } - for (var i=0; i < len; i++){ - guid = guidFor(this[i]); - delete this[guid]; - delete this[i]; + if (isNamespace) { + Ember.deprecate("Namespaces should not begin with lowercase.", /^[A-Z]/.test(prop)); + obj[NAME_KEY] = prop; } + } +} - set(this, 'length', 0); +var NAME_KEY = Ember.NAME_KEY = Ember.GUID_KEY + '_name'; - Ember.propertyDidChange(this, 'firstObject'); - Ember.propertyDidChange(this, 'lastObject'); - this.enumerableContentDidChange(len, 0); +function superClassString(mixin) { + var superclass = mixin.superclass; + if (superclass) { + if (superclass[NAME_KEY]) { return superclass[NAME_KEY]; } + else { return superClassString(superclass); } + } else { + return; + } +} - return this; - }, +function classToString() { + if (!Ember.BOOTED && !this[NAME_KEY]) { + processAllNamespaces(); + } - /** - Returns true if the passed object is also an enumerable that contains the - same objects as the receiver. + var ret; - ```javascript - var colors = ["red", "green", "blue"], - same_colors = new Ember.Set(colors); + 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); + } - same_colors.isEqual(colors); // true - same_colors.isEqual(["purple", "brown"]); // false - ``` + return ret; +} - @method isEqual - @param {Ember.Set} obj the other object. - @return {Boolean} - */ - isEqual: function(obj) { - // fail fast - if (!Ember.Enumerable.detect(obj)) return false; +function processAllNamespaces() { + var unprocessedNamespaces = !Namespace.PROCESSED, + unprocessedMixins = Ember.anyUnprocessedMixins; - var loc = get(this, 'length'); - if (get(obj, 'length') !== loc) return false; + if (unprocessedNamespaces) { + findNamespaces(); + Namespace.PROCESSED = true; + } - while(--loc >= 0) { - if (!obj.contains(this[loc])) return false; + if (unprocessedNamespaces || unprocessedMixins) { + var namespaces = Namespace.NAMESPACES, namespace; + for (var i=0, l=namespaces.length; i 0 ? this[this.length-1] : null; - this.remove(obj); - return obj; - }, +(function() { +/** +@module ember +@submodule ember-runtime +*/ - /** - Inserts the given object on to the end of the set. It returns - the set itself. +var OUT_OF_RANGE_EXCEPTION = "Index out of range"; +var EMPTY = []; - This is an alias for `Ember.MutableEnumerable.addObject()`. +var get = Ember.get, set = Ember.set; - ```javascript - var colors = new Ember.Set(); - colors.push("red"); // ["red"] - colors.push("green"); // ["red", "green"] - colors.push("blue"); // ["red", "green", "blue"] - ``` +/** + 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. - @method push - @return {Ember.Set} The set itself. - */ - push: Ember.aliasMethod('addObject'), + A simple example of usage: - /** - Removes the last element from the set and returns it, or `null` if it's empty. + ```javascript + var pets = ['dog', 'cat', 'fish']; + var ap = Ember.ArrayProxy.create({ content: Ember.A(pets) }); - This is an alias for `Ember.Set.pop()`. + ap.get('firstObject'); // 'dog' + ap.set('content', ['amoeba', 'paramecium']); + ap.get('firstObject'); // 'amoeba' + ``` - ```javascript - var colors = new Ember.Set(["green", "blue"]); - colors.shift(); // "blue" - colors.shift(); // "green" - colors.shift(); // null - ``` + 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`: - @method shift - @return {Object} The removed object from the set or null. - */ - shift: Ember.aliasMethod('pop'), + ```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(); + } + }); - /** - Inserts the given object on to the end of the set. It returns - the set itself. + ap.get('firstObject'); // . 'DOG' + ``` - This is an alias of `Ember.Set.push()` + @class ArrayProxy + @namespace Ember + @extends Ember.Object + @uses Ember.MutableArray +*/ +Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray, +/** @scope Ember.ArrayProxy.prototype */ { - ```javascript - var colors = new Ember.Set(); - colors.unshift("red"); // ["red"] - colors.unshift("green"); // ["red", "green"] - colors.unshift("blue"); // ["red", "green", "blue"] - ``` + /** + The content array. Must be an object that implements `Ember.Array` and/or + `Ember.MutableArray.` - @method unshift - @return {Ember.Set} The set itself. + @property content + @type Ember.Array */ - unshift: Ember.aliasMethod('push'), + content: null, /** - Adds each object in the passed enumerable to the set. + 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. - This is an alias of `Ember.MutableEnumerable.addObjects()` + @property arrangedContent + */ + arrangedContent: Ember.computed.alias('content'), - ```javascript - var colors = new Ember.Set(); - colors.addEach(["red", "green", "blue"]); // ["red", "green", "blue"] - ``` + /** + 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. - @method addEach - @param {Ember.Enumerable} objects the objects to add. - @return {Ember.Set} The set itself. + 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 */ - addEach: Ember.aliasMethod('addObjects'), + objectAtContent: function(idx) { + return get(this, 'arrangedContent').objectAt(idx); + }, /** - Removes each object in the passed enumerable to the set. - - This is an alias of `Ember.MutableEnumerable.removeObjects()` + 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. - ```javascript - var colors = new Ember.Set(["red", "green", "blue"]); - colors.removeEach(["red", "blue"]); // ["green"] - ``` + This method will only be called if content is non-`null`. - @method removeEach - @param {Ember.Enumerable} objects the objects to remove. - @return {Ember.Set} The set itself. + @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} */ - removeEach: Ember.aliasMethod('removeObjects'), + replaceContent: function(idx, amt, objects) { + get(this, 'content').replace(idx, amt, objects); + }, - // .......................................................... - // PRIVATE ENUMERABLE SUPPORT - // + /** + @private - init: function(items) { - this._super(); - if (items) this.addObjects(items); - }, + Invoked when the content property is about to change. Notifies observers that the + entire array content will change. - // implement Ember.Enumerable - nextObject: function(idx) { - return this[idx]; - }, + @method _contentWillChange + */ + _contentWillChange: Ember.beforeObserver(function() { + this._teardownContent(); + }, 'content'), - // more optimized version - firstObject: Ember.computed(function() { - return this.length > 0 ? this[0] : undefined; - }), + _teardownContent: function() { + var content = get(this, 'content'); - // more optimized version - lastObject: Ember.computed(function() { - return this.length > 0 ? this[this.length-1] : undefined; - }), + if (content) { + content.removeArrayObserver(this, { + willChange: 'contentArrayWillChange', + didChange: 'contentArrayDidChange' + }); + } + }, - // implements Ember.MutableEnumerable - addObject: function(obj) { - if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); - if (none(obj)) return this; // nothing to do + contentArrayWillChange: Ember.K, + contentArrayDidChange: Ember.K, - var guid = guidFor(obj), - idx = this[guid], - len = get(this, 'length'), - added ; + /** + @private - if (idx>=0 && idx=0 && idx=0; - }, + _teardownArrangedContent: function() { + var arrangedContent = get(this, 'arrangedContent'); - copy: function() { - var C = this.constructor, ret = new C(), loc = get(this, 'length'); - set(ret, 'length', loc); - while(--loc>=0) { - ret[loc] = this[loc]; - ret[guidFor(this[loc])] = loc; + if (arrangedContent) { + arrangedContent.removeArrayObserver(this, { + willChange: 'arrangedContentArrayWillChange', + didChange: 'arrangedContentArrayDidChange' + }); } - 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(',')); - } + 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) { + var content = get(this, 'content'); + Ember.assert('The content property of '+ this.constructor + ' should be set before modifying it', content); + if (content) this.replaceContent(idx, amt, objects); + return this; + }, + replace: function() { + if (get(this, 'arrangedContent') === get(this, 'content')) { + this._replace.apply(this, arguments); + } else { + throw new Ember.Error("Using replace on an arranged ArrayProxy is not allowed."); + } + }, -(function() { -/** -@module ember -@submodule ember-runtime -*/ + _insertAt: function(idx, object) { + if (idx > get(this, 'content.length')) throw new Error(OUT_OF_RANGE_EXCEPTION); + this._replace(idx, 0, [object]); + return this; + }, -/** - `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. + insertAt: function(idx, object) { + if (get(this, 'arrangedContent') === get(this, 'content')) { + return this._insertAt(idx, object); + } else { + throw new Ember.Error("Using insertAt on an arranged ArrayProxy is not allowed."); + } + }, - @class Object - @namespace Ember - @extends Ember.CoreObject - @uses Ember.Observable -*/ -Ember.Object = Ember.CoreObject.extend(Ember.Observable); -Ember.Object.toString = function() { return "Ember.Object"; }; + removeAt: function(start, len) { + if ('number' === typeof start) { + var content = get(this, 'content'), + arrangedContent = get(this, 'arrangedContent'), + indices = [], i; -})(); + if ((start < 0) || (start >= get(this, 'length'))) { + throw new Error(OUT_OF_RANGE_EXCEPTION); + } + if (len === undefined) len = 1; + // Get a list of indices in original content to remove + for (i=start; i=idx) { + var item = content.objectAt(loc); + if (item) { + Ember.addBeforeObserver(item, keyName, proxy, 'contentKeyWillChange'); + Ember.addObserver(item, keyName, proxy, 'contentKeyDidChange'); - ```javascript - var pets = ['dog', 'cat', 'fish']; - var ap = Ember.ArrayProxy.create({ content: Ember.A(pets) }); + // keep track of the index each item was found at so we can map + // it back when the obj changes. + guid = guidFor(item); + if (!objects[guid]) objects[guid] = []; + objects[guid].push(loc); + } + } +} - ap.get('firstObject'); // 'dog' - ap.set('content', ['amoeba', 'paramecium']); - ap.get('firstObject'); // 'amoeba' - ``` +function removeObserverForContentKey(content, keyName, proxy, idx, loc) { + var objects = proxy._objects; + if (!objects) objects = proxy._objects = {}; + var indicies, guid; - 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`: + while(--loc>=idx) { + var item = content.objectAt(loc); + if (item) { + Ember.removeBeforeObserver(item, keyName, proxy, 'contentKeyWillChange'); + Ember.removeObserver(item, keyName, proxy, 'contentKeyDidChange'); - ```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(); - } - }); + guid = guidFor(item); + indicies = objects[guid]; + indicies[indicies.indexOf(loc)] = null; + } + } +} - ap.get('firstObject'); // . 'DOG' - ``` +/** + This is the object instance returned when you get the `@each` property on an + array. It uses the unknownProperty handler to automatically create + EachArray instances for property names. - @class ArrayProxy + @private + @class EachProxy @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.` +Ember.EachProxy = Ember.Object.extend({ - @property content - @type Ember.Array - */ - content: null, + init: function(content) { + this._super(); + this._content = content; + content.addArrayObserver(this); - /** - 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); + // in case someone is already observing some keys make sure they are + // added + forEach(Ember.watchedEvents(this), function(eventName) { + this.didAddListener(eventName); + }, this); }, /** - @private - - Invoked when the content property is about to change. Notifies observers that the - entire array content will change. + You can directly access mapped properties by simply requesting them. + The `unknownProperty` handler will generate an EachArray of each item. - @method _contentWillChange + @method unknownProperty + @param keyName {String} + @param value {anything} */ - _contentWillChange: Ember.beforeObserver(function() { - this._teardownContent(); - }, 'content'), - - _teardownContent: function() { - var content = get(this, 'content'); - - if (content) { - content.removeArrayObserver(this, { - willChange: 'contentArrayWillChange', - didChange: 'contentArrayDidChange' - }); - } + unknownProperty: function(keyName, value) { + var ret; + ret = new EachArray(this._content, keyName, this); + Ember.defineProperty(this, keyName, null, ret); + this.beginObservingContentKey(keyName); + return ret; }, - contentArrayWillChange: Ember.K, - contentArrayDidChange: Ember.K, - - /** - @private - - Invoked when the content property changes. Notifies observers that the - entire array content has changed. + // .......................................................... + // ARRAY CHANGES + // Invokes whenever the content array itself changes. - @method _contentDidChange - */ - _contentDidChange: Ember.observer(function() { - var content = get(this, 'content'); + arrayWillChange: function(content, idx, removedCnt, addedCnt) { + var keys = this._keys, key, lim; - Ember.assert("Can't set ArrayProxy's content to itself", content !== this); + lim = removedCnt>0 ? idx+removedCnt : -1; + Ember.beginPropertyChanges(this); - this._setupContent(); - }, 'content'), + for(key in keys) { + if (!keys.hasOwnProperty(key)) { continue; } - _setupContent: function() { - var content = get(this, 'content'); + if (lim>0) removeObserverForContentKey(content, key, this, idx, lim); - if (content) { - content.addArrayObserver(this, { - willChange: 'contentArrayWillChange', - didChange: 'contentArrayDidChange' - }); + Ember.propertyWillChange(this, key); } - }, - - _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.propertyWillChange(this._content, '@each'); + Ember.endPropertyChanges(this); + }, - Ember.assert("Can't set ArrayProxy's content to itself", arrangedContent !== this); + arrayDidChange: function(content, idx, removedCnt, addedCnt) { + var keys = this._keys, key, lim; - this._setupArrangedContent(); + lim = addedCnt>0 ? idx+addedCnt : -1; + Ember.beginPropertyChanges(this); - this.arrangedContentDidChange(this); - this.arrangedContentArrayDidChange(this, 0, undefined, len); - }, 'arrangedContent'), + for(key in keys) { + if (!keys.hasOwnProperty(key)) { continue; } - _setupArrangedContent: function() { - var arrangedContent = get(this, 'arrangedContent'); + if (lim>0) addObserverForContentKey(content, key, this, idx, lim); - if (arrangedContent) { - arrangedContent.addArrayObserver(this, { - willChange: 'arrangedContentArrayWillChange', - didChange: 'arrangedContentArrayDidChange' - }); + Ember.propertyDidChange(this, key); } + + Ember.propertyDidChange(this._content, '@each'); + Ember.endPropertyChanges(this); }, - _teardownArrangedContent: function() { - var arrangedContent = get(this, 'arrangedContent'); + // .......................................................... + // LISTEN FOR NEW OBSERVERS AND OTHER EVENT LISTENERS + // Start monitoring keys based on who is listening... - if (arrangedContent) { - arrangedContent.removeArrayObserver(this, { - willChange: 'arrangedContentArrayWillChange', - didChange: 'arrangedContentArrayDidChange' - }); + didAddListener: function(eventName) { + if (IS_OBSERVER.test(eventName)) { + this.beginObservingContentKey(eventName.slice(0, -7)); } }, - arrangedContentWillChange: Ember.K, - arrangedContentDidChange: Ember.K, - - objectAt: function(idx) { - return get(this, 'content') && this.objectAtContent(idx); + didRemoveListener: function(eventName) { + if (IS_OBSERVER.test(eventName)) { + this.stopObservingContentKey(eventName.slice(0, -7)); + } }, - 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; - }, + // .......................................................... + // CONTENT KEY OBSERVING + // Actual watch keys on the source content. - arrangedContentArrayWillChange: function(item, idx, removedCnt, addedCnt) { - this.arrayContentWillChange(idx, removedCnt, addedCnt); + 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]++; + } }, - arrangedContentArrayDidChange: function(item, idx, removedCnt, addedCnt) { - this.arrayContentDidChange(idx, removedCnt, addedCnt); + 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); + } }, - init: function() { - this._super(); - this._setupContent(); - this._setupArrangedContent(); + contentKeyWillChange: function(obj, keyName) { + Ember.propertyWillChange(this, keyName); }, - willDestroy: function() { - this._teardownArrangedContent(); - this._teardownContent(); + contentKeyDidChange: function(obj, keyName) { + Ember.propertyDidChange(this, keyName); } + }); + })(); @@ -11408,138 +11793,154 @@ Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray, @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); -} +var get = Ember.get, set = Ember.set; -function contentPropertyDidChange(content, contentKey) { - var key = contentKey.slice(8); // remove "content." - if (key in this) { return; } // if shadowed in proxy - propertyDidChange(this, key); -} +// Add Ember.Array to Array.prototype. Remove methods with native +// implementations and supply some more optimized versions of generic methods +// because they are so common. +var NativeArray = Ember.Mixin.create(Ember.MutableArray, Ember.Observable, Ember.Copyable, { -/** - `Ember.ObjectProxy` forwards all properties not defined by the proxy itself - to a proxied `content` object. + // 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); + }, - ```javascript - object = Ember.Object.create({ - name: 'Foo' - }); + objectAt: function(idx) { + return this[idx]; + }, - proxy = Ember.ObjectProxy.create({ - content: object - }); + // primitive for array support. + replace: function(idx, amt, objects) { - // Access and change existing properties - proxy.get('name') // 'Foo' - proxy.set('name', 'Bar'); - object.get('name') // 'Bar' + if (this.isFrozen) throw Ember.FROZEN_ERROR ; - // Create new 'description' property on `object` - proxy.set('description', 'Foo is a whizboo baz'); - object.get('description') // 'Foo is a whizboo baz' - ``` + // 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); - While `content` is unset, setting a property to be delegated will throw an - Error. + if (!objects || objects.length === 0) { + this.splice(idx, amt) ; + } else { + var args = [idx, amt].concat(objects) ; + this.splice.apply(this,args) ; + } - ```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 - ``` + this.arrayContentDidChange(idx, amt, len); + return this ; + }, - Delegated properties can be bound to and will change when content is updated. + // 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 ; + }, - Computed properties on the proxy itself can depend on delegated properties. + // If browser did not implement indexOf natively, then override with + // specialized version + indexOf: function(object, startAt) { + var idx, len = this.length; - ```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') - }); + if (startAt === undefined) startAt = 0; + else startAt = (startAt < 0) ? Math.ceil(startAt) : Math.floor(startAt); + if (startAt < 0) startAt += len; - proxy = ProxyWithComputedProperty.create(); + 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.Object + @extends Ember.Mixin + @uses Ember.MutableArray + @uses Ember.MutableEnumerable + @uses Ember.Copyable + @uses Ember.Freezable */ -Ember.ObjectProxy = Ember.Object.extend( -/** @scope Ember.ObjectProxy.prototype */ { - /** - The object whose properties will be forwarded. +Ember.NativeArray = NativeArray; - @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'), +/** + Creates an `Ember.NativeArray` from an Array like object. + Does not modify the original object. - isTruthy: Ember.computed.bool('content'), + @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); +}; - _debugContainerKey: null, +/** + Activates the mixin on the Array.prototype if not already applied. Calling + this method more than once is safe. - willWatchProperty: function (key) { - var contentKey = 'content.' + key; - addBeforeObserver(this, contentKey, null, contentPropertyWillChange); - addObserver(this, contentKey, null, contentPropertyDidChange); - }, + @method activate + @for Ember.NativeArray + @static + @return {void} +*/ +Ember.NativeArray.activate = function() { + NativeArray.apply(Array.prototype); - didUnwatchProperty: function (key) { - var contentKey = 'content.' + key; - removeBeforeObserver(this, contentKey, null, contentPropertyWillChange); - removeObserver(this, contentKey, null, contentPropertyDidChange); - }, + Ember.A = function(arr) { return arr || []; }; +}; - unknownProperty: function (key) { - var content = get(this, 'content'); - if (content) { - return get(content, key); - } - }, +if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) { + Ember.NativeArray.activate(); +} - 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); - } -}); })(); @@ -11551,362 +11952,452 @@ Ember.ObjectProxy = Ember.Object.extend( @submodule ember-runtime */ +var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, none = Ember.isNone, fmt = Ember.String.fmt; -var set = Ember.set, get = Ember.get, guidFor = Ember.guidFor; -var forEach = Ember.EnumerableUtils.forEach; +/** + An unordered collection of objects. -var EachArray = Ember.Object.extend(Ember.Array, { + A Set works a bit like an array except that its items are not ordered. You + can create a set to efficiently test for membership for an object. You can + also iterate through a set just like an array, even accessing objects by + index, however there is no guarantee as to their order. - init: function(content, keyName, owner) { - this._super(); - this._keyName = keyName; - this._owner = owner; - this._content = content; - }, + All Sets are observable via the Enumerable Observer API - which works + on any enumerable object including both Sets and Arrays. - objectAt: function(idx) { - var item = this._content.objectAt(idx); - return item && get(item, this._keyName); - }, + ## Creating a Set - length: Ember.computed(function() { - var content = this._content; - return content ? get(content, 'length') : 0; - }) + 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()`. -var IS_OBSERVER = /^.+:(before|change)$/; + ```javascript + // creates a new empty set + var foundNames = new Ember.Set(); -function addObserverForContentKey(content, keyName, proxy, idx, loc) { - var objects = proxy._objects, guid; - if (!objects) objects = proxy._objects = {}; + // creates a set with four names in it. + var names = new Ember.Set(["Charles", "Tom", "Juan", "Alex"]); // :P - while(--loc>=idx) { - var item = content.objectAt(loc); - if (item) { - Ember.addBeforeObserver(item, keyName, proxy, 'contentKeyWillChange'); - Ember.addObserver(item, keyName, proxy, 'contentKeyDidChange'); + // creates a copy of the names set. + var namesCopy = new Ember.Set(names); - // 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); - } - } -} + // same as above. + var anotherNamesCopy = names.copy(); + ``` -function removeObserverForContentKey(content, keyName, proxy, idx, loc) { - var objects = proxy._objects; - if (!objects) objects = proxy._objects = {}; - var indicies, guid; + ## Adding/Removing Objects - while(--loc>=idx) { - var item = content.objectAt(loc); - if (item) { - Ember.removeBeforeObserver(item, keyName, proxy, 'contentKeyWillChange'); - Ember.removeObserver(item, keyName, proxy, 'contentKeyDidChange'); + 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. - guid = guidFor(item); - indicies = objects[guid]; - indicies[indicies.indexOf(loc)] = null; - } - } -} + 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. -/** - 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. + NOTE: You cannot add/remove `null` or `undefined` to a set. Any attempt to do + so will be ignored. - @private - @class EachProxy + 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.Object + @extends Ember.CoreObject + @uses Ember.MutableEnumerable + @uses Ember.Copyable + @uses Ember.Freezable + @since Ember 0.9 */ -Ember.EachProxy = Ember.Object.extend({ +Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Ember.Freezable, + /** @scope Ember.Set.prototype */ { - init: function(content) { - this._super(); - this._content = content; - content.addArrayObserver(this); + // .......................................................... + // 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); - // in case someone is already observing some keys make sure they are - // added - forEach(Ember.watchedEvents(this), function(eventName) { - this.didAddListener(eventName); - }, this); + Ember.propertyDidChange(this, 'firstObject'); + Ember.propertyDidChange(this, 'lastObject'); + this.enumerableContentDidChange(len, 0); + + return this; }, /** - You can directly access mapped properties by simply requesting them. - The `unknownProperty` handler will generate an EachArray of each item. + Returns true if the passed object is also an enumerable that contains the + same objects as the receiver. - @method unknownProperty - @param keyName {String} - @param value {anything} + ```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} */ - unknownProperty: function(keyName, value) { - var ret; - ret = new EachArray(this._content, keyName, this); - Ember.defineProperty(this, keyName, null, ret); - this.beginObservingContentKey(keyName); - return ret; - }, + isEqual: function(obj) { + // fail fast + if (!Ember.Enumerable.detect(obj)) return false; - // .......................................................... - // ARRAY CHANGES - // Invokes whenever the content array itself changes. + var loc = get(this, 'length'); + if (get(obj, 'length') !== loc) return false; - arrayWillChange: function(content, idx, removedCnt, addedCnt) { - var keys = this._keys, key, array, lim; + while(--loc >= 0) { + if (!obj.contains(this[loc])) return false; + } - lim = removedCnt>0 ? idx+removedCnt : -1; - Ember.beginPropertyChanges(this); + return true; + }, - for(key in keys) { - if (!keys.hasOwnProperty(key)) { continue; } + /** + 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. - if (lim>0) removeObserverForContentKey(content, key, this, idx, lim); + This is an alias for `Ember.MutableEnumerable.addObject()`. - Ember.propertyWillChange(this, key); - } + ```javascript + var colors = new Ember.Set(); + colors.add("blue"); // ["blue"] + colors.add("blue"); // ["blue"] + colors.add("red"); // ["blue", "red"] + colors.add(null); // ["blue", "red"] + colors.add(undefined); // ["blue", "red"] + ``` - Ember.propertyWillChange(this._content, '@each'); - Ember.endPropertyChanges(this); - }, + @method add + @param {Object} obj The object to add. + @return {Ember.Set} The set itself. + */ + add: Ember.aliasMethod('addObject'), - arrayDidChange: function(content, idx, removedCnt, addedCnt) { - var keys = this._keys, key, array, lim; + /** + 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()`. - lim = addedCnt>0 ? idx+addedCnt : -1; - Ember.beginPropertyChanges(this); + ```javascript + var colors = new Ember.Set(["red", "green", "blue"]); + colors.remove("red"); // ["blue", "green"] + colors.remove("purple"); // ["blue", "green"] + colors.remove(null); // ["blue", "green"] + ``` - for(key in keys) { - if (!keys.hasOwnProperty(key)) { continue; } + @method remove + @param {Object} obj The object to remove + @return {Ember.Set} The set itself. + */ + remove: Ember.aliasMethod('removeObject'), - if (lim>0) addObserverForContentKey(content, key, this, idx, lim); + /** + Removes the last element from the set and returns it, or `null` if it's empty. - Ember.propertyDidChange(this, key); - } + ```javascript + var colors = new Ember.Set(["green", "blue"]); + colors.pop(); // "blue" + colors.pop(); // "green" + colors.pop(); // null + ``` - Ember.propertyDidChange(this._content, '@each'); - Ember.endPropertyChanges(this); + @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; }, - // .......................................................... - // LISTEN FOR NEW OBSERVERS AND OTHER EVENT LISTENERS - // Start monitoring keys based on who is listening... + /** + Inserts the given object on to the end of the set. It returns + the set itself. - didAddListener: function(eventName) { - if (IS_OBSERVER.test(eventName)) { - this.beginObservingContentKey(eventName.slice(0, -7)); - } - }, + This is an alias for `Ember.MutableEnumerable.addObject()`. - didRemoveListener: function(eventName) { - if (IS_OBSERVER.test(eventName)) { - this.stopObservingContentKey(eventName.slice(0, -7)); - } - }, + ```javascript + var colors = new Ember.Set(); + colors.push("red"); // ["red"] + colors.push("green"); // ["red", "green"] + colors.push("blue"); // ["red", "green", "blue"] + ``` - // .......................................................... - // CONTENT KEY OBSERVING - // Actual watch keys on the source content. + @method push + @return {Ember.Set} The set itself. + */ + push: Ember.aliasMethod('addObject'), - 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]++; - } - }, + /** + Removes the last element from the set and returns it, or `null` if it's empty. - stopObservingContentKey: function(keyName) { - var keys = this._keys; - if (keys && (keys[keyName]>0) && (--keys[keyName]<=0)) { - var content = this._content, - len = get(content, 'length'); - removeObserverForContentKey(content, keyName, this, 0, len); - } - }, + This is an alias for `Ember.Set.pop()`. - contentKeyWillChange: function(obj, keyName) { - Ember.propertyWillChange(this, keyName); - }, + ```javascript + var colors = new Ember.Set(["green", "blue"]); + colors.shift(); // "blue" + colors.shift(); // "green" + colors.shift(); // null + ``` - contentKeyDidChange: function(obj, keyName) { - Ember.propertyDidChange(this, keyName); - } + @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()` -(function() { -/** -@module ember -@submodule ember-runtime -*/ + ```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'), -var get = Ember.get, set = Ember.set; + /** + Removes each object in the passed enumerable to the 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, { + This is an alias of `Ember.MutableEnumerable.removeObjects()` - // 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); + ```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); }, - objectAt: function(idx) { + // implement Ember.Enumerable + nextObject: 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); + // more optimized version + firstObject: Ember.computed(function() { + return this.length > 0 ? this[0] : undefined; + }), - if (!objects || objects.length === 0) { - this.splice(idx, amt) ; - } else { - var args = [idx, amt].concat(objects) ; - this.splice.apply(this,args) ; - } + // more optimized version + lastObject: Ember.computed(function() { + return this.length > 0 ? this[this.length-1] : undefined; + }), - this.arrayContentDidChange(idx, amt, len); - return this ; - }, + // implements Ember.MutableEnumerable + addObject: function(obj) { + if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); + if (none(obj)) return this; // nothing to do - // If 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 ; - }, + var guid = guidFor(obj), + idx = this[guid], + len = get(this, 'length'), + added ; - // If browser did not implement indexOf natively, then override with - // specialized version - indexOf: function(object, startAt) { - var idx, len = this.length; + if (idx>=0 && idx=0;idx--) { - if (this[idx] === object) return idx ; - } - return -1; + return this; }, - copy: function(deep) { - if (deep) { - return this.map(function(item){ return Ember.copy(item, true); }); - } + // implements Ember.MutableEnumerable + removeObject: function(obj) { + if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); + if (none(obj)) return this; // nothing to do - return this.slice(); - } -}); + var guid = guidFor(obj), + idx = this[guid], + len = get(this, 'length'), + isFirst = idx === 0, + isLast = idx === len-1, + last, removed; -// 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); -} + if (idx>=0 && idx=0; + }, - Ember.A = function(arr) { return arr || []; }; -}; + 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; + }, -if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) { - Ember.NativeArray.activate(); -} + toString: function() { + var len = this.length, idx, array = []; + for(idx = 0; idx < len; idx++) { + array[idx] = this[idx]; + } + return fmt("Ember.Set<%@>", [array.join(',')]); + } +}); })(); @@ -11914,7 +12405,6 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) { (function() { var DeferredMixin = Ember.DeferredMixin, // mixins/deferred - EmberObject = Ember.Object, // system/object get = Ember.get; var Deferred = Ember.Object.extend(DeferredMixin); @@ -12238,7 +12728,6 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { if (isSorted) { var addedObjects = array.slice(idx, idx+addedCount); - var arrangedContent = get(this, 'arrangedContent'); forEach(addedObjects, function(item) { this.insertItemSorted(item); @@ -12308,8 +12797,8 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { @submodule ember-runtime */ -var get = Ember.get, set = Ember.set, isGlobalPath = Ember.isGlobalPath, - forEach = Ember.EnumerableUtils.forEach, replace = Ember.EnumerableUtils.replace; +var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach, + replace = Ember.EnumerableUtils.replace; /** `Ember.ArrayController` provides a way for you to publish a collection of @@ -12437,7 +12926,8 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, objectAtContent: function(idx) { var length = get(this, 'length'), - object = get(this,'arrangedContent').objectAt(idx); + arrangedContent = get(this,'arrangedContent'), + object = arrangedContent && arrangedContent.objectAt(idx); if (idx >= 0 && idx < length) { var controllerClass = this.lookupItemController(object); @@ -12457,20 +12947,20 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, arrangedContentDidChange: function() { this._super(); - this._resetSubContainers(); + this._resetSubControllers(); }, arrayContentDidChange: function(idx, removedCnt, addedCnt) { - var subContainers = get(this, 'subContainers'), - subContainersToRemove = subContainers.slice(idx, idx+removedCnt); + var subControllers = get(this, '_subControllers'), + subControllersToRemove = subControllers.slice(idx, idx+removedCnt); - forEach(subContainersToRemove, function(subContainer) { - if (subContainer) { subContainer.destroy(); } + forEach(subControllersToRemove, function(subController) { + if (subController) { subController.destroy(); } }); - replace(subContainers, idx, removedCnt, new Array(addedCnt)); + replace(subControllers, idx, removedCnt, new Array(addedCnt)); - // The shadow array of subcontainers must be updated before we trigger + // The shadow array of subcontrollers must be updated before we trigger // observers, otherwise observers will get the wrong subcontainer when // calling `objectAt` this._super(idx, removedCnt, addedCnt); @@ -12478,43 +12968,40 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, init: function() { this._super(); - if (!this.get('content')) { this.set('content', Ember.A()); } - this._resetSubContainers(); + if (!this.get('content')) { Ember.defineProperty(this, 'content', undefined, Ember.A()); } + this.set('_subControllers', Ember.A()); }, controllerAt: function(idx, object, controllerClass) { var container = get(this, 'container'), - subContainers = get(this, 'subContainers'), - subContainer = subContainers[idx], - controller; + subControllers = get(this, '_subControllers'), + subController = subControllers[idx]; - if (!subContainer) { - subContainer = subContainers[idx] = container.child(); + if (!subController) { + subController = container.lookup("controller:" + controllerClass, { singleton: false }); + subControllers[idx] = subController; } - controller = subContainer.lookup("controller:" + controllerClass); - if (!controller) { + if (!subController) { throw new Error('Could not resolve itemController: "' + controllerClass + '"'); } - controller.set('target', this); - controller.set('content', object); + subController.set('target', this); + subController.set('content', object); - return controller; + return subController; }, - subContainers: null, + _subControllers: null, - _resetSubContainers: function() { - var subContainers = get(this, 'subContainers'); + _resetSubControllers: function() { + var subControllers = get(this, '_subControllers'); - if (subContainers) { - forEach(subContainers, function(subContainer) { - if (subContainer) { subContainer.destroy(); } - }); - } + forEach(subControllers, function(subController) { + if (subController) { subController.destroy(); } + }); - this.set('subContainers', Ember.A()); + this.set('_subControllers', Ember.A()); } }); @@ -12572,7 +13059,7 @@ Ember Runtime */ 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)); +Ember.assert("Ember Views require jQuery 1.8, 1.9 or 2.0", jQuery && (jQuery().jquery.match(/^((1\.(8|9))|2.0)(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_JQUERY)); /** Alias for jQuery @@ -12591,15 +13078,16 @@ Ember.$ = jQuery; @module ember @submodule ember-views */ +if (Ember.$) { + // http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#dndevents + var dragEvents = Ember.String.w('dragstart drag dragenter dragleave dragover drop dragend'); -// 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'] }; -}); + // 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'] }; + }); +} })(); @@ -12616,7 +13104,8 @@ Ember.EnumerableUtils.forEach(dragEvents, function(eventName) { // Internet Explorer prior to 9 does not allow setting innerHTML if the first element // is a "zero-scope" element. This problem can be worked around by making // the first node an invisible text node. We, like Modernizr, use ­ -var needsShy = (function(){ + +var needsShy = this.document && (function(){ var testEl = document.createElement('div'); testEl.innerHTML = "
    "; testEl.firstChild.innerHTML = ""; @@ -12626,7 +13115,7 @@ var needsShy = (function(){ // 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 movesWhitespace = this.document && (function() { var testEl = document.createElement('div'); testEl.innerHTML = "Test: Value"; return testEl.childNodes[0].nodeValue === 'Test:' && @@ -12712,9 +13201,11 @@ var setInnerHTML = function(element, html) { if (canSetInnerHTML(tagName)) { setInnerHTMLWithoutFix(element, html); } else { - Ember.assert("Can't set innerHTML on "+element.tagName+" in this browser", element.outerHTML); + // Firefox versions < 11 do not have support for element.outerHTML. + var outerHTML = element.outerHTML || new XMLSerializer().serializeToString(element); + Ember.assert("Can't set innerHTML on "+element.tagName+" in this browser", outerHTML); - var startTag = element.outerHTML.match(new RegExp("<"+tagName+"([^>]*)>", 'i'))[0], + var startTag = outerHTML.match(new RegExp("<"+tagName+"([^>]*)>", 'i'))[0], endTag = ''; var wrapper = document.createElement('div'); @@ -12751,11 +13242,6 @@ Ember.ViewUtils = { */ var get = Ember.get, set = Ember.set; -var indexOf = Ember.ArrayPolyfills.indexOf; - - - - var ClassSet = function() { this.seen = {}; @@ -12927,7 +13413,7 @@ Ember._RenderBuffer.prototype = */ addClass: function(className) { // lazily create elementClasses - var elementClasses = this.elementClasses = (this.elementClasses || new ClassSet()); + this.elementClasses = (this.elementClasses || new ClassSet()); this.elementClasses.add(className); this.classes = this.elementClasses.list; @@ -13032,7 +13518,7 @@ Ember._RenderBuffer.prototype = @chainable */ style: function(name, value) { - var style = this.elementStyle = (this.elementStyle || {}); + this.elementStyle = (this.elementStyle || {}); this.elementStyle[name] = value; return this; @@ -13202,7 +13688,9 @@ Ember._RenderBuffer.prototype = */ string: function() { if (this._element) { - return this.element().outerHTML; + // Firefox versions < 11 do not have support for element.outerHTML. + return this.element().outerHTML || + new XMLSerializer().serializeToString(this.element()); } else { return this.innerString(); } @@ -13458,8 +13946,9 @@ Ember.EventDispatcher = Ember.Object.extend( // 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'); +var queues = Ember.run.queues, + indexOf = Ember.ArrayPolyfills.indexOf; +queues.splice(indexOf.call(queues, 'actions')+1, 0, 'render', 'afterRender'); })(); @@ -13495,8 +13984,7 @@ Ember.ControllerMixin.reopen({ }, _modelDidChange: Ember.observer(function() { - var containers = get(this, '_childContainers'), - container; + var containers = get(this, '_childContainers'); for (var prop in containers) { if (!containers.hasOwnProperty(prop)) { continue; } @@ -13525,9 +14013,8 @@ var states = {}; @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 get = Ember.get, set = Ember.set; +var guidFor = Ember.guidFor; var a_forEach = Ember.EnumerableUtils.forEach; var a_addObject = Ember.EnumerableUtils.addObject; @@ -13544,7 +14031,7 @@ var childViewsProperty = Ember.computed(function() { 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."); + Ember.deprecate("Manipulating an Ember.ContainerView through its childViews property is deprecated. Please use the ContainerView instance itself as an Ember.MutableArray."); return view.replace(idx, removedCount, addedViews); } throw new Error("childViews is immutable"); @@ -14249,7 +14736,7 @@ class: * `mouseEnter` * `mouseLeave` - Form events: + Form events: * `submit` * `change` @@ -14257,7 +14744,7 @@ class: * `focusOut` * `input` - HTML5 drag and drop events: + HTML5 drag and drop events: * `dragStart` * `drag` @@ -14803,7 +15290,6 @@ Ember.View = Ember.CoreView.extend( // JavaScript property changes. var observer = function() { elem = this.$(); - if (!elem) { return; } attributeValue = get(this, property); @@ -14998,6 +15484,10 @@ Ember.View = Ember.CoreView.extend( not have an HTML representation yet, `createElement()` will be called automatically. + If your application uses the `rootElement` property, you must append + the view within that element. Rendering views outside of the `rootElement` + is not supported. + Note that this method just schedules the view to be appended; the DOM element will not be appended to the document body until all bindings have finished synchronizing. @@ -15072,9 +15562,9 @@ Ember.View = Ember.CoreView.extend( 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. + Called when the element of the view has been inserted into the DOM + or after the view was re-rendered. Override this function to do any + set up that requires an element in the document body. @event didInsertElement */ @@ -15671,10 +16161,23 @@ Ember.View = Ember.CoreView.extend( }, registerObserver: function(root, path, target, observer) { - Ember.addObserver(root, path, target, observer); + if (!observer && 'function' === typeof target) { + observer = target; + target = null; + } + + var view = this, + stateCheckedObserver = function(){ + view.currentState.invokeObserver(this, observer); + }, + scheduledObserver = function() { + Ember.run.scheduleOnce('render', this, stateCheckedObserver); + }; + + Ember.addObserver(root, path, target, scheduledObserver); this.one('willClearRender', function() { - Ember.removeObserver(root, path, target, observer); + Ember.removeObserver(root, path, target, scheduledObserver); }); } @@ -15705,17 +16208,24 @@ Ember.View = Ember.CoreView.extend( // once the view has been inserted into the DOM, legal manipulations // are done on the DOM element. +function notifyMutationListeners() { + Ember.run.once(Ember.View, 'notifyMutationListeners'); +} + var DOMManager = { prepend: function(view, html) { view.$().prepend(html); + notifyMutationListeners(); }, after: function(view, html) { view.$().after(html); + notifyMutationListeners(); }, html: function(view, html) { view.$().html(html); + notifyMutationListeners(); }, replace: function(view) { @@ -15725,15 +16235,18 @@ var DOMManager = { view._insertElementLater(function() { Ember.$(element).replaceWith(get(view, 'element')); + notifyMutationListeners(); }); }, remove: function(view) { view.$().remove(); + notifyMutationListeners(); }, empty: function(view) { view.$().empty(); + notifyMutationListeners(); } }; @@ -15794,14 +16307,14 @@ Ember.View.reopenClass({ `className` and optional `falsyClassName`. - if a `className` or `falsyClassName` has been specified: - - if the value is truthy and `className` 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, + - 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 + - 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` + - if the value is not `false`, `undefined` or `null`, the `value` is returned - if none of the above rules apply, `null` is returned @@ -15848,6 +16361,20 @@ Ember.View.reopenClass({ } }); +var mutation = Ember.Object.extend(Ember.Evented).create(); + +Ember.View.addMutationListener = function(callback) { + mutation.on('change', callback); +}; + +Ember.View.removeMutationListener = function(callback) { + mutation.off('change', callback); +}; + +Ember.View.notifyMutationListeners = function() { + mutation.trigger('change'); +}; + /** Global views hash @@ -15873,6 +16400,9 @@ Ember.View.applyAttributeBindings = function(elem, name, value) { elem.attr(name, value); } } else if (name === 'value' || type === 'boolean') { + // We can't set properties to undefined + if (value === undefined) { value = null; } + if (value !== elem.prop(name)) { // value and booleans should always be properties elem.prop(name, value); @@ -15928,7 +16458,8 @@ Ember.View.states._default = { return false; }, - rerender: Ember.K + rerender: Ember.K, + invokeObserver: Ember.K }; })(); @@ -15979,7 +16510,7 @@ Ember.merge(preRender, { @submodule ember-views */ -var get = Ember.get, set = Ember.set, meta = Ember.meta; +var get = Ember.get, set = Ember.set; var inBuffer = Ember.View.states.inBuffer = Ember.create(Ember.View.states._default); @@ -16049,6 +16580,10 @@ Ember.merge(inBuffer, { } return value; + }, + + invokeObserver: function(target, observer) { + observer.call(target); } }); @@ -16063,7 +16598,7 @@ Ember.merge(inBuffer, { @submodule ember-views */ -var get = Ember.get, set = Ember.set, meta = Ember.meta; +var get = Ember.get, set = Ember.set; var hasElement = Ember.View.states.hasElement = Ember.create(Ember.View.states._default); @@ -16136,6 +16671,10 @@ Ember.merge(hasElement, { } else { return true; // continue event propagation } + }, + + invokeObserver: function(target, observer) { + observer.call(target); } }); @@ -16204,8 +16743,6 @@ Ember.View.cloneStates = function(from) { 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]); @@ -16226,7 +16763,7 @@ var states = Ember.View.cloneStates(Ember.View.states); @submodule ember-views */ -var get = Ember.get, set = Ember.set, meta = Ember.meta; +var get = Ember.get, set = Ember.set; var forEach = Ember.EnumerableUtils.forEach; /** @@ -16651,7 +17188,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; Given an empty `` and the following code: - ```javascript + ```javascript someItemsView = Ember.CollectionView.create({ classNames: ['a-collection'], content: ['A','B','C'], @@ -16724,7 +17261,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; } else { viewClass = App.SongView; } - this._super(viewClass, attrs); + return this._super(viewClass, attrs); } }); ``` @@ -16904,7 +17441,7 @@ Ember.CollectionView = Ember.ContainerView.extend( */ arrayDidChange: function(content, start, removed, added) { var itemViewClass = get(this, 'itemViewClass'), - addedViews = [], view, item, idx, len, itemTagName; + addedViews = [], view, item, idx, len; if ('string' === typeof itemViewClass) { itemViewClass = get(itemViewClass); @@ -17007,15 +17544,15 @@ define("metamorph", var K = function(){}, guid = 0, - document = window.document, + document = this.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, + supportsRange = document && ('createRange' in document) && (typeof Range !== 'undefined') && Range.prototype.createContextualFragment, // Internet Explorer prior to 9 does not allow setting innerHTML if the first element // is a "zero-scope" element. This problem can be worked around by making // the first node an invisible text node. We, like Modernizr, use ­ - needsShy = (function(){ + needsShy = document && (function(){ var testEl = document.createElement('div'); testEl.innerHTML = "
    "; testEl.firstChild.innerHTML = ""; @@ -17026,7 +17563,7 @@ define("metamorph", // 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() { + movesWhitespace = document && (function() { var testEl = document.createElement('div'); testEl.innerHTML = "Test: Value"; return testEl.childNodes[0].nodeValue === 'Test:' && @@ -17460,7 +17997,7 @@ define("metamorph", (function() { /** @module ember -@submodule ember-handlebars +@submodule ember-handlebars-compiler */ // Eliminate dependency on any Ember to simplify precompilation workflow @@ -17470,8 +18007,12 @@ var objectCreate = Object.create || function(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]+)?/)); +var Handlebars = this.Handlebars || (Ember.imports && Ember.imports.Handlebars); +if(!Handlebars && typeof require === 'function') { + Handlebars = require('handlebars'); +} + +Ember.assert("Ember Handlebars requires Handlebars 1.0.0-rc.3 or greater. Include a SCRIPT tag in the HTML HEAD linking to the Handlebars file before you link to Ember.", Handlebars && Handlebars.COMPILER_REVISION === 2); /** Prepares the Handlebars templating library for use inside Ember's view @@ -17823,7 +18364,7 @@ Ember.Handlebars.registerHelper('helperMissing', function(path, options) { ## Example with bound options - Bound hash options are also supported. Example: + Bound hash options are also supported. Example: ```handlebars {{repeat text countBinding="numRepeats"}} @@ -17861,15 +18402,15 @@ Ember.Handlebars.registerHelper('helperMissing', function(path, options) { {{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. - + which property the dependent keys would belong to. + ## Use with unbound helper - The {{unbound}} helper can be used with bound helper invocations + The {{unbound}} helper can be used with bound helper invocations to render them in their unbound form, e.g. ```handlebars - {{unbound capitalize name}} + {{unbound capitalize name}} ``` In this example, if the name property changes, the helper @@ -17895,7 +18436,7 @@ Ember.Handlebars.registerBoundHelper = function(name, fn) { view = data.view, currentContext = (options.contexts && options.contexts[0]) || this, normalized, - pathRoot, path, + pathRoot, path, loc, hashOption; // Detect bound options (e.g. countBinding="otherCount") @@ -17942,10 +18483,10 @@ Ember.Handlebars.registerBoundHelper = function(name, fn) { view.appendChild(bindView); - view.registerObserver(pathRoot, path, bindView, rerenderBoundHelperView); + view.registerObserver(pathRoot, path, bindView, bindView.rerender); for (var i=0, l=dependentKeys.length; i ``` 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 + A class name is provided via the syntax `somePropertyName:class-name-if-true`. ```javascript @@ -19057,15 +19597,13 @@ EmberHandlebars.registerHelper('bindAttr', function(options) { 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); + // Note: don't add observer when path is 'this' or path + // is whole keyword e.g. {{#each x in list}} ... {{bindAttr attr="x"}} + if (path !== 'this' && !(normalized.isKeyword && normalized.path === '' )) { + view.registerObserver(normalized.root, normalized.path, observer); } // if this changes, also change the logic in ember-views/lib/views/view.js @@ -19100,9 +19638,9 @@ EmberHandlebars.registerHelper('bindAttr', function(options) { @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 + @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 + @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 @@ -19179,12 +19717,8 @@ EmberHandlebars.bindClasses = function(context, classBindings, view, bindAttrId, } }; - invoker = function() { - Ember.run.scheduleOnce('render', observer); - }; - if (path !== '' && path !== 'this') { - view.registerObserver(pathRoot, path, invoker); + view.registerObserver(pathRoot, path, observer); } // We've already setup the observer; now we just need to figure out the @@ -19218,7 +19752,6 @@ EmberHandlebars.bindClasses = function(context, classBindings, view, bindAttrId, */ var get = Ember.get, set = Ember.set; -var PARENT_VIEW_PATH = /^parentView\./; var EmberHandlebars = Ember.Handlebars; EmberHandlebars.ViewHelper = Ember.Object.create({ @@ -19234,6 +19767,11 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ dup = true; } + if (hash.tag) { + extensions.tagName = hash.tag; + dup = true; + } + if (classes) { classes = classes.split(' '); extensions.classNames = classes; @@ -19260,6 +19798,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ if (dup) { hash = Ember.$.extend({}, hash); delete hash.id; + delete hash.tag; delete hash['class']; delete hash.classBinding; } @@ -19792,7 +20331,7 @@ Ember.Handlebars.registerHelper('unbound', function(property, fn) { // 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)); + out = helper.apply(this, Array.prototype.slice.call(arguments, 1)); delete options.data.isUnbound; return out; } @@ -19895,7 +20434,7 @@ Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { Ember.removeBeforeObserver(this, 'content', null, '_contentWillChange'); Ember.removeObserver(this, 'content', null, '_contentDidChange'); - callback.apply(this); + callback.call(this); Ember.addBeforeObserver(this, 'content', null, '_contentWillChange'); Ember.addObserver(this, 'content', null, '_contentDidChange'); @@ -20087,7 +20626,7 @@ GroupedEach.prototype = { ```handlebars {{#view App.MyView }} - {{each view.items itemViewClass="App.AnItemView"}} + {{each view.items itemViewClass="App.AnItemView"}} {{/view}} ``` @@ -20118,7 +20657,7 @@ GroupedEach.prototype = {
    Greetings Sara
    ``` - + ### 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 @@ -20126,10 +20665,10 @@ GroupedEach.prototype = { `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(){ @@ -20137,17 +20676,17 @@ GroupedEach.prototype = { }.property('isEmployed', 'isSeekingWork') }) ``` - + ```handlebars - {{#each person in Developers itemController="developer"}} + {{#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 [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 @@ -20206,6 +20745,14 @@ Ember.Handlebars.registerHelper('each', function(path, options) { ``` + ```handlebars + {{#if isUser}} + {{template "user_info"}} + {{else}} + {{template "unlogged_user_info"}} + {{/if}} + ``` + This helper looks for templates in the global `Ember.TEMPLATES` hash. If you add ` + + The `data-template-name` attribute of a partial template + is prefixed with an underscore. + + ```html + + ``` + + @method partial + @for Ember.Handlebars.helpers + @param {String} partialName the name of the template to render minus the leading underscore +*/ + Ember.Handlebars.registerHelper('partial', function(name, options) { var nameParts = name.split("/"), lastPart = nameParts[nameParts.length - 1]; nameParts[nameParts.length - 1] = "_" + lastPart; - var underscoredName = nameParts.join("/"); - - var template = Ember.TEMPLATES[underscoredName], - deprecatedTemplate = Ember.TEMPLATES[name]; + var view = options.data.view, + underscoredName = nameParts.join("/"), + template = view.templateForName(underscoredName), + deprecatedTemplate = view.templateForName(name); Ember.deprecate("You tried to render the partial " + name + ", which should be at '" + underscoredName + "', but Ember found '" + name + "'. Please use a leading underscore in your partials", template); Ember.assert("Unable to find partial with name '"+name+"'.", template || deprecatedTemplate); @@ -20392,7 +20975,7 @@ Ember.Checkbox = Ember.View.extend({ tagName: 'input', - attributeBindings: ['type', 'checked', 'disabled', 'tabindex'], + attributeBindings: ['type', 'checked', 'disabled', 'tabindex', 'name'], type: "checkbox", checked: false, @@ -20501,8 +21084,8 @@ var get = Ember.get, set = Ember.set; ## 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 + `pattern`, `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 @@ -20524,7 +21107,7 @@ Ember.TextField = Ember.View.extend(Ember.TextSupport, classNames: ['ember-text-field'], tagName: "input", - attributeBindings: ['type', 'value', 'size', 'pattern'], + attributeBindings: ['type', 'value', 'size', 'pattern', 'name'], /** The `value` attribute of the input element. As the user inputs text, this @@ -20570,9 +21153,9 @@ Ember.TextField = Ember.View.extend(Ember.TextSupport, 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 + @property action + @type String + @default null */ action: null, @@ -20784,7 +21367,7 @@ Ember.TextArea = Ember.View.extend(Ember.TextSupport, { classNames: ['ember-text-area'], tagName: "textarea", - attributeBindings: ['rows', 'cols'], + attributeBindings: ['rows', 'cols', 'name'], rows: null, cols: null, @@ -21109,7 +21692,7 @@ function program3(depth0,data) { return buffer; }), - attributeBindings: ['multiple', 'disabled', 'tabindex'], + attributeBindings: ['multiple', 'disabled', 'tabindex', 'name'], /** The `multiple` attribute of the select element. Indicates whether multiple @@ -21413,8 +21996,7 @@ Ember.Handlebars.bootstrap = function(ctx) { Ember.$(selectors, ctx) .each(function() { // Get a reference to the script tag - var script = Ember.$(this), - type = script.attr('type'); + var script = Ember.$(this); var compile = (script.attr('type') === 'text/x-raw-handlebars') ? Ember.$.proxy(Handlebars.compile, Handlebars) : @@ -22642,7 +23224,8 @@ DSL.prototype = { }, push: function(url, name, callback) { - if (url === "" || url === "/") { this.explicitIndex = true; } + var parts = name.split('.'); + if (url === "" || url === "/" || parts[parts.length-1] === "index") { this.explicitIndex = true; } this.matches.push([url, name, callback]); }, @@ -22697,32 +23280,42 @@ Ember.RouterDSL = DSL; @submodule ember-routing */ -Ember.controllerFor = function(container, controllerName, context) { - return container.lookup('controller:' + controllerName) || +Ember.controllerFor = function(container, controllerName, context, lookupOptions) { + return container.lookup('controller:' + controllerName, lookupOptions) || Ember.generateController(container, controllerName, context); }; - +/** + Generates a controller automatically if none was provided. + The type of generated controller depends on the context. + You can customize your generated controllers by defining + `App.ObjectController` and `App.ArrayController` +*/ Ember.generateController = function(container, controllerName, context) { - var controller; + var controller, DefaultController, fullName; if (context && Ember.isArray(context)) { - controller = Ember.ArrayController.extend({ + DefaultController = container.resolve('controller:array'); + controller = DefaultController.extend({ content: context }); } else if (context) { - controller = Ember.ObjectController.extend({ + DefaultController = container.resolve('controller:object'); + controller = DefaultController.extend({ content: context }); } else { - controller = Ember.Controller.extend(); + DefaultController = container.resolve('controller:basic'); + controller = DefaultController.extend(); } controller.toString = function() { return "(generated " + controllerName + " controller)"; }; - container.register('controller', controllerName, controller); - return container.lookup('controller:' + controllerName); + + fullName = 'controller:' + controllerName; + container.register(fullName, controller); + return container.lookup(fullName); }; })(); @@ -22736,7 +23329,7 @@ Ember.generateController = function(container, controllerName, context) { */ var Router = requireModule("router"); -var get = Ember.get, set = Ember.set, classify = Ember.String.classify; +var get = Ember.get, set = Ember.set; var DefaultView = Ember._MetamorphView; function setupLocation(router) { @@ -22785,8 +23378,8 @@ Ember.Router = Ember.Object.extend({ setupRouter(this, router, location); - container.register('view', 'default', DefaultView); - container.register('view', 'toplevel', Ember.View.extend()); + container.register('view:default', DefaultView); + container.register('view:toplevel', Ember.View.extend()); location.onUpdateURL(function(url) { self.handleURL(url); @@ -22878,10 +23471,13 @@ Ember.Router.reopenClass({ }); function getHandlerFunction(router) { - var seen = {}, container = router.container; + var seen = {}, container = router.container, + DefaultRoute = container.resolve('route:basic'); return function(name) { - var handler = container.lookup('route:' + name); + var routeName = 'route:' + name, + handler = container.lookup(routeName); + if (seen[name]) { return handler; } seen[name] = true; @@ -22890,8 +23486,8 @@ function getHandlerFunction(router) { if (name === 'loading') { return {}; } if (name === 'failure') { return router.constructor.defaultFailureHandler; } - container.register('route', name, Ember.Route.extend()); - handler = container.lookup('route:' + name); + container.register(routeName, DefaultRoute.extend()); + handler = container.lookup(routeName); } handler.routeName = name; @@ -22899,19 +23495,6 @@ function getHandlerFunction(router) { }; } -function handlerIsActive(router, handlerName) { - var handler = router.container.lookup('route:' + handlerName), - currentHandlerInfos = router.router.currentHandlerInfos, - handlerInfo; - - for (var i=0, l=currentHandlerInfos.length; i 0 ? !Ember.isNone(arguments[0]) : true); + if (typeof name === 'object' && !options) { options = name; name = this.routeName; @@ -23543,6 +24134,34 @@ function teardownView(route) { +(function() { +Ember.onLoad('Ember.Handlebars', function() { + var handlebarsResolve = Ember.Handlebars.resolveParams, + map = Ember.ArrayPolyfills.map, + get = Ember.get; + + function resolveParams(context, params, options) { + var resolved = handlebarsResolve(context, params, options); + return map.call(resolved, unwrap); + + function unwrap(object, i) { + if (params[i] === 'controller') { return object; } + + if (Ember.ControllerMixin.detect(object)) { + return unwrap(get(object, 'model')); + } else { + return object; + } + } + } + + Ember.Router.resolveParams = resolveParams; +}); + +})(); + + + (function() { /** @module ember @@ -23552,7 +24171,7 @@ function teardownView(route) { var get = Ember.get, set = Ember.set; Ember.onLoad('Ember.Handlebars', function(Handlebars) { - var resolveParams = Ember.Handlebars.resolveParams, + var resolveParams = Ember.Router.resolveParams, isSimpleClick = Ember.ViewUtils.isSimpleClick; function fullRouteName(router, name) { @@ -23581,7 +24200,25 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { return ret.concat(resolvedPaths(linkView.parameters)); } - var LinkView = Ember.View.extend({ + /** + Renders a link to the supplied route. + + When the rendered link matches the current route, and the same object instance is passed into the helper, + then the link is given class="active" by default. + + You may re-open LinkView in order to change the default active class: + + ``` javascript + Ember.LinkView.reopen({ + activeClass: "is-active" + }) + ``` + + @class LinkView + @namespace Ember + @extends Ember.View + **/ + var LinkView = Ember.LinkView = Ember.View.extend({ tagName: 'a', namedRoute: null, currentWhen: null, @@ -23676,42 +24313,52 @@ var get = Ember.get, set = Ember.set; Ember.onLoad('Ember.Handlebars', function(Handlebars) { /** @module ember - @submodule ember-handlebars + @submodule ember-routing */ 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. + The `outlet` helper is a placeholder that the router will fill in with + the appropriate template based on the current state of the application. ``` 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`. + By default, a template based on Ember's naming conventions will be rendered + into the `outlet` (e.g. `App.PostsRoute` will render the `posts` template). + + You can render a different template by using the `render()` method in the + route's `renderTemplate` hook. The following will render the `favoritePost` + template into the `outlet`. ``` javascript - # Instantiate App.PostsView and assign to `view`, so as to render into outlet. - controller.connectOutlet('posts'); + App.PostsRoute = Ember.Route.extend({ + renderTemplate: function() { + this.render('favoritePost'); + } + }); ``` - You can also specify a particular name other than `view`: + You can create custom named outlets for more control. ``` handlebars - {{outlet masterView}} - {{outlet detailView}} + {{outlet favoritePost}} + {{outlet posts}} ``` - Then, you can control several outlets from a single controller. + Then you can define what template is rendered into each outlet in your + route. + ``` 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'); + App.PostsRoute = Ember.Route.extend({ + renderTemplate: function() { + this.render('favoritePost', { outlet: 'favoritePost' }); + this.render('posts', { outlet: 'posts' }); + } + }); ``` @method outlet @@ -23763,15 +24410,15 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { The default target for `{{action}}`s in the rendered template is the named controller. - @method action + @method render @for Ember.Handlebars.helpers - @param {String} actionName - @param {Object?} model + @param {String} name + @param {Object?} contextString @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; + var container, router, controller, view, context, lookupOptions; if (arguments.length === 2) { options = contextString; @@ -23780,20 +24427,21 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { if (typeof contextString === 'string') { context = Ember.Handlebars.get(options.contexts[1], contextString, options); + lookupOptions = { singleton: false }; } 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)); + Ember.assert("You can only use the {{render}} helper once without a model object as its second argument, as in {{render \"post\" post}}.", context || !router || !router._lookupActiveView(name)); view = container.lookup('view:' + name) || container.lookup('view:default'); if (controller = options.hash.controller) { - controller = container.lookup('controller:' + controller); + controller = container.lookup('controller:' + controller, lookupOptions); } else { - controller = Ember.controllerFor(container, name, context); + controller = Ember.controllerFor(container, name, context, lookupOptions); } if (controller && context) { @@ -23814,7 +24462,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { options.hash.template = container.lookup('template:' + name); options.hash.controller = controller; - if (router) { + if (router && !context) { router._connectActiveView(name, view); } @@ -23834,7 +24482,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { */ Ember.onLoad('Ember.Handlebars', function(Handlebars) { - var resolveParams = Ember.Handlebars.resolveParams, + var resolveParams = Ember.Router.resolveParams, isSimpleClick = Ember.ViewUtils.isSimpleClick; var EmberHandlebars = Ember.Handlebars, @@ -23857,22 +24505,39 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { registeredActions: {} }; - ActionHelper.registerAction = function(actionName, options) { + var keys = ["alt", "shift", "meta", "ctrl"]; + + var isAllowedClick = function(event, allowedKeys) { + if (typeof allowedKeys === "undefined") { + return isSimpleClick(event); + } + + var allowed = true; + + keys.forEach(function(key) { + if (event[key + "Key"] && allowedKeys.indexOf(key) === -1) { + allowed = false; + } + }); + + return allowed; + }; + + ActionHelper.registerAction = function(actionName, options, allowedKeys) { var actionId = (++Ember.uuid).toString(); ActionHelper.registeredActions[actionId] = { eventName: options.eventName, handler: function(event) { - if (!isSimpleClick(event)) { return true; } + if (!isAllowedClick(event, allowedKeys)) { return true; } + event.preventDefault(); if (options.bubbles === false) { event.stopPropagation(); } - var view = options.view, - contexts = options.contexts, - target = options.target; + var target = options.target; if (target.target) { target = handlebarsGet(target.root, target.target, target.options); @@ -23995,6 +24660,21 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { is created. Having an instance of `Ember.Application` will satisfy this requirement. + ### Specifying whitelisted modifier keys + + By default the `{{action}}` helper will ignore click event with pressed modifier + keys. You can supply an `allowedKeys` option to specify which keys should not be ignored. + + ```handlebars + + ``` + + This way the `{{action}}` will fire when clicking with the alt key pressed down. + ### Specifying a Target There are several possible target objects for `{{action}}` helpers: @@ -24075,8 +24755,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { contexts = a_slice.call(arguments, 1, -1); var hash = options.hash, - view = options.data.view, - controller, link; + controller; // create a hash to pass along to registerAction var action = { @@ -24089,7 +24768,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { params: contexts }; - action.view = view = get(view, 'concreteView'); + action.view = options.data.view; var root, target; @@ -24103,7 +24782,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { action.target = { root: root, target: target, options: options }; action.bubbles = hash.bubbles; - var actionId = ActionHelper.registerAction(actionName, action); + var actionId = ActionHelper.registerAction(actionName, action, hash.allowedKeys); return new SafeString('data-ember-action="' + actionId + '"'); }); @@ -24460,7 +25139,7 @@ Ember.HashLocation = Ember.Object.extend({ set(self, 'lastSetURL', null); - callback(location.hash.substr(1)); + callback(path); }); }); }, @@ -24501,7 +25180,7 @@ Ember.Location.registerImplementation('hash', Ember.HashLocation); */ var get = Ember.get, set = Ember.set; -var popstateReady = false; +var popstateFired = false; /** Ember.HistoryLocation implements the location API using the browser's @@ -24515,6 +25194,7 @@ Ember.HistoryLocation = Ember.Object.extend({ init: function() { set(this, 'location', get(this, 'location') || window.location); + this._initialUrl = this.getURL(); this.initState(); }, @@ -24567,7 +25247,6 @@ Ember.HistoryLocation = Ember.Object.extend({ path = this.formatURL(path); if (this.getState() && this.getState().path !== path) { - popstateReady = true; this.pushState(path); } }, @@ -24585,7 +25264,6 @@ Ember.HistoryLocation = Ember.Object.extend({ path = this.formatURL(path); if (this.getState() && this.getState().path !== path) { - popstateReady = true; this.replaceState(path); } }, @@ -24639,8 +25317,10 @@ Ember.HistoryLocation = Ember.Object.extend({ self = this; Ember.$(window).bind('popstate.ember-location-'+guid, function(e) { - if(!popstateReady) { - return; + // Ignore initial page load popstate event in Chrome + if(!popstateFired) { + popstateFired = true; + if (self.getURL() === self._initialUrl) { return; } } callback(self.getURL()); }); @@ -24804,6 +25484,218 @@ Ember.DAG = DAG; +(function() { +/** +@module ember +@submodule ember-application +*/ + +var get = Ember.get, + classify = Ember.String.classify, + capitalize = Ember.String.capitalize, + decamelize = Ember.String.decamelize; + +/** + The DefaultResolver defines the default lookup rules to resolve + container lookups before consulting the container for registered + items: + + * templates are looked up on `Ember.TEMPLATES` + * other names are looked up on the application after converting + the name. For example, `controller:post` looks up + `App.PostController` by default. + * there are some nuances (see examples below) + + ### How Resolving Works + + The container calls this object's `resolve` method with the + `fullName` argument. + + It first parses the fullName into an object using `parseName`. + + Then it checks for the presence of a type-specific instance + method of the form `resolve[Type]` and calls it if it exists. + For example if it was resolving 'template:post', it would call + the `resolveTemplate` method. + + Its last resort is to call the `resolveOther` method. + + The methods of this object are designed to be easy to override + in a subclass. For example, you could enhance how a template + is resolved like so: + + ```javascript + App = Ember.Application.create({ + resolver: Ember.DefaultResolver.extend({ + resolveTemplate: function(parsedName) { + var resolvedTemplate = this._super(parsedName); + if (resolvedTemplate) { return resolvedTemplate; } + return Ember.TEMPLATES['not_found']; + } + }) + }); + ``` + + Some examples of how names are resolved: + + ``` + 'template:post' //=> Ember.TEMPLATES['post'] + 'template:posts/byline' //=> Ember.TEMPLATES['posts/byline'] + 'template:posts.byline' //=> Ember.TEMPLATES['posts/byline'] + 'template:blogPost' //=> Ember.TEMPLATES['blogPost'] + // OR + // Ember.TEMPLATES['blog_post'] + 'controller:post' //=> App.PostController + 'controller:posts.index' //=> App.PostsIndexController + 'controller:blog/post' //=> Blog.PostController + 'controller:basic' //=> Ember.Controller + 'route:post' //=> App.PostRoute + 'route:posts.index' //=> App.PostsIndexRoute + 'route:blog/post' //=> Blog.PostRoute + 'route:basic' //=> Ember.Route + 'view:post' //=> App.PostView + 'view:posts.index' //=> App.PostsIndexView + 'view:blog/post' //=> Blog.PostView + 'view:basic' //=> Ember.View + 'foo:post' //=> App.PostFoo + ``` + + @class DefaultResolver + @namespace Ember + @extends Ember.Object +*/ +Ember.DefaultResolver = Ember.Object.extend({ + /** + This will be set to the Application instance when it is + created. + + @property namespace + */ + namespace: null, + /** + This method is called via the container's resolver method. + It parses the provided `fullName` and then looks up and + returns the appropriate template or class. + + @method resolve + @param {String} fullName the lookup string + @return {Object} the resolved factory + */ + resolve: function(fullName) { + var parsedName = this.parseName(fullName), + typeSpecificResolveMethod = this[parsedName.resolveMethodName]; + if (typeSpecificResolveMethod) { + var resolved = typeSpecificResolveMethod.call(this, parsedName); + if (resolved) { return resolved; } + } + return this.resolveOther(parsedName); + }, + /** + Convert the string name of the form "type:name" to + a Javascript object with the parsed aspects of the name + broken out. + + @protected + @method parseName + */ + parseName: function(fullName) { + var nameParts = fullName.split(":"), + type = nameParts[0], fullNameWithoutType = nameParts[1], + name = fullNameWithoutType, + namespace = get(this, 'namespace'), + root = namespace; + + if (type !== 'template' && name.indexOf('/') !== -1) { + var parts = name.split('/'); + name = parts[parts.length - 1]; + var namespaceName = capitalize(parts.slice(0, -1).join('.')); + root = Ember.Namespace.byName(namespaceName); + + Ember.assert('You are looking for a ' + name + ' ' + type + ' in the ' + namespaceName + ' namespace, but the namespace could not be found', root); + } + + return { + fullName: fullName, + type: type, + fullNameWithoutType: fullNameWithoutType, + name: name, + root: root, + resolveMethodName: "resolve" + classify(type) + }; + }, + /** + Look up the template in Ember.TEMPLATES + + @protected + @method resolveTemplate + */ + resolveTemplate: function(parsedName) { + var templateName = parsedName.fullNameWithoutType.replace(/\./g, '/'); + + if (Ember.TEMPLATES[templateName]) { + return Ember.TEMPLATES[templateName]; + } + + templateName = decamelize(templateName); + if (Ember.TEMPLATES[templateName]) { + return Ember.TEMPLATES[templateName]; + } + }, + /** + Given a parseName object (output from `parseName`), apply + the conventions expected by `Ember.Router` + + @protected + @method useRouterNaming + */ + useRouterNaming: function(parsedName) { + parsedName.name = parsedName.name.replace(/\./g, '_'); + if (parsedName.name === 'basic') { + parsedName.name = ''; + } + }, + /** + @protected + @method resolveController + */ + resolveController: function(parsedName) { + this.useRouterNaming(parsedName); + return this.resolveOther(parsedName); + }, + /** + @protected + @method resolveRoute + */ + resolveRoute: function(parsedName) { + this.useRouterNaming(parsedName); + return this.resolveOther(parsedName); + }, + /** + @protected + @method resolveView + */ + resolveView: function(parsedName) { + this.useRouterNaming(parsedName); + return this.resolveOther(parsedName); + }, + /** + Look up the specified object (from parsedName) on the appropriate + namespace (usually on the Application) + + @protected + @method resolveOther + */ + resolveOther: function(parsedName) { + var className = classify(parsedName.name) + classify(parsedName.type), + factory = get(parsedName.root, className); + if (factory) { return factory; } + } +}); + +})(); + + + (function() { /** @module ember @@ -24812,6 +25704,7 @@ Ember.DAG = DAG; var get = Ember.get, set = Ember.set, classify = Ember.String.classify, + capitalize = Ember.String.capitalize, decamelize = Ember.String.decamelize; /** @@ -24837,11 +25730,14 @@ var get = Ember.get, set = Ember.set, ``` By default, calling `Ember.Application.create()` will automatically initialize - your application by calling the `Ember.Application.initialize()` method. If + 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. + You can define a `ready` method on the `Ember.Application` instance, which + will be run by Ember when the application is initialized. + 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. @@ -24902,7 +25798,7 @@ var get = Ember.get, set = Ember.set, 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). + [Ember View Layer guide](http://emberjs.com/guides/understanding-ember/the-view-layer/#toc_event-delegation). ### Initializers @@ -24913,7 +25809,7 @@ var get = Ember.get, set = Ember.set, name: "store", initialize: function(container, application) { - container.register('store', 'main', application.Store); + container.register('store:main', application.Store); } }); ``` @@ -24955,7 +25851,8 @@ var get = Ember.get, set = Ember.set, @namespace Ember @extends Ember.Namespace */ -var Application = Ember.Application = Ember.Namespace.extend({ + +var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin, { /** The root DOM element of the Application. This can be specified as an @@ -25017,8 +25914,6 @@ var Application = Ember.Application = Ember.Namespace.extend({ */ 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, @@ -25032,14 +25927,16 @@ var Application = Ember.Application = Ember.Namespace.extend({ 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('-------------------------------'); + if ( Ember.LOG_VERSION ) { + Ember.LOG_VERSION = false; // we only need to see this once per Application#init + Ember.debug('-------------------------------'); + Ember.debug('Ember.VERSION : ' + Ember.VERSION); + Ember.debug('Handlebars.VERSION : ' + Ember.Handlebars.VERSION); + Ember.debug('jQuery.VERSION : ' + Ember.$().jquery); + Ember.debug('-------------------------------'); + } }, /** @@ -25088,31 +25985,13 @@ var Application = Ember.Application = Ember.Namespace.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, + The initialization itself is scheduled on the actions queue which ensures that application loading finishes before booting. @@ -25125,10 +26004,14 @@ var Application = Ember.Application = Ember.Namespace.extend({ */ scheduleInitialize: function() { var self = this; - this.$().ready(function() { - if (self.isDestroyed || self.isInitialized) return; - Ember.run.once(self, 'initialize'); - }); + + if (!this.$ || this.$.isReady) { + Ember.run.schedule('actions', self, '_initialize'); + } else { + this.$().ready(function(){ + Ember.run(self, '_initialize'); + }); + } }, /** @@ -25219,6 +26102,20 @@ var Application = Ember.Application = Ember.Namespace.extend({ container.injection.apply(container, arguments); }, + /** + @private + @deprecated + + Calling initialize manually is not supported. + + Please see Ember.Application#advanceReadiness and + Ember.Application#deferReadiness. + + @method initialize + **/ + initialize: function(){ + Ember.deprecate('Calling initialize manually is not supported. Please see Ember.Application#advanceReadiness and Ember.Application#deferReadiness'); + }, /** @private @@ -25228,15 +26125,13 @@ var Application = Ember.Application = Ember.Namespace.extend({ choose to defer readiness. For example, an authentication hook might want to defer readiness until the auth token has been retrieved. - @method initialize + @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; + _initialize: function() { + if (this.isDestroyed) { return; } // At this point, the App.Router must already be assigned - this.__container__.register('router', 'main', this.Router); + this.register('router:main', this.Router); this.runInitializers(); Ember.runLoadHooks('application', this); @@ -25253,9 +26148,10 @@ var Application = Ember.Application = Ember.Namespace.extend({ get(this, '__container__').destroy(); this.buildContainer(); - this.isInitialized = false; - this.initialize(); - this.startRouting(); + Ember.run.schedule('actions', this, function(){ + this._initialize(); + this.startRouting(); + }); }, /** @@ -25267,7 +26163,7 @@ var Application = Ember.Application = Ember.Namespace.extend({ container = this.__container__, graph = new Ember.DAG(), namespace = this, - properties, i, initializer; + i, initializer; for (i=0; i -1) { + result = result.replace(/\.(.)/g, function(m) { return m.charAt(1).toUpperCase(); }); } - var className = classify(name) + classify(type); - var factory = get(namespace, className); + if (name.indexOf('_') > -1) { + result = result.replace(/_(.)/g, function(m) { return m.charAt(1).toUpperCase(); }); + } - if (factory) { return factory; } - }; + return type + ':' + result; + } else { + return fullName; + } } Ember.runLoadHooks('Ember.Application', Ember.Application); - })(); @@ -25492,7 +26409,7 @@ Ember.runLoadHooks('Ember.Application', Ember.Application); (function() { /** @module ember -@submodule ember-routing +@submodule ember-application */ var get = Ember.get, set = Ember.set; @@ -25548,7 +26465,7 @@ Ember.ControllerMixin.reopen({ }, controllerFor: function(controllerName) { - Ember.deprecate("Controller#controllerFor is depcrecated, please use Controller#needs instead"); + Ember.deprecate("Controller#controllerFor is deprecated, please use Controller#needs instead"); var container = get(this, 'container'); return container.lookup('controller:' + controllerName); }, @@ -25648,7 +26565,7 @@ Ember.State = Ember.Object.extend(Ember.Evented, }, init: function() { - var states = get(this, 'states'), foundStates; + var states = get(this, 'states'); set(this, 'childStates', Ember.A()); set(this, 'eventTransitions', get(this, 'eventTransitions') || {}); @@ -25798,7 +26715,7 @@ Ember.State.reopenClass({ transitionTo: function(target) { var transitionFunction = function(stateManager, contextOrEvent) { - var contexts = [], transitionArgs, + var contexts = [], Event = Ember.$ && Ember.$.Event; if (contextOrEvent && (Event && contextOrEvent instanceof Event)) { @@ -26824,8 +27741,8 @@ Ember States })(); -// Version: v1.0.0-rc.1 -// Last commit: 8b061b4 (2013-02-15 12:10:22 -0800) +// Version: v1.0.0-rc.2 +// Last commit: 656fa6e (2013-03-29 13:40:38 -0700) (function() { -- cgit v1.2.3