diff options
author | Peter Kozak <spag@golwen.net> | 2013-06-22 10:22:48 +0200 |
---|---|---|
committer | Peter Kozak <spag@golwen.net> | 2013-06-22 10:22:48 +0200 |
commit | 08ecc91d89a2b6a83daee4a1ffda5520e38e53c6 (patch) | |
tree | 2f15799d3948eaa26abacef033c687b4362f602b | |
parent | d21bf5513aa28187deb6b6c1ba21635b97ff82a4 (diff) | |
parent | 4ce04ec03bc8f362d48ded43833645e7f454cb5c (diff) |
Merge branch 'develop' of github.com:amooma/GS5 into develop
-rw-r--r-- | app/controllers/api/v1/calls_controller.rb | 24 | ||||
-rw-r--r-- | app/controllers/switchboard_entries_controller.rb | 2 | ||||
-rw-r--r-- | app/views/switchboard_entries/_form_core.html.haml | 1 | ||||
-rw-r--r-- | app/views/switchboard_entries/show.html.haml | 5 | ||||
-rw-r--r-- | app/views/switchboards/show.html.erb | 22 | ||||
-rw-r--r-- | config/routes.rb | 1 | ||||
-rw-r--r-- | public/js/app.js | 25 | ||||
-rw-r--r-- | public/js/libs/ember-data.js | 4832 | ||||
-rw-r--r-- | public/js/libs/ember.js | 6819 | ||||
-rw-r--r-- | public/js/libs/handlebars.js | 2454 | ||||
-rw-r--r-- | public/js/libs/new-ember-data.js | 8431 |
11 files changed, 8447 insertions, 14169 deletions
diff --git a/app/controllers/api/v1/calls_controller.rb b/app/controllers/api/v1/calls_controller.rb new file mode 100644 index 0000000..e6fbed4 --- /dev/null +++ b/app/controllers/api/v1/calls_controller.rb @@ -0,0 +1,24 @@ +module Api + module V1 + class CallsController < ApplicationController + respond_to :json + + def index + @calls = Call.limit(10) + + respond_with @calls + end + + def show + @call = Call.find(params[:id]) + + if params[:transfer_blind] + @call.transfer_blind(params[:transfer_blind]) + end + + respond_with @call + end + + end + end +end diff --git a/app/controllers/switchboard_entries_controller.rb b/app/controllers/switchboard_entries_controller.rb index ef6c72e..5b41816 100644 --- a/app/controllers/switchboard_entries_controller.rb +++ b/app/controllers/switchboard_entries_controller.rb @@ -58,7 +58,7 @@ class SwitchboardEntriesController < ApplicationController private def switchboard_entry_params - params.require(:switchboard_entry).permit(:name, :sip_account_id) + params.require(:switchboard_entry).permit(:name, :sip_account_id, :switchable) end def spread_breadcrumbs diff --git a/app/views/switchboard_entries/_form_core.html.haml b/app/views/switchboard_entries/_form_core.html.haml index 6f340c2..2caaba5 100644 --- a/app/views/switchboard_entries/_form_core.html.haml +++ b/app/views/switchboard_entries/_form_core.html.haml @@ -1,3 +1,4 @@ .inputs = f.association :sip_account, :collection => @sip_accounts, :label => t('switchboard_entries.form.sip_account_id.label'), :hint => conditional_hint('switchboard_entries.form.sip_account_id.hint'), :autofocus => true, :include_blank => false = f.input :name, :label => t('switchboard_entries.form.name.label'), :hint => conditional_hint('switchboard_entries.form.name.hint') + = f.input :switchable, :as => :boolean, :label => t('switchboard_entries.form.switchable.label'), :hint => conditional_hint('switchboard_entries.form.switchable.hint')
\ No newline at end of file diff --git a/app/views/switchboard_entries/show.html.haml b/app/views/switchboard_entries/show.html.haml index b519781..85b8166 100644 --- a/app/views/switchboard_entries/show.html.haml +++ b/app/views/switchboard_entries/show.html.haml @@ -18,5 +18,10 @@ %strong= t('switchboard_entries.show.position') + ":" %td = @switchboard_entry.position + %tr + %td + %strong= t('switchboard_entries.show.switchable') + ":" + %td + = @switchboard_entry.switchable = render :partial => 'shared/show_edit_destroy_part', :locals => {:parent => @switchboard, :child => @switchboard_entry }
\ No newline at end of file diff --git a/app/views/switchboards/show.html.erb b/app/views/switchboards/show.html.erb index 2a2765f..4877613 100644 --- a/app/views/switchboards/show.html.erb +++ b/app/views/switchboards/show.html.erb @@ -26,16 +26,27 @@ {{avatar_img switchboardEntry.avatar_src}} <small> <p> + {{#if switchboardEntry.name}} <span class="label">{{switchboardEntry.name}}</span> + {{/if}} {{#each phoneNumber in switchboardEntry.sipAccount.phoneNumberShortList}} <span class="label"> {{phoneNumber.number}} </span> + + {{#if activeCalls.length}} + <p> + {{#each activeCall in activeCalls}} + <button {{action transfer_blind activeCall.id phoneNumber.number}} class="btn btn-small">Blind Transfer ({{activeCall.b_caller_id_number}})</button> + {{/each}} + </p> + {{/if}} {{/each}} </p> {{show_callstate switchboardEntry.callstate}} + {{#if switchboardEntry.sipAccount.calls.length}} <p> Anrufe: @@ -51,17 +62,6 @@ </p> {{/if}} - {{#if activeCalls.length}} - <p> - Verbinden mit: - <br> - {{#each activeCall in activeCalls}} - <button {{action blind_transfer}}> - {{activeCall.b_caller_id_number}} - </button> - {{/each}} - </p> - {{/if}} </small> </div> </li> diff --git a/config/routes.rb b/config/routes.rb index 46562eb..eeb9c0b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -11,6 +11,7 @@ Gemeinschaft42c::Application.routes.draw do resources :sip_accounts, :only => [:show, :index] resources :pager_groups resources :phone_numbers, :only => [:show, :index] + resources :calls, :only => [:index, :show, :update] end resources :rows diff --git a/public/js/app.js b/public/js/app.js index 893b4af..da887c4 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -1,5 +1,4 @@ App = Ember.Application.create({ - LOG_TRANSITIONS: true, rootElement: '#emberjs-container', // Reload the switchboard every x milliseconds @@ -9,11 +8,6 @@ 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); } } @@ -32,15 +26,15 @@ App.SwitchboardRoute = Ember.Route.extend({ // Controller App.SwitchboardController = Ember.ObjectController.extend({ - blind_transfer: function() { - console.log('transfer') + transfer_blind: function(call_id, destination) { + console.log('test') + request_url = '/api/v1/calls/' + call_id + '.json'; + jQuery.get(request_url, { transfer_blind: destination }); } }); // Models -App.Store = DS.Store.extend({ - revision: 12 -}); +App.Store = DS.Store.extend(); DS.RESTAdapter.configure("plurals", { switchboard_entry: "switchboard_entries" @@ -54,8 +48,6 @@ App.Switchboard = DS.Model.extend({ switchboardEntrys: DS.hasMany('App.SwitchboardEntry'), activeCalls: DS.hasMany('App.ActiveCall'), name: DS.attr('string') - - }); App.SwitchboardEntry = DS.Model.extend({ @@ -116,8 +108,7 @@ App.Call = DS.Model.extend({ } else { return false } - }.property('b_callstate') - + }.property('b_callstate') }); App.store.adapter.serializer.configure(App.PhoneNumber, { sideloadAs: 'phone_numbers' }); @@ -127,5 +118,7 @@ Ember.Handlebars.registerBoundHelper('avatar_img', function(value) { }); Ember.Handlebars.registerBoundHelper('show_callstate', function(value) { - return new Handlebars.SafeString('<span class="label">' + value + '</span>'); + if (value) { + return new Handlebars.SafeString('<span class="label">' + value + '</span>'); + } }); diff --git a/public/js/libs/ember-data.js b/public/js/libs/ember-data.js index 00a6e25..798478b 100644 --- a/public/js/libs/ember-data.js +++ b/public/js/libs/ember-data.js @@ -1,10 +1,119 @@ -// Last commit: 5fd6d65 (2013-03-28 01:13:50 +0100) +// Version: v0.13-33-g8cf224d +// Last commit: 8cf224d (2013-06-19 00:23:32 -0400) (function() { -window.DS = Ember.Namespace.create({ - // this one goes past 11 - CURRENT_API_REVISION: 12 +var define, requireModule; + +(function() { + var registry = {}, seen = {}; + + define = function(name, deps, callback) { + registry[name] = { deps: deps, callback: callback }; + }; + + requireModule = function(name) { + if (seen[name]) { return seen[name]; } + seen[name] = {}; + + var mod, deps, callback, reified , exports; + + mod = registry[name]; + + if (!mod) { + throw new Error("Module '" + name + "' not found."); + } + + deps = mod.deps; + callback = mod.callback; + reified = []; + exports; + + for (var i=0, l=deps.length; i<l; i++) { + if (deps[i] === 'exports') { + reified.push(exports = {}); + } else { + reified.push(requireModule(deps[i])); + } + } + + var value = callback.apply(this, reified); + return seen[name] = exports || value; + }; +})(); +(function() { +/** + @module data + @main data +*/ + +/** + All Ember Data methods and functions are defined inside of this namespace. + + @class DS + @static +*/ + +window.DS = Ember.Namespace.create(); + +})(); + + + +(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) { + 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'); + } + }); }); })(); @@ -12,16 +121,79 @@ window.DS = Ember.Namespace.create({ (function() { -var DeferredMixin = Ember.DeferredMixin, // ember-runtime/mixins/deferred - Evented = Ember.Evented, // ember-runtime/mixins/evented +/** + * Date.parse with progressive enhancement for ISO 8601 <https://github.com/csnover/js-iso8601> + * © 2011 Colin Snover <http://zetafleet.com> + * Released under MIT license. + */ + +Ember.Date = Ember.Date || {}; + +var origParse = Date.parse, numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ]; +Ember.Date.parse = function (date) { + var timestamp, struct, minutesOffset = 0; + + // ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date Time String Format string + // before falling back to any implementation-specific date parsing, so that’s what we do, even if native + // implementations could be faster + // 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm + if ((struct = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec(date))) { + // avoid NaN timestamps caused by “undefined” values being passed to Date.UTC + for (var i = 0, k; (k = numericKeys[i]); ++i) { + struct[k] = +struct[k] || 0; + } + + // allow undefined days and months + struct[2] = (+struct[2] || 1) - 1; + struct[3] = +struct[3] || 1; + + if (struct[8] !== 'Z' && struct[9] !== undefined) { + minutesOffset = struct[10] * 60 + struct[11]; + + if (struct[9] === '+') { + minutesOffset = 0 - minutesOffset; + } + } + + timestamp = Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7]); + } + else { + timestamp = origParse ? origParse(date) : NaN; + } + + return timestamp; +}; + +if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Date) { + Date.parse = Ember.Date.parse; +} + +})(); + + + +(function() { + +})(); + + + +(function() { +var Evented = Ember.Evented, // ember-runtime/mixins/evented + Deferred = Ember.DeferredMixin, // ember-runtime/mixins/evented run = Ember.run, // ember-metal/run-loop get = Ember.get; // ember-metal/accessors -var LoadPromise = Ember.Mixin.create(Evented, DeferredMixin, { +var LoadPromise = Ember.Mixin.create(Evented, Deferred, { init: function() { this._super.apply(this, arguments); - this.one('didLoad', function() { - run(this, 'resolve', this); + + this.one('didLoad', this, function() { + this.resolve(this); + }); + + this.one('becameError', this, function() { + this.reject(this); }); if (get(this, 'isLoaded')) { @@ -37,6 +209,9 @@ DS.LoadPromise = LoadPromise; (function() { +/** +*/ + var get = Ember.get, set = Ember.set; var LoadPromise = DS.LoadPromise; // system/mixins/load_promise @@ -47,9 +222,19 @@ var LoadPromise = DS.LoadPromise; // system/mixins/load_promise time. You should not create record arrays yourself. Instead, an instance of DS.RecordArray or its subclasses will be returned by your application's store in response to queries. + + @module data + @submodule data-record-array + @main data-record-array + + @class RecordArray + @namespace DS + @extends Ember.ArrayProxy + @uses Ember.Evented + @uses DS.LoadPromise */ -DS.RecordArray = Ember.ArrayProxy.extend(Ember.Evented, LoadPromise, { +DS.RecordArray = Ember.ArrayProxy.extend(LoadPromise, { /** The model type contained by this record array. @@ -111,8 +296,19 @@ DS.RecordArray = Ember.ArrayProxy.extend(Ember.Evented, LoadPromise, { (function() { +/** + @module data + @submodule data-record-array +*/ + var get = Ember.get; +/** + @class FilteredRecordArray + @namespace DS + @extends DS.RecordArray + @constructor +*/ DS.FilteredRecordArray = DS.RecordArray.extend({ filterFunction: null, isLoaded: true, @@ -123,8 +319,8 @@ DS.FilteredRecordArray = DS.RecordArray.extend({ }, updateFilter: Ember.observer(function() { - var store = get(this, 'store'); - store.updateRecordArrayFilter(this, get(this, 'type'), get(this, 'filterFunction')); + var manager = get(this, 'manager'); + manager.updateFilter(this, get(this, 'type'), get(this, 'filterFunction')); }, 'filterFunction') }); @@ -133,8 +329,19 @@ DS.FilteredRecordArray = DS.RecordArray.extend({ (function() { +/** + @module data + @submodule data-record-array +*/ + var get = Ember.get, set = Ember.set; +/** + @class AdapterPopulatedRecordArray + @namespace DS + @extends DS.RecordArray + @constructor +*/ DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({ query: null, @@ -144,18 +351,13 @@ DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({ }, load: function(references) { - var store = get(this, 'store'), type = get(this, 'type'); - - this.beginPropertyChanges(); - set(this, 'content', Ember.A(references)); - set(this, 'isLoaded', true); - this.endPropertyChanges(); + this.setProperties({ + content: Ember.A(references), + isLoaded: true + }); - var self = this; // TODO: does triggering didLoad event should be the last action of the runLoop? - Ember.run.once(function() { - self.trigger('didLoad'); - }); + Ember.run.once(this, 'trigger', 'didLoad'); } }); @@ -164,6 +366,11 @@ DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({ (function() { +/** + @module data + @submodule data-record-array +*/ + var get = Ember.get, set = Ember.set; /** @@ -195,6 +402,11 @@ var get = Ember.get, set = Ember.set; We call the record to which a relationship belongs the relationship's _owner_. + + @class ManyArray + @namespace DS + @extends DS.RecordArray + @constructor */ DS.ManyArray = DS.RecordArray.extend({ init: function() { @@ -211,6 +423,15 @@ DS.ManyArray = DS.RecordArray.extend({ */ owner: null, + /** + @private + + `true` if the relationship is polymorphic, `false` otherwise. + + @property {Boolean} + */ + isPolymorphic: false, + // LOADING STATE isLoaded: false, @@ -230,17 +451,16 @@ DS.ManyArray = DS.RecordArray.extend({ fetch: function() { var references = get(this, 'content'), store = get(this, 'store'), - type = get(this, 'type'), owner = get(this, 'owner'); - store.fetchUnloadedReferences(type, references, owner); + store.fetchUnloadedReferences(references, owner); }, // Overrides Ember.Array's replace method to implement replaceContent: function(index, removed, added) { // Map the array of record objects into an array of client ids. added = added.map(function(record) { - Ember.assert("You can only add records of " + (get(this, 'type') && get(this, 'type').toString()) + " to this relationship.", !get(this, 'type') || (get(this, 'type') === record.constructor)); + Ember.assert("You can only add records of " + (get(this, 'type') && get(this, 'type').toString()) + " to this relationship.", !get(this, 'type') || (get(this, 'type').detectInstance(record)) ); return get(record, '_reference'); }, this); @@ -328,6 +548,8 @@ DS.ManyArray = DS.RecordArray.extend({ type = get(this, 'type'), record; + Ember.assert("You can not create records of " + (get(this, 'type') && get(this, 'type').toString()) + " on this polymorphic relationship.", !get(this, 'isPolymorphic')); + transaction = transaction || get(owner, 'transaction'); record = store.createRecord.call(store, type, hash, transaction); @@ -343,14 +565,22 @@ DS.ManyArray = DS.RecordArray.extend({ (function() { +/** + @module data + @submodule data-record-array +*/ })(); (function() { -var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt, - removeObject = Ember.EnumerableUtils.removeObject, forEach = Ember.EnumerableUtils.forEach; +var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; + +/** + @module data + @submodule data-transaction +*/ /** A transaction allows you to collect multiple records into a unit of work @@ -430,8 +660,6 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt, calling commit. */ -var arrayDefault = function() { return []; }; - DS.Transaction = Ember.Object.extend({ /** @private @@ -440,15 +668,7 @@ DS.Transaction = Ember.Object.extend({ type. */ init: function() { - set(this, 'buckets', { - clean: Ember.OrderedSet.create(), - created: Ember.OrderedSet.create(), - updated: Ember.OrderedSet.create(), - deleted: Ember.OrderedSet.create(), - inflight: Ember.OrderedSet.create() - }); - - set(this, 'relationships', Ember.OrderedSet.create()); + set(this, 'records', Ember.OrderedSet.create()); }, /** @@ -475,11 +695,11 @@ DS.Transaction = Ember.Object.extend({ isDefault: Ember.computed(function() { return this === get(this, 'store.defaultTransaction'); - }), + }).volatile(), /** Adds an existing record to this transaction. Only records without - modficiations (i.e., records whose `isDirty` property is `false`) + modificiations (i.e., records whose `isDirty` property is `false`) can be added to a transaction. @param {DS.Model} record the record to add to the transaction @@ -487,29 +707,57 @@ DS.Transaction = Ember.Object.extend({ add: function(record) { Ember.assert("You must pass a record into transaction.add()", record instanceof DS.Model); - var recordTransaction = get(record, 'transaction'), - defaultTransaction = get(this, 'store.defaultTransaction'); + var store = get(this, 'store'); + var adapter = get(store, '_adapter'); + var serializer = get(adapter, 'serializer'); + serializer.eachEmbeddedRecord(record, function(embeddedRecord, embeddedType) { + if (embeddedType === 'load') { return; } + + this.add(embeddedRecord); + }, this); - // Make `add` idempotent - if (recordTransaction === this) { return; } + this.adoptRecord(record); + }, - // XXX it should be possible to move a dirty transaction from the default transaction + relationships: Ember.computed(function() { + var relationships = Ember.OrderedSet.create(), + records = get(this, 'records'), + store = get(this, 'store'); - // we could probably make this work if someone has a valid use case. Do you? - Ember.assert("Once a record has changed, you cannot move it into a different transaction", !get(record, 'isDirty')); + records.forEach(function(record) { + var reference = get(record, '_reference'); + var changes = store.relationshipChangesFor(reference); + for(var i = 0; i < changes.length; i++) { + relationships.add(changes[i]); + } + }); - Ember.assert("Models cannot belong to more than one transaction at a time.", recordTransaction === defaultTransaction); + return relationships; + }).volatile(), - this.adoptRecord(record); - }, + commitDetails: Ember.computed(function() { + var commitDetails = Ember.MapWithDefault.create({ + defaultValue: function() { + return { + created: Ember.OrderedSet.create(), + updated: Ember.OrderedSet.create(), + deleted: Ember.OrderedSet.create() + }; + } + }); - relationshipBecameDirty: function(relationship) { - get(this, 'relationships').add(relationship); - }, + var records = get(this, 'records'), + store = get(this, 'store'); - relationshipBecameClean: function(relationship) { - get(this, 'relationships').remove(relationship); - }, + records.forEach(function(record) { + if(!get(record, 'isDirty')) return; + record.send('willCommit'); + var adapter = store.adapterForType(record.constructor); + commitDetails.get(adapter)[get(record, 'dirtyType')].add(record); + }); + + return commitDetails; + }).volatile(), /** Commits the transaction, which causes all of the modified records that @@ -522,36 +770,22 @@ DS.Transaction = Ember.Object.extend({ */ commit: function() { var store = get(this, 'store'); - var adapter = get(store, '_adapter'); - var defaultTransaction = get(store, 'defaultTransaction'); - - var iterate = function(records) { - var set = records.copy(); - set.forEach(function (record) { - record.send('willCommit'); - }); - return set; - }; - - var relationships = get(this, 'relationships'); - - var commitDetails = { - created: iterate(this.bucketForType('created')), - updated: iterate(this.bucketForType('updated')), - deleted: iterate(this.bucketForType('deleted')), - relationships: relationships - }; - if (this === defaultTransaction) { + if (get(this, 'isDefault')) { set(store, 'defaultTransaction', store.transaction()); } this.removeCleanRecords(); - if (!commitDetails.created.isEmpty() || !commitDetails.updated.isEmpty() || !commitDetails.deleted.isEmpty() || !relationships.isEmpty()) { - if (adapter && adapter.commit) { adapter.commit(store, commitDetails); } - else { throw fmt("Adapter is either null or does not implement `commit` method", this); } - } + var commitDetails = get(this, 'commitDetails'), + relationships = get(this, 'relationships'); + + commitDetails.forEach(function(adapter, commitDetails) { + Ember.assert("You tried to commit records but you have no adapter", adapter); + Ember.assert("You tried to commit records but your adapter does not implement `commit`", adapter.commit); + + adapter.commit(store, commitDetails); + }); // Once we've committed the transaction, there is no need to // keep the OneToManyChanges around. Destroy them so they @@ -575,21 +809,36 @@ DS.Transaction = Ember.Object.extend({ current transaction should not be used again. */ rollback: function() { - // Loop through all of the records in each of the dirty states - // and initiate a rollback on them. As a side effect of telling - // the record to roll back, it should also move itself out of - // the dirty bucket and into the clean bucket. - ['created', 'updated', 'deleted', 'inflight'].forEach(function(bucketType) { - var records = this.bucketForType(bucketType); - forEach(records, function(record) { - record.send('rollback'); - }); - records.clear(); - }, this); + // Destroy all relationship changes and compute + // all references affected + var references = Ember.OrderedSet.create(); + var relationships = get(this, 'relationships'); + relationships.forEach(function(r) { + references.add(r.firstRecordReference); + references.add(r.secondRecordReference); + r.destroy(); + }); + + var records = get(this, 'records'); + records.forEach(function(record) { + if (!record.get('isDirty')) return; + record.send('rollback'); + }); // Now that all records in the transaction are guaranteed to be // clean, migrate them all to the store's default transaction. this.removeCleanRecords(); + + // Remaining associated references are not part of the transaction, but + // can still have hasMany's which have not been reloaded + references.forEach(function(r) { + if (r && r.record) { + var record = r.record; + record.suspendRelationshipObservers(function() { + record.reloadHasManys(); + }); + } + }, this); }, /** @@ -615,27 +864,12 @@ DS.Transaction = Ember.Object.extend({ Removes all of the records in the transaction's clean bucket. */ removeCleanRecords: function() { - var clean = this.bucketForType('clean'); - clean.forEach(function(record) { - this.remove(record); - }, this); - clean.clear(); - }, - - /** - @private - - Returns the bucket for the given bucket type. For example, you might call - `this.bucketForType('updated')` to get the `Ember.Map` that contains all - of the records that have changes pending. - - @param {String} bucketType the type of bucket - @returns Ember.Map - */ - bucketForType: function(bucketType) { - var buckets = get(this, 'buckets'); - - return get(buckets, bucketType); + var records = get(this, 'records'); + records.forEach(function(record) { + if(!record.get('isDirty')) { + this.remove(record); + } + }, this); }, /** @@ -656,81 +890,51 @@ DS.Transaction = Ember.Object.extend({ var oldTransaction = get(record, 'transaction'); if (oldTransaction) { - oldTransaction.removeFromBucket('clean', record); + oldTransaction.removeRecord(record); } - this.addToBucket('clean', record); + get(this, 'records').add(record); set(record, 'transaction', this); }, /** - @private - - Adds a record to the named bucket. + @private - @param {String} bucketType one of `clean`, `created`, `updated`, or `deleted` + Removes the record without performing the normal checks + to ensure that the record is re-added to the store's + default transaction. */ - addToBucket: function(bucketType, record) { - this.bucketForType(bucketType).add(record); - }, - - /** - @private - - Removes a record from the named bucket. - - @param {String} bucketType one of `clean`, `created`, `updated`, or `deleted` - */ - removeFromBucket: function(bucketType, record) { - this.bucketForType(bucketType).remove(record); - }, - - /** - @private - - Called by a record's state manager to indicate that the record has entered - a dirty state. The record will be moved from the `clean` bucket and into - the appropriate dirty bucket. - - @param {String} bucketType one of `created`, `updated`, or `deleted` - */ - recordBecameDirty: function(bucketType, record) { - this.removeFromBucket('clean', record); - this.addToBucket(bucketType, record); - }, - - /** - @private - - Called by a record's state manager to indicate that the record has entered - inflight state. The record will be moved from its current dirty bucket and into - the `inflight` bucket. + removeRecord: function(record) { + get(this, 'records').remove(record); + } - @param {String} bucketType one of `created`, `updated`, or `deleted` - */ - recordBecameInFlight: function(kind, record) { - this.removeFromBucket(kind, record); - this.addToBucket('inflight', record); - }, +}); - recordIsMoving: function(kind, record) { - this.removeFromBucket(kind, record); - this.addToBucket('clean', record); - }, +DS.Transaction.reopenClass({ + ensureSameTransaction: function(records){ + var transactions = Ember.A(); + forEach( records, function(record){ + if (record){ transactions.pushObject(get(record, 'transaction')); } + }); - /** - @private + var transaction = transactions.reduce(function(prev, t) { + if (!get(t, 'isDefault')) { + if (prev === null) { return t; } + Ember.assert("All records in a changed relationship must be in the same transaction. You tried to change the relationship between records when one is in " + t + " and the other is in " + prev, t === prev); + } - Called by a record's state manager to indicate that the record has entered - a clean state. The record will be moved from its current dirty or inflight bucket and into - the `clean` bucket. + return prev; + }, null); - @param {String} bucketType one of `created`, `updated`, or `deleted` - */ - recordBecameClean: function(kind, record) { - this.removeFromBucket(kind, record); - this.remove(record); - } + if (transaction) { + forEach( records, function(record){ + if (record){ transaction.add(record); } + }); + } else { + transaction = transactions.objectAt(0); + } + return transaction; + } }); })(); @@ -738,11 +942,9 @@ DS.Transaction = Ember.Object.extend({ (function() { -var classify = Ember.String.classify, get = Ember.get; +var get = Ember.get; /** -@private - The Mappable mixin is designed for classes that would like to behave as a map for configuration purposes. @@ -791,11 +993,16 @@ var classify = Ember.String.classify, get = Ember.get; }) }); - The function passed to `generateMapFunctionFor` is invoked every time a - new value is added to the map. + The function passed to `generateMapFunctionFor` is invoked every time a + new value is added to the map. + + @class _Mappable + @private + @namespace DS + @extends Ember.Mixin **/ -var resolveMapConflict = function(oldValue, newValue, mappingsKey) { +var resolveMapConflict = function(oldValue, newValue) { return oldValue; }; @@ -809,7 +1016,7 @@ var transformMapValue = function(key, value) { DS._Mappable = Ember.Mixin.create({ createInstanceMapFor: function(mapName) { - var instanceMeta = Ember.metaPath(this, ['DS.Mappable'], true); + var instanceMeta = getMappableMeta(this); instanceMeta.values = instanceMeta.values || {}; @@ -829,7 +1036,7 @@ DS._Mappable = Ember.Mixin.create({ }, _copyMap: function(mapName, klass, instanceMap) { - var classMeta = Ember.metaPath(klass, ['DS.Mappable'], true); + var classMeta = getMappableMeta(klass); var classMap = classMeta[mapName]; if (classMap) { @@ -844,7 +1051,7 @@ DS._Mappable = Ember.Mixin.create({ var newValue = transformedValue; if (oldValue) { - newValue = (this.constructor.resolveMapConflict || resolveMapConflict)(oldValue, newValue, mapName); + newValue = (this.constructor.resolveMapConflict || resolveMapConflict)(oldValue, newValue); } instanceMap.set(transformedKey, newValue); @@ -856,7 +1063,8 @@ DS._Mappable = Ember.Mixin.create({ DS._Mappable.generateMapFunctionFor = function(mapName, transform) { return function(key, value) { - var meta = Ember.metaPath(this, ['DS.Mappable'], true); + var meta = getMappableMeta(this); + var map = meta[mapName] || Ember.MapWithDefault.create({ defaultValue: function() { return {}; } }); @@ -867,6 +1075,20 @@ DS._Mappable.generateMapFunctionFor = function(mapName, transform) { }; }; +function getMappableMeta(obj) { + var meta = Ember.meta(obj, true), + keyName = 'DS.Mappable', + value = meta[keyName]; + + if (!value) { meta[keyName] = {}; } + + if (!meta.hasOwnProperty(keyName)) { + meta[keyName] = Ember.create(meta[keyName]); + } + + return meta[keyName]; +} + })(); @@ -874,8 +1096,17 @@ DS._Mappable.generateMapFunctionFor = function(mapName, transform) { (function() { /*globals Ember*/ /*jshint eqnull:true*/ -var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt, once = Ember.run.once; +/** + @module data + @submodule data-store +*/ + +var get = Ember.get, set = Ember.set; +var once = Ember.run.once; +var isNone = Ember.isNone; var forEach = Ember.EnumerableUtils.forEach; +var map = Ember.EnumerableUtils.map; + // These values are used in the data cache when clientIds are // needed but the underlying data has not yet been loaded by // the server. @@ -890,10 +1121,13 @@ var CREATED = { created: true }; // scheme: // // * +id+ means an identifier managed by an external source, provided inside -// the data provided by that source. +// the data provided by that source. These are always coerced to be strings +// before being used internally. // * +clientId+ means a transient numerical identifier generated at runtime by // the data store. It is important primarily because newly created objects may // not yet have an externally generated id. +// * +reference+ means a record reference object, which holds metadata about a +// record, even if it has not yet been fully materialized. // * +type+ means a subclass of DS.Model. // Used by the store to normalize IDs entering the store. Despite the fact @@ -906,32 +1140,48 @@ var coerceId = function(id) { return id == null ? null : id+''; }; -var map = Ember.EnumerableUtils.map; /** The store contains all of the data for records loaded from the server. - It is also responsible for creating instances of DS.Model that wraps + It is also responsible for creating instances of DS.Model that wrap the individual data for a record, so that they can be bound to in your Handlebars templates. - Create a new store like this: + Define your application's store like this: + + MyApp.Store = DS.Store.extend(); - MyApp.store = DS.Store.create(); + Most Ember.js applications will only have a single `DS.Store` that is + automatically created by their `Ember.Application`. - You can retrieve DS.Model instances from the store in several ways. To retrieve - a record for a specific id, use the `find()` method: + You can retrieve models from the store in several ways. To retrieve a record + for a specific id, use `DS.Model`'s `find()` method: - var record = MyApp.store.find(MyApp.Contact, 123); + var person = App.Person.find(123); - By default, the store will talk to your backend using a standard REST mechanism. - You can customize how the store talks to your backend by specifying a custom adapter: + If your application has multiple `DS.Store` instances (an unusual case), you can + specify which store should be used: + + var person = store.find(App.Person, 123); + + In general, you should retrieve models using the methods on `DS.Model`; you should + rarely need to interact with the store directly. + + By default, the store will talk to your backend using a standard REST mechanism. + You can customize how the store talks to your backend by specifying a custom adapter: MyApp.store = DS.Store.create({ adapter: 'MyApp.CustomAdapter' }); - You can learn more about writing a custom adapter by reading the `DS.Adapter` - documentation. + You can learn more about writing a custom adapter by reading the `DS.Adapter` + documentation. + + @class Store + @namespace DS + @extends Ember.Object + @uses DS._Mappable + @constructor */ DS.Store = Ember.Object.extend(DS._Mappable, { @@ -939,42 +1189,23 @@ DS.Store = Ember.Object.extend(DS._Mappable, { Many methods can be invoked without specifying which store should be used. In those cases, the first store created will be used as the default. If an application has multiple stores, it should specify which store to use - when performing actions, such as finding records by id. + when performing actions, such as finding records by ID. The init method registers this store as the default if none is specified. */ init: function() { - // Enforce API revisioning. See BREAKING_CHANGES.md for more. - var revision = get(this, 'revision'); - - if (revision !== DS.CURRENT_API_REVISION && !Ember.ENV.TESTING) { - throw new Error("Error: The Ember Data library has had breaking API changes since the last time you updated the library. Please review the list of breaking changes at https://github.com/emberjs/data/blob/master/BREAKING_CHANGES.md, then update your store's `revision` property to " + DS.CURRENT_API_REVISION); - } - if (!get(DS, 'defaultStore') || get(this, 'isDefaultStore')) { set(DS, 'defaultStore', this); } // internal bookkeeping; not observable this.typeMaps = {}; - this.recordCache = []; - this.clientIdToId = {}; - this.clientIdToType = {}; - this.clientIdToData = {}; - this.clientIdToPrematerializedData = {}; - this.recordArraysByClientId = {}; + this.recordArrayManager = DS.RecordArrayManager.create({ + store: this + }); this.relationshipChanges = {}; - this.recordReferences = {}; - - // Internally, we maintain a map of all unloaded IDs requested by - // a ManyArray. As the adapter loads data into the store, the - // store notifies any interested ManyArrays. When the ManyArray's - // total number of loading records drops to zero, it becomes - // `isLoaded` and fires a `didLoad` event. - this.loadingRecordArrays = {}; - - this._recordsToSave = Ember.OrderedSet.create(); + set(this, 'currentTransaction', this.transaction()); set(this, 'defaultTransaction', this.transaction()); }, @@ -999,31 +1230,6 @@ DS.Store = Ember.Object.extend(DS._Mappable, { return DS.Transaction.create({ store: this }); }, - ensureSameTransaction: function(records){ - var transactions = Ember.A(); - forEach( records, function(record){ - if (record){ transactions.pushObject(get(record, 'transaction')); } - }); - - var transaction = transactions.reduce(function(prev, t) { - if (!get(t, 'isDefault')) { - if (prev === null) { return t; } - Ember.assert("All records in a changed relationship must be in the same transaction. You tried to change the relationship between records when one is in " + t + " and the other is in " + prev, t === prev); - } - - return prev; - }, null); - - if (transaction) { - forEach( records, function(record){ - if (record){ transaction.add(record); } - }); - } else { - transaction = transactions.objectAt(0); - } - return transaction; - - }, /** @private @@ -1042,48 +1248,23 @@ DS.Store = Ember.Object.extend(DS._Mappable, { @param {DS.Model} record */ materializeData: function(record) { - var clientId = get(record, 'clientId'), - cidToData = this.clientIdToData, - adapter = this.adapterForType(record.constructor), - data = cidToData[clientId]; - - cidToData[clientId] = MATERIALIZED; + var reference = get(record, '_reference'), + data = reference.data, + adapter = this.adapterForType(record.constructor); - var prematerialized = this.clientIdToPrematerializedData[clientId]; + reference.data = MATERIALIZED; - // Ensures the record's data structures are setup - // before being populated by the adapter. record.setupData(); if (data !== CREATED) { // Instructs the adapter to extract information from the // opaque data and materialize the record's attributes and // relationships. - adapter.materialize(record, data, prematerialized); + adapter.materialize(record, data, reference.prematerialized); } }, /** - @private - - Returns true if there is already a record for this clientId. - - This is used to determine whether cleanup is required, so that - "changes" to unmaterialized records do not trigger mass - materialization. - - For example, if a parent record in a relationship with a large - number of children is deleted, we want to avoid materializing - those children. - - @param {Object} reference - @return {Boolean} - */ - recordIsMaterialized: function(reference) { - return !!this.recordCache[reference.clientId]; - }, - - /** The adapter to use to communicate to a backend server or other persistence layer. This can be specified as an instance, a class, or a property path that specifies @@ -1091,7 +1272,14 @@ DS.Store = Ember.Object.extend(DS._Mappable, { @property {DS.Adapter|String} */ - adapter: 'DS.RESTAdapter', + adapter: Ember.computed(function(){ + if (!Ember.testing) { + Ember.debug("A custom DS.Adapter was not provided as the 'Adapter' property of your application's Store. The default (DS.RESTAdapter) will be used."); + } + + return 'DS.RESTAdapter'; + }).property(), + /** @private @@ -1165,6 +1353,7 @@ DS.Store = Ember.Object.extend(DS._Mappable, { If you want to create a record inside of a given transaction, use `transaction.createRecord()` instead of `store.createRecord()`. + @method createRecord @param {subclass of DS.Model} type @param {Object} properties a hash of properties to set on the newly created record. @@ -1197,15 +1386,17 @@ DS.Store = Ember.Object.extend(DS._Mappable, { // give the adapter an opportunity to generate one. Typically, // client-side ID generators will use something like uuid.js // to avoid conflicts. - var adapter; - if (Ember.isNone(id)) { - adapter = this.adapterForType(type); + + if (isNone(id)) { + var adapter = this.adapterForType(type); + if (adapter && adapter.generateIdForRecord) { id = coerceId(adapter.generateIdForRecord(this, record)); properties.id = id; } } + // Coerce ID to a string id = coerceId(id); // Create a new `clientId` and associate it with the @@ -1214,24 +1405,20 @@ DS.Store = Ember.Object.extend(DS._Mappable, { // the sentinel value CREATED as the data for this // clientId. If we see this value later, we will skip // materialization. - var clientId = this.pushData(CREATED, id, type); + var reference = this.createReference(type, id); + reference.data = CREATED; - // Now that we have a clientId, attach it to the record we + // Now that we have a reference, attach it to the record we // just created. - set(record, 'clientId', clientId); + set(record, '_reference', reference); + reference.record = record; // Move the record out of its initial `empty` state into // the `loaded` state. record.loadedData(); - // Make sure the data is set up so the record doesn't - // try to materialize its nonexistent data. record.setupData(); - // Store the record we just created in the record cache for - // this clientId. - this.recordCache[clientId] = record; - // Set the properties specified on the record. record.setProperties(properties); @@ -1320,6 +1507,10 @@ DS.Store = Ember.Object.extend(DS._Mappable, { You can check whether a query results `RecordArray` has loaded by checking its `isLoaded` property. + + @method find + @param {DS.Model} type + @param {Object|String|Integer|null} id */ find: function(type, id) { if (id === undefined) { @@ -1347,22 +1538,42 @@ DS.Store = Ember.Object.extend(DS._Mappable, { `getByReference`. */ findById: function(type, id) { - var clientId = this.typeMapFor(type).idToCid[id]; + var reference; - if (clientId) { - return this.findByClientId(type, clientId); + if (this.hasReferenceForId(type, id)) { + reference = this.referenceForId(type, id); + + if (reference.data !== UNLOADED) { + return this.recordForReference(reference); + } } - clientId = this.pushData(LOADING, id, type); + if (!reference) { + reference = this.createReference(type, id); + } + + reference.data = LOADING; // create a new instance of the model type in the // 'isLoading' state - var record = this.materializeRecord(type, clientId, id); + var record = this.materializeRecord(reference); - // let the adapter set the data, possibly async - var adapter = this.adapterForType(type); - if (adapter && adapter.find) { adapter.find(this, type, id); } - else { throw "Adapter is either null or does not implement `find` method"; } + if (reference.data === LOADING) { + // let the adapter set the data, possibly async + var adapter = this.adapterForType(type), + store = this; + + Ember.assert("You tried to find a record but you have no adapter (for " + type + ")", adapter); + Ember.assert("You tried to find a record but your adapter does not implement `find`", adapter.find); + + var thenable = adapter.find(this, type, id); + + if (thenable && thenable.then) { + thenable.then(null /* for future use */, function(error) { + store.recordWasError(record); + }); + } + } return record; }, @@ -1370,56 +1581,45 @@ DS.Store = Ember.Object.extend(DS._Mappable, { reloadRecord: function(record) { var type = record.constructor, adapter = this.adapterForType(type), + store = this, id = get(record, 'id'); Ember.assert("You cannot update a record without an ID", id); Ember.assert("You tried to update a record but you have no adapter (for " + type + ")", adapter); Ember.assert("You tried to update a record but your adapter does not implement `find`", adapter.find); - adapter.find(this, type, id); + var thenable = adapter.find(this, type, id); + + if (thenable && thenable.then) { + thenable.then(null /* for future use */, function(error) { + store.recordWasError(record); + }); + } }, /** @private - This method returns a record for a given clientId. - - If there is no record object yet for the clientId, this method materializes - a new record object. This allows adapters to eagerly load large amounts of - data into the store, and avoid incurring the cost to create the objects - until they are requested. - - Several parts of Ember Data call this method: + This method returns a record for a given record refeence. - * findById, if a clientId already exists for a given type and - id combination - * OneToManyChange, which is backed by clientIds, when getChild, - getOldParent or getNewParent are called - * RecordArray, which is backed by clientIds, when an object at - a particular index is looked up + If no record for the reference has yet been materialized, this method will + materialize a new `DS.Model` instance. This allows adapters to eagerly load + large amounts of data into the store, and avoid incurring the cost of + creating models until they are requested. In short, it's a convenient way to get a record for a known - clientId, materializing it if necessary. + record reference, materializing it if necessary. - @param {Class} type - @param {Number|String} clientId + @param {Object} reference + @returns {DS.Model} */ - findByClientId: function(type, clientId) { - var cidToData, record, id; - - record = this.recordCache[clientId]; + recordForReference: function(reference) { + var record = reference.record; if (!record) { // create a new instance of the model type in the // 'isLoading' state - id = this.clientIdToId[clientId]; - record = this.materializeRecord(type, clientId, id); - - cidToData = this.clientIdToData; - - if (typeof cidToData[clientId] === 'object') { - record.loadedData(); - } + record = this.materializeRecord(reference); } return record; @@ -1428,27 +1628,25 @@ DS.Store = Ember.Object.extend(DS._Mappable, { /** @private - Given a type and array of `clientId`s, determines which of those + Given an array of `reference`s, determines which of those `clientId`s has not yet been loaded. In preparation for loading, this method also marks any unloaded `clientId`s as loading. */ - neededReferences: function(type, references) { - var neededReferences = [], - cidToData = this.clientIdToData, - reference; + unloadedReferences: function(references) { + var unloadedReferences = []; for (var i=0, l=references.length; i<l; i++) { - reference = references[i]; + var reference = references[i]; - if (cidToData[reference.clientId] === UNLOADED) { - neededReferences.push(reference); - cidToData[reference.clientId] = LOADING; + if (reference.data === UNLOADED) { + unloadedReferences.push(reference); + reference.data = LOADING; } } - return neededReferences; + return unloadedReferences; }, /** @@ -1457,61 +1655,72 @@ DS.Store = Ember.Object.extend(DS._Mappable, { This method is the entry point that relationships use to update themselves when their underlying data changes. - First, it determines which of its `clientId`s are still unloaded, - then converts the needed `clientId`s to IDs and invokes `findMany` - on the adapter. + First, it determines which of its `reference`s are still unloaded, + then invokes `findMany` on the adapter. */ - fetchUnloadedReferences: function(type, references, owner) { - var neededReferences = this.neededReferences(type, references); - this.fetchMany(type, neededReferences, owner); + fetchUnloadedReferences: function(references, owner) { + var unloadedReferences = this.unloadedReferences(references); + this.fetchMany(unloadedReferences, owner); }, /** @private - This method takes a type and list of `clientId`s, converts the - `clientId`s into IDs, and then invokes the adapter's `findMany` + This method takes a list of `reference`s, groups the `reference`s by type, + converts the `reference`s into IDs, and then invokes the adapter's `findMany` method. + The `reference`s are grouped by type to invoke `findMany` on adapters + for each unique type in `reference`s. + It is used both by a brand new relationship (via the `findMany` method) or when the data underlying an existing relationship changes (via the `fetchUnloadedReferences` method). */ - fetchMany: function(type, references, owner) { + fetchMany: function(references, owner) { if (!references.length) { return; } - var ids = map(references, function(reference) { - return reference.id; + // Group By Type + var referencesByTypeMap = Ember.MapWithDefault.create({ + defaultValue: function() { return Ember.A(); } + }); + forEach(references, function(reference) { + referencesByTypeMap.get(reference.type).push(reference); }); - var adapter = this.adapterForType(type); - if (adapter && adapter.findMany) { adapter.findMany(this, type, ids, owner); } - else { throw "Adapter is either null or does not implement `findMany` method"; } - }, + forEach(referencesByTypeMap, function(type) { + var references = referencesByTypeMap.get(type), + ids = map(references, function(reference) { return reference.id; }); - referenceForId: function(type, id) { - var clientId = this.clientIdForId(type, id); - return this.referenceForClientId(clientId); - }, + var adapter = this.adapterForType(type); - referenceForClientId: function(clientId) { - var references = this.recordReferences; + Ember.assert("You tried to load many records but you have no adapter (for " + type + ")", adapter); + Ember.assert("You tried to load many records but your adapter does not implement `findMany`", adapter.findMany); - if (references[clientId]) { - return references[clientId]; - } + adapter.findMany(this, type, ids, owner); + }, this); + }, - var type = this.clientIdToType[clientId]; + hasReferenceForId: function(type, id) { + id = coerceId(id); - return references[clientId] = { - id: this.idForClientId(clientId), - clientId: clientId, - type: type - }; + return !!this.typeMapFor(type).idToReference[id]; }, - recordForReference: function(reference) { - return this.findByClientId(reference.type, reference.clientId); + referenceForId: function(type, id) { + id = coerceId(id); + + // Check to see if we have seen this type/id pair before. + var reference = this.typeMapFor(type).idToReference[id]; + + // If not, create a reference for it but don't populate it + // with any data yet. + if (!reference) { + reference = this.createReference(type, id); + reference.data = UNLOADED; + } + + return reference; }, /** @@ -1528,32 +1737,36 @@ DS.Store = Ember.Object.extend(DS._Mappable, { * create a new ManyArray whose content is *all* of the clientIds * notify the ManyArray of the number of its elements that are already loaded - * insert the unloaded clientIds into the `loadingRecordArrays` + * insert the unloaded references into the `loadingRecordArrays` bookkeeping structure, which will allow the `ManyArray` to know when all of its loading elements are loaded from the server. * ask the adapter to load the unloaded elements, by invoking findMany with the still-unloaded IDs. */ - findMany: function(type, ids, record, relationship) { - // 1. Convert ids to client ids - // 2. Determine which of the client ids need to be loaded - // 3. Create a new ManyArray whose content is ALL of the clientIds - // 4. Decrement the ManyArray's counter by the number of loaded clientIds - // 5. Put the ManyArray into our bookkeeping data structure, keyed on + findMany: function(type, idsOrReferencesOrOpaque, record, relationship) { + // 1. Determine which of the client ids need to be loaded + // 2. Create a new ManyArray whose content is ALL of the clientIds + // 3. Decrement the ManyArray's counter by the number of loaded clientIds + // 4. Put the ManyArray into our bookkeeping data structure, keyed on // the needed clientIds - // 6. Ask the adapter to load the records for the unloaded clientIds (but + // 5. Ask the adapter to load the records for the unloaded clientIds (but // convert them back to ids) - if (!Ember.isArray(ids)) { + if (!Ember.isArray(idsOrReferencesOrOpaque)) { var adapter = this.adapterForType(type); - if (adapter && adapter.findHasMany) { adapter.findHasMany(this, record, relationship, ids); } - else if (ids !== undefined) { throw fmt("Adapter is either null or does not implement `findHasMany` method", this); } - return this.createManyArray(type, Ember.A()); + if (adapter && adapter.findHasMany) { + adapter.findHasMany(this, record, relationship, idsOrReferencesOrOpaque); + } else if (idsOrReferencesOrOpaque !== undefined) { + Ember.assert("You tried to load many records but you have no adapter (for " + type + ")", adapter); + Ember.assert("You tried to load many records but your adapter does not implement `findHasMany`", adapter.findHasMany); + } + + return this.recordArrayManager.createManyArray(type, Ember.A()); } // Coerce server IDs into Record Reference - var references = map(ids, function(reference) { + var references = map(idsOrReferencesOrOpaque, function(reference) { if (typeof reference !== 'object' && reference !== null) { return this.referenceForId(type, reference); } @@ -1561,32 +1774,26 @@ DS.Store = Ember.Object.extend(DS._Mappable, { return reference; }, this); - var neededReferences = this.neededReferences(type, references), - manyArray = this.createManyArray(type, Ember.A(references)), - loadingRecordArrays = this.loadingRecordArrays, - reference, clientId, i, l; + var unloadedReferences = this.unloadedReferences(references), + manyArray = this.recordArrayManager.createManyArray(type, Ember.A(references)), + reference, i, l; // Start the decrementing counter on the ManyArray at the number of // records we need to load from the adapter - manyArray.loadingRecordsCount(neededReferences.length); + manyArray.loadingRecordsCount(unloadedReferences.length); - if (neededReferences.length) { - for (i=0, l=neededReferences.length; i<l; i++) { - reference = neededReferences[i]; - clientId = reference.clientId; + if (unloadedReferences.length) { + for (i=0, l=unloadedReferences.length; i<l; i++) { + reference = unloadedReferences[i]; // keep track of the record arrays that a given loading record // is part of. This way, if the same record is in multiple // ManyArrays, all of their loading records counters will be // decremented when the adapter provides the data. - if (loadingRecordArrays[clientId]) { - loadingRecordArrays[clientId].push(manyArray); - } else { - this.loadingRecordArrays[clientId] = [ manyArray ]; - } + this.recordArrayManager.registerWaitingRecordArray(manyArray, reference); } - this.fetchMany(type, neededReferences, record); + this.fetchMany(unloadedReferences, record); } else { // all requested records are available manyArray.set('isLoaded', true); @@ -1600,8 +1807,6 @@ DS.Store = Ember.Object.extend(DS._Mappable, { }, /** - @private - This method delegates a query to the adapter. This is the one place where adapter-level semantics are exposed to the application. @@ -1609,6 +1814,8 @@ DS.Store = Ember.Object.extend(DS._Mappable, { language for all server-side queries, and then require all adapters to implement them. + @private + @method findQuery @param {Class} type @param {Object} query an opaque query to be used by the adapter @return {DS.AdapterPopulatedRecordArray} @@ -1616,8 +1823,12 @@ DS.Store = Ember.Object.extend(DS._Mappable, { findQuery: function(type, query) { var array = DS.AdapterPopulatedRecordArray.create({ type: type, query: query, content: Ember.A([]), store: this }); var adapter = this.adapterForType(type); - if (adapter && adapter.findQuery) { adapter.findQuery(this, type, query, array); } - else { throw "Adapter is either null or does not implement `findQuery` method"; } + + Ember.assert("You tried to load a query but you have no adapter (for " + type + ")", adapter); + Ember.assert("You tried to load a query but your adapter does not implement `findQuery`", adapter.findQuery); + + adapter.findQuery(this, type, query, array); + return array; }, @@ -1632,28 +1843,31 @@ DS.Store = Ember.Object.extend(DS._Mappable, { @return {DS.AdapterPopulatedRecordArray} */ findAll: function(type) { - var array = this.all(type); - this.fetchAll(type, array); - return array; + return this.fetchAll(type, this.all(type)); }, /** @private */ fetchAll: function(type, array) { - var sinceToken = this.typeMapFor(type).sinceToken, - adapter = this.adapterForType(type); + var adapter = this.adapterForType(type), + sinceToken = this.typeMapFor(type).metadata.since; set(array, 'isUpdating', true); - if (adapter && adapter.findAll) { adapter.findAll(this, type, sinceToken); } - else { throw "Adapter is either null or does not implement `findAll` method"; } + Ember.assert("You tried to load all records but you have no adapter (for " + type + ")", adapter); + Ember.assert("You tried to load all records but your adapter does not implement `findAll`", adapter.findAll); + + adapter.findAll(this, type, sinceToken); + + return array; }, /** */ - sinceForType: function(type, sinceToken) { - this.typeMapFor(type).sinceToken = sinceToken; + metaForType: function(type, property, data) { + var target = this.typeMapFor(type).metadata; + set(target, property, data); }, /** @@ -1673,6 +1887,7 @@ DS.Store = Ember.Object.extend(DS._Mappable, { Also note that multiple calls to `all` for a given type will always return the same RecordArray. + @method all @param {Class} type @return {DS.RecordArray} */ @@ -1682,8 +1897,14 @@ DS.Store = Ember.Object.extend(DS._Mappable, { if (findAllCache) { return findAllCache; } - var array = DS.RecordArray.create({ type: type, content: Ember.A([]), store: this, isLoaded: true }); - this.registerRecordArray(array, type); + var array = DS.RecordArray.create({ + type: type, + content: Ember.A([]), + store: this, + isLoaded: true + }); + + this.recordArrayManager.registerFilteredRecordArray(array, type); typeMap.findAllCache = array; return array; @@ -1713,9 +1934,9 @@ DS.Store = Ember.Object.extend(DS._Mappable, { In this scenario, you might want to consider filtering the raw data before loading it into the store. + @method filter @param {Class} type @param {Function} filter - @return {DS.FilteredRecordArray} */ filter: function(type, query, filter) { @@ -1726,9 +1947,15 @@ DS.Store = Ember.Object.extend(DS._Mappable, { filter = query; } - var array = DS.FilteredRecordArray.create({ type: type, content: Ember.A([]), store: this, filterFunction: filter }); + var array = DS.FilteredRecordArray.create({ + type: type, + content: Ember.A([]), + store: this, + manager: this.recordArrayManager, + filterFunction: filter + }); - this.registerRecordArray(array, type, filter); + this.recordArrayManager.registerFilteredRecordArray(array, type, filter); return array; }, @@ -1743,7 +1970,8 @@ DS.Store = Ember.Object.extend(DS._Mappable, { @return {boolean} */ recordIsLoaded: function(type, id) { - return !Ember.isNone(this.typeMapFor(type).idToCid[id]); + if (!this.hasReferenceForId(type, id)) { return false; } + return typeof this.referenceForId(type, id).data === 'object'; }, // ............ @@ -1776,64 +2004,42 @@ DS.Store = Ember.Object.extend(DS._Mappable, { if (get(record, 'isDeleted')) { return; } - var cidToData = this.clientIdToData, - clientId = reference.clientId, - data = cidToData[clientId]; - - if (typeof data === "object") { - this.updateRecordArrays(type, clientId); + if (typeof reference.data === "object") { + this.recordArrayManager.referenceDidChange(reference); } }, - // ................. - // . BASIC ADAPTER . - // ................. - - scheduleSave: function(record) { - this._recordsToSave.add(record); - Ember.run.once(this, 'flushSavedRecords'); - }, - - flushSavedRecords: function() { - var created = Ember.OrderedSet.create(); - var updated = Ember.OrderedSet.create(); - var deleted = Ember.OrderedSet.create(); - - this._recordsToSave.forEach(function(record) { - if (get(record, 'isNew')) { - created.add(record); - } else if (get(record, 'isDeleted')) { - deleted.add(record); - } else { - updated.add(record); - } - }); - - this._recordsToSave.clear(); - - get(this, '_adapter').commit(this, { - created: created, - updated: updated, - deleted: deleted - }); - }, - // .............. // . PERSISTING . // .............. /** - This method delegates committing to the store's implicit + This method delegates saving to the store's implicit transaction. Calling this method is essentially a request to persist any changes to records that were not explicitly added to a transaction. */ - commit: function() { + save: function() { + once(this, 'commitDefaultTransaction'); + }, + commit: Ember.aliasMethod('save'), + + commitDefaultTransaction: function() { get(this, 'defaultTransaction').commit(); }, + scheduleSave: function(record) { + get(this, 'currentTransaction').add(record); + once(this, 'flushSavedRecords'); + }, + + flushSavedRecords: function() { + get(this, 'currentTransaction').commit(); + set(this, 'currentTransaction', this.transaction()); + }, + /** Adapters should call this method if they would like to acknowledge that all changes related to a record (other than relationship @@ -1869,14 +2075,14 @@ DS.Store = Ember.Object.extend(DS._Mappable, { @param {Object} data optional data (see above) */ didSaveRecord: function(record, data) { - record.adapterDidCommit(); - if (data) { this.updateId(record, data); this.updateRecordData(record, data); } else { this.didUpdateAttributes(record); } + + record.adapterDidCommit(); }, /** @@ -2046,7 +2252,9 @@ DS.Store = Ember.Object.extend(DS._Mappable, { @param {DS.Model} relationshipName */ didUpdateRelationship: function(record, relationshipName) { - var relationship = this.relationshipChangeFor(get(record, 'clientId'), relationshipName); + var clientId = get(record, '_reference').clientId; + + var relationship = this.relationshipChangeFor(clientId, relationshipName); //TODO(Igor) if (relationship) { relationship.adapterDidUpdate(); } }, @@ -2111,11 +2319,7 @@ DS.Store = Ember.Object.extend(DS._Mappable, { @param {Object} data */ updateRecordData: function(record, data) { - var clientId = get(record, 'clientId'), - cidToData = this.clientIdToData; - - cidToData[clientId] = data; - + get(record, '_reference').data = data; record.didChangeData(); }, @@ -2130,17 +2334,16 @@ DS.Store = Ember.Object.extend(DS._Mappable, { @param {Object} data */ updateId: function(record, data) { - var typeMap = this.typeMapFor(record.constructor), - clientId = get(record, 'clientId'), + var type = record.constructor, + typeMap = this.typeMapFor(type), + reference = get(record, '_reference'), oldId = get(record, 'id'), - type = record.constructor, id = this.preprocessData(type, data); Ember.assert("An adapter cannot assign a new id to a record that already has an id. " + record + " had id: " + oldId + " and you tried to update it with " + id + ". This likely happened because your server returned data in response to a find or update that had a different id than the one you sent.", oldId === null || id === oldId); - typeMap.idToCid[id] = clientId; - this.clientIdToId[clientId] = id; - this.referenceForClientId(clientId).id = id; + typeMap.idToReference[id] = reference; + reference.id = id; }, /** @@ -2160,266 +2363,27 @@ DS.Store = Ember.Object.extend(DS._Mappable, { return this.adapterForType(type).extractId(type, data); }, - // ................. - // . RECORD ARRAYS . - // ................. - - /** - @private - - Register a RecordArray for a given type to be backed by - a filter function. This will cause the array to update - automatically when records of that type change attribute - values or states. - - @param {DS.RecordArray} array - @param {Class} type - @param {Function} filter - */ - registerRecordArray: function(array, type, filter) { - var recordArrays = this.typeMapFor(type).recordArrays; - - recordArrays.push(array); - - this.updateRecordArrayFilter(array, type, filter); - }, - - /** - @private - - Create a `DS.ManyArray` for a type and list of clientIds - and index the `ManyArray` under each clientId. This allows - us to efficiently remove records from `ManyArray`s when - they are deleted. - - @param {Class} type - @param {Array} clientIds - - @return {DS.ManyArray} - */ - createManyArray: function(type, clientIds) { - var array = DS.ManyArray.create({ type: type, content: clientIds, store: this }); - - clientIds.forEach(function(clientId) { - var recordArrays = this.recordArraysForClientId(clientId); - recordArrays.add(array); - }, this); - - return array; - }, - - /** - @private - - This method is invoked if the `filterFunction` property is - changed on a `DS.FilteredRecordArray`. - - It essentially re-runs the filter from scratch. This same - method is invoked when the filter is created in th first place. - */ - updateRecordArrayFilter: function(array, type, filter) { - var typeMap = this.typeMapFor(type), - cidToData = this.clientIdToData, - clientIds = typeMap.clientIds, - clientId, data, shouldFilter, record; - - for (var i=0, l=clientIds.length; i<l; i++) { - clientId = clientIds[i]; - shouldFilter = false; - - data = cidToData[clientId]; - - if (typeof data === 'object') { - if (record = this.recordCache[clientId]) { - if (!get(record, 'isDeleted')) { shouldFilter = true; } - } else { - shouldFilter = true; - } - - if (shouldFilter) { - this.updateRecordArray(array, filter, type, clientId); - } - } - } - }, - - updateRecordArraysLater: function(type, clientId) { - Ember.run.once(this, function() { - this.updateRecordArrays(type, clientId); - }); - }, - - /** - @private - - This method is invoked whenever data is loaded into the store - by the adapter or updated by the adapter, or when an attribute - changes on a record. - - It updates all filters that a record belongs to. - - To avoid thrashing, it only runs once per run loop per record. - - @param {Class} type - @param {Number|String} clientId - */ - updateRecordArrays: function(type, clientId) { - var recordArrays = this.typeMapFor(type).recordArrays, - filter; - - recordArrays.forEach(function(array) { - filter = get(array, 'filterFunction'); - this.updateRecordArray(array, filter, type, clientId); - }, this); - - // loop through all manyArrays containing an unloaded copy of this - // clientId and notify them that the record was loaded. - var manyArrays = this.loadingRecordArrays[clientId]; - - if (manyArrays) { - for (var i=0, l=manyArrays.length; i<l; i++) { - manyArrays[i].loadedRecord(); - } - - this.loadingRecordArrays[clientId] = null; - } - }, - - /** - @private - - Update an individual filter. - - @param {DS.FilteredRecordArray} array - @param {Function} filter - @param {Class} type - @param {Number|String} clientId - */ - updateRecordArray: function(array, filter, type, clientId) { - var shouldBeInArray, record; - - if (!filter) { - shouldBeInArray = true; - } else { - record = this.findByClientId(type, clientId); - shouldBeInArray = filter(record); - } - - var content = get(array, 'content'); - - var recordArrays = this.recordArraysForClientId(clientId); - var reference = this.referenceForClientId(clientId); - - if (shouldBeInArray) { - recordArrays.add(array); - array.addReference(reference); - } else if (!shouldBeInArray) { - recordArrays.remove(array); - array.removeReference(reference); - } - }, - - /** - @private - - When a record is deleted, it is removed from all its - record arrays. - - @param {DS.Model} record - */ - removeFromRecordArrays: function(record) { - var reference = get(record, '_reference'); - var recordArrays = this.recordArraysForClientId(reference.clientId); - - recordArrays.forEach(function(array) { - array.removeReference(reference); - }); - }, - - // ............ - // . INDEXING . - // ............ - - /** - @private - - Return a list of all `DS.RecordArray`s a clientId is - part of. - - @return {Object(clientId: Ember.OrderedSet)} - */ - recordArraysForClientId: function(clientId) { - var recordArrays = get(this, 'recordArraysByClientId'); - var ret = recordArrays[clientId]; - - if (!ret) { - ret = recordArrays[clientId] = Ember.OrderedSet.create(); - } - - return ret; - }, - - typeMapFor: function(type) { - var typeMaps = get(this, 'typeMaps'); - var guidForType = Ember.guidFor(type); - - var typeMap = typeMaps[guidForType]; - - if (typeMap) { - return typeMap; - } else { - return (typeMaps[guidForType] = - { - idToCid: {}, - clientIds: [], - recordArrays: [] - }); - } - }, - /** @private - - For a given type and id combination, returns the client id used by the store. - If no client id has been assigned yet, one will be created and returned. - - @param {DS.Model} type - @param {String|Number} id + Returns a map of IDs to client IDs for a given type. */ - clientIdForId: function(type, id) { - id = coerceId(id); - - var clientId = this.typeMapFor(type).idToCid[id]; - if (clientId !== undefined) { return clientId; } - - return this.pushData(UNLOADED, id, type); - }, - - /** - @private + typeMapFor: function(type) { + var typeMaps = get(this, 'typeMaps'), + guid = Ember.guidFor(type), + typeMap; - This method works exactly like `clientIdForId`, but does not - require looking up the `typeMap` for every `clientId` and - invoking a method per `clientId`. - */ - clientIdsForIds: function(type, ids) { - var typeMap = this.typeMapFor(type), - idToClientIdMap = typeMap.idToCid; + typeMap = typeMaps[guid]; - return map(ids, function(id) { - id = coerceId(id); + if (typeMap) { return typeMap; } - var clientId = idToClientIdMap[id]; - if (clientId) { return clientId; } - return this.pushData(UNLOADED, id, type); - }, this); - }, + typeMap = { + idToReference: {}, + references: [], + metadata: {} + }; - typeForClientId: function(clientId) { - return this.clientIdToType[clientId]; - }, + typeMaps[guid] = typeMap; - idForClientId: function(clientId) { - return this.clientIdToId[clientId]; + return typeMap; }, // ................ @@ -2455,31 +2419,18 @@ DS.Store = Ember.Object.extend(DS._Mappable, { id = coerceId(id); - var typeMap = this.typeMapFor(type), - cidToData = this.clientIdToData, - clientId = typeMap.idToCid[id], - cidToPrematerialized = this.clientIdToPrematerializedData; - - if (clientId !== undefined) { - cidToData[clientId] = data; - cidToPrematerialized[clientId] = prematerialized; + var reference = this.referenceForId(type, id); - var record = this.recordCache[clientId]; - if (record) { - once(record, 'loadedData'); - } - } else { - clientId = this.pushData(data, id, type); - cidToPrematerialized[clientId] = prematerialized; + if (reference.record) { + once(reference.record, 'loadedData'); } - this.updateRecordArraysLater(type, clientId); + reference.data = data; + reference.prematerialized = prematerialized; - return this.referenceForClientId(clientId); - }, + this.recordArrayManager.referenceDidChange(reference); - prematerialize: function(reference, prematerialized) { - this.clientIdToPrematerializedData[reference.clientId] = prematerialized; + return reference; }, loadMany: function(type, ids, dataList) { @@ -2496,7 +2447,12 @@ DS.Store = Ember.Object.extend(DS._Mappable, { }, loadHasMany: function(record, key, ids) { - record.materializeHasMany(key, ids); + //It looks sad to have to do the conversion in the store + var type = record.get(key + '.type'), + tuples = map(ids, function(id) { + return {id: id, type: type}; + }); + record.materializeHasMany(key, tuples); // Update any existing many arrays that use the previous IDs, // if necessary. @@ -2514,77 +2470,73 @@ DS.Store = Ember.Object.extend(DS._Mappable, { /** @private - Stores data for the specified type and id combination and returns - the client id. + Creates a new reference for a given type & ID pair. Metadata about the + record can be stored in the reference without having to create a full-blown + DS.Model instance. - @param {Object} data - @param {String|Number} id @param {DS.Model} type - @returns {Number} + @param {String|Number} id + @returns {Reference} */ - pushData: function(data, id, type) { - var typeMap = this.typeMapFor(type); - - var idToClientIdMap = typeMap.idToCid, - clientIdToIdMap = this.clientIdToId, - clientIdToTypeMap = this.clientIdToType, - clientIds = typeMap.clientIds, - cidToData = this.clientIdToData; - - Ember.assert('The id ' + id + ' has already been used with another record of type ' + type.toString() + '.', !id || !idToClientIdMap[id]); + createReference: function(type, id) { + var typeMap = this.typeMapFor(type), + idToReference = typeMap.idToReference; - var clientId = ++this.clientIdCounter; + Ember.assert('The id ' + id + ' has already been used with another record of type ' + type.toString() + '.', !id || !idToReference[id]); - cidToData[clientId] = data; - clientIdToTypeMap[clientId] = type; + var reference = { + id: id, + clientId: this.clientIdCounter++, + type: type + }; // if we're creating an item, this process will be done // later, once the object has been persisted. if (id) { - idToClientIdMap[id] = clientId; - clientIdToIdMap[clientId] = id; + idToReference[id] = reference; } - clientIds.push(clientId); + typeMap.references.push(reference); - return clientId; + return reference; }, // .......................... // . RECORD MATERIALIZATION . // .......................... - materializeRecord: function(type, clientId, id) { - var record; - - this.recordCache[clientId] = record = type._create({ + materializeRecord: function(reference) { + var record = reference.type._create({ + id: reference.id, store: this, - clientId: clientId + _reference: reference }); - set(record, 'id', id); + reference.record = record; get(this, 'defaultTransaction').adoptRecord(record); record.loadingData(); + + if (typeof reference.data === 'object') { + record.loadedData(); + } + return record; }, dematerializeRecord: function(record) { - var id = get(record, 'id'), - clientId = get(record, 'clientId'), - type = this.typeForClientId(clientId), + var reference = get(record, '_reference'), + type = reference.type, + id = reference.id, typeMap = this.typeMapFor(type); record.updateRecordArrays(); - delete this.recordCache[clientId]; - delete this.clientIdToId[clientId]; - delete this.clientIdToType[clientId]; - delete this.clientIdToData[clientId]; - delete this.recordArraysByClientId[clientId]; + if (id) { delete typeMap.idToReference[id]; } - if (id) { delete typeMap.idToCid[id]; } + var loc = typeMap.references.indexOf(reference); + typeMap.references.splice(loc, 1); }, willDestroy: function() { @@ -2625,7 +2577,9 @@ DS.Store = Ember.Object.extend(DS._Mappable, { delete changes[clientId][parentClientId][key][type]; }, - relationshipChangeFor: function(clientId, childKey, parentClientId, parentKey, type) { + relationshipChangeFor: function(clientReference, childKey, parentReference, parentKey, type) { + var clientId = clientReference.clientId, + parentClientId = parentReference ? parentReference.clientId : parentReference; var changes = this.relationshipChanges; var key = childKey + parentKey; if (!(clientId in changes) || !(parentClientId in changes[clientId])){ @@ -2695,7 +2649,7 @@ DS.Store = Ember.Object.extend(DS._Mappable, { // .............................. recordAttributeDidChange: function(reference, attributeName, newValue, oldValue) { - var record = this.recordForReference(reference), + var record = reference.record, dirtySet = new Ember.OrderedSet(), adapter = this.adapterForType(record.constructor); @@ -2761,6 +2715,11 @@ DS.Store.reopenClass({ (function() { +/** + @module data + @submodule data-model +*/ + var get = Ember.get, set = Ember.set, once = Ember.run.once, arrayMap = Ember.ArrayPolyfills.map; @@ -2910,9 +2869,13 @@ var get = Ember.get, set = Ember.set, } }) - 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. + 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. + + @class States + @namespace DS + @extends Ember.State */ var stateProperty = Ember.computed(function(key) { @@ -2939,16 +2902,17 @@ 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; + get(manager, 'record')._changesToSync[context.name] = change; }; var didSetProperty = function(manager, context) { - var change = get(manager, 'record')._changesToSync[context.attributeName]; + var change = get(manager, 'record')._changesToSync[context.name]; change.value = get(get(manager, 'record'), context.name); change.sync(); }; DS.State = Ember.State.extend({ + isLoading: stateProperty, isLoaded: stateProperty, isReloading: stateProperty, isDirty: stateProperty, @@ -3020,15 +2984,6 @@ var DirtyState = DS.State.extend({ // 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, @@ -3041,24 +2996,15 @@ var DirtyState = DS.State.extend({ }, becameClean: function(manager) { - var record = get(manager, 'record'), - dirtyType = get(this, 'dirtyType'); + var record = get(manager, 'record'); record.withTransaction(function(t) { - t.recordBecameClean(dirtyType, record); + t.remove(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'); }, @@ -3076,29 +3022,32 @@ var DirtyState = DS.State.extend({ // TRANSITIONS enter: function(manager) { - var dirtyType = get(this, 'dirtyType'), - record = get(manager, 'record'); + var record = get(manager, 'record'); record.becameInFlight(); - - record.withTransaction(function (t) { - t.recordBecameInFlight(dirtyType, record); - }); }, // EVENTS + + materializingData: function(manager) { + set(manager, 'lastDirtyType', get(this, 'dirtyType')); + manager.transitionTo('materializing'); + }, + didCommit: function(manager) { var dirtyType = get(this, 'dirtyType'), record = get(manager, 'record'); record.withTransaction(function(t) { - t.recordBecameClean('inflight', record); + t.remove(record); }); manager.transitionTo('saved'); manager.send('invokeLifecycleCallbacks', dirtyType); }, + didChangeData: didChangeData, + becameInvalid: function(manager, errors) { var record = get(manager, 'record'); @@ -3122,12 +3071,12 @@ var DirtyState = DS.State.extend({ isValid: false, exit: function(manager) { - var record = get(manager, 'record'); + var record = get(manager, 'record'); - record.withTransaction(function (t) { - t.recordBecameClean('inflight', record); - }); - }, + record.withTransaction(function (t) { + t.remove(record); + }); + }, // EVENTS deleteRecord: function(manager) { @@ -3188,10 +3137,6 @@ 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'); } @@ -3208,18 +3153,15 @@ 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(); + record.clearRelationships(); } }); var states = { rootState: Ember.State.create({ // FLAGS + isLoading: false, isLoaded: false, isReloading: false, isDirty: false, @@ -3254,11 +3196,19 @@ var states = { // Usually, this process is asynchronous, using an // XHR to retrieve the data. loading: DS.State.create({ + // FLAGS + isLoading: true, + // EVENTS loadedData: didChangeData, materializingData: function(manager) { manager.transitionTo('loaded.materializing.firstTime'); + }, + + becameError: function(manager) { + manager.transitionTo('error'); + manager.send('invokeLifecycleCallbacks'); } }), @@ -3274,9 +3224,6 @@ var states = { // SUBSTATES materializing: DS.State.create({ - // FLAGS - isLoaded: false, - // EVENTS willSetProperty: Ember.K, didSetProperty: Ember.K, @@ -3289,6 +3236,9 @@ var states = { // SUBSTATES firstTime: DS.State.create({ + // FLAGS + isLoaded: false, + exit: function(manager) { var record = get(manager, 'record'); @@ -3353,8 +3303,22 @@ var states = { }, unloadRecord: function(manager) { + var record = get(manager, 'record'); + + // clear relationships before moving to deleted state + // otherwise it fails + record.clearRelationships(); manager.transitionTo('deleted.saved'); - get(manager, 'record').clearRelationships(); + }, + + didCommit: function(manager) { + var record = get(manager, 'record'); + + record.withTransaction(function(t) { + t.remove(record); + }); + + manager.send('invokeLifecycleCallbacks', get(manager, 'lastDirtyType')); }, invokeLifecycleCallbacks: function(manager, dirtyType) { @@ -3364,6 +3328,8 @@ var states = { } else { record.trigger('didUpdate', record); } + + record.trigger('didCommit', record); } }), @@ -3393,7 +3359,7 @@ var states = { var record = get(manager, 'record'), store = get(record, 'store'); - store.removeFromRecordArrays(record); + store.recordArrayManager.remove(record); }, // SUBSTATES @@ -3402,14 +3368,6 @@ var states = { // 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) { @@ -3426,9 +3384,8 @@ var states = { var record = get(manager, 'record'); record.withTransaction(function(t) { - t.recordBecameClean('deleted', record); + t.remove(record); }); - manager.transitionTo('loaded.materializing'); } }), @@ -3446,10 +3403,6 @@ var states = { var record = get(manager, 'record'); record.becameInFlight(); - - record.withTransaction(function (t) { - t.recordBecameInFlight('deleted', record); - }); }, // EVENTS @@ -3457,7 +3410,7 @@ var states = { var record = get(manager, 'record'); record.withTransaction(function(t) { - t.recordBecameClean('inflight', record); + t.remove(record); }); manager.transitionTo('saved'); @@ -3483,6 +3436,7 @@ var states = { invokeLifecycleCallbacks: function(manager) { var record = get(manager, 'record'); record.trigger('didDelete', record); + record.trigger('didCommit', record); } }) }), @@ -3534,7 +3488,22 @@ var retrieveFromCurrentState = Ember.computed(function(key, value) { return get(get(this, 'stateManager.currentState'), key); }).property('stateManager.currentState').readOnly(); +/** + + The model class that all Ember Data records descend from. + + @module data + @submodule data-model + @main data-model + + @class Model + @namespace DS + @extends Ember.Object + @constructor +*/ + DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { + isLoading: retrieveFromCurrentState, isLoaded: retrieveFromCurrentState, isReloading: retrieveFromCurrentState, isDirty: retrieveFromCurrentState, @@ -3543,6 +3512,7 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { isError: retrieveFromCurrentState, isNew: retrieveFromCurrentState, isValid: retrieveFromCurrentState, + dirtyType: retrieveFromCurrentState, clientId: null, id: null, @@ -3554,12 +3524,12 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { Create a JSON representation of the record, using the serialization strategy of the store's adapter. - Available options: + @method serialize + @param {Object} options 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) { @@ -3567,22 +3537,75 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { return store.serialize(this, options); }, + /** + Use {{#crossLink "DS.JSONSerializer"}}DS.JSONSerializer{{/crossLink}} to + get the JSON representation of a record. + + @method toJSON + @param {Object} options Available options: + + * `includeId`: `true` if the record's ID should be included in the + JSON representation. + + @returns {Object} A JSON representation of the object. + */ toJSON: function(options) { var serializer = DS.JSONSerializer.create(); return serializer.serialize(this, options); }, + /** + Fired when the record is loaded from the server. + + @event didLoad + */ didLoad: Ember.K, + + /** + Fired when the record is reloaded from the server. + + @event didReload + */ didReload: Ember.K, + + /** + Fired when the record is updated. + + @event didUpdate + */ didUpdate: Ember.K, + + /** + Fired when the record is created. + + @event didCreate + */ didCreate: Ember.K, + + /** + Fired when the record is deleted. + + @event didDelete + */ didDelete: Ember.K, + + /** + Fired when the record becomes invalid. + + @event becameInvalid + */ becameInvalid: Ember.K, + + /** + Fired when the record enters the error state. + + @event becameError + */ becameError: Ember.K, data: Ember.computed(function() { if (!this._data) { - this.materializeData(); + this.setupData(); } return this._data; @@ -3612,7 +3635,6 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { }, _setup: function() { - this._relationshipChanges = {}; this._changesToSync = {}; }, @@ -3637,21 +3659,6 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { 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'); }, @@ -3664,13 +3671,10 @@ 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') { - get(this, name).clear(); + this.clearHasMany(relationship); } }, this); }, @@ -3704,18 +3708,19 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { }, dataDidChange: Ember.observer(function() { - var relationships = get(this.constructor, 'relationshipsByName'); + this.reloadHasManys(); + this.send('finishedMaterializing'); + }, 'data'), + reloadHasManys: 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); @@ -3726,8 +3731,15 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { 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; } + if (typeof id === 'object') { + if( id.clientId ) { + // if it was already a reference, return the reference + return id; + } else { + // <id, type> tuple for a polymorphic association. + return store.referenceForId(id.type, id.id); + } + } return store.referenceForId(type, id); }); @@ -3739,7 +3751,7 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { Ember.run.once(this, this.updateRecordArrays); }, - setupData: function(prematerialized) { + setupData: function() { this._data = { attributes: {}, belongsTo: {}, @@ -3761,12 +3773,45 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { this._data.attributes[name] = value; }, - materializeHasMany: function(name, ids) { - this._data.hasMany[name] = ids; + materializeHasMany: function(name, tuplesOrReferencesOrOpaque) { + var tuplesOrReferencesOrOpaqueType = typeof tuplesOrReferencesOrOpaque; + + if (tuplesOrReferencesOrOpaque && tuplesOrReferencesOrOpaqueType !== 'string' && tuplesOrReferencesOrOpaque.length > 1) { + Ember.assert('materializeHasMany expects tuples, references or opaque token, not ' + tuplesOrReferencesOrOpaque[0], tuplesOrReferencesOrOpaque[0].hasOwnProperty('id') && tuplesOrReferencesOrOpaque[0].type); + } + + if( tuplesOrReferencesOrOpaqueType === "string" ) { + this._data.hasMany[name] = tuplesOrReferencesOrOpaque; + } else { + var references = tuplesOrReferencesOrOpaque; + + if (tuplesOrReferencesOrOpaque && Ember.isArray(tuplesOrReferencesOrOpaque)) { + references = this._convertTuplesToReferences(tuplesOrReferencesOrOpaque); + } + + this._data.hasMany[name] = references; + } + }, + + materializeBelongsTo: function(name, tupleOrReference) { + if (tupleOrReference) { Ember.assert('materializeBelongsTo expects a tuple or a reference, not a ' + tupleOrReference, !tupleOrReference || (tupleOrReference.hasOwnProperty('id') && tupleOrReference.hasOwnProperty('type'))); } + + this._data.belongsTo[name] = tupleOrReference; + }, + + _convertTuplesToReferences: function(tuplesOrReferences) { + return map(tuplesOrReferences, function(tupleOrReference) { + return this._convertTupleToReference(tupleOrReference); + }, this); }, - materializeBelongsTo: function(name, id) { - this._data.belongsTo[name] = id; + _convertTupleToReference: function(tupleOrReference) { + var store = get(this, 'store'); + if(tupleOrReference.clientId) { + return tupleOrReference; + } else { + return store.referenceForId(tupleOrReference.type, tupleOrReference.id); + } }, rollback: function() { @@ -3815,10 +3860,54 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { becameInFlight: function() { }, - // FOR USE BY THE BASIC ADAPTER + /** + @private + + */ + resolveOn: function(successEvent) { + var model = this; + + return new Ember.RSVP.Promise(function(resolve, reject) { + function success() { + this.off('becameError', error); + this.off('becameInvalid', error); + resolve(this); + } + function error() { + this.off(successEvent, success); + reject(this); + } + + model.one(successEvent, success); + model.one('becameError', error); + model.one('becameInvalid', error); + }); + }, + + /** + Save the record. + @method save + */ save: function() { this.get('store').scheduleSave(this); + + return this.resolveOn('didCommit'); + }, + + /** + Reload the record from the adapter. + + This will only work if the record has already finished loading + and has not yet been modified (`isLoaded` but not `isDirty`, + or `isSaving`). + + @method reload + */ + reload: function() { + this.send('reloadRecord'); + + return this.resolveOn('didReload'); }, // FOR USE DURING COMMIT PROCESS @@ -3840,10 +3929,6 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { this.updateRecordArraysLater(); }, - _reference: Ember.computed(function() { - return get(this, 'store').referenceForClientId(get(this, 'clientId')); - }), - adapterDidInvalidate: function(errors) { this.send('becameInvalid', errors); }, @@ -3880,18 +3965,67 @@ var storeAlias = function(methodName) { }; DS.Model.reopenClass({ - isLoaded: storeAlias('recordIsLoaded'), - find: storeAlias('find'), - all: storeAlias('all'), - query: storeAlias('findQuery'), - filter: storeAlias('filter'), + /** @private + Alias DS.Model's `create` method to `_create`. This allows us to create DS.Model + instances from within the store, but if end users accidentally call `create()` + (instead of `createRecord()`), we can raise an error. + */ _create: DS.Model.create, + /** @private + + Override the class' `create()` method to raise an error. This prevents end users + from inadvertently calling `create()` instead of `createRecord()`. The store is + still able to create instances by calling the `_create()` method. + */ create: function() { throw new Ember.Error("You should not call `create` on a model. Instead, call `createRecord` with the attributes you would like to set."); }, + /** + See {{#crossLink "DS.Store/find:method"}}`DS.Store.find()`{{/crossLink}}. + + @method find + @param {Object|String|Array|null} query A query to find records by. + + */ + find: storeAlias('find'), + + /** + See {{#crossLink "DS.Store/all:method"}}`DS.Store.all()`{{/crossLink}}. + + @method all + @return {DS.RecordArray} + */ + all: storeAlias('all'), + + /** + See {{#crossLink "DS.Store/findQuery:method"}}`DS.Store.findQuery()`{{/crossLink}}. + + @method query + @param {Object} query an opaque query to be used by the adapter + @return {DS.AdapterPopulatedRecordArray} + */ + query: storeAlias('findQuery'), + + /** + See {{#crossLink "DS.Store/filter:method"}}`DS.Store.filter()`{{/crossLink}}. + + @method filter + @param {Function} filter + @return {DS.FilteredRecordArray} + */ + filter: storeAlias('filter'), + + /** + See {{#crossLink "DS.Store/createRecord:method"}}`DS.Store.createRecord()`{{/crossLink}}. + + @method createRecord + @param {Object} properties a hash of properties to set on the + newly created record. + @returns DS.Model + */ createRecord: storeAlias('createRecord') }); @@ -3900,7 +4034,13 @@ DS.Model.reopenClass({ (function() { +/** + @module data + @submodule data-model +*/ + var get = Ember.get; + DS.Model.reopenClass({ attributes: Ember.computed(function() { var map = Ember.Map.create(); @@ -3918,29 +4058,6 @@ DS.Model.reopenClass({ }) }); -var AttributeChange = DS.AttributeChange = function(options) { - this.reference = options.reference; - this.store = options.store; - this.name = options.name; - this.oldValue = options.oldValue; -}; - -AttributeChange.createChange = function(options) { - return new AttributeChange(options); -}; - -AttributeChange.prototype = { - sync: function() { - this.store.recordAttributeDidChange(this.reference, this.name, this.value, this.oldValue); - - // TODO: Use this object in the commit process - this.destroy(); - }, - - destroy: function() { - delete this.store.recordForReference(this.reference)._changesToSync[this.name]; - } -}; DS.Model.reopen({ eachAttribute: function(callback, binding) { @@ -3966,7 +4083,11 @@ function getAttr(record, options, key) { var value = attributes[key]; if (value === undefined) { - value = options.defaultValue; + if (typeof options.defaultValue === "function") { + value = options.defaultValue(); + } else { + value = options.defaultValue; + } } return value; @@ -4001,6 +4122,551 @@ DS.attr = function(type, options) { (function() { +/** + @module data + @submodule data-model +*/ + +})(); + + + +(function() { +/** + An AttributeChange object is created whenever a record's + attribute changes value. It is used to track changes to a + record between transaction commits. +*/ + +var 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(); + }, + + /** + If the AttributeChange is destroyed (either by being rolled back + or being committed), remove it from the list of pending changes + on the record. + */ + destroy: function() { + var record = this.reference.record; + + delete record._changesToSync[this.name]; + } +}; + +})(); + + + +(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.changeType = options.changeType; + this.store = options.store; + + this.committed = {}; +}; + +DS.RelationshipChangeAdd = function(options){ + DS.RelationshipChange.call(this, options); +}; + +DS.RelationshipChangeRemove = function(options){ + DS.RelationshipChange.call(this, options); +}; + +/** @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, otherKind; + var knownKind = knownSide.kind; + + var inverse = recordType.inverseFor(knownKey); + + if (inverse){ + key = inverse.name; + otherKind = inverse.kind; + } + + if (!inverse){ + return knownKind === "belongsTo" ? "oneToNone" : "manyToNone"; + } + else{ + if(otherKind === "belongsTo"){ + return knownKind === "belongsTo" ? "oneToOne" : "manyToOne"; + } + else{ + return knownKind === "belongsTo" ? "oneToMany" : "manyToMany"; + } + } + +}; + +DS.RelationshipChange.createChange = function(firstRecordReference, secondRecordReference, store, options){ + // Get the type of the child based on the child's client ID + var firstRecordType = firstRecordReference.type, changeType; + changeType = DS.RelationshipChange.determineRelationshipType(firstRecordType, options); + if (changeType === "oneToMany"){ + return DS.OneToManyChange.createChange(firstRecordReference, secondRecordReference, store, options); + } + else if (changeType === "manyToOne"){ + return DS.OneToManyChange.createChange(secondRecordReference, firstRecordReference, store, options); + } + else if (changeType === "oneToNone"){ + return DS.OneToNoneChange.createChange(firstRecordReference, secondRecordReference, store, options); + } + else if (changeType === "manyToNone"){ + return DS.ManyToNoneChange.createChange(firstRecordReference, secondRecordReference, store, options); + } + else if (changeType === "oneToOne"){ + return DS.OneToOneChange.createChange(firstRecordReference, secondRecordReference, store, options); + } + else if (changeType === "manyToMany"){ + return DS.ManyToManyChange.createChange(firstRecordReference, secondRecordReference, store, options); + } +}; + +/** @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) { + // If the name of the belongsTo side of the relationship is specified, + // use that + // If the type of the parent is specified, look it up on the child's type + // definition. + var key = options.key; + + var change = DS.RelationshipChange._createChange({ + parentReference: parentReference, + childReference: childReference, + firstRecordReference: childReference, + secondRecordReference: parentReference, + firstRecordKind: "hasMany", + secondRecordKind: "hasMany", + store: store, + changeType: options.changeType, + firstRecordName: key + }); + + store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + + + return change; +}; + +/** @private */ +DS.OneToOneChange.createChange = function(childReference, parentReference, store, options) { + var key; + + // If the name of the belongsTo side of the relationship is specified, + // use that + // If the type of the parent is specified, look it up on the child's type + // definition. + if (options.parentType) { + key = options.parentType.inverseFor(options.key).name; + } else if (options.key) { + key = options.key; + } else { + Ember.assert("You must pass either a parentType or belongsToName option to OneToManyChange.forChildAndParent", false); + } + + var change = DS.RelationshipChange._createChange({ + 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) { + var key; + + // If the name of the belongsTo side of the relationship is specified, + // use that + // If the type of the parent is specified, look it up on the child's type + // definition. + if (options.parentType) { + key = options.parentType.inverseFor(options.key).name; + DS.OneToManyChange.maintainInvariant( options, store, 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, change.getSecondRecordName(), change); + + + return change; +}; + + +DS.OneToManyChange.maintainInvariant = function(options, store, childReference, key){ + var child = childReference.record; + + if (options.changeType === "add" && child) { + var oldParent = get(child, key); + if (oldParent){ + var correspondingChange = DS.OneToManyChange.createChange(childReference, oldParent.get('_reference'), store, { + parentType: options.parentType, + hasManyName: options.hasManyName, + changeType: "remove", + key: options.key + }); + store.addRelationshipChangeFor(childReference, key, options.parentReference, correspondingChange.getSecondRecordName(), correspondingChange); + correspondingChange.sync(); + } + } +}; + +DS.OneToManyChange.ensureSameTransaction = function(changes){ + var records = Ember.A(); + forEach(changes, function(change){ + records.addObject(change.getSecondRecord()); + records.addObject(change.getFirstRecord()); + }); + + return DS.Transaction.ensureSameTransaction(records); +}; + +DS.RelationshipChange.prototype = { + + getSecondRecordName: function() { + var name = this.secondRecordName, parent; + + if (!name) { + parent = this.secondRecordReference; + if (!parent) { return; } + + var childType = this.firstRecordReference.type; + var inverse = childType.inverseFor(this.firstRecordName); + this.secondRecordName = inverse.name; + } + + return this.secondRecordName; + }, + + /** + Get the name of the relationship on the belongsTo side. + + @return {String} + */ + getFirstRecordName: function() { + var name = this.firstRecordName; + return name; + }, + + /** @private */ + destroy: function() { + var childReference = this.childReference, + belongsToName = this.getFirstRecordName(), + hasManyName = this.getSecondRecordName(), + store = this.store; + + store.removeRelationshipChangeFor(childReference, belongsToName, this.parentReference, hasManyName, this.changeType); + }, + + /** @private */ + getByReference: function(reference) { + // return null or undefined if the original reference was null or undefined + if (!reference) { return reference; } + + if (reference.record) { + return reference.record; + } + }, + + getSecondRecord: function(){ + return this.getByReference(this.secondRecordReference); + }, + + /** @private */ + getFirstRecord: function() { + return this.getByReference(this.firstRecordReference); + }, + + /** + @private + + Make sure that all three parts of the relationship change are part of + the same transaction. If any of the three records is clean and in the + default transaction, and the rest are in a different transaction, move + them all into that transaction. + */ + ensureSameTransaction: function() { + var child = this.getFirstRecord(), + parentRecord = this.getSecondRecord(); + + var transaction = DS.Transaction.ensureSameTransaction([child, parentRecord]); + + this.transaction = transaction; + return transaction; + }, + + callChangeEvents: function(){ + var 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); + + this.ensureSameTransaction(); + + 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.firstRecordKind === "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); + + this.ensureSameTransaction(firstRecord, secondRecord, secondRecordName, firstRecordName); + + 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.firstRecordKind === "hasMany"){ + firstRecord.suspendRelationshipObservers(function(){ + get(firstRecord, firstRecordName).removeObject(secondRecord); + }); + } + } + + this.coalesce(); +}; + +})(); + + + +(function() { +/** + @module data + @submodule data-changes +*/ })(); @@ -4008,7 +4674,7 @@ DS.attr = function(type, options) { (function() { var get = Ember.get, set = Ember.set, - none = Ember.isNone; + isNone = 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))); @@ -4018,26 +4684,43 @@ DS.belongsTo = function(type, options) { var meta = { type: type, isRelationship: true, options: options, kind: 'belongsTo' }; return Ember.computed(function(key, value) { + if (typeof type === 'string') { + type = get(this, type, false) || get(Ember.lookup, type); + } + if (arguments.length === 2) { + Ember.assert("You can only add a record of " + type.toString() + " to this relationship", !value || type.detectInstance(value)); return value === undefined ? null : value; } var data = get(this, 'data').belongsTo, - store = get(this, 'store'), id; + store = get(this, 'store'), belongsTo; - if (typeof type === 'string') { - type = get(this, type, false) || get(Ember.lookup, type); - } + belongsTo = data[key]; - id = data[key]; + // TODO (tomdale) The value of the belongsTo in the data hash can be + // one of: + // 1. null/undefined + // 2. a record reference + // 3. a tuple returned by the serializer's polymorphism code + // + // We should really normalize #3 to be the same as #2 to reduce the + // complexity here. - if(!id) { + if (isNone(belongsTo)) { return null; - } else if (typeof id === 'object') { - return store.recordForReference(id); - } else { - return store.find(type, id); } + + // The data has been normalized to a record reference, so + // just ask the store for the record for that reference, + // materializing it if necessary. + if (belongsTo.clientId) { + return store.recordForReference(belongsTo); + } + + // The data has been normalized into a type/id pair by the + // serializer's polymorphism code. + return store.findById(belongsTo.type, belongsTo.id); }).property('data').meta(meta); }; @@ -4086,7 +4769,7 @@ DS.Model.reopen({ (function() { -var get = Ember.get, set = Ember.set; +var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; var hasRelationship = function(type, options) { options = options || {}; @@ -4101,10 +4784,14 @@ var hasRelationship = function(type, options) { type = get(this, type, false) || get(Ember.lookup, type); } + //ids can be references or opaque token + //(e.g. `{url: '/relationship'}`) that will be passed to the adapter ids = data[key]; + relationship = store.findMany(type, ids, this, meta); set(relationship, 'owner', this); set(relationship, 'name', key); + set(relationship, 'isPolymorphic', options.polymorphic); return relationship; }).property().meta(meta); @@ -4115,6 +4802,40 @@ DS.hasMany = function(type, options) { return hasRelationship(type, options); }; +function clearUnmaterializedHasMany(record, relationship) { + var data = get(record, 'data').hasMany; + + var references = data[relationship.key]; + + if (!references) { return; } + + var inverse = record.constructor.inverseFor(relationship.key); + + if (inverse) { + forEach(references, function(reference) { + var childRecord; + + if (childRecord = reference.record) { + record.suspendRelationshipObservers(function() { + set(childRecord, inverse.name, null); + }); + } + }); + } +} + +DS.Model.reopen({ + clearHasMany: function(relationship) { + var hasMany = this.cacheFor(relationship.name); + + if (hasMany) { + hasMany.clear(); + } else { + clearUnmaterializedHasMany(this, relationship); + } + } +}); + })(); @@ -4208,6 +4929,53 @@ DS.Model.reopenClass({ return relationship && relationship.type; }, + inverseFor: function(name) { + var inverseType = this.typeForRelationship(name); + + if (!inverseType) { return null; } + + var options = this.metaForProperty(name).options; + var inverseName, inverseKind; + + if (options.inverse) { + inverseName = options.inverse; + inverseKind = Ember.get(inverseType, 'relationshipsByName').get(inverseName).kind; + } else { + var possibleRelationships = findPossibleInverses(this, inverseType); + + if (possibleRelationships.length === 0) { return null; } + + Ember.assert("You defined the '" + name + "' relationship on " + this + ", but multiple possible inverse relationships of type " + this + " were found on " + inverseType + ".", possibleRelationships.length === 1); + + inverseName = possibleRelationships[0].name; + inverseKind = possibleRelationships[0].kind; + } + + function findPossibleInverses(type, inverseType, possibleRelationships) { + possibleRelationships = possibleRelationships || []; + + var relationshipMap = get(inverseType, 'relationships'); + if (!relationshipMap) { return; } + + var relationships = relationshipMap.get(type); + if (relationships) { + possibleRelationships.push.apply(possibleRelationships, relationshipMap.get(type)); + } + + if (type.superclass) { + findPossibleInverses(type.superclass, inverseType, possibleRelationships); + } + + return possibleRelationships; + } + + return { + type: inverseType, + name: inverseName, + kind: inverseKind + }; + }, + /** The model's relationships as a map, keyed on the type of the relationship. The value of each entry is an array containing a descriptor @@ -4226,7 +4994,7 @@ DS.Model.reopenClass({ relationships, like this: var relationships = Ember.get(App.Blog, 'relationships'); - associatons.get(App.User); + relationships.get(App.User); //=> [ { name: 'users', kind: 'hasMany' }, // { name: 'owner', kind: 'belongsTo' } ] relationships.get(App.Post); @@ -4330,6 +5098,8 @@ DS.Model.reopenClass({ type = get(this, type, false) || get(Ember.lookup, type); } + Ember.assert("You specified a hasMany (" + meta.type + ") on " + meta.parentType + " but " + meta.type + " was not found.", type); + if (!types.contains(type)) { Ember.assert("Trying to sideload " + name + " on " + this.toString() + " but the type doesn't exist.", !!type); types.push(type); @@ -4416,7 +5186,7 @@ DS.Model.reopenClass({ @readOnly */ fields: Ember.computed(function() { - var map = Ember.Map.create(), type; + var map = Ember.Map.create(); this.eachComputedProperty(function(name, meta) { if (meta.isRelationship) { @@ -4473,69 +5243,15 @@ DS.Model.reopen({ } }); -/** - @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]; -}; +(function() { /** - @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} + @module data + @submodule data-relationships */ -DS._inverseTypeFor = function(modelType, relationshipName) { - var relationships = get(modelType, 'relationshipsByName'), - relationship = relationships.get(relationshipName); - - if (relationship) { return relationship.type; } -}; })(); @@ -4543,642 +5259,206 @@ DS._inverseTypeFor = function(modelType, relationshipName) { (function() { var get = Ember.get, set = Ember.set; +var once = Ember.run.once; 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); - } +DS.RecordArrayManager = Ember.Object.extend({ + init: function() { + this.filteredRecordArrays = Ember.MapWithDefault.create({ + defaultValue: function() { return []; } + }); - var change = DS.RelationshipChange._createChange({ - parentReference: parentReference, - childReference: childReference, - firstRecordReference: childReference, - secondRecordReference: parentReference, - firstRecordKind: "belongsTo", - secondRecordKind: "hasMany", - store: store, - changeType: options.changeType, - firstRecordName: key - }); + this.changedReferences = []; + }, - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); + referenceDidChange: function(reference) { + this.changedReferences.push(reference); + once(this, this.updateRecordArrays); + }, + recordArraysForReference: function(reference) { + reference.recordArrays = reference.recordArrays || Ember.OrderedSet.create(); + return reference.recordArrays; + }, - return change; -}; + /** + @private + This method is invoked whenever data is loaded into the store + by the adapter or updated by the adapter, or when an attribute + changes on a record. -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(); - } - } -}; + It updates all filters that a record belongs to. -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; - }); -}; + To avoid thrashing, it only runs once per run loop per record. -DS.RelationshipChange.prototype = { + @param {Class} type + @param {Number|String} clientId + */ + updateRecordArrays: function() { + forEach(this.changedReferences, function(reference) { + var type = reference.type, + recordArrays = this.filteredRecordArrays.get(type), + filter; + + forEach(recordArrays, function(array) { + filter = get(array, 'filterFunction'); + this.updateRecordArray(array, filter, type, reference); + }, this); - getSecondRecordName: function() { - var name = this.secondRecordName, store = this.store, parent; + // loop through all manyArrays containing an unloaded copy of this + // clientId and notify them that the record was loaded. + var manyArrays = reference.loadingRecordArrays; - if (!name) { - parent = this.secondRecordReference; - if (!parent) { return; } + if (manyArrays) { + for (var i=0, l=manyArrays.length; i<l; i++) { + manyArrays[i].loadedRecord(); + } - var childType = this.firstRecordReference.type; - var inverseType = DS._inverseTypeFor(childType, this.firstRecordName); - name = inverseHasManyName(inverseType, childType, this.firstRecordName); - this.secondRecordName = name; - } + reference.loadingRecordArrays = []; + } + }, this); - return name; + this.changedReferences = []; }, /** - 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 - /** @private */ - destroy: function() { - var childReference = this.childReference, - belongsToName = this.getFirstRecordName(), - hasManyName = this.getSecondRecordName(), - store = this.store, - child, oldParent, newParent, lastParent, transaction; + Update an individual filter. - store.removeRelationshipChangeFor(childReference, belongsToName, this.parentReference, hasManyName, this.changeType); + @param {DS.FilteredRecordArray} array + @param {Function} filter + @param {Class} type + @param {Number|String} clientId + */ + updateRecordArray: function(array, filter, type, reference) { + var shouldBeInArray, record; - if (transaction = this.transaction) { - transaction.relationshipBecameClean(this); + if (!filter) { + shouldBeInArray = true; + } else { + record = this.store.recordForReference(reference); + shouldBeInArray = filter(record); } - }, - /** @private */ - getByReference: function(reference) { - var store = this.store; - - // return null or undefined if the original reference was null or undefined - if (!reference) { return reference; } + var recordArrays = this.recordArraysForReference(reference); - if (store.recordIsMaterialized(reference)) { - return store.recordForReference(reference); + if (shouldBeInArray) { + recordArrays.add(array); + array.addReference(reference); + } else if (!shouldBeInArray) { + recordArrays.remove(array); + array.removeReference(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); - } + When a record is deleted, it is removed from all its + record arrays. - if (child) { - this.store.recordBelongsToDidChange(dirtySet, child, this); - } + @param {DS.Model} record + */ + remove: function(record) { + var reference = get(record, '_reference'); + var recordArrays = reference.recordArrays || []; - dirtySet.forEach(function(record) { - record.adapterDidDirty(); + recordArrays.forEach(function(array) { + array.removeReference(reference); }); }, - 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(); -}; + /** + @private -DS.RelationshipChangeRemove.prototype.changeType = "remove"; -DS.RelationshipChangeRemove.prototype.sync = function() { - var secondRecordName = this.getSecondRecordName(), - firstRecordName = this.getFirstRecordName(), - firstRecord = this.getFirstRecord(), - secondRecord = this.getSecondRecord(); + This method is invoked if the `filterFunction` property is + changed on a `DS.FilteredRecordArray`. - //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); + It essentially re-runs the filter from scratch. This same + method is invoked when the filter is created in th first place. + */ + updateFilter: function(array, type, filter) { + var typeMap = this.store.typeMapFor(type), + references = typeMap.references, + reference, data, shouldFilter, record; - var transaction = this.ensureSameTransaction(firstRecord, secondRecord, secondRecordName, firstRecordName); - transaction.relationshipBecameDirty(this); + for (var i=0, l=references.length; i<l; i++) { + reference = references[i]; + shouldFilter = false; - this.callChangeEvents(); + data = reference.data; - 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 (typeof data === 'object') { + if (record = reference.record) { + if (!get(record, 'isDeleted')) { shouldFilter = true; } + } else { + shouldFilter = true; + } - 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); - }); + if (shouldFilter) { + this.updateRecordArray(array, filter, type, reference); + } + } } - } - - 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: + /** + @private - App.Store = DS.Store.extend({ - adapter: 'App.MyCustomAdapter' - }); + Create a `DS.ManyArray` for a type and list of record references, and index + the `ManyArray` under each reference. This allows us to efficiently remove + records from `ManyArray`s when they are deleted. - App.PostsController = Ember.ArrayController.extend({ - // ... - }); + @param {Class} type + @param {Array} references - 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. + @return {DS.ManyArray} + */ + createManyArray: function(type, references) { + var manyArray = DS.ManyArray.create({ + type: type, + content: references, + store: this.store + }); - 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. -*/ + references.forEach(function(reference) { + var arrays = this.recordArraysForReference(reference); + arrays.add(manyArray); + }, this); -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()); - } - } - }); + return manyArray; + }, - 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", + /** + @private - initialize: function(container, application) { - application.register('store:main', application.Store); + Register a RecordArray for a given type to be backed by + a filter function. This will cause the array to update + automatically when records of that type change attribute + values or states. - // Eagerly generate the store so defaultStore is populated. - // TODO: Do this in a finisher hook - container.lookup('store:main'); - } - }); + @param {DS.RecordArray} array + @param {Class} type + @param {Function} filter + */ + registerFilteredRecordArray: function(array, type, filter) { + var recordArrays = this.filteredRecordArrays.get(type); + recordArrays.push(array); - Application.initializer({ - name: "injectStore", + this.updateFilter(array, type, filter); + }, - initialize: function(container, application) { - application.inject('controller', 'store', 'store:main'); - application.inject('route', 'store', 'store:main'); - } - }); + // Internally, we maintain a map of all unloaded IDs requested by + // a ManyArray. As the adapter loads data into the store, the + // store notifies any interested ManyArrays. When the ManyArray's + // total number of loading records drops to zero, it becomes + // `isLoaded` and fires a `didLoad` event. + registerWaitingRecordArray: function(array, reference) { + var loadingRecordArrays = reference.loadingRecordArrays || []; + loadingRecordArrays.push(array); + reference.loadingRecordArrays = loadingRecordArrays; } }); @@ -5388,36 +5668,50 @@ function mustImplement(name) { relationship types are added. However, it will remain backwards-compatible, so the mere existence of new features should not break existing adapters. + + @module data + @submodule data-serializer + @main data-serializer + + @class Serializer + @namespace DS + @extends Ember.Object + @constructor */ + DS.Serializer = Ember.Object.extend({ init: function() { this.mappings = Ember.Map.create(); + this.aliases = Ember.Map.create(); this.configurations = Ember.Map.create(); this.globalConfigurations = {}; }, extract: mustImplement('extract'), extractMany: mustImplement('extractMany'), + extractId: mustImplement('extractId'), + extractAttribute: mustImplement('extractAttribute'), + extractHasMany: mustImplement('extractHasMany'), + extractBelongsTo: mustImplement('extractBelongsTo'), - extractRecordRepresentation: function(loader, type, json, shouldSideload) { - var mapping = this.mappingForType(type); - var embeddedData, prematerialized = {}, reference; + extractRecordRepresentation: function(loader, type, data, shouldSideload) { + var prematerialized = {}, reference; if (shouldSideload) { - reference = loader.sideload(type, json); + reference = loader.sideload(type, data); } else { - reference = loader.load(type, json); + reference = loader.load(type, data); } this.eachEmbeddedHasMany(type, function(name, relationship) { - var embeddedData = json[this.keyFor(relationship)]; + var embeddedData = this.extractEmbeddedData(data, 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)]; + var embeddedData = this.extractEmbeddedData(data, this.keyFor(relationship)); if (!isNone(embeddedData)) { this.extractEmbeddedBelongsTo(loader, relationship, embeddedData, reference, prematerialized); } @@ -5432,7 +5726,8 @@ DS.Serializer = Ember.Object.extend({ var references = map.call(array, function(item) { if (!item) { return; } - var reference = this.extractRecordRepresentation(loader, relationship.type, item, true); + var foundType = this.extractEmbeddedType(relationship, item), + reference = this.extractRecordRepresentation(loader, foundType, 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. @@ -5441,6 +5736,16 @@ DS.Serializer = Ember.Object.extend({ reference.parent = parent; } + // If the embedded children have an inverse belongs-to, set the + // inverse to the current record in their prematerialized data. + var parentType = relationship.parentType, + inverse = parentType.inverseFor(relationship.key); + + if (inverse) { + var inverseName = inverse.name; + reference.prematerialized[inverseName] = parent; + } + return reference; }, this); @@ -5448,7 +5753,8 @@ DS.Serializer = Ember.Object.extend({ }, extractEmbeddedBelongsTo: function(loader, relationship, data, parent, prematerialized) { - var reference = this.extractRecordRepresentation(loader, relationship.type, data, true); + var foundType = this.extractEmbeddedType(relationship, data), + reference = this.extractRecordRepresentation(loader, foundType, data, true); prematerialized[relationship.key] = reference; // If the embedded record should also be saved back when serializing the parent, @@ -5459,6 +5765,34 @@ DS.Serializer = Ember.Object.extend({ } }, + /** + A hook you can use to customize how the record's type is extracted from + the serialized data. + + The `extractEmbeddedType` hook is called with: + + * the relationship + * the serialized representation of the record + + By default, it returns the type of the relationship. + + @method extractEmbeddedType + @param {Object} relationship an object representing the relationship + @param {any} data the serialized representation of the record + */ + extractEmbeddedType: function(relationship, data) { + return relationship.type; + }, + + /** + A hook you need to implement in order to extract + the data associated with an embedded record. + + @param {any} data the serialized representation of the record + @param {String} key the key that represents the embedded record + */ + extractEmbeddedData: mustImplement(), + //....................... //. SERIALIZATION HOOKS //....................... @@ -5474,11 +5808,13 @@ DS.Serializer = Ember.Object.extend({ * If the option hash contains `includeId`, add the record's ID to the serialized form. By default, `serialize` calls `addId` if appropriate. + * If the option hash contains `includeType`, add the record's type to the serialized form. * 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`. + @method serialize @param {DS.Model} record the record to serialize @param {Object} [options] a hash of options @returns {any} the serialized form of the record @@ -5494,6 +5830,10 @@ DS.Serializer = Ember.Object.extend({ } } + if (options.includeType) { + this.addType(serialized, record.constructor); + } + this.addAttributes(serialized, record); this.addRelationships(serialized, record); @@ -5506,6 +5846,7 @@ DS.Serializer = Ember.Object.extend({ Given an attribute type and value, convert the value into the serialized form using the transform registered for that type. + @method serializeValue @param {any} value the value to convert to the serialized form @param {String} attributeType the registered type (e.g. `string` or `boolean`) @@ -5544,8 +5885,9 @@ DS.Serializer = Ember.Object.extend({ 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. + custom transforms, you should rarely need to override this hook. + @method addAttributes @param {any} data the serialized representation that is being built @param {DS.Model} record the record to serialize */ @@ -5559,11 +5901,12 @@ DS.Serializer = Ember.Object.extend({ A hook you can use to customize how the key/value pair is added to the serialized data. + @method addAttribute @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, + addAttribute: mustImplement('addAttribute'), /** A hook you can use to customize how the record's id is added to @@ -5576,17 +5919,44 @@ DS.Serializer = Ember.Object.extend({ `primaryKey` hook into consideration) * the serialized id (after calling the `serializeId` hook) + @method addId @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, + addId: mustImplement('addId'), + + /** + A hook you can use to customize how the record's type is added to + the serialized data. + + The `addType` hook is called with: + + * the serialized representation being built + * the serialized id (after calling the `serializeId` hook) + + @method addType + @param {any} data the serialized representation that is being built + @param {DS.Model subclass} type the type of the record + */ + addType: Ember.K, + + /** + Creates an empty hash that will be filled in by the hooks called from the + `serialize()` method. + + @method createSerializedForm + @return {Object} + */ + createSerializedForm: function() { + return {}; + }, /** 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 + By default, `addRelationships` 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. @@ -5594,6 +5964,7 @@ DS.Serializer = Ember.Object.extend({ `addHasMany`, and register mappings, you should rarely need to override this hook. + @method addRelationships @param {any} data the serialized representation that is being built @param {DS.Model} record the record to serialize */ @@ -5624,12 +5995,13 @@ DS.Serializer = Ember.Object.extend({ Additional properties may be added in the future. + @method addBelongsTo @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, + addBelongsTo: mustImplement('addBelongsTo'), /** A hook you can use to add a `hasMany` relationship to the @@ -5649,12 +6021,13 @@ DS.Serializer = Ember.Object.extend({ Additional properties may be added in the future. + @method addHasMany @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, + addHasMany: mustImplement('addHasMany'), /** NAMING CONVENTIONS @@ -5691,6 +6064,7 @@ DS.Serializer = Ember.Object.extend({ }); ``` + @method 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 @@ -5736,6 +6110,7 @@ DS.Serializer = Ember.Object.extend({ }); ``` + @method primaryKey @param {DS.Model subclass} type @returns {String} the primary key for the type */ @@ -5766,6 +6141,7 @@ DS.Serializer = Ember.Object.extend({ }); ``` + @method 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 @@ -5807,6 +6183,7 @@ DS.Serializer = Ember.Object.extend({ }); ``` + @method keyForHasMany @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 @@ -5839,7 +6216,7 @@ DS.Serializer = Ember.Object.extend({ 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); + Ember.assert("You tried to use an attribute type (" + attributeType + ") that has not been registered", transform); return transform.deserialize(value); }, @@ -5860,48 +6237,88 @@ DS.Serializer = Ember.Object.extend({ record.materializeAttribute(attributeName, value); }, - materializeRelationships: function(record, hash, prematerialized) { + materializeRelationships: function(record, serialized, prematerialized) { record.eachRelationship(function(name, relationship) { if (relationship.kind === 'hasMany') { if (prematerialized && prematerialized.hasOwnProperty(name)) { - record.materializeHasMany(name, prematerialized[name]); + var tuplesOrReferencesOrOpaque = this._convertPrematerializedHasMany(relationship.type, prematerialized[name]); + record.materializeHasMany(name, tuplesOrReferencesOrOpaque); } else { - this.materializeHasMany(name, record, hash, relationship, prematerialized); + this.materializeHasMany(name, record, serialized, relationship, prematerialized); } } else if (relationship.kind === 'belongsTo') { if (prematerialized && prematerialized.hasOwnProperty(name)) { - record.materializeBelongsTo(name, prematerialized[name]); + var tupleOrReference = this._convertTuple(relationship.type, prematerialized[name]); + record.materializeBelongsTo(name, tupleOrReference); } else { - this.materializeBelongsTo(name, record, hash, relationship, prematerialized); + this.materializeBelongsTo(name, record, serialized, 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)); + var type = record.constructor, + key = this._keyForHasMany(type, relationship.key), + idsOrTuples = this.extractHasMany(type, hash, key), + tuples = idsOrTuples; + + if(idsOrTuples && Ember.isArray(idsOrTuples)) { + tuples = this._convertTuples(relationship.type, idsOrTuples); + } + + record.materializeHasMany(name, tuples); }, materializeBelongsTo: function(name, record, hash, relationship) { - var key = this._keyForBelongsTo(record.constructor, relationship.key); - record.materializeBelongsTo(name, this.extractBelongsTo(record.constructor, hash, key)); - }, + var type = record.constructor, + key = this._keyForBelongsTo(type, relationship.key), + idOrTuple, + tuple = null; + + if(relationship.options && relationship.options.polymorphic) { + idOrTuple = this.extractBelongsToPolymorphic(type, hash, key); + } else { + idOrTuple = this.extractBelongsTo(type, hash, key); + } - _extractEmbeddedRelationship: function(type, hash, name, relationshipType) { - var key = this['_keyFor' + relationshipType](type, name); + if(!isNone(idOrTuple)) { + tuple = this._convertTuple(relationship.type, idOrTuple); + } - if (this.embeddedType(type, name)) { - return this['extractEmbedded' + relationshipType](type, hash, key); + record.materializeBelongsTo(name, tuple); + }, + + _convertPrematerializedHasMany: function(type, prematerializedHasMany) { + var tuplesOrReferencesOrOpaque; + if( typeof prematerializedHasMany === 'string' ) { + tuplesOrReferencesOrOpaque = prematerializedHasMany; + } else { + tuplesOrReferencesOrOpaque = this._convertTuples(type, prematerializedHasMany); } + return tuplesOrReferencesOrOpaque; }, - _extractEmbeddedBelongsTo: function(type, hash, name) { - return this._extractEmbeddedRelationship(type, hash, name, 'BelongsTo'); + _convertTuples: function(type, idsOrTuples) { + return map.call(idsOrTuples, function(idOrTuple) { + return this._convertTuple(type, idOrTuple); + }, this); }, - _extractEmbeddedHasMany: function(type, hash, name) { - return this._extractEmbeddedRelationship(type, hash, name, 'HasMany'); + _convertTuple: function(type, idOrTuple) { + var foundType; + + if (typeof idOrTuple === 'object') { + if (DS.Model.detect(idOrTuple.type)) { + return idOrTuple; + } else { + foundType = this.typeFromAlias(idOrTuple.type); + Ember.assert("Unable to resolve type " + idOrTuple.type + ". You may need to configure your serializer aliases.", !!foundType); + + return {id: idOrTuple.id, type: foundType}; + } + } + return {id: idOrTuple, type: type}; }, /** @@ -5914,6 +6331,7 @@ DS.Serializer = Ember.Object.extend({ method will return the configured value. Otherwise, it will call the public `primaryKey` hook. + @method _primaryKey @param {DS.Model subclass} type @returns {String} the primary key for the type */ @@ -5944,6 +6362,7 @@ DS.Serializer = Ember.Object.extend({ 4. Invoke the public `addAttribute` hook with the hash, key, and transformed value. + @method _addAttribute @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 @@ -5966,6 +6385,7 @@ DS.Serializer = Ember.Object.extend({ It then invokes the public `addId` hook with the primary key and the serialized id. + @method _addId @param {any} data the serialized representation that is being built @param {Ember.Model subclass} type @param {any} id the materialized id from the record @@ -5983,6 +6403,7 @@ DS.Serializer = Ember.Object.extend({ an attribute name. It first checks for any mappings before calling the public hook `keyForAttributeName`. + @method _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 @@ -6000,6 +6421,7 @@ DS.Serializer = Ember.Object.extend({ a belongsTo relationship. It first checks for any mappings before calling the public hook `keyForBelongsTo`. + @method _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 @@ -6029,6 +6451,7 @@ DS.Serializer = Ember.Object.extend({ a hasMany relationship. It first checks for any mappings before calling the public hook `keyForHasMany`. + @method _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 @@ -6044,6 +6467,7 @@ DS.Serializer = Ember.Object.extend({ This method converts the relationship name to a key for serialization, and then invokes the public `addBelongsTo` hook. + @method _addBelongsTo @param {any} data the serialized representation that is being built @param {DS.Model} record the record to serialize @param {String} name the relationship name @@ -6060,6 +6484,7 @@ DS.Serializer = Ember.Object.extend({ This method converts the relationship name to a key for serialization, and then invokes the public `addHasMany` hook. + @method _addHasMany @param {any} data the serialized representation that is being built @param {DS.Model} record the record to serialize @param {String} name the relationship name @@ -6080,6 +6505,7 @@ DS.Serializer = Ember.Object.extend({ If a mapping is found, and the mapping has a key defined, use that instead of invoking the hook. + @method _keyFromMappingOrHook @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 @@ -6107,11 +6533,11 @@ DS.Serializer = Ember.Object.extend({ registerEnumTransform: function(type, objects) { var transform = { - deserialize: function(deserialized) { - return Ember.A(objects).objectAt(deserialized); + deserialize: function(serialized) { + return Ember.A(objects).objectAt(serialized); }, - serialize: function(serialized) { - return Ember.EnumerableUtils.indexOf(objects, serialized); + serialize: function(deserialized) { + return Ember.EnumerableUtils.indexOf(objects, deserialized); }, values: objects }; @@ -6132,12 +6558,25 @@ DS.Serializer = Ember.Object.extend({ return; } - var config = Ember.create(this.globalConfigurations); + var config, alias; + + if (configuration.alias) { + alias = configuration.alias; + this.aliases.set(alias, type); + delete configuration.alias; + } + + config = Ember.create(this.globalConfigurations); Ember.merge(config, configuration); this.configurations.set(type, config); }, + typeFromAlias: function(alias) { + this._completeAliases(); + return this.aliases.get(alias); + }, + mappingForType: function(type) { this._reifyMappings(); return this.mappings.get(type) || {}; @@ -6148,6 +6587,59 @@ DS.Serializer = Ember.Object.extend({ return this.configurations.get(type) || this.globalConfigurations; }, + _completeAliases: function() { + this._pluralizeAliases(); + this._reifyAliases(); + }, + + _pluralizeAliases: function() { + if (this._didPluralizeAliases) { return; } + + var aliases = this.aliases, + sideloadMapping = this.aliases.sideloadMapping, + plural, + self = this; + + aliases.forEach(function(key, type) { + plural = self.pluralize(key); + Ember.assert("The '" + key + "' alias has already been defined", !aliases.get(plural)); + aliases.set(plural, type); + }); + + // This map is only for backward compatibility with the `sideloadAs` option. + if (sideloadMapping) { + sideloadMapping.forEach(function(key, type) { + Ember.assert("The '" + key + "' alias has already been defined", !aliases.get(key) || (aliases.get(key)===type) ); + aliases.set(key, type); + }); + delete this.aliases.sideloadMapping; + } + + this._didPluralizeAliases = true; + }, + + _reifyAliases: function() { + if (this._didReifyAliases) { return; } + + var aliases = this.aliases, + reifiedAliases = Ember.Map.create(), + foundType; + + aliases.forEach(function(key, type) { + if (typeof type === 'string') { + foundType = Ember.get(Ember.lookup, type); + Ember.assert("Could not find model at path " + key, type); + + reifiedAliases.set(key, foundType); + } else { + reifiedAliases.set(key, type); + } + }); + + this.aliases = reifiedAliases; + this._didReifyAliases = true; + }, + _reifyMappings: function() { if (this._didReifyMappings) { return; } @@ -6216,8 +6708,6 @@ DS.Serializer = Ember.Object.extend({ }, 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); } @@ -6225,8 +6715,6 @@ DS.Serializer = Ember.Object.extend({ }, eachEmbeddedHasManyRecord: function(record, callback, binding) { - var type = record.constructor; - this.eachEmbeddedHasMany(record.constructor, function(name, relationship, embeddedType) { var array = get(record, name); for (var i=0, l=get(array, 'length'); i<l; i++) { @@ -6253,6 +6741,33 @@ DS.Serializer = Ember.Object.extend({ } } }, this); + }, + + // 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; + } } }); @@ -6262,29 +6777,38 @@ DS.Serializer = Ember.Object.extend({ (function() { -var none = Ember.isNone; +var isNone = Ember.isNone, isEmpty = Ember.isEmpty; + +/** + @module data + @submodule data-transforms +*/ /** DS.Transforms is a hash of transforms used by DS.Serializer. + + @class JSONTransforms + @static + @namespace DS */ DS.JSONTransforms = { string: { deserialize: function(serialized) { - return none(serialized) ? null : String(serialized); + return isNone(serialized) ? null : String(serialized); }, serialize: function(deserialized) { - return none(deserialized) ? null : String(deserialized); + return isNone(deserialized) ? null : String(deserialized); } }, number: { deserialize: function(serialized) { - return none(serialized) ? null : Number(serialized); + return isEmpty(serialized) ? null : Number(serialized); }, serialize: function(deserialized) { - return none(deserialized) ? null : Number(deserialized); + return isEmpty(deserialized) ? null : Number(deserialized); } }, @@ -6313,22 +6837,11 @@ DS.JSONTransforms = { date: { deserialize: function(serialized) { var type = typeof serialized; - var date = null; - - if (type === "string" || type === "number") { - // this is a fix for Safari 5.1.5 on Mac which does not accept timestamps as yyyy-mm-dd - if (type === "string" && serialized.search(/^\d{4}-\d{2}-\d{2}$/) !== -1) { - serialized += "T00:00:00Z"; - } - - date = new Date(serialized); - - // this is a fix for IE8 which does not accept timestamps in ISO 8601 format - if (type === "string" && isNaN(date)) { - date = new Date(Date.parse(serialized.replace(/\-/ig, '/').replace(/Z$/, '').split('.')[0])); - } - return date; + if (type === "string") { + return new Date(Ember.Date.parse(serialized)); + } else if (type === "number") { + return new Date(serialized); } else if (serialized === null || serialized === undefined) { // if the value is not present in the data, // return undefined, not null. @@ -6374,9 +6887,19 @@ DS.JSONTransforms = { (function() { -var get = Ember.get, set = Ember.set; +/** + @module data + @submodule data-serializers +*/ -var generatedId = 0; +/** + @class JSONSerializer + @constructor + @namespace DS + @extends DS.Serializer +*/ + +var get = Ember.get, set = Ember.set; DS.JSONSerializer = DS.Serializer.extend({ init: function() { @@ -6387,6 +6910,7 @@ DS.JSONSerializer = DS.Serializer.extend({ } this.sideloadMapping = Ember.Map.create(); + this.metadataMapping = Ember.Map.create(); this.configure({ meta: 'meta', @@ -6395,21 +6919,23 @@ DS.JSONSerializer = DS.Serializer.extend({ }, configure: function(type, configuration) { + var key; + if (type && !configuration) { + for(key in type){ + this.metadataMapping.set(get(type, key), key); + } + return this._super(type); } - var sideloadAs = configuration.sideloadAs; + var sideloadAs = configuration.sideloadAs, + sideloadMapping; 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; - + sideloadMapping = this.aliases.sideloadMapping || Ember.Map.create(); + sideloadMapping.set(sideloadAs, type); + this.aliases.sideloadMapping = sideloadMapping; delete configuration.sideloadAs; } @@ -6432,18 +6958,6 @@ DS.JSONSerializer = DS.Serializer.extend({ hash[key] = value; }, - /** - @private - - Creates an empty hash that will be filled in by the hooks called from the - `serialize()` method. - - @return {Object} - */ - createSerializedForm: function() { - return {}; - }, - extractAttribute: function(type, hash, attributeName) { var key = this._keyForAttributeName(type, attributeName); return hash[key]; @@ -6463,6 +6977,10 @@ DS.JSONSerializer = DS.Serializer.extend({ } }, + extractEmbeddedData: function(hash, key) { + return hash[key]; + }, + extractHasMany: function(type, hash, key) { return hash[key]; }, @@ -6471,24 +6989,53 @@ DS.JSONSerializer = DS.Serializer.extend({ return hash[key]; }, + extractBelongsToPolymorphic: function(type, hash, key) { + var keyForId = this.keyForPolymorphicId(key), + keyForType, + id = hash[keyForId]; + + if (id) { + keyForType = this.keyForPolymorphicType(key); + return {id: id, type: hash[keyForType]}; + } + + return null; + }, + addBelongsTo: function(hash, record, key, relationship) { var type = record.constructor, name = relationship.key, value = null, - embeddedChild; + includeType = (relationship.options && relationship.options.polymorphic), + embeddedChild, + child, + id; if (this.embeddedType(type, name)) { if (embeddedChild = get(record, name)) { - value = this.serialize(embeddedChild, { includeId: true }); + value = this.serialize(embeddedChild, { includeId: true, includeType: includeType }); } hash[key] = value; } else { - var id = get(record, relationship.key+'.id'); - if (!Ember.isNone(id)) { hash[key] = id; } + child = get(record, relationship.key); + id = get(child, 'id'); + + if (relationship.options && relationship.options.polymorphic && !Ember.isNone(id)) { + this.addBelongsToPolymorphic(hash, key, id, child.constructor); + } else { + hash[key] = id === undefined ? null : this.serializeId(id); + } } }, + addBelongsToPolymorphic: function(hash, key, id, type) { + var keyForId = this.keyForPolymorphicId(key), + keyForType = this.keyForPolymorphicType(key); + hash[keyForId] = id; + hash[keyForType] = this.rootForType(type); + }, + /** Adds a has-many relationship to the JSON hash being built. @@ -6504,10 +7051,12 @@ DS.JSONSerializer = DS.Serializer.extend({ should be saved @param {Object} relationship metadata about the relationship being serialized */ + addHasMany: function(hash, record, key, relationship) { var type = record.constructor, name = relationship.key, serializedHasMany = [], + includeType = (relationship.options && relationship.options.polymorphic), manyArray, embeddedType; // If the has-many is not embedded, there is nothing to do. @@ -6519,7 +7068,7 @@ DS.JSONSerializer = DS.Serializer.extend({ // Build up the array of serialized records manyArray.forEach(function (record) { - serializedHasMany.push(this.serialize(record, { includeId: true })); + serializedHasMany.push(this.serialize(record, { includeId: true, includeType: includeType })); }, this); // Set the appropriate property of the serialized JSON to the @@ -6527,6 +7076,11 @@ DS.JSONSerializer = DS.Serializer.extend({ hash[key] = serializedHasMany; }, + addType: function(hash, type) { + var keyForType = this.keyForEmbeddedType(); + hash[keyForType] = this.rootForType(type); + }, + // EXTRACTION extract: function(loader, json, type, record) { @@ -6538,6 +7092,8 @@ DS.JSONSerializer = DS.Serializer.extend({ if (json[root]) { if (record) { loader.updateId(record, json[root]); } this.extractRecordRepresentation(loader, type, json[root]); + } else { + Ember.Logger.warn("Extract requested, but no data given for " + type + ". This may cause weird problems."); } }, @@ -6563,12 +7119,32 @@ DS.JSONSerializer = DS.Serializer.extend({ }, extractMeta: function(loader, type, json) { - var meta = json[this.configOption(type, 'meta')], since; - if (!meta) { return; } + var meta = this.configOption(type, 'meta'), + data = json, value; + + if(meta && json[meta]){ + data = json[meta]; + } + + this.metadataMapping.forEach(function(property, key){ + value = data[property]; + if(!Ember.isNone(value)){ + loader.metaForType(type, key, value); + } + }); + }, + + extractEmbeddedType: function(relationship, data) { + var foundType = relationship.type; + if(relationship.options && relationship.options.polymorphic) { + var key = this.keyFor(relationship), + keyForEmbeddedType = this.keyForEmbeddedType(key); - if (since = meta[this.configOption(type, 'since')]) { - loader.sinceForType(type, since); + foundType = this.typeFromAlias(data[keyForEmbeddedType]); + delete data[keyForEmbeddedType]; } + + return foundType; }, /** @@ -6590,20 +7166,17 @@ DS.JSONSerializer = DS.Serializer.extend({ sideload: function(loader, type, json, root) { var sideloadedType; - this.normalizeSideloadMappings(); this.configureSideloadMappingForType(type); for (var prop in json) { if (!json.hasOwnProperty(prop) || prop === root || - prop === this.configOption(type, 'meta')) { + !!this.metadataMapping.get(prop)) { continue; } - sideloadedType = this.sideloadMapping.get(prop); - Ember.assert("Your server returned a hash with the key " + prop + - " but you have no mapping for it", - !!sideloadedType); + sideloadedType = this.typeFromAlias(prop); + Ember.assert("Your server returned a hash with the key " + prop + " but you have no mapping for it", !!sideloadedType); this.loadValue(loader, sideloadedType, json[prop]); } @@ -6612,26 +7185,6 @@ DS.JSONSerializer = DS.Serializer.extend({ /** @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. @@ -6645,11 +7198,9 @@ DS.JSONSerializer = DS.Serializer.extend({ type.eachRelatedType(function(relatedType) { if (!configured.contains(relatedType)) { - var root = this.sideloadMappingForType(relatedType); - if (!root) { - root = this.defaultSideloadRootForType(relatedType); - this.sideloadMapping.set(root, relatedType); - } + var root = this.defaultSideloadRootForType(relatedType); + this.aliases.set(root, relatedType); + this.configureSideloadMappingForType(relatedType, configured); } }, this); @@ -6665,33 +7216,44 @@ DS.JSONSerializer = DS.Serializer.extend({ } }, - // HELPERS + /** + A hook you can use in your serializer subclass to customize + how a polymorphic association's name is converted into a key for the id. - // define a plurals hash in your subclass to define - // special-case pluralization - pluralize: function(name) { - var plurals = this.configurations.get('plurals'); - return (plurals && plurals[name]) || name + "s"; + @param {String} name the association name to convert into a key + + @return {String} the key + */ + keyForPolymorphicId: function(key){ + return key; }, - // use the same plurals hash to determine - // special-case singularization - singularize: function(name) { - var plurals = this.configurations.get('plurals'); - if (plurals) { - for (var i in plurals) { - if (plurals[i] === name) { - return i; - } - } - } - if (name.lastIndexOf('s') === name.length - 1) { - return name.substring(0, name.length - 1); - } else { - return name; - } + /** + A hook you can use in your serializer subclass to customize + how a polymorphic association's name is converted into a key for the type. + + @param {String} name the association name to convert into a key + + @return {String} the key + */ + keyForPolymorphicType: function(key){ + return this.keyForPolymorphicId(key) + '_type'; + }, + + /** + A hook you can use in your serializer subclass to customize + the key used to store the type of a record of an embedded polymorphic association. + + By default, this method return 'type'. + + @return {String} the key + */ + keyForEmbeddedType: function() { + return 'type'; }, + // HELPERS + /** @private @@ -6702,7 +7264,7 @@ DS.JSONSerializer = DS.Serializer.extend({ `user_group`. @param {DS.Model subclass} type - @returns {String} name of the root element + @return {String} name of the root element */ rootForType: function(type) { var typeString = type.toString(); @@ -6718,26 +7280,10 @@ DS.JSONSerializer = DS.Serializer.extend({ /** @private - Determines the root name mapped to a particular sideloaded type. - - @param {DS.Model subclass} type - @returns {String} name of the root element, if any is registered - */ - sideloadMappingForType: function(type) { - this.sideloadMapping.forEach(function(key, value) { - if (type === value) { - return key; - } - }); - }, - - /** - @private - The default root name for a particular sideloaded type. @param {DS.Model subclass} type - @returns {String} name of the root element + @return {String} name of the root element */ defaultSideloadRootForType: function(type) { return this.pluralize(this.rootForType(type)); @@ -6749,6 +7295,13 @@ DS.JSONSerializer = DS.Serializer.extend({ (function() { +/** + @module data + @submodule data-adapter +*/ + +var get = Ember.get, set = Ember.set, merge = Ember.merge; + function loaderFor(store) { return { load: function(type, data, prematerialized) { @@ -6766,7 +7319,7 @@ function loaderFor(store) { populateArray: Ember.K, sideload: function(type, data) { - return store.load(type, data); + return store.adapterForType(type).load(store, type, data); }, sideloadMany: function(type, array) { @@ -6774,11 +7327,11 @@ function loaderFor(store) { }, prematerialize: function(reference, prematerialized) { - store.prematerialize(reference, prematerialized); + reference.prematerialized = prematerialized; }, - sinceForType: function(type, since) { - store.sinceForType(type, since); + metaForType: function(type, property, data) { + store.metaForType(type, property, data); } }; } @@ -6802,7 +7355,6 @@ DS.loaderFor = loaderFor; To tell your store which adapter to use, set its `adapter` property: App.store = DS.Store.create({ - revision: 3, adapter: App.MyAdapter.create() }); @@ -6815,17 +7367,22 @@ DS.loaderFor = loaderFor; * `updateRecord()` * `deleteRecord()` - To improve the network performance of your application, you can optimize - your adapter by overriding these lower-level methods: + To improve the network performance of your application, you can optimize + your adapter by overriding these lower-level methods: * `findMany()` * `createRecords()` * `updateRecords()` * `deleteRecords()` * `commit()` -*/ -var get = Ember.get, set = Ember.set, merge = Ember.merge; + For an example implementation, see {{#crossLink "DS.RestAdapter"}} the + included REST adapter.{{/crossLink}}. + + @class Adapter + @namespace DS + @extends Ember.Object +*/ DS.Adapter = Ember.Object.extend(DS._Mappable, { @@ -6862,28 +7419,27 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { For example: - ```javascript - adapter.load(store, App.Person, { - id: 123, - firstName: "Yehuda", - lastName: "Katz", - occupations: [{ - id: 345, - title: "Tricycle Mechanic" - }] - }); - ``` + adapter.load(store, App.Person, { + id: 123, + firstName: "Yehuda", + lastName: "Katz", + occupations: [{ + id: 345, + title: "Tricycle Mechanic" + }] + }); This will load the payload for the `App.Person` with ID `123` and the embedded `App.Occupation` with ID `345`. + @method load @param {DS.Store} store @param {subclass of DS.Model} type @param {any} payload */ load: function(store, type, payload) { var loader = loaderFor(store); - get(this, 'serializer').extractRecordRepresentation(loader, type, payload); + return get(this, 'serializer').extractRecordRepresentation(loader, type, payload); }, /** @@ -6900,12 +7456,13 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { For example, the `RESTAdapter` saves newly created records by making an Ajax request. When the server returns, the adapter calls didCreateRecord. If the server returns a response body, - it is passed as the payload. + it is passed as the payload. + @method didCreateRecord @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} record - @param {any} payload + @param {any} payload */ didCreateRecord: function(store, type, record, payload) { store.didSaveRecord(record); @@ -6926,17 +7483,18 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { Acknowledges that the adapter has finished creating several records. Your adapter should call this method from `createRecords` when it - has saved multiple created records to its persistent storage + has saved multiple created records to its persistent storage received an acknowledgement. If the persistent storage returns a new payload in response to the creation, and you want to update the existing record with the new information, pass the payload as the fourth parameter. + @method didCreateRecords @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} record - @param {any} payload + @param {any} payload */ didCreateRecords: function(store, type, records, payload) { records.forEach(function(record) { @@ -6962,16 +7520,16 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { update or delete, and you want to update the existing record with the new information, pass the payload as the fourth parameter. + @method didSaveRecord @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} record - @param {any} payload + @param {any} payload */ didSaveRecord: function(store, type, record, payload) { store.didSaveRecord(record); - var serializer = get(this, 'serializer'), - mappings = serializer.mappingForType(type); + var serializer = get(this, 'serializer'); serializer.eachEmbeddedRecord(record, function(embeddedRecord, embeddedType) { if (embeddedType === 'load') { return; } @@ -6995,10 +7553,11 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { If the persistent storage returns a new payload in response to the update, pass the payload as the fourth parameter. + @method didUpdateRecord @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} record - @param {any} payload + @param {any} payload */ didUpdateRecord: function() { this.didSaveRecord.apply(this, arguments); @@ -7014,17 +7573,18 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { If the persistent storage returns a new payload in response to the deletion, pass the payload as the fourth parameter. + @method didDeleteRecord @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} record - @param {any} payload + @param {any} payload */ didDeleteRecord: function() { this.didSaveRecord.apply(this, arguments); }, /** - Acknowledges that the adapter has finished updating or deleting + Acknowledges that the adapter has finished updating or deleting multiple records. Your adapter should call this method from its `updateRecords` or @@ -7034,10 +7594,11 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { If the persistent storage returns a new payload in response to the creation, pass the payload as the fourth parameter. + @method didSaveRecords @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} records - @param {any} payload + @param {any} payload */ didSaveRecords: function(store, type, records, payload) { records.forEach(function(record) { @@ -7060,10 +7621,11 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { If the persistent storage returns a new payload in response to the update, pass the payload as the fourth parameter. + @method didUpdateRecords @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} records - @param {any} payload + @param {any} payload */ didUpdateRecords: function() { this.didSaveRecords.apply(this, arguments); @@ -7079,10 +7641,11 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { If the persistent storage returns a new payload in response to the deletion, pass the payload as the fourth parameter. + @method didDeleteRecords @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} records - @param {any} payload + @param {any} payload */ didDeleteRecords: function() { this.didSaveRecords.apply(this, arguments); @@ -7098,9 +7661,10 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { to your find method so that the store knows which record to associate the new data with. + @method didFindRecord @param {DS.Store} store @param {subclass of DS.Model} type - @param {any} payload + @param {any} payload @param {String} id */ didFindRecord: function(store, type, payload, id) { @@ -7122,9 +7686,10 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { You adapter should call this method from its `findAll` method with the response from the backend. + @method didFindAll @param {DS.Store} store @param {subclass of DS.Model} type - @param {any} payload + @param {any} payload */ didFindAll: function(store, type, payload) { var loader = DS.loaderFor(store), @@ -7141,9 +7706,10 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { Your adapter should call this method from its `findQuery` method with the response from the backend. + @method didFindQuery @param {DS.Store} store @param {subclass of DS.Model} type - @param {any} payload + @param {any} payload @param {DS.AdapterPopulatedRecordArray} recordArray */ didFindQuery: function(store, type, payload, recordArray) { @@ -7162,9 +7728,10 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { You adapter should call this method from its `findMany` method with the response from the backend. + @method didFindMany @param {DS.Store} store @param {subclass of DS.Model} type - @param {any} payload + @param {any} payload */ didFindMany: function(store, type, payload) { var loader = DS.loaderFor(store); @@ -7179,6 +7746,7 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { Your adapter should call this method to indicate that the backend returned an error for a request. + @method didError @param {DS.Store} store @param {subclass of DS.Model} type @param {DS.Model} record @@ -7217,6 +7785,7 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { Once it registers a transform for a given type, it ignores subsequent transforms for the same attribute type. + @method registerSerializerTransforms @param {Class} klass the DS.Adapter subclass to extract the transforms from @param {DS.Serializer} serializer the serializer to register @@ -7225,6 +7794,7 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { */ registerSerializerTransforms: function(klass, serializer, seen) { var transforms = klass._registeredTransforms, superclass, prop; + var enumTransforms = klass._registeredEnumTransforms; for (prop in transforms) { if (!transforms.hasOwnProperty(prop) || prop in seen) { continue; } @@ -7233,6 +7803,13 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { serializer.registerTransform(prop, transforms[prop]); } + for (prop in enumTransforms) { + if (!enumTransforms.hasOwnProperty(prop) || prop in seen) { continue; } + seen[prop] = true; + + serializer.registerEnumTransform(prop, enumTransforms[prop]); + } + if (superclass = klass.superclass) { this.registerSerializerTransforms(superclass, serializer, seen); } @@ -7245,6 +7822,7 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { registers any class-registered mappings on the adapter's serializer. + @method registerSerializerMappings @param {Class} klass the DS.Adapter subclass to extract the transforms from @param {DS.Serializer} serializer the serializer to register the @@ -7267,17 +7845,19 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { Here is an example `find` implementation: - find: function(store, type, id) { - var url = type.url; - url = url.fmt(id); + find: function(store, type, id) { + var url = type.url; + url = url.fmt(id); - jQuery.getJSON(url, function(data) { - // data is a hash of key/value pairs. If your server returns a - // root, simply do something like: - // store.load(type, id, data.person) - store.load(type, id, data); - }); - } + jQuery.getJSON(url, function(data) { + // data is a hash of key/value pairs. If your server returns a + // root, simply do something like: + // store.load(type, id, data.person) + store.load(type, id, data); + }); + } + + @method find */ find: null, @@ -7288,7 +7868,7 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { }, /** - A public method that allows you to register an enumerated + A public method that allows you to register an enumerated type on your adapter. This is useful if you want to utilize a text representation of an integer value. @@ -7296,21 +7876,23 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { in your app, but you want to persist those as 0,1,2 in your backend. You would first register the transform on your adapter instance: - adapter.registerEnumTransform('priority', ['low', 'medium', 'high']); + adapter.registerEnumTransform('priority', ['low', 'medium', 'high']); You would then refer to the 'priority' DS.attr in your model: - App.Task = DS.Model.extend({ - priority: DS.attr('priority') - }); + + App.Task = DS.Model.extend({ + priority: DS.attr('priority') + }); And lastly, you would set/get the text representation on your model instance, but the transformed result will be the index number of the type. - App: myTask.get('priority') => 'low' - Server Response / Load: { myTask: {priority: 0} } + App: myTask.get('priority') => 'low' + Server Response / Load: { myTask: {priority: 0} } + @method registerEnumTransform @param {String} type of the transform - @param {Array} array of String objects to use for the enumerated values. + @param {Array} array of String objects to use for the enumerated values. This is an ordered list and the index values will be used for the transform. */ registerEnumTransform: function(attributeType, objects) { @@ -7335,6 +7917,10 @@ DS.Adapter = Ember.Object.extend(DS._Mappable, { var uuid = App.generateUUIDWithStatisticallyLowOddsOfCollision(); return uuid; } + + @method generateIdForRecord + @param {DS.Store} store + @param {DS.Model} record */ generateIdForRecord: null, @@ -7430,6 +8016,14 @@ DS.Adapter.reopenClass({ this._registeredTransforms = registeredTransforms; }, + registerEnumTransform: function(attributeType, objects) { + var registeredEnumTransforms = this._registeredEnumTransforms || {}; + + registeredEnumTransforms[attributeType] = objects; + + this._registeredEnumTransforms = registeredEnumTransforms; + }, + map: DS._Mappable.generateMapFunctionFor('attributes', function(key, newValue, map) { var existingValue = map.get(key); @@ -7450,7 +8044,7 @@ DS.Adapter.reopenClass({ merge(existingValue, newValue); }), - resolveMapConflict: function(oldValue, newValue, mappingsKey) { + resolveMapConflict: function(oldValue, newValue) { merge(newValue, oldValue); return newValue; @@ -7462,6 +8056,18 @@ DS.Adapter.reopenClass({ (function() { +/** + @module data + @submodule data-serializers +*/ + +/** + @class FixtureSerializer + @constructor + @namespace DS + @extends DS.Serializer +*/ + var get = Ember.get, set = Ember.set; DS.FixtureSerializer = DS.Serializer.extend({ @@ -7494,18 +8100,6 @@ DS.FixtureSerializer = DS.Serializer.extend({ 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); @@ -7548,7 +8142,32 @@ DS.FixtureSerializer = DS.Serializer.extend({ }, extractBelongsTo: function(type, hash, key) { - return hash[key]; + var val = hash[key]; + if (val != null) { + val = val + ''; + } + return val; + }, + + extractBelongsToPolymorphic: function(type, hash, key) { + var keyForId = this.keyForPolymorphicId(key), + keyForType, + id = hash[keyForId]; + + if (id) { + keyForType = this.keyForPolymorphicType(key); + return {id: id, type: hash[keyForType]}; + } + + return null; + }, + + keyForPolymorphicId: function(key) { + return key; + }, + + keyForPolymorphicType: function(key) { + return key + '_type'; } }); @@ -7557,6 +8176,11 @@ DS.FixtureSerializer = DS.Serializer.extend({ (function() { +/** + @module data + @submodule data-adapters +*/ + var get = Ember.get, fmt = Ember.String.fmt, dump = Ember.get(window, 'JSON.stringify') || function(object) { return object.toString(); }; @@ -7569,6 +8193,10 @@ var get = Ember.get, fmt = Ember.String.fmt, system would do. Its possible to do develop your entire application with `DS.FixtureAdapter`. + @class FixtureAdapter + @constructor + @namespace DS + @extends DS.Adapter */ DS.FixtureAdapter = DS.Adapter.extend({ @@ -7585,8 +8213,9 @@ DS.FixtureAdapter = DS.Adapter.extend({ if (type.FIXTURES) { var fixtures = Ember.A(type.FIXTURES); return fixtures.map(function(fixture){ - if(!fixture.id){ - throw new Error(fmt('the id property must be defined for fixture %@', [dump(fixture)])); + var fixtureIdType = typeof fixture.id; + if(fixtureIdType !== "number" && fixtureIdType !== "string"){ + throw new Error(fmt('the id property must be defined as a number or string for fixture %@', [dump(fixture)])); } fixture.id = fixture.id + ''; return fixture; @@ -7721,8 +8350,6 @@ DS.FixtureAdapter = DS.Adapter.extend({ @private */ deleteLoadedFixture: function(type, record) { - var id = this.extractId(type, record); - var existingFixture = this.findExistingFixture(type, record); if(existingFixture) { @@ -7740,8 +8367,6 @@ DS.FixtureAdapter = DS.Adapter.extend({ }, findFixtureById: function(fixtures, id) { - var adapter = this; - return Ember.A(fixtures).find(function(r) { if(''+get(r, 'id') === ''+id) { return true; @@ -7767,6 +8392,20 @@ DS.FixtureAdapter = DS.Adapter.extend({ (function() { +/** + @module data + @submodule data-serializers +*/ + +/** + @class RESTSerializer + @constructor + @namespace DS + @extends DS.Serializer +*/ + +var get = Ember.get; + DS.RESTSerializer = DS.JSONSerializer.extend({ keyForAttributeName: function(type, name) { return Ember.String.decamelize(name); @@ -7790,6 +8429,27 @@ DS.RESTSerializer = DS.JSONSerializer.extend({ } return this.singularize(key) + "_ids"; + }, + + keyForPolymorphicId: function(key) { + return key; + }, + + keyForPolymorphicType: function(key) { + return key.replace(/_id$/, '_type'); + }, + + extractValidationErrors: function(type, json) { + var errors = {}; + + get(type, 'attributes').forEach(function(name) { + var key = this._keyForAttributeName(type, name); + if (json['errors'].hasOwnProperty(key)) { + errors[name] = json['errors'][key]; + } + }, this); + + return errors; } }); @@ -7798,9 +8458,18 @@ DS.RESTSerializer = DS.JSONSerializer.extend({ (function() { -/*global jQuery*/ +/** + @module data + @submodule data-adapters +*/ -var get = Ember.get, set = Ember.set, merge = Ember.merge; +var get = Ember.get, set = Ember.set; + +DS.rejectionHandler = function(reason) { + Ember.Logger.assert([reason, reason.message, reason.stack]); + + throw reason; +}; /** The REST adapter allows your store to communicate with an HTTP server by @@ -7856,8 +8525,14 @@ var get = Ember.get, set = Ember.set, merge = Ember.merge; } } ``` + + @class RESTAdapter + @constructor + @namespace DS + @extends DS.Adapter */ DS.RESTAdapter = DS.Adapter.extend({ + namespace: null, bulkCommit: false, since: 'since', @@ -7873,26 +8548,6 @@ DS.RESTAdapter = DS.Adapter.extend({ return !reference.parent; }, - createRecord: function(store, type, record) { - var root = this.rootForType(type); - - var data = {}; - data[root] = this.serialize(record, { includeId: true }); - - this.ajax(this.buildURL(root), "POST", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didCreateRecord(store, type, record, json); - }); - }, - error: function(xhr) { - this.didError(store, type, record, xhr); - } - }); - }, - dirtyRecordsForRecordChange: function(dirtySet, record) { this._dirtyTree(dirtySet, record); }, @@ -7924,7 +8579,26 @@ DS.RESTAdapter = DS.Adapter.extend({ } }, + createRecord: function(store, type, record) { + var root = this.rootForType(type); + var adapter = this; + var data = {}; + + data[root] = this.serialize(record, { includeId: true }); + + return this.ajax(this.buildURL(root), "POST", { + data: data + }).then(function(json){ + adapter.didCreateRecord(store, type, record, json); + }, function(xhr) { + adapter.didError(store, type, record, xhr); + throw xhr; + }).then(null, DS.rejectionHandler); + }, + createRecords: function(store, type, records) { + var adapter = this; + if (get(this, 'bulkCommit') === false) { return this._super(store, type, records); } @@ -7938,153 +8612,144 @@ DS.RESTAdapter = DS.Adapter.extend({ data[plural].push(this.serialize(record, { includeId: true })); }, this); - this.ajax(this.buildURL(root), "POST", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didCreateRecords(store, type, records, json); - }); - } - }); + return this.ajax(this.buildURL(root), "POST", { + data: data + }).then(function(json) { + adapter.didCreateRecords(store, type, records, json); + }).then(null, DS.rejectionHandler); }, updateRecord: function(store, type, record) { - var id = get(record, 'id'); - var root = this.rootForType(type); + var id, root, adapter, data; - var data = {}; + id = get(record, 'id'); + root = this.rootForType(type); + adapter = this; + + data = {}; data[root] = this.serialize(record); - this.ajax(this.buildURL(root, id), "PUT", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didSaveRecord(store, type, record, json); - }); - }, - error: function(xhr) { - this.didError(store, type, record, xhr); - } - }); + return this.ajax(this.buildURL(root, id), "PUT",{ + data: data + }).then(function(json){ + adapter.didUpdateRecord(store, type, record, json); + }, function(xhr) { + adapter.didError(store, type, record, xhr); + throw xhr; + }).then(null, DS.rejectionHandler); }, updateRecords: function(store, type, records) { + var root, plural, adapter, data; + if (get(this, 'bulkCommit') === false) { return this._super(store, type, records); } - var root = this.rootForType(type), - plural = this.pluralize(root); + root = this.rootForType(type); + plural = this.pluralize(root); + adapter = this; + + data = {}; - var data = {}; data[plural] = []; + records.forEach(function(record) { data[plural].push(this.serialize(record, { includeId: true })); }, this); - this.ajax(this.buildURL(root, "bulk"), "PUT", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didSaveRecords(store, type, records, json); - }); - } - }); + return this.ajax(this.buildURL(root, "bulk"), "PUT", { + data: data + }).then(function(json) { + adapter.didUpdateRecords(store, type, records, json); + }).then(null, DS.rejectionHandler); }, deleteRecord: function(store, type, record) { - var id = get(record, 'id'); - var root = this.rootForType(type); + var id, root, adapter; - this.ajax(this.buildURL(root, id), "DELETE", { - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didSaveRecord(store, type, record, json); - }); - } - }); + id = get(record, 'id'); + root = this.rootForType(type); + adapter = this; + + return this.ajax(this.buildURL(root, id), "DELETE").then(function(json){ + adapter.didDeleteRecord(store, type, record, json); + }, function(xhr){ + adapter.didError(store, type, record, xhr); + throw xhr; + }).then(null, DS.rejectionHandler); }, deleteRecords: function(store, type, records) { + var root, plural, serializer, adapter, data; + if (get(this, 'bulkCommit') === false) { return this._super(store, type, records); } - var root = this.rootForType(type), - plural = this.pluralize(root), - serializer = get(this, 'serializer'); + root = this.rootForType(type); + plural = this.pluralize(root); + serializer = get(this, 'serializer'); + adapter = this; + + data = {}; - var data = {}; data[plural] = []; records.forEach(function(record) { data[plural].push(serializer.serializeId( get(record, 'id') )); }); - this.ajax(this.buildURL(root, 'bulk'), "DELETE", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didSaveRecords(store, type, records, json); - }); - } - }); + return this.ajax(this.buildURL(root, 'bulk'), "DELETE", { + data: data + }).then(function(json){ + adapter.didDeleteRecords(store, type, records, json); + }).then(null, DS.rejectionHandler); }, find: function(store, type, id) { - var root = this.rootForType(type); + var root = this.rootForType(type), adapter = this; - this.ajax(this.buildURL(root, id), "GET", { - success: function(json) { - Ember.run(this, function(){ - this.didFindRecord(store, type, json, id); - }); - } - }); + return this.ajax(this.buildURL(root, id), "GET"). + then(function(json){ + adapter.didFindRecord(store, type, json, id); + }).then(null, DS.rejectionHandler); }, findAll: function(store, type, since) { - var root = this.rootForType(type); + var root, adapter; - this.ajax(this.buildURL(root), "GET", { - data: this.sinceQuery(since), - success: function(json) { - Ember.run(this, function(){ - this.didFindAll(store, type, json); - }); - } - }); + root = this.rootForType(type); + adapter = this; + + return this.ajax(this.buildURL(root), "GET",{ + data: this.sinceQuery(since) + }).then(function(json) { + adapter.didFindAll(store, type, json); + }).then(null, DS.rejectionHandler); }, findQuery: function(store, type, query, recordArray) { - var root = this.rootForType(type); + var root = this.rootForType(type), + adapter = this; - this.ajax(this.buildURL(root), "GET", { - data: query, - success: function(json) { - Ember.run(this, function(){ - this.didFindQuery(store, type, json, recordArray); - }); - } - }); + return this.ajax(this.buildURL(root), "GET", { + data: query + }).then(function(json){ + adapter.didFindQuery(store, type, json, recordArray); + }).then(null, DS.rejectionHandler); }, findMany: function(store, type, ids, owner) { - var root = this.rootForType(type); + var root = this.rootForType(type), + adapter = this; + ids = this.serializeIds(ids); - this.ajax(this.buildURL(root), "GET", { - data: {ids: ids}, - success: function(json) { - Ember.run(this, function(){ - this.didFindMany(store, type, json); - }); - } - }); + return this.ajax(this.buildURL(root), "GET", { + data: {ids: ids} + }).then(function(json) { + adapter.didFindMany(store, type, json); + }).then(null, DS.rejectionHandler); }, /** @@ -8092,7 +8757,7 @@ DS.RESTAdapter = DS.Adapter.extend({ This method serializes a list of IDs using `serializeId` - @returns {Array} an array of serialized IDs + @return {Array} an array of serialized IDs */ serializeIds: function(ids) { var serializer = get(this, 'serializer'); @@ -8104,25 +8769,41 @@ DS.RESTAdapter = DS.Adapter.extend({ didError: function(store, type, record, xhr) { if (xhr.status === 422) { - var data = JSON.parse(xhr.responseText); - store.recordWasInvalid(record, data['errors']); + var json = JSON.parse(xhr.responseText), + serializer = get(this, 'serializer'), + errors = serializer.extractValidationErrors(type, json); + + store.recordWasInvalid(record, errors); } else { this._super.apply(this, arguments); } }, ajax: function(url, type, hash) { - hash.url = url; - hash.type = type; - hash.dataType = 'json'; - hash.contentType = 'application/json; charset=utf-8'; - hash.context = this; + var adapter = this; - if (hash.data && type !== 'GET') { - hash.data = JSON.stringify(hash.data); - } + return new Ember.RSVP.Promise(function(resolve, reject) { + hash = hash || {}; + hash.url = url; + hash.type = type; + hash.dataType = 'json'; + hash.context = adapter; + + if (hash.data && type !== 'GET') { + hash.contentType = 'application/json; charset=utf-8'; + hash.data = JSON.stringify(hash.data); + } + + hash.success = function(json) { + Ember.run(null, resolve, json); + }; - jQuery.ajax(hash); + hash.error = function(jqXHR, textStatus, errorThrown) { + Ember.run(null, reject, jqXHR); + }; + + Ember.$.ajax(hash); + }); }, url: "", @@ -8144,7 +8825,7 @@ DS.RESTAdapter = DS.Adapter.extend({ Ember.assert("Record URL (" + record + ") must not start with slash", !record || record.toString().charAt(0) !== "/"); Ember.assert("URL suffix (" + suffix + ") must not start with slash", !suffix || suffix.toString().charAt(0) !== "/"); - if (this.namespace !== undefined) { + if (!Ember.isNone(this.namespace)) { url.push(this.namespace); } @@ -8163,257 +8844,19 @@ DS.RESTAdapter = DS.Adapter.extend({ } }); - })(); (function() { -var camelize = Ember.String.camelize, - capitalize = Ember.String.capitalize, - get = Ember.get, - map = Ember.ArrayPolyfills.map, - registeredTransforms; - -var passthruTransform = { - serialize: function(value) { return value; }, - deserialize: function(value) { return value; } -}; - -var defaultTransforms = { - string: passthruTransform, - boolean: passthruTransform, - number: passthruTransform -}; - -function camelizeKeys(json) { - var value; - - for (var prop in json) { - value = json[prop]; - delete json[prop]; - json[camelize(prop)] = value; - } -} - -function munge(json, callback) { - callback(json); -} - -function applyTransforms(json, type, transformType) { - var transforms = registeredTransforms[transformType]; - - Ember.assert("You are trying to apply the '" + transformType + "' transforms, but you didn't register any transforms with that name", transforms); - - get(type, 'attributes').forEach(function(name, attribute) { - var attributeType = attribute.type, - value = json[name]; - - var transform = transforms[attributeType] || defaultTransforms[attributeType]; - - Ember.assert("Your model specified the '" + attributeType + "' type for the '" + name + "' attribute, but no transform for that type was registered", transform); - - json[name] = transform.deserialize(value); - }); -} - -function ObjectProcessor(json, type, store) { - this.json = json; - this.type = type; - this.store = store; -} - -ObjectProcessor.prototype = { - camelizeKeys: function() { - camelizeKeys(this.json); - return this; - }, - - munge: function(callback) { - munge(this.json, callback); - return this; - }, - - applyTransforms: function(transformType) { - applyTransforms(this.json, this.type, transformType); - return this; - } -}; - -function LoadObjectProcessor() { - ObjectProcessor.apply(this, arguments); -} - -LoadObjectProcessor.prototype = Ember.create(ObjectProcessor.prototype); - -LoadObjectProcessor.prototype.load = function() { - this.store.load(this.type, {}, this.json); -}; - -function loadObjectProcessorFactory(store, type) { - return function(json) { - return new LoadObjectProcessor(json, type, store); - }; -} - -function ArrayProcessor(json, type, array, store) { - this.json = json; - this.type = type; - this.array = array; - this.store = store; -} - -ArrayProcessor.prototype = { - load: function() { - var store = this.store, - type = this.type; - - var references = this.json.map(function(object) { - return store.load(type, {}, object); - }); - - this.array.load(references); - }, - - camelizeKeys: function() { - this.json.forEach(camelizeKeys); - return this; - }, - - munge: function(callback) { - this.json.forEach(function(object) { - munge(object, callback); - }); - return this; - }, - - applyTransforms: function(transformType) { - var type = this.type; - - this.json.forEach(function(object) { - applyTransforms(object, type, transformType); - }); - - return this; - } -}; - -function arrayProcessorFactory(store, type, array) { - return function(json) { - return new ArrayProcessor(json, type, array, store); - }; -} - -var HasManyProcessor = function(json, store, record, relationship) { - this.json = json; - this.store = store; - this.record = record; - this.type = record.constructor; - this.relationship = relationship; -}; - -HasManyProcessor.prototype = Ember.create(ArrayProcessor.prototype); - -HasManyProcessor.prototype.load = function() { - var store = this.store; - var ids = map.call(this.json, function(obj) { return obj.id; }); - - store.loadMany(this.relationship.type, this.json); - store.loadHasMany(this.record, this.relationship.key, ids); -}; - -function hasManyProcessorFactory(store, record, relationship) { - return function(json) { - return new HasManyProcessor(json, store, record, relationship); - }; -} - -function SaveProcessor(record, store, type, includeId) { - this.record = record; - ObjectProcessor.call(this, record.toJSON({ includeId: includeId }), type, store); -} - -SaveProcessor.prototype = Ember.create(ObjectProcessor.prototype); - -SaveProcessor.prototype.save = function(callback) { - callback(this.json); -}; - -function saveProcessorFactory(store, type, includeId) { - return function(record) { - return new SaveProcessor(record, store, type, includeId); - }; -} - -DS.BasicAdapter = DS.Adapter.extend({ - find: function(store, type, id) { - var sync = type.sync; - - Ember.assert("You are trying to use the BasicAdapter to find id '" + id + "' of " + type + " but " + type + ".sync was not found", sync); - Ember.assert("The sync code on " + type + " does not implement find(), but you are trying to find id '" + id + "'.", sync.find); - - sync.find(id, loadObjectProcessorFactory(store, type)); - }, - - findQuery: function(store, type, query, recordArray) { - var sync = type.sync; - - Ember.assert("You are trying to use the BasicAdapter to query " + type + " but " + type + ".sync was not found", sync); - Ember.assert("The sync code on " + type + " does not implement query(), but you are trying to query " + type + ".", sync.query); - - sync.query(query, arrayProcessorFactory(store, type, recordArray)); - }, - - findHasMany: function(store, record, relationship, data) { - var name = capitalize(relationship.key), - sync = record.constructor.sync, - processor = hasManyProcessorFactory(store, record, relationship); - - var options = { - relationship: relationship.key, - data: data - }; - - if (sync['find'+name]) { - sync['find' + name](record, options, processor); - } else if (sync.findHasMany) { - sync.findHasMany(record, options, processor); - } else { - Ember.assert("You are trying to use the BasicAdapter to find the " + relationship.key + " has-many relationship, but " + record.constructor + ".sync did not implement findHasMany or find" + name + ".", false); - } - }, - - createRecord: function(store, type, record) { - var sync = type.sync; - sync.createRecord(record, saveProcessorFactory(store, type)); - }, - - updateRecord: function(store, type, record) { - var sync = type.sync; - sync.updateRecord(record, saveProcessorFactory(store, type, true)); - }, - - deleteRecord: function(store, type, record) { - var sync = type.sync; - sync.deleteRecord(record, saveProcessorFactory(store, type, true)); - } -}); - -DS.registerTransforms = function(kind, object) { - registeredTransforms[kind] = object; -}; - -DS.clearTransforms = function() { - registeredTransforms = {}; -}; - -DS.clearTransforms(); - -})(); - +/** + Adapters included with Ember-Data. + @module data + @submodule data-adapters -(function() { + @main data-adapters +*/ })(); @@ -8440,5 +8883,12 @@ DS.clearTransforms(); //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE //SOFTWARE. +/** + Ember Data + @module data +*/ + })(); + +})(); diff --git a/public/js/libs/ember.js b/public/js/libs/ember.js index 7f44724..2bfa603 100644 --- a/public/js/libs/ember.js +++ b/public/js/libs/ember.js @@ -1,5 +1,5 @@ -// Version: v1.0.0-rc.2 -// Last commit: 656fa6e (2013-03-29 13:40:38 -0700) +// Version: v1.0.0-rc.5-1-gf84c193 +// Last commit: f84c193 (2013-06-01 13:57:19 -0400) (function() { @@ -151,8 +151,8 @@ Ember.deprecateFunc = function(message, func) { })(); -// Version: v1.0.0-rc.2 -// Last commit: 656fa6e (2013-03-29 13:40:38 -0700) +// Version: v1.0.0-rc.5-1-gf84c193 +// Last commit: f84c193 (2013-06-01 13:57:19 -0400) (function() { @@ -169,11 +169,18 @@ var define, requireModule; if (seen[name]) { return seen[name]; } seen[name] = {}; - var mod = registry[name], - deps = mod.deps, - callback = mod.callback, - reified = [], - exports; + var mod, deps, callback, reified, exports; + + mod = registry[name]; + + if (!mod) { + throw new Error("Module '" + name + "' not found."); + } + + deps = mod.deps; + callback = mod.callback; + reified = []; + exports; for (var i=0, l=deps.length; i<l; i++) { if (deps[i] === 'exports') { @@ -212,7 +219,7 @@ var define, requireModule; @class Ember @static - @version 1.0.0-rc.2 + @version 1.0.0-rc.5 */ if ('undefined' === typeof Ember) { @@ -239,10 +246,10 @@ Ember.toString = function() { return "Ember"; }; /** @property VERSION @type String - @default '1.0.0-rc.2' + @default '1.0.0-rc.5' @final */ -Ember.VERSION = '1.0.0-rc.2'; +Ember.VERSION = '1.0.0-rc.5'; /** Standard environmental variables. You can define these in a global `ENV` @@ -391,7 +398,7 @@ Ember.onerror = null; /** @private - Wrap code block in a try/catch if {{#crossLink "Ember/onerror"}}{{/crossLink}} is set. + Wrap code block in a try/catch if `Ember.onerror` is set. @method handleErrors @for Ember @@ -467,7 +474,7 @@ Ember.none = Ember.deprecateFunc("Ember.none is deprecated. Please use Ember.isN @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); + return Ember.isNone(obj) || (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) ; @@ -500,6 +507,13 @@ var platform = Ember.platform = {}; */ Ember.create = Object.create; +// IE8 has Object.create but it couldn't treat property descripters. +if (Ember.create) { + if (Ember.create({a: 1}, {a: {value: 2}}).a !== 2) { + Ember.create = null; + } +} + // STUB_OBJECT_CREATE allows us to override other libraries that stub // Object.create different than we would prefer if (!Ember.create || Ember.ENV.STUB_OBJECT_CREATE) { @@ -635,6 +649,103 @@ if (Ember.ENV.MANDATORY_SETTER && !platform.hasPropertyAccessors) { (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 */ @@ -723,7 +834,7 @@ Ember.guidFor = function guidFor(obj) { if (obj === undefined) return "(undefined)"; if (obj === null) return "(null)"; - var cache, ret; + var ret; var type = typeof obj; // Don't allow prototype changes to String etc. to change the guidFor @@ -872,6 +983,7 @@ Ember.setMeta = function setMeta(obj, property, value) { }; /** + @deprecated @private In order to store defaults for a class, a prototype may need to create @@ -904,6 +1016,7 @@ Ember.setMeta = function setMeta(obj, property, value) { shared with its constructor */ Ember.metaPath = function metaPath(obj, path, writable) { + Ember.deprecate("Ember.metaPath is deprecated and will be removed from future releases."); var meta = Ember.meta(obj, writable), keyName, value; for (var i=0, l=path.length; i<l; i++) { @@ -1036,7 +1149,7 @@ Ember.canInvoke = canInvoke; @param {Object} obj The object to check for the method @param {String} methodName The method name to check for @param {Array} [args] The arguments to pass to the method - @return {anything} the return value of the invoked method or undefined if it cannot be invoked + @return {*} the return value of the invoked method or undefined if it cannot be invoked */ Ember.tryInvoke = function(obj, methodName, args) { if (canInvoke(obj, methodName)) { @@ -1067,7 +1180,7 @@ var needsFinallyFix = (function() { @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, + @return {*} The return value is the that of the finalizer, unless that valueis undefined, in which case it is the return value of the tryable */ @@ -1118,13 +1231,13 @@ if (needsFinallyFix) { @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, + @return {*} The return value is the that of the finalizer, unless that value is undefined, in which case it is the return value of the tryable. */ if (needsFinallyFix) { Ember.tryCatchFinally = function(tryable, catchable, finalizer, binding) { - var result, finalResult, finalError, finalReturn; + var result, finalResult, finalError; binding = binding || this; @@ -1162,6 +1275,79 @@ if (needsFinallyFix) { }; } +// ........................................ +// TYPING & ARRAY MESSAGING +// + +var TYPE_MAP = {}; +var t = "Boolean Number String Function Array Date RegExp Object".split(" "); +Ember.ArrayPolyfills.forEach.call(t, function(name) { + TYPE_MAP[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +var toString = Object.prototype.toString; + +/** + Returns a consistent type for the passed item. + + Use this instead of the built-in `typeof` to get the type of an item. + It will return the same result across all browsers and includes a bit + more detail. Here is what will be returned: + + | Return Value | Meaning | + |---------------|------------------------------------------------------| + | 'string' | String primitive | + | 'number' | Number primitive | + | 'boolean' | Boolean primitive | + | 'null' | Null value | + | 'undefined' | Undefined value | + | 'function' | A function | + | 'array' | An instance of Array | + | 'class' | An Ember class (created using Ember.Object.extend()) | + | 'instance' | An Ember object instance | + | 'error' | An instance of the Error object | + | 'object' | A JavaScript object not inheriting from Ember.Object | + + Examples: + + ```javascript + Ember.typeOf(); // 'undefined' + Ember.typeOf(null); // 'null' + Ember.typeOf(undefined); // 'undefined' + Ember.typeOf('michael'); // 'string' + Ember.typeOf(101); // 'number' + Ember.typeOf(true); // 'boolean' + Ember.typeOf(Ember.makeArray); // 'function' + Ember.typeOf([1,2,90]); // 'array' + Ember.typeOf(Ember.Object.extend()); // 'class' + Ember.typeOf(Ember.Object.create()); // 'instance' + Ember.typeOf(new Error('teamocil')); // 'error' + + // "normal" JavaScript object + Ember.typeOf({a: 'b'}); // 'object' + ``` + + @method typeOf + @for Ember + @param {Object} item the item to check + @return {String} the type +*/ +Ember.typeOf = function(item) { + var ret; + + ret = (item === null || item === undefined) ? String(item) : TYPE_MAP[toString.call(item)] || 'object'; + + if (ret === 'function') { + if (Ember.Object && Ember.Object.detect(item)) ret = 'class'; + } else if (ret === 'object') { + if (item instanceof Error) ret = 'error'; + else if (Ember.Object && item instanceof Ember.Object) ret = 'instance'; + else ret = 'object'; + } + + return ret; +}; + })(); @@ -1342,17 +1528,23 @@ Ember.subscribe = Ember.Instrumentation.subscribe; (function() { +var map, forEach, indexOf, concat; +concat = Array.prototype.concat; +map = Array.prototype.map || Ember.ArrayPolyfills.map; +forEach = Array.prototype.forEach || Ember.ArrayPolyfills.forEach; +indexOf = Array.prototype.indexOf || Ember.ArrayPolyfills.indexOf; + var utils = Ember.EnumerableUtils = { map: function(obj, callback, thisArg) { - return obj.map ? obj.map.call(obj, callback, thisArg) : Array.prototype.map.call(obj, callback, thisArg); + return obj.map ? obj.map.call(obj, callback, thisArg) : map.call(obj, callback, thisArg); }, forEach: function(obj, callback, thisArg) { - return obj.forEach ? obj.forEach.call(obj, callback, thisArg) : Array.prototype.forEach.call(obj, callback, thisArg); + return obj.forEach ? obj.forEach.call(obj, callback, thisArg) : forEach.call(obj, callback, thisArg); }, indexOf: function(obj, element, index) { - return obj.indexOf ? obj.indexOf.call(obj, element, index) : Array.prototype.indexOf.call(obj, element, index); + return obj.indexOf ? obj.indexOf.call(obj, element, index) : indexOf.call(obj, element, index); }, indexesOf: function(obj, elements) { @@ -1375,7 +1567,7 @@ var utils = Ember.EnumerableUtils = { if (array.replace) { return array.replace(idx, amt, objects); } else { - var args = Array.prototype.concat.apply([idx, amt], objects); + var args = concat.apply([idx, amt], objects); return array.splice.apply(array, args); } }, @@ -1383,8 +1575,8 @@ var utils = Ember.EnumerableUtils = { intersection: function(array1, array2) { var intersection = []; - array1.forEach(function(element) { - if (array2.indexOf(element) >= 0) { + utils.forEach(array1, function(element) { + if (utils.indexOf(array2, element) >= 0) { intersection.push(element); } }); @@ -1398,98 +1590,982 @@ var utils = Ember.EnumerableUtils = { (function() { -/*jshint newcap:false*/ /** @module ember-metal */ -// NOTE: There is a bug in jshint that doesn't recognize `Object()` without `new` -// as being ok unless both `newcap:false` and not `use strict`. -// https://github.com/jshint/jshint/issues/392 +var META_KEY = Ember.META_KEY, get; -// Testing this is not ideal, but we want to use native functions -// if available, but not to use versions created by libraries like Prototype -var isNativeFunc = function(func) { - // This should probably work in all browsers likely to have ES5 array methods - return func && Function.prototype.toString.call(func).indexOf('[native code]') > -1; +var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; + +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 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; + } + + Ember.assert("Cannot call get with '"+ keyName +"' on an undefined object.", obj !== undefined); + + if (obj === null || keyName.indexOf('.') !== -1) { + return getPath(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; + } }; -// 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"; +// Currently used only by Ember Data tests +if (Ember.config.overrideAccessors) { + Ember.get = get; + Ember.config.overrideAccessors(); + get = Ember.get; +} - if (this === void 0 || this === null) { - throw new TypeError(); +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); } - var t = Object(this); - var len = t.length >>> 0; - if (typeof fun !== "function") { - throw new TypeError(); + // must return some kind of path to be valid else other things will break. + if (!path || path.length===0) throw new Error('Invalid Path'); + + return [ target, path ]; +} + +var getPath = Ember._getPath = function(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; } - 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); + parts = path.split("."); + len = parts.length; + for (idx = 0; root != null && idx < len; idx++) { + root = get(root, parts[idx], true); + if (root && root.isDestroyed) { return undefined; } + } + return root; +}; + +/** + @private + + Normalizes a target/path pair to reflect that actual target/path that should + be observed, etc. This takes into account passing in global property + paths (i.e. a path beginning with a captial letter not defined on the + target) and * separators. + + @method normalizeTuple + @for Ember + @param {Object} target The current target. May be `null`. + @param {String} path A path on the target or a global property path. + @return {Array} a temporary array with the normalized target/path pair. +*/ +Ember.normalizeTuple = function(target, path) { + return normalizeTuple(target, path); +}; + +Ember.getWithDefault = function(root, key, defaultValue) { + var value = get(root, key); + + if (value === undefined) { return defaultValue; } + return value; +}; + + +Ember.get = get; +Ember.getPath = Ember.deprecateFunc('getPath is deprecated since get now supports paths', Ember.get); + +})(); + + + +(function() { +/** +@module ember-metal +*/ + +var o_create = Ember.create, + metaFor = Ember.meta, + META_KEY = Ember.META_KEY, + /* listener flags */ + ONCE = 1, SUSPENDED = 2; + +/* + 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, flags] + ] + } + } + +*/ + +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], + flags = actions[i][2], + actionIndex = indexOf(otherActions, target, method); + + if (actionIndex === -1) { + otherActions.push([target, method, flags]); } } +} - return res; +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], + flags = actions[i][2], + actionIndex = indexOf(otherActions, target, method); + + if (actionIndex !== -1) { continue; } + + otherActions.push([target, method, flags]); + diffActions.push([target, method, flags]); + } + + 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` + @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); + + if (!method && 'function' === typeof target) { + method = target; + target = null; + } + + var actions = actionsFor(obj, eventName), + actionIndex = indexOf(actions, target, method), + flags = 0; + + if (once) flags |= ONCE; + + if (actionIndex !== -1) { return; } + + actions.push([target, method, flags]); + + if ('function' === typeof obj.didAddListener) { + obj.didAddListener(eventName, target, method); + } +} + +/** + Remove an event listener + + Arguments should match those passed to `Ember.addListener`. + + @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) { + 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[2] |= SUSPENDED; // 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[2] &= ~SUSPENDED; } } + + return Ember.tryFinally(tryable, finalizer); +} + +/** + @private + + Suspend listener during callback. + + This should only be used by the target of the event listener + when it is taking an action that would cause the event, e.g. + an object might suspend its property change listener while it is + setting that property. + + @method suspendListener + @for Ember + @param obj + @param {Array} eventName Array of event names + @param {Object|Function} targetOrMethod A target object or a function + @param {Function|String} method A function or the name of a function to be called on `target` + @param {Function} callback +*/ +function suspendListeners(obj, eventNames, target, method, callback) { + if (!method && 'function' === typeof target) { + method = target; + target = null; + } + + var suspendedActions = [], + eventName, actions, action, i, l; + + for (i=0, l=eventNames.length; i<l; i++) { + eventName = eventNames[i]; + actions = actionsFor(obj, eventName); + var actionIndex = indexOf(actions, target, method); + + if (actionIndex !== -1) { + action = actions[actionIndex].slice(); + action[2] |= SUSPENDED; + actions[actionIndex] = action; + suspendedActions.push(action); + } + } + + function tryable() { return callback.call(target); } + + function finalizer() { + for (i = 0, l = suspendedActions.length; i < l; i++) { + suspendedActions[i][2] &= ~SUSPENDED; + } + } + + return Ember.tryFinally(tryable, finalizer); +} + +/** + @private + + Return a list of currently watched events + + @method watchedEvents + @for Ember + @param obj +*/ +function watchedEvents(obj) { + var listeners = obj[META_KEY].listeners, ret = []; + + if (listeners) { + for(var eventName in listeners) { + if (listeners[eventName]) { ret.push(eventName); } + } + } + return ret; +} + +/** + @method sendEvent + @for Ember + @param obj + @param {String} eventName + @param {Array} params + @param {Array} actions + @return true +*/ +function sendEvent(obj, eventName, params, actions) { + // first give object a chance to handle it + if (obj !== Ember && 'function' === typeof obj.sendEvent) { + obj.sendEvent(eventName, params); + } + + if (!actions) { + var meta = obj[META_KEY]; + actions = meta && meta.listeners && meta.listeners[eventName]; + } + + if (!actions) { return; } + + for (var i = actions.length - 1; i >= 0; i--) { // looping in reverse for once listeners + var action = actions[i]; + if (!action) { continue; } + var target = action[0], method = action[1], flags = action[2]; + if (flags & SUSPENDED) { continue; } + if (flags & ONCE) { removeListener(obj, eventName, target, method); } + if (!target) { target = obj; } + if ('string' === typeof method) { method = target[method]; } + if (params) { + method.apply(target, params); + } else { + method.call(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() { +var guidFor = Ember.guidFor, + sendEvent = Ember.sendEvent; + +/* + this.observerSet = { + [senderGuid]: { // variable name: `keySet` + [keyName]: listIndex + } + }, + this.observers = [ + { + sender: obj, + keyName: keyName, + eventName: eventName, + listeners: [ + [target, method, flags] + ] + }, + ... + ] +*/ +var ObserverSet = Ember._ObserverSet = function() { + this.clear(); }; -// 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"; +ObserverSet.prototype.add = function(sender, keyName, eventName) { + var observerSet = this.observerSet, + observers = this.observers, + senderGuid = guidFor(sender), + keySet = observerSet[senderGuid], + index; - if (this === void 0 || this === null) { - throw new TypeError(); + 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; +}; - var t = Object(this); - var len = t.length >>> 0; - if (typeof fun !== "function") { - throw new TypeError(); +ObserverSet.prototype.flush = function() { + var observers = this.observers, i, len, observer, sender; + this.clear(); + for (i=0, len=observers.length; i < len; ++i) { + observer = observers[i]; + sender = observer.sender; + if (sender.isDestroying || sender.isDestroyed) { continue; } + sendEvent(sender, observer.eventName, [sender, observer.keyName], observer.listeners); } +}; - var thisp = arguments[1]; - for (var i = 0; i < len; i++) { - if (i in t) { - fun.call(thisp, t[i], i, t); +ObserverSet.prototype.clear = function() { + this.observerSet = {}; + this.observers = []; +}; +})(); + + + +(function() { +var metaFor = Ember.meta, + guidFor = Ember.guidFor, + tryFinally = Ember.tryFinally, + sendEvent = Ember.sendEvent, + listenersUnion = Ember.listenersUnion, + listenersDiff = Ember.listenersDiff, + ObserverSet = Ember._ObserverSet, + beforeObserverSet = new ObserverSet(), + observerSet = new ObserverSet(), + deferred = 0; + +// .......................................................... +// 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} +*/ +var propertyWillChange = Ember.propertyWillChange = function(obj, keyName) { + var m = metaFor(obj, false), + watching = m.watching[keyName] > 0 || keyName === 'length', + proto = m.proto, + desc = m.descs[keyName]; + + if (!watching) { return; } + if (proto === obj) { return; } + if (desc && desc.willChange) { desc.willChange(obj, keyName); } + dependentKeysWillChange(obj, keyName, m); + chainsWillChange(obj, keyName, m); + notifyBeforeObservers(obj, keyName); +}; + +/** + 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} +*/ +var propertyDidChange = Ember.propertyDidChange = function(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); + notifyObservers(obj, keyName); +}; + +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; } +} + +function iterDeps(method, obj, depKey, seen, meta) { + var guid = guidFor(obj); + if (!seen[guid]) seen[guid] = {}; + if (seen[guid][depKey]) return; + seen[guid][depKey] = true; + + var deps = meta.deps; + deps = deps && deps[depKey]; + if (deps) { + for(var key in deps) { + var desc = meta.descs[key]; + if (desc && desc._suspended === obj) continue; + method(obj, key); } } +} + +var chainsWillChange = function(obj, keyName, m, arg) { + if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do + + var nodes = m.chainWatchers; + + nodes = nodes[keyName]; + if (!nodes) { return; } + + for(var i = 0, l = nodes.length; i < l; i++) { + nodes[i].willChange(arg); + } }; -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; } +var chainsDidChange = function(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); } - return -1; }; -Ember.ArrayPolyfills = { - map: arrayMap, - forEach: arrayForEach, - indexOf: arrayIndexOf +Ember.overrideChains = function(obj, keyName, m) { + chainsDidChange(obj, keyName, m, true); }; -if (Ember.SHIM_ES5) { - if (!Array.prototype.map) { - Array.prototype.map = arrayMap; +/** + @method beginPropertyChanges + @chainable +*/ +var beginPropertyChanges = Ember.beginPropertyChanges = function() { + deferred++; +}; + +/** + @method endPropertyChanges +*/ +var endPropertyChanges = Ember.endPropertyChanges = function() { + deferred--; + if (deferred<=0) { + beforeObserverSet.clear(); + observerSet.flush(); } +}; - if (!Array.prototype.forEach) { - Array.prototype.forEach = arrayForEach; +/** + Make a series of property changes together in an + exception-safe way. + + ```javascript + Ember.changeProperties(function() { + obj1.set('foo', mayBlowUpWhenSet); + obj2.set('bar', baz); + }); + ``` + + @method changeProperties + @param {Function} callback + @param [binding] +*/ +Ember.changeProperties = function(cb, binding){ + beginPropertyChanges(); + tryFinally(cb, endPropertyChanges, binding); +}; + +var notifyBeforeObservers = function(obj, keyName) { + if (obj.isDestroying) { return; } + + var eventName = keyName + ':before', listeners, diff; + if (deferred) { + listeners = beforeObserverSet.add(obj, keyName, eventName); + diff = listenersDiff(obj, eventName, listeners); + sendEvent(obj, eventName, [obj, keyName], diff); + } else { + sendEvent(obj, eventName, [obj, keyName]); } +}; - if (!Array.prototype.indexOf) { - Array.prototype.indexOf = arrayIndexOf; +var notifyObservers = function(obj, keyName) { + if (obj.isDestroying) { return; } + + var eventName = keyName + ':change', listeners; + if (deferred) { + listeners = observerSet.add(obj, keyName, eventName); + listenersUnion(obj, eventName, listeners); + } else { + sendEvent(obj, eventName, [obj, keyName]); + } +}; +})(); + + + +(function() { +// META_KEY +// _getPath +// propertyWillChange, propertyDidChange + +var META_KEY = Ember.META_KEY, + MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, + IS_GLOBAL = /^([A-Z$]|([0-9][A-Z$]))/, + getPath = Ember._getPath; + +/** + Sets the value of a property on an object, respecting computed properties + and notifying observers and other listeners of the change. If the + property is not defined but the object implements the `unknownProperty` + method then that will be invoked as well. + + If you plan to run on IE8 and older browsers then you should use this + method anytime you want to set a property on an object that you don't + know for sure is private. (Properties beginning with an underscore '_' + are considered private.) + + On all newer browsers, you only need to use this method to set + properties if the property might not be defined on the object and you want + to respect the `unknownProperty` handler. Otherwise you can ignore this + method. + + @method set + @for Ember + @param {Object} obj The object to modify. + @param {String} keyName The property key to set + @param {Object} value The value to set + @return {Object} the passed value. +*/ +var set = function set(obj, keyName, value, tolerant) { + if (typeof obj === 'string') { + Ember.assert("Path '" + obj + "' must be global if no obj is given.", IS_GLOBAL.test(obj)); + value = keyName; + keyName = obj; + obj = null; + } + + if (!obj || keyName.indexOf('.') !== -1) { + return setPath(obj, keyName, value, tolerant); + } + + Ember.assert("You need to provide an object and key to `set`.", !!obj && keyName !== undefined); + Ember.assert('calling set on destroyed object', !obj.isDestroyed); + + var meta = obj[META_KEY], desc = meta && meta.descs[keyName], + isUnknown, currentValue; + if (desc) { + desc.set(obj, keyName, value); + } else { + isUnknown = 'object' === typeof obj && !(keyName in obj); + + // setUnknownProperty is called if `obj` is an object, + // the property does not already exist, and the + // `setUnknownProperty` method exists on the object + if (isUnknown && 'function' === typeof obj.setUnknownProperty) { + obj.setUnknownProperty(keyName, value); + } else if (meta && meta.watching[keyName] > 0) { + if (MANDATORY_SETTER) { + currentValue = meta.values[keyName]; + } else { + currentValue = obj[keyName]; + } + // only trigger a change if the value has changed + if (value !== currentValue) { + Ember.propertyWillChange(obj, keyName); + if (MANDATORY_SETTER) { + if (currentValue === undefined && !(keyName in obj)) { + Ember.defineProperty(obj, keyName, null, value); // setup mandatory setter + } else { + meta.values[keyName] = value; + } + } else { + obj[keyName] = value; + } + Ember.propertyDidChange(obj, keyName); + } + } else { + obj[keyName] = value; + } } + return value; +}; + +// Currently used only by Ember Data tests +if (Ember.config.overrideAccessors) { + Ember.set = set; + Ember.config.overrideAccessors(); + set = Ember.set; } +function setPath(root, path, value, tolerant) { + var keyName; + + // get the last part of the path + keyName = path.slice(path.lastIndexOf('.') + 1); + + // get the first part of the part + path = path.slice(0, path.length-(keyName.length+1)); + + // unless the path is this, look up the first part to + // get the root + if (path !== 'this') { + root = getPath(root, path); + } + + if (!keyName || keyName.length === 0) { + throw new Error('You passed an empty path'); + } + + if (!root) { + if (tolerant) { return; } + else { throw new Error('Object in path '+path+' could not be found or was destroyed.'); } + } + + return set(root, keyName, value); +} + +Ember.set = set; +Ember.setPath = Ember.deprecateFunc('setPath is deprecated since set now supports paths', Ember.set); + +/** + Error-tolerant form of `Ember.set`. Will not blow up if any part of the + chain is `undefined`, `null`, or destroyed. + + This is primarily used when syncing bindings, which may try to update after + an object has been destroyed. + + @method trySet + @for Ember + @param {Object} obj The object to modify. + @param {String} path The property path to set + @param {Object} value The value to set +*/ +Ember.trySet = function(root, path, value) { + return set(root, path, value, true); +}; +Ember.trySetPath = Ember.deprecateFunc('trySetPath has been renamed to trySet', Ember.trySet); + })(); @@ -1518,7 +2594,9 @@ if (Ember.SHIM_ES5) { 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, +var get = Ember.get, + set = Ember.set, + guidFor = Ember.guidFor, indexOf = Ember.ArrayPolyfills.indexOf; var copy = function(obj) { @@ -1537,6 +2615,7 @@ var copyMap = function(original, newObject) { newObject.keys = keys; newObject.values = values; + newObject.length = original.length; return newObject; }; @@ -1633,7 +2712,7 @@ OrderedSet.prototype = { */ forEach: function(fn, self) { // allow mutation during iteration - var list = this.list.slice(); + var list = this.toArray(); for (var i = 0, j = list.length; i < j; i++) { fn.call(self, list[i]); @@ -1656,7 +2735,7 @@ OrderedSet.prototype = { var set = new OrderedSet(); set.presenceSet = copy(this.presenceSet); - set.list = this.list.slice(); + set.list = this.toArray(); return set; } @@ -1697,11 +2776,21 @@ Map.create = function() { Map.prototype = { /** + This property will change as the number of objects in the map changes. + + @property length + @type number + @default 0 + */ + length: 0, + + + /** Retrieve the value associated with a given key. @method get - @param {anything} key - @return {anything} the value associated with the key, or `undefined` + @param {*} key + @return {*} the value associated with the key, or `undefined` */ get: function(key) { var values = this.values, @@ -1715,8 +2804,8 @@ Map.prototype = { provided, the new value will replace the old value. @method set - @param {anything} key - @param {anything} value + @param {*} key + @param {*} value */ set: function(key, value) { var keys = this.keys, @@ -1725,13 +2814,14 @@ Map.prototype = { keys.add(key); values[guid] = value; + set(this, 'length', keys.list.length); }, /** Removes a value from the map for an associated key. @method remove - @param {anything} key + @param {*} key @return {Boolean} true if an item was removed, false otherwise */ remove: function(key) { @@ -1739,13 +2829,12 @@ Map.prototype = { // to use in browsers that are not ES6 friendly; var keys = this.keys, values = this.values, - guid = guidFor(key), - value; + guid = guidFor(key); if (values.hasOwnProperty(guid)) { keys.remove(key); - value = values[guid]; delete values[guid]; + set(this, 'length', keys.list.length); return true; } else { return false; @@ -1756,7 +2845,7 @@ Map.prototype = { Check whether a key is present. @method has - @param {anything} key + @param {*} key @return {Boolean} true if the item was present, false otherwise */ has: function(key) { @@ -1774,7 +2863,7 @@ Map.prototype = { @method forEach @param {Function} callback - @param {anything} self if passed, the `this` value inside the + @param {*} self if passed, the `this` value inside the callback. By default, `this` is the map. */ forEach: function(callback, self) { @@ -1803,7 +2892,7 @@ Map.prototype = { @private @constructor @param [options] - @param {anything} [options.defaultValue] + @param {*} [options.defaultValue] */ var MapWithDefault = Ember.MapWithDefault = function(options) { Map.call(this); @@ -1814,7 +2903,7 @@ var MapWithDefault = Ember.MapWithDefault = function(options) { @method create @static @param [options] - @param {anything} [options.defaultValue] + @param {*} [options.defaultValue] @return {Ember.MapWithDefault|Ember.Map} If options are passed, returns `Ember.MapWithDefault` otherwise returns `Ember.Map` */ @@ -1832,8 +2921,8 @@ 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 + @param {*} key + @return {*} the value associated with the key, or the default value */ MapWithDefault.prototype.get = function(key) { var hasValue = this.has(key); @@ -1866,322 +2955,6 @@ MapWithDefault.prototype.copy = 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 use those. -// Otherwise simulate accessors by looking up the property directly on the -// object. - -/** - Gets the value of a property on an object. If the property is computed, - the function will be invoked. If the property is not defined but the - object implements the `unknownProperty` method then that will be invoked. - - If you plan to run on IE8 and older browsers then you should use this - method anytime you want to retrieve a property on an object that you don't - know for sure is private. (Properties beginning with an underscore '_' - are considered private.) - - On all newer browsers, you only need to use this method to retrieve - properties if the property might not be defined on the object and you want - to respect the `unknownProperty` handler. Otherwise you can ignore this - method. - - Note that if the object itself is `undefined`, this method will throw - an error. - - @method get - @for Ember - @param {Object} obj The object to retrieve from. - @param {String} keyName The property key to retrieve - @return {Object} the property value or `null`. -*/ -get = function get(obj, keyName) { - // Helpers that operate with 'this' within an #each - if (keyName === '') { - return obj; - } - - if (!keyName && 'string'===typeof obj) { - keyName = obj; - obj = null; - } - - if (!obj || keyName.indexOf('.') !== -1) { - Ember.assert("Cannot call get with '"+ keyName +"' on an undefined object.", obj !== undefined); - return getPath(obj, keyName); - } - - Ember.assert("You need to provide an object and key to `get`.", !!obj && keyName); - - var meta = obj[META_KEY], desc = meta && meta.descs[keyName], ret; - if (desc) { - return desc.get(obj, keyName); - } else { - if (MANDATORY_SETTER && meta && meta.watching[keyName] > 0) { - ret = meta.values[keyName]; - } else { - ret = obj[keyName]; - } - - if (ret === undefined && - 'object' === typeof obj && !(keyName in obj) && 'function' === typeof obj.unknownProperty) { - return obj.unknownProperty(keyName); - } - - return ret; - } -}; - -/** - Sets the value of a property on an object, respecting computed properties - and notifying observers and other listeners of the change. If the - property is not defined but the object implements the `unknownProperty` - method then that will be invoked as well. - - If you plan to run on IE8 and older browsers then you should use this - method anytime you want to set a property on an object that you don't - know for sure is private. (Properties beginning with an underscore '_' - are considered private.) - - On all newer browsers, you only need to use this method to set - properties if the property might not be defined on the object and you want - to respect the `unknownProperty` handler. Otherwise you can ignore this - method. - - @method set - @for Ember - @param {Object} obj The object to modify. - @param {String} keyName The property key to set - @param {Object} value The value to set - @return {Object} the passed value. -*/ -set = function set(obj, keyName, value, tolerant) { - if (typeof obj === 'string') { - Ember.assert("Path '" + obj + "' must be global if no obj is given.", IS_GLOBAL.test(obj)); - value = keyName; - keyName = obj; - obj = null; - } - - if (!obj || keyName.indexOf('.') !== -1) { - return setPath(obj, keyName, value, tolerant); - } - - Ember.assert("You need to provide an object and key to `set`.", !!obj && keyName !== undefined); - Ember.assert('calling set on destroyed object', !obj.isDestroyed); - - var meta = obj[META_KEY], desc = meta && meta.descs[keyName], - isUnknown, currentValue; - if (desc) { - desc.set(obj, keyName, value); - } else { - isUnknown = 'object' === typeof obj && !(keyName in obj); - - // setUnknownProperty is called if `obj` is an object, - // the property does not already exist, and the - // `setUnknownProperty` method exists on the object - if (isUnknown && 'function' === typeof obj.setUnknownProperty) { - obj.setUnknownProperty(keyName, value); - } else if (meta && meta.watching[keyName] > 0) { - if (MANDATORY_SETTER) { - currentValue = meta.values[keyName]; - } else { - currentValue = obj[keyName]; - } - // only trigger a change if the value has changed - if (value !== currentValue) { - Ember.propertyWillChange(obj, keyName); - if (MANDATORY_SETTER) { - if (currentValue === undefined && !(keyName in obj)) { - Ember.defineProperty(obj, keyName, null, value); // setup mandatory setter - } else { - meta.values[keyName] = value; - } - } else { - obj[keyName] = value; - } - Ember.propertyDidChange(obj, keyName); - } - } else { - obj[keyName] = value; - } - } - return value; -}; - -// Currently used only by Ember Data tests -if (Ember.config.overrideAccessors) { - Ember.get = get; - Ember.set = set; - Ember.config.overrideAccessors(); - get = Ember.get; - set = Ember.set; -} - -function firstKey(path) { - return path.match(FIRST_KEY)[0]; -} - -// assumes path is already normalized -function normalizeTuple(target, path) { - var hasThis = HAS_THIS.test(path), - isGlobal = !hasThis && IS_GLOBAL_PATH.test(path), - key; - - if (!target || isGlobal) target = Ember.lookup; - if (hasThis) path = path.slice(5); - - if (target === Ember.lookup) { - key = firstKey(path); - target = get(target, key); - path = path.slice(key.length+1); - } - - // must return some kind of path to be valid else other things will break. - if (!path || path.length===0) throw new Error('Invalid Path'); - - return [ target, path ]; -} - -function getPath(root, path) { - var hasThis, parts, tuple, idx, len; - - // If there is no root and path is a key name, return that - // property from the global object. - // E.g. get('Ember') -> Ember - if (root === null && path.indexOf('.') === -1) { return get(Ember.lookup, path); } - - // detect complicated paths and normalize them - hasThis = HAS_THIS.test(path); - - if (!root || hasThis) { - tuple = normalizeTuple(root, path); - root = tuple[0]; - path = tuple[1]; - tuple.length = 0; - } - - parts = path.split("."); - len = parts.length; - for (idx=0; root && idx<len; idx++) { - root = get(root, parts[idx], true); - if (root && root.isDestroyed) { return undefined; } - } - return root; -} - -function setPath(root, path, value, tolerant) { - var keyName; - - // get the last part of the path - keyName = path.slice(path.lastIndexOf('.') + 1); - - // get the first part of the part - path = path.slice(0, path.length-(keyName.length+1)); - - // unless the path is this, look up the first part to - // get the root - if (path !== 'this') { - root = getPath(root, path); - } - - if (!keyName || keyName.length === 0) { - throw new Error('You passed an empty path'); - } - - if (!root) { - if (tolerant) { return; } - else { throw new Error('Object in path '+path+' could not be found or was destroyed.'); } - } - - return set(root, keyName, value); -} - -/** - @private - - Normalizes a target/path pair to reflect that actual target/path that should - be observed, etc. This takes into account passing in global property - paths (i.e. a path beginning with a captial letter not defined on the - target) and * separators. - - @method normalizeTuple - @for Ember - @param {Object} target The current target. May be `null`. - @param {String} path A path on the target or a global property path. - @return {Array} a temporary array with the normalized target/path pair. -*/ -Ember.normalizeTuple = function(target, path) { - return normalizeTuple(target, path); -}; - -Ember.getWithDefault = function(root, key, defaultValue) { - var value = get(root, key); - - if (value === undefined) { return defaultValue; } - return value; -}; - - -Ember.get = get; -Ember.getPath = Ember.deprecateFunc('getPath is deprecated since get now supports paths', Ember.get); - -Ember.set = set; -Ember.setPath = Ember.deprecateFunc('setPath is deprecated since set now supports paths', Ember.set); - -/** - Error-tolerant form of `Ember.set`. Will not blow up if any part of the - chain is `undefined`, `null`, or destroyed. - - This is primarily used when syncing bindings, which may try to update after - an object has been destroyed. - - @method trySet - @for Ember - @param {Object} obj The object to modify. - @param {String} path The property path to set - @param {Object} value The value to set -*/ -Ember.trySet = function(root, path, value) { - return set(root, path, value, true); -}; -Ember.trySetPath = Ember.deprecateFunc('trySetPath has been renamed to trySet', Ember.trySet); - -/** - Returns true if the provided path is global (e.g., `MyApp.fooController.bar`) - instead of local (`foo.bar.baz`). - - @method isGlobalPath - @for Ember - @private - @param {String} path - @return Boolean -*/ -Ember.isGlobalPath = function(path) { - return IS_GLOBAL.test(path); -}; - - -})(); - - - -(function() { -/** -@module ember-metal -*/ - var META_KEY = Ember.META_KEY, metaFor = Ember.meta, objectDefineProperty = Ember.platform.defineProperty; @@ -2193,7 +2966,7 @@ var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; // /** - Objects of this type can implement an interface to responds requests to + Objects of this type can implement an interface to respond to requests to get and set. The default implementation handles simple properties. You generally won't need to create or subclass this directly. @@ -2203,7 +2976,7 @@ var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; @private @constructor */ -var Descriptor = Ember.Descriptor = function() {}; +Ember.Descriptor = function() {}; // .......................................................... // DEFINING PROPERTIES API @@ -2263,7 +3036,7 @@ var DEFAULT_GETTER_FUNCTION = Ember.DEFAULT_GETTER_FUNCTION = function(name) { @param {Ember.Descriptor} [desc] an instance of `Ember.Descriptor` (typically a computed property) or an ES5 descriptor. You must provide this or `data` but not both. - @param {anything} [data] something other than a descriptor, that will + @param {*} [data] something other than a descriptor, that will become the explicit value of this property. */ Ember.defineProperty = function(obj, keyName, desc, data, meta) { @@ -2334,119 +3107,8 @@ Ember.defineProperty = function(obj, keyName, desc, data, meta) { (function() { -// Ember.tryFinally -/** -@module ember-metal -*/ - -var AFTER_OBSERVERS = ':change'; -var BEFORE_OBSERVERS = ':before'; - -var guidFor = Ember.guidFor; - -var deferred = 0; - -/* - this.observerSet = { - [senderGuid]: { // variable name: `keySet` - [keyName]: listIndex - } - }, - this.observers = [ - { - sender: obj, - keyName: keyName, - eventName: eventName, - listeners: [ - [target, method, onceFlag, suspendedFlag] - ] - }, - ... - ] -*/ -function ObserverSet() { - this.clear(); -} - -ObserverSet.prototype.add = function(sender, keyName, eventName) { - var observerSet = this.observerSet, - observers = this.observers, - senderGuid = Ember.guidFor(sender), - keySet = observerSet[senderGuid], - index; - - if (!keySet) { - observerSet[senderGuid] = keySet = {}; - } - index = keySet[keyName]; - if (index === undefined) { - index = observers.push({ - sender: sender, - keyName: keyName, - eventName: eventName, - listeners: [] - }) - 1; - keySet[keyName] = index; - } - return observers[index].listeners; -}; - -ObserverSet.prototype.flush = function() { - var observers = this.observers, i, len, observer, sender; - this.clear(); - for (i=0, len=observers.length; i < len; ++i) { - observer = observers[i]; - sender = observer.sender; - if (sender.isDestroying || sender.isDestroyed) { continue; } - Ember.sendEvent(sender, observer.eventName, [sender, observer.keyName], observer.listeners); - } -}; - -ObserverSet.prototype.clear = function() { - this.observerSet = {}; - this.observers = []; -}; - -var beforeObserverSet = new ObserverSet(), observerSet = new ObserverSet(); - -/** - @method beginPropertyChanges - @chainable -*/ -Ember.beginPropertyChanges = function() { - deferred++; -}; - -/** - @method endPropertyChanges -*/ -Ember.endPropertyChanges = function() { - deferred--; - if (deferred<=0) { - beforeObserverSet.clear(); - observerSet.flush(); - } -}; - -/** - Make a series of property changes together in an - exception-safe way. - - ```javascript - Ember.changeProperties(function() { - obj1.set('foo', mayBlowUpWhenSet); - obj2.set('bar', baz); - }); - ``` - - @method changeProperties - @param {Function} callback - @param [binding] -*/ -Ember.changeProperties = function(cb, binding){ - Ember.beginPropertyChanges(); - Ember.tryFinally(cb, Ember.endPropertyChanges, binding); -}; +var changeProperties = Ember.changeProperties, + set = Ember.set; /** Set a list of properties on an object. These properties are set inside @@ -2459,214 +3121,116 @@ Ember.changeProperties = function(cb, binding){ @return target */ Ember.setProperties = function(self, hash) { - Ember.changeProperties(function(){ + changeProperties(function(){ for(var prop in hash) { - if (hash.hasOwnProperty(prop)) Ember.set(self, prop, hash[prop]); + if (hash.hasOwnProperty(prop)) { set(self, prop, hash[prop]); } } }); return self; }; +})(); -function changeEvent(keyName) { - return keyName+AFTER_OBSERVERS; -} - -function beforeEvent(keyName) { - return keyName+BEFORE_OBSERVERS; -} - -/** - @method addObserver - @param obj - @param {String} path - @param {Object|Function} targetOrMethod - @param {Function|String} [method] -*/ -Ember.addObserver = function(obj, path, target, method) { - Ember.addListener(obj, changeEvent(path), target, method); - Ember.watch(obj, path); - return this; -}; -Ember.observersFor = function(obj, path) { - return Ember.listenersFor(obj, changeEvent(path)); -}; - -/** - @method removeObserver - @param obj - @param {String} path - @param {Object|Function} targetOrMethod - @param {Function|String} [method] -*/ -Ember.removeObserver = function(obj, path, target, method) { - Ember.unwatch(obj, path); - Ember.removeListener(obj, changeEvent(path), target, method); - return this; -}; +(function() { +var metaFor = Ember.meta, // utils.js + typeOf = Ember.typeOf, // utils.js + MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, + o_defineProperty = Ember.platform.defineProperty; -/** - @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; -}; +Ember.watchKey = function(obj, keyName) { + // can't watch length on Array - it is special... + if (keyName === 'length' && typeOf(obj) === 'array') { return; } -// 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); -}; + var m = metaFor(obj), watching = m.watching, desc; -Ember._suspendObserver = function(obj, path, target, method, callback) { - return Ember._suspendListener(obj, changeEvent(path), target, method, callback); -}; + // activate watching first time + if (!watching[keyName]) { + watching[keyName] = 1; + desc = m.descs[keyName]; + if (desc && desc.willWatch) { desc.willWatch(obj, keyName); } -var map = Ember.ArrayPolyfills.map; + if ('function' === typeof obj.willWatchProperty) { + obj.willWatchProperty(keyName); + } -Ember._suspendBeforeObservers = function(obj, paths, target, method, callback) { - var events = map.call(paths, beforeEvent); - return Ember._suspendListeners(obj, events, target, method, callback); + if (MANDATORY_SETTER && keyName in obj) { + m.values[keyName] = obj[keyName]; + o_defineProperty(obj, keyName, { + configurable: true, + enumerable: true, + set: Ember.MANDATORY_SETTER_FUNCTION, + get: Ember.DEFAULT_GETTER_FUNCTION(keyName) + }); + } + } else { + watching[keyName] = (watching[keyName] || 0) + 1; + } }; -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.unwatchKey = function(obj, keyName) { + var m = metaFor(obj), watching = m.watching, desc; -Ember.notifyBeforeObservers = function(obj, keyName) { - if (obj.isDestroying) { return; } + if (watching[keyName] === 1) { + watching[keyName] = 0; + desc = m.descs[keyName]; - var eventName = beforeEvent(keyName), listeners, listenersDiff; - if (deferred) { - listeners = beforeObserverSet.add(obj, keyName, eventName); - listenersDiff = Ember.listenersDiff(obj, eventName, listeners); - Ember.sendEvent(obj, eventName, [obj, keyName], listenersDiff); - } else { - Ember.sendEvent(obj, eventName, [obj, keyName]); - } -}; + if (desc && desc.didUnwatch) { desc.didUnwatch(obj, keyName); } -Ember.notifyObservers = function(obj, keyName) { - if (obj.isDestroying) { return; } + if ('function' === typeof obj.didUnwatchProperty) { + obj.didUnwatchProperty(keyName); + } - var eventName = changeEvent(keyName), listeners; - if (deferred) { - listeners = observerSet.add(obj, keyName, eventName); - Ember.listenersUnion(obj, eventName, listeners); - } else { - Ember.sendEvent(obj, eventName, [obj, keyName]); + if (MANDATORY_SETTER && keyName in obj) { + o_defineProperty(obj, keyName, { + configurable: true, + enumerable: true, + writable: true, + value: m.values[keyName] + }); + delete m.values[keyName]; + } + } else if (watching[keyName] > 1) { + watching[keyName]--; } }; - })(); (function() { -/** -@module ember-metal -*/ - -var guidFor = Ember.guidFor, // utils.js - metaFor = Ember.meta, // utils.js - get = Ember.get, // accessors.js - set = Ember.set, // accessors.js - normalizeTuple = Ember.normalizeTuple, // accessors.js - GUID_KEY = Ember.GUID_KEY, // utils.js - META_KEY = Ember.META_KEY, // utils.js - // circular reference observer depends on Ember.watch - // we should move change events to this file or its own property_events.js +var metaFor = Ember.meta, // utils.js + get = Ember.get, // property_get.js + normalizeTuple = Ember.normalizeTuple, // property_get.js forEach = Ember.ArrayPolyfills.forEach, // array.js - FIRST_KEY = /^([^\.\*]+)/, - IS_PATH = /[\.\*]/; - -var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, -o_defineProperty = Ember.platform.defineProperty; + warn = Ember.warn, + watchKey = Ember.watchKey, + unwatchKey = Ember.unwatchKey, + propertyWillChange = Ember.propertyWillChange, + propertyDidChange = Ember.propertyDidChange, + FIRST_KEY = /^([^\.\*]+)/; function firstKey(path) { return path.match(FIRST_KEY)[0]; } -// returns true if the passed path is just a keyName -function isKeyName(path) { - return path==='*' || !IS_PATH.test(path); -} - -// .......................................................... -// DEPENDENT KEYS -// - -function iterDeps(method, obj, depKey, seen, meta) { - - var guid = guidFor(obj); - if (!seen[guid]) seen[guid] = {}; - if (seen[guid][depKey]) return; - seen[guid][depKey] = true; - - var deps = meta.deps; - deps = deps && deps[depKey]; - if (deps) { - for(var key in deps) { - var desc = meta.descs[key]; - if (desc && desc._suspended === obj) continue; - method(obj, key); - } - } -} - - -var WILL_SEEN, DID_SEEN; +var pendingQueue = []; -// called whenever a property is about to change to clear the cache of any dependent keys (and notify those properties of changes, etc...) -function dependentKeysWillChange(obj, depKey, meta) { - if (obj.isDestroying) { return; } +// attempts to add the pendingQueue chains again. If some of them end up +// back in the queue and reschedule is true, schedules a timeout to try +// again. +Ember.flushPendingChains = function() { + if (pendingQueue.length === 0) { return; } // nothing to do - var seen = WILL_SEEN, top = !seen; - if (top) { seen = WILL_SEEN = {}; } - iterDeps(propertyWillChange, obj, depKey, seen, meta); - if (top) { WILL_SEEN = null; } -} + var queue = pendingQueue; + pendingQueue = []; -// called whenever a property has just changed to update dependent keys -function dependentKeysDidChange(obj, depKey, meta) { - if (obj.isDestroying) { return; } + forEach.call(queue, function(q) { q[0].add(q[1]); }); - var seen = DID_SEEN, top = !seen; - if (top) { seen = DID_SEEN = {}; } - iterDeps(propertyDidChange, obj, depKey, seen, meta); - if (top) { DID_SEEN = null; } -} + warn('Watching an undefined global, Ember expects watched globals to be setup by the time the run loop is flushed, check for typos', pendingQueue.length === 0); +}; -// .......................................................... -// CHAIN -// function addChainWatcher(obj, keyName, node) { if (!obj || ('object' !== typeof obj)) { return; } // nothing to do @@ -2679,10 +3243,10 @@ function addChainWatcher(obj, keyName, node) { if (!nodes[keyName]) { nodes[keyName] = []; } nodes[keyName].push(node); - Ember.watch(obj, keyName); + watchKey(obj, keyName); } -function removeChainWatcher(obj, keyName, node) { +var removeChainWatcher = Ember.removeChainWatcher = function(obj, keyName, node) { if (!obj || 'object' !== typeof obj) { return; } // nothing to do var m = metaFor(obj, false); @@ -2696,24 +3260,8 @@ function removeChainWatcher(obj, keyName, node) { if (nodes[i] === node) { nodes.splice(i, 1); } } } - Ember.unwatch(obj, keyName); -} - -var pendingQueue = []; - -// attempts to add the pendingQueue chains again. If some of them end up -// back in the queue and reschedule is true, schedules a timeout to try -// again. -function flushPendingChains() { - if (pendingQueue.length === 0) { return; } // nothing to do - - var queue = pendingQueue; - pendingQueue = []; - - forEach.call(queue, function(q) { q[0].add(q[1]); }); - - Ember.warn('Watching an undefined global, Ember expects watched globals to be setup by the time the run loop is flushed, check for typos', pendingQueue.length === 0); -} + unwatchKey(obj, keyName); +}; function isProto(pvalue) { return metaFor(pvalue, false).proto === pvalue; @@ -2722,8 +3270,7 @@ function isProto(pvalue) { // A ChainNode watches a single key on an object. If you provide a starting // value for the key then the node won't actually watch it. For a root node // pass null for parent and key and object for value. -var ChainNode = function(parent, key, value) { - var obj; +var ChainNode = Ember._ChainNode = function(parent, key, value) { this._parent = parent; this._key = key; @@ -2895,9 +3442,9 @@ ChainNodePrototype.chainWillChange = function(chain, path, depth) { if (this._parent) { this._parent.chainWillChange(this, path, depth+1); } else { - if (depth > 1) { Ember.propertyWillChange(this.value(), path); } + if (depth > 1) { propertyWillChange(this.value(), path); } path = 'this.' + path; - if (this._paths[path] > 0) { Ember.propertyWillChange(this.value(), path); } + if (this._paths[path] > 0) { propertyWillChange(this.value(), path); } } }; @@ -2906,9 +3453,9 @@ ChainNodePrototype.chainDidChange = function(chain, path, depth) { if (this._parent) { this._parent.chainDidChange(this, path, depth+1); } else { - if (depth > 1) { Ember.propertyDidChange(this.value(), path); } + if (depth > 1) { propertyDidChange(this.value(), path); } path = 'this.' + path; - if (this._paths[path] > 0) { Ember.propertyDidChange(this.value(), path); } + if (this._paths[path] > 0) { propertyDidChange(this.value(), path); } } }; @@ -2944,6 +3491,24 @@ ChainNodePrototype.didChange = function(suppressEvent) { if (this._parent) { this._parent.chainDidChange(this, this._key, 1); } }; +Ember.finishChains = function(obj) { + var m = metaFor(obj, false), chains = m.chains; + if (chains) { + if (chains.value() !== obj) { + m.chains = chains = chains.copy(obj); + } + chains.didChange(true); + } +}; +})(); + + + +(function() { +var metaFor = Ember.meta, // utils.js + typeOf = Ember.typeOf, // utils.js + ChainNode = Ember._ChainNode; // chains.js + // get the chains for the current object. If the current object has // chains inherited from the proto they will be cloned and reconfigured for // the current object. @@ -2957,40 +3522,55 @@ function chainsFor(obj) { return ret; } -Ember.overrideChains = function(obj, keyName, m) { - chainsDidChange(obj, keyName, m, true); -}; +Ember.watchPath = function(obj, keyPath) { + // can't watch length on Array - it is special... + if (keyPath === 'length' && typeOf(obj) === 'array') { return; } -function chainsWillChange(obj, keyName, m, arg) { - if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do + var m = metaFor(obj), watching = m.watching; - var nodes = m.chainWatchers; + if (!watching[keyPath]) { // activate watching first time + watching[keyPath] = 1; + chainsFor(obj).add(keyPath); + } else { + watching[keyPath] = (watching[keyPath] || 0) + 1; + } +}; - nodes = nodes[keyName]; - if (!nodes) { return; } +Ember.unwatchPath = function(obj, keyPath) { + var m = metaFor(obj), watching = m.watching; - for(var i = 0, l = nodes.length; i < l; i++) { - nodes[i].willChange(arg); + if (watching[keyPath] === 1) { + watching[keyPath] = 0; + chainsFor(obj).remove(keyPath); + } else if (watching[keyPath] > 1) { + watching[keyPath]--; } -} +}; +})(); -function chainsDidChange(obj, keyName, m, arg) { - if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do - var nodes = m.chainWatchers; - nodes = nodes[keyName]; - if (!nodes) { return; } +(function() { +/** +@module ember-metal +*/ - // 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); - } -} +var metaFor = Ember.meta, // utils.js + GUID_KEY = Ember.GUID_KEY, // utils.js + META_KEY = Ember.META_KEY, // utils.js + removeChainWatcher = Ember.removeChainWatcher, + watchKey = Ember.watchKey, // watch_key.js + unwatchKey = Ember.unwatchKey, + watchPath = Ember.watchPath, // watch_path.js + unwatchPath = Ember.unwatchPath, + typeOf = Ember.typeOf, // utils.js + generateGuid = Ember.generateGuid, + IS_PATH = /[\.\*]/; -// .......................................................... -// WATCH -// +// returns true if the passed path is just a keyName +function isKeyName(path) { + return path==='*' || !IS_PATH.test(path); +} /** @private @@ -3006,40 +3586,15 @@ function chainsDidChange(obj, keyName, m, arg) { @param obj @param {String} keyName */ -Ember.watch = function(obj, keyName) { +Ember.watch = function(obj, keyPath) { // can't watch length on Array - it is special... - if (keyName === 'length' && Ember.typeOf(obj) === 'array') { return this; } - - var m = metaFor(obj), watching = m.watching, desc; - - // activate watching first time - if (!watching[keyName]) { - watching[keyName] = 1; - if (isKeyName(keyName)) { - desc = m.descs[keyName]; - if (desc && desc.willWatch) { desc.willWatch(obj, keyName); } - - if ('function' === typeof obj.willWatchProperty) { - obj.willWatchProperty(keyName); - } - - if (MANDATORY_SETTER && keyName in obj) { - m.values[keyName] = obj[keyName]; - o_defineProperty(obj, keyName, { - configurable: true, - enumerable: true, - set: Ember.MANDATORY_SETTER_FUNCTION, - get: Ember.DEFAULT_GETTER_FUNCTION(keyName) - }); - } - } else { - chainsFor(obj).add(keyName); - } + if (keyPath === 'length' && typeOf(obj) === 'array') { return; } - } else { - watching[keyName] = (watching[keyName] || 0) + 1; + if (isKeyName(keyPath)) { + watchKey(obj, keyPath); + } else { + watchPath(obj, keyPath); } - return this; }; Ember.isWatching = function isWatching(obj, key) { @@ -3047,43 +3602,17 @@ Ember.isWatching = function isWatching(obj, key) { return (meta && meta.watching[key]) > 0; }; -Ember.watch.flushPending = flushPendingChains; +Ember.watch.flushPending = Ember.flushPendingChains; -Ember.unwatch = function(obj, keyName) { +Ember.unwatch = function(obj, keyPath) { // can't watch length on Array - it is special... - if (keyName === 'length' && Ember.typeOf(obj) === 'array') { return this; } - - var m = metaFor(obj), watching = m.watching, desc; - - 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 (keyPath === 'length' && typeOf(obj) === 'array') { return; } - 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]--; + if (isKeyName(keyPath)) { + unwatchKey(obj, keyPath); + } else { + unwatchPath(obj, keyPath); } - - return this; }; /** @@ -3102,96 +3631,15 @@ Ember.rewatch = function(obj) { // make sure the object has its own guid. if (GUID_KEY in obj && !obj.hasOwnProperty(GUID_KEY)) { - Ember.generateGuid(obj, '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) { - 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 = []; /** @@ -3271,7 +3719,7 @@ var get = Ember.get, This function returns a map of unique dependencies for a given object and key. */ -function keysForDep(obj, depsMeta, depKey) { +function keysForDep(depsMeta, depKey) { var keys = depsMeta[depKey]; if (!keys) { // if there are no dependencies yet for a the given key @@ -3285,8 +3733,8 @@ function keysForDep(obj, depsMeta, depKey) { return keys; } -function metaForDeps(obj, meta) { - return keysForDep(obj, meta, 'deps'); +function metaForDeps(meta) { + return keysForDep(meta, 'deps'); } function addDependentKeys(desc, obj, keyName, meta) { @@ -3295,12 +3743,12 @@ function addDependentKeys(desc, obj, keyName, meta) { var depKeys = desc._dependentKeys, depsMeta, idx, len, depKey, keys; if (!depKeys) return; - depsMeta = metaForDeps(obj, meta); + depsMeta = metaForDeps(meta); for(idx = 0, len = depKeys.length; idx < len; idx++) { depKey = depKeys[idx]; // Lookup keys meta for depKey - keys = keysForDep(obj, depsMeta, depKey); + keys = keysForDep(depsMeta, depKey); // Increment the number of times depKey depends on keyName. keys[keyName] = (keys[keyName] || 0) + 1; // Watch the depKey @@ -3314,12 +3762,12 @@ function removeDependentKeys(desc, obj, keyName, meta) { var depKeys = desc._dependentKeys, depsMeta, idx, len, depKey, keys; if (!depKeys) return; - depsMeta = metaForDeps(obj, meta); + depsMeta = metaForDeps(meta); for(idx = 0, len = depKeys.length; idx < len; idx++) { depKey = depKeys[idx]; // Lookup keys meta for depKey - keys = keysForDep(obj, depsMeta, depKey); + keys = keysForDep(depsMeta, depKey); // Increment the number of times depKey depends on keyName. keys[keyName] = (keys[keyName] || 0) - 1; // Watch the depKey @@ -3350,24 +3798,15 @@ ComputedProperty.prototype = new Ember.Descriptor(); var ComputedPropertyPrototype = ComputedProperty.prototype; -/** - Call on a computed property to set it into cacheable mode. When in this - mode the computed property will automatically cache the return value of - your function until one of the dependent keys changes. - - ```javascript - MyApp.president = Ember.Object.create({ - fullName: function() { - return this.get('firstName') + ' ' + this.get('lastName'); +/* + Properties are cacheable by default. Computed property will automatically + cache the return value of your function until one of the dependent keys changes. - // After calculating the value of this function, Ember will - // return that value without re-executing this function until - // one of the dependent properties change. - }.property('firstName', 'lastName') - }); - ``` + Call `volatile()` to set it into non-cached mode. When in this mode + the computed property will not automatically cache the return value. - Properties are cacheable by default. + However, if a property is properly observable, there is no reason to disable + caching. @method cacheable @param {Boolean} aFlag optional set to `false` to disable caching @@ -3631,7 +4070,6 @@ ComputedPropertyPrototype.teardown = function(obj, keyName) { The function should accept two parameters, key and value. If value is not undefined you should set the value first. In either case return the current value of the property. - @method computed @for Ember @param {Function} func The computed property function. @@ -3669,7 +4107,7 @@ Ember.computed = function(func) { @param {Object} obj the object whose property you want to check @param {String} key the name of the property whose cached value you want to return - @return {any} the cached value + @return {*} the cached value */ Ember.cacheFor = function cacheFor(obj, key) { var cache = metaFor(obj, false).cache; @@ -3873,7 +4311,7 @@ registerComputedWithProperties('or', function(properties) { @for Ember @param {String} dependentKey, [dependentKey...] @return {Ember.ComputedProperty} computed property which returns - the first trouthy value of given list of properties. + the first truthy value of given list of properties. */ registerComputedWithProperties('any', function(properties) { for (var key in properties) { @@ -3924,6 +4362,48 @@ Ember.computed.alias = function(dependentKey) { }; /** + @method computed.oneWay + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which creates an + one way computed property to the original value for property. + + Where `computed.alias` aliases `get` and `set`, and allows for bidirectional + data flow, `computed.oneWay` only provides an aliased `get`. The `set` will + not mutate the upstream property, rather causes the current property to + become the value set. This causes the downstream property to permentantly + diverge from the upstream property. + + ```javascript + User = Ember.Object.extend({ + firstName: null, + lastName: null, + nickName: Ember.computed.oneWay('firstName') + }); + + user = User.create({ + firstName: 'Teddy', + lastName: 'Zeenny' + }); + + user.get('nickName'); + # 'Teddy' + + user.set('nickName', 'TeddyBear'); + # 'TeddyBear' + + user.get('firstName'); + # 'Teddy' + ``` +*/ +Ember.computed.oneWay = function(dependentKey) { + return Ember.computed(dependentKey, function() { + return get(this, dependentKey); + }); +}; + + +/** @method computed.defaultTo @for Ember @param {String} defaultPath @@ -3932,7 +4412,6 @@ Ember.computed.alias = function(dependentKey) { */ Ember.computed.defaultTo = function(defaultPath) { return Ember.computed(function(key, newValue, cachedValue) { - var result; if (arguments.length === 1) { return cachedValue != null ? cachedValue : get(this, defaultPath); } @@ -3945,567 +4424,611 @@ Ember.computed.defaultTo = function(defaultPath) { (function() { +// Ember.tryFinally /** @module ember-metal */ -var o_create = Ember.create, - metaFor = Ember.meta, - META_KEY = Ember.META_KEY; - -/* - The event system uses a series of nested hashes to store listeners on an - object. When a listener is registered, or when an event arrives, these - hashes are consulted to determine which target and action pair to invoke. - - The hashes are stored in the object's meta hash, and look like this: - - // Object's meta hash - { - listeners: { // variable name: `listenerSet` - "foo:changed": [ // variable name: `actions` - [target, method, onceFlag, suspendedFlag] - ] - } - } - -*/ - -function indexOf(array, target, method) { - var index = -1; - for (var i = 0, l = array.length; i < l; i++) { - if (target === array[i][0] && method === array[i][1]) { index = i; break; } - } - return index; -} - -function actionsFor(obj, eventName) { - var meta = metaFor(obj, true), - actions; - - if (!meta.listeners) { meta.listeners = {}; } - - if (!meta.hasOwnProperty('listeners')) { - // setup inherited copy of the listeners object - meta.listeners = o_create(meta.listeners); - } - - actions = meta.listeners[eventName]; - - // if there are actions, but the eventName doesn't exist in our listeners, then copy them from the prototype - if (actions && !meta.listeners.hasOwnProperty(eventName)) { - actions = meta.listeners[eventName] = meta.listeners[eventName].slice(); - } else if (!actions) { - actions = meta.listeners[eventName] = []; - } +var AFTER_OBSERVERS = ':change'; +var BEFORE_OBSERVERS = ':before'; - return actions; +function changeEvent(keyName) { + return keyName+AFTER_OBSERVERS; } -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 beforeEvent(keyName) { + return keyName+BEFORE_OBSERVERS; } -function actionsDiff(obj, eventName, otherActions) { - var meta = obj[META_KEY], - actions = meta && meta.listeners && meta.listeners[eventName], - diffActions = []; - - if (!actions) { return; } - for (var i = actions.length - 1; i >= 0; i--) { - var target = actions[i][0], - method = actions[i][1], - once = actions[i][2], - suspended = actions[i][3], - actionIndex = indexOf(otherActions, target, method); - - if (actionIndex !== -1) { continue; } - - otherActions.push([target, method, once, suspended]); - diffActions.push([target, method, once, suspended]); - } +/** + @method addObserver + @param obj + @param {String} path + @param {Object|Function} targetOrMethod + @param {Function|String} [method] +*/ +Ember.addObserver = function(obj, path, target, method) { + Ember.addListener(obj, changeEvent(path), target, method); + Ember.watch(obj, path); + return this; +}; - return diffActions; -} +Ember.observersFor = function(obj, path) { + return Ember.listenersFor(obj, changeEvent(path)); +}; /** - Add an event listener - - @method addListener - @for Ember + @method removeObserver @param obj - @param {String} eventName - @param {Object|Function} targetOrMethod A target object or a function - @param {Function|String} method A function or the name of a function to be called on `target` - @param {Boolean} once A flag whether a function should only be called once + @param {String} path + @param {Object|Function} targetOrMethod + @param {Function|String} [method] */ -function addListener(obj, eventName, target, method, once) { - Ember.assert("You must pass at least an object and event name to Ember.addListener", !!obj && !!eventName); +Ember.removeObserver = function(obj, path, target, method) { + Ember.unwatch(obj, path); + Ember.removeListener(obj, changeEvent(path), target, method); + return this; +}; - if (!method && 'function' === typeof target) { - method = target; - target = null; - } +/** + @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; +}; - var actions = actionsFor(obj, eventName), - actionIndex = indexOf(actions, target, method); +// Suspend observer during callback. +// +// This should only be used by the target of the observer +// while it is setting the observed path. +Ember._suspendBeforeObserver = function(obj, path, target, method, callback) { + return Ember._suspendListener(obj, beforeEvent(path), target, method, callback); +}; - if (actionIndex !== -1) { return; } +Ember._suspendObserver = function(obj, path, target, method, callback) { + return Ember._suspendListener(obj, changeEvent(path), target, method, callback); +}; - actions.push([target, method, once, undefined]); +var map = Ember.ArrayPolyfills.map; - if ('function' === typeof obj.didAddListener) { - obj.didAddListener(eventName, target, method); - } -} +Ember._suspendBeforeObservers = function(obj, paths, target, method, callback) { + var events = map.call(paths, beforeEvent); + return Ember._suspendListeners(obj, events, target, method, callback); +}; -/** - Remove an event listener +Ember._suspendObservers = function(obj, paths, target, method, callback) { + var events = map.call(paths, changeEvent); + return Ember._suspendListeners(obj, events, target, method, callback); +}; - Arguments should match those passed to {{#crossLink "Ember/addListener"}}{{/crossLink}} +Ember.beforeObserversFor = function(obj, path) { + return Ember.listenersFor(obj, beforeEvent(path)); +}; - @method removeListener - @for Ember +/** + @method removeBeforeObserver @param obj - @param {String} eventName - @param {Object|Function} targetOrMethod A target object or a function - @param {Function|String} method A function or the name of a function to be called on `target` + @param {String} path + @param {Object|Function} targetOrMethod + @param {Function|String} [method] */ -function removeListener(obj, eventName, target, method) { - Ember.assert("You must pass at least an object and event name to Ember.removeListener", !!obj && !!eventName); - - if (!method && 'function' === typeof target) { - method = target; - target = null; - } +Ember.removeBeforeObserver = function(obj, path, target, method) { + Ember.unwatch(obj, path); + Ember.removeListener(obj, beforeEvent(path), target, method); + return this; +}; +})(); - 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); +(function() { +define("backburner", + ["backburner/deferred_action_queues","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var DeferredActionQueues = __dependency1__.DeferredActionQueues; + + var slice = [].slice, + pop = [].pop, + debouncees = [], + timers = [], + autorun, laterTimer, laterTimerExpiresAt; + + function Backburner(queueNames, options) { + this.queueNames = queueNames; + this.options = options || {}; + if (!this.options.defaultQueue) { + this.options.defaultQueue = queueNames[0]; + } + this.instanceStack = []; } - } - if (method) { - _removeListener(target, method); - } else { - var meta = obj[META_KEY], - actions = meta && meta.listeners && meta.listeners[eventName]; + Backburner.prototype = { + queueNames: null, + options: null, + currentInstance: null, + instanceStack: null, - if (!actions) { return; } - for (var i = actions.length - 1; i >= 0; i--) { - _removeListener(actions[i][0], actions[i][1]); - } - } -} + begin: function() { + var onBegin = this.options && this.options.onBegin, + previousInstance = this.currentInstance; -/** - @private + if (previousInstance) { + this.instanceStack.push(previousInstance); + } - Suspend listener during callback. + this.currentInstance = new DeferredActionQueues(this.queueNames, this.options); + if (onBegin) { + onBegin(this.currentInstance, previousInstance); + } + }, - 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. + end: function() { + var onEnd = this.options && this.options.onEnd, + currentInstance = this.currentInstance, + nextInstance = null; - @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; - } + try { + currentInstance.flush(); + } finally { + this.currentInstance = null; - var actions = actionsFor(obj, eventName), - actionIndex = indexOf(actions, target, method), - action; + if (this.instanceStack.length) { + nextInstance = this.instanceStack.pop(); + this.currentInstance = nextInstance; + } - 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 - } + if (onEnd) { + onEnd(currentInstance, nextInstance); + } + } + }, - function tryable() { return callback.call(target); } - function finalizer() { if (action) { action[3] = undefined; } } + run: function(target, method /*, args */) { + var ret; + this.begin(); - return Ember.tryFinally(tryable, finalizer); -} + if (!method) { + method = target; + target = null; + } -/** - @private + if (typeof method === 'string') { + method = target[method]; + } - Suspend listener during callback. + // Prevent Safari double-finally. + var finallyAlreadyCalled = false; + try { + if (arguments.length > 2) { + ret = method.apply(target, slice.call(arguments, 2)); + } else { + ret = method.call(target); + } + } finally { + if (!finallyAlreadyCalled) { + finallyAlreadyCalled = true; + this.end(); + } + } + return ret; + }, - 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. + defer: function(queueName, target, method /* , args */) { + if (!method) { + method = target; + target = null; + } - @method suspendListener - @for Ember - @param obj - @param {Array} eventName Array of event names - @param {Object|Function} targetOrMethod A target object or a function - @param {Function|String} method A function or the name of a function to be called on `target` - @param {Function} callback -*/ -function suspendListeners(obj, eventNames, target, method, callback) { - if (!method && 'function' === typeof target) { - method = target; - target = null; - } + if (typeof method === 'string') { + method = target[method]; + } - var suspendedActions = [], - eventName, actions, action, i, l; + var stack = this.DEBUG ? new Error().stack : undefined, + args = arguments.length > 3 ? slice.call(arguments, 3) : undefined; + if (!this.currentInstance) { createAutorun(this); } + return this.currentInstance.schedule(queueName, target, method, args, false, stack); + }, - for (i=0, l=eventNames.length; i<l; i++) { - eventName = eventNames[i]; - actions = actionsFor(obj, eventName); - var actionIndex = indexOf(actions, target, method); + deferOnce: function(queueName, target, method /* , args */) { + if (!method) { + method = target; + target = null; + } - if (actionIndex !== -1) { - action = actions[actionIndex].slice(); - action[3] = true; - actions[actionIndex] = action; - suspendedActions.push(action); - } - } + if (typeof method === 'string') { + method = target[method]; + } - function tryable() { return callback.call(target); } + var stack = this.DEBUG ? new Error().stack : undefined, + args = arguments.length > 3 ? slice.call(arguments, 3) : undefined; + if (!this.currentInstance) { createAutorun(this); } + return this.currentInstance.schedule(queueName, target, method, args, true, stack); + }, - function finalizer() { - for (i = 0, l = suspendedActions.length; i < l; i++) { - suspendedActions[i][3] = undefined; - } - } + setTimeout: function() { + var self = this, + wait = pop.call(arguments), + target = arguments[0], + method = arguments[1], + executeAt = (+new Date()) + wait; - return Ember.tryFinally(tryable, finalizer); -} + if (!method) { + method = target; + target = null; + } -/** - @private + if (typeof method === 'string') { + method = target[method]; + } - Return a list of currently watched events + var fn, args; + if (arguments.length > 2) { + args = slice.call(arguments, 2); - @method watchedEvents - @for Ember - @param obj -*/ -function watchedEvents(obj) { - var listeners = obj[META_KEY].listeners, ret = []; + fn = function() { + method.apply(target, args); + }; + } else { + fn = function() { + method.call(target); + }; + } - if (listeners) { - for(var eventName in listeners) { - if (listeners[eventName]) { ret.push(eventName); } - } - } - return ret; -} + // find position to insert - TODO: binary search + var i, l; + for (i = 0, l = timers.length; i < l; i += 2) { + if (executeAt < timers[i]) { break; } + } -/** - @method sendEvent - @for Ember - @param obj - @param {String} eventName - @param {Array} params - @param {Array} actions - @return true -*/ -function sendEvent(obj, eventName, params, actions) { - // first give object a chance to handle it - if (obj !== Ember && 'function' === typeof obj.sendEvent) { - obj.sendEvent(eventName, params); - } + timers.splice(i, 0, executeAt, fn); - if (!actions) { - var meta = obj[META_KEY]; - actions = meta && meta.listeners && meta.listeners[eventName]; - } + if (laterTimer && laterTimerExpiresAt < executeAt) { return fn; } - if (!actions) { return; } + if (laterTimer) { + clearTimeout(laterTimer); + laterTimer = null; + } + laterTimer = window.setTimeout(function() { + executeTimers(self); + laterTimer = null; + laterTimerExpiresAt = null; + }, wait); + laterTimerExpiresAt = executeAt; + + return fn; + }, - for (var i = actions.length - 1; i >= 0; i--) { // looping in reverse for once listeners - if (!actions[i] || actions[i][3] === true) { continue; } + debounce: function(target, method /* , args, wait */) { + var self = this, + args = arguments, + wait = pop.call(args), + debouncee; - var target = actions[i][0], - method = actions[i][1], - once = actions[i][2]; + for (var i = 0, l = debouncees.length; i < l; i++) { + debouncee = debouncees[i]; + if (debouncee[0] === target && debouncee[1] === method) { return; } // do nothing + } - if (once) { removeListener(obj, eventName, target, method); } - if (!target) { target = obj; } - if ('string' === typeof method) { method = target[method]; } - if (params) { - method.apply(target, params); - } else { - method.call(target); - } - } - return true; -} + var timer = window.setTimeout(function() { + self.run.apply(self, args); -/** - @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]; + // remove debouncee + var index = -1; + for (var i = 0, l = debouncees.length; i < l; i++) { + debouncee = debouncees[i]; + if (debouncee[0] === target && debouncee[1] === method) { + index = i; + break; + } + } - return !!(actions && actions.length); -} + if (index > -1) { debouncees.splice(index, 1); } + }, wait); -/** - @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]; + debouncees.push([target, method, timer]); + }, - if (!actions) { return ret; } + cancelTimers: function() { + for (var i = 0, l = debouncees.length; i < l; i++) { + clearTimeout(debouncees[i][2]); + } + debouncees = []; - for (var i = 0, l = actions.length; i < l; i++) { - var target = actions[i][0], - method = actions[i][1]; - ret.push([target, method]); - } + if (laterTimer) { + clearTimeout(laterTimer); + laterTimer = null; + } + timers = []; - return ret; -} + if (autorun) { + clearTimeout(autorun); + autorun = null; + } + }, -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; + hasTimers: function() { + return !!timers.length || autorun; + }, -})(); + cancel: function(timer) { + if (typeof timer === 'object' && timer.queue && timer.method) { // we're cancelling a deferOnce + return timer.queue.cancel(timer); + } else if (typeof timer === 'function') { // we're cancelling a setTimeout + for (var i = 0, l = timers.length; i < l; i += 2) { + if (timers[i + 1] === timer) { + timers.splice(i, 2); // remove the two elements + return true; + } + } + } + } + }; + Backburner.prototype.schedule = Backburner.prototype.defer; + Backburner.prototype.scheduleOnce = Backburner.prototype.deferOnce; + Backburner.prototype.later = Backburner.prototype.setTimeout; + function createAutorun(backburner) { + backburner.begin(); + autorun = window.setTimeout(function() { + backburner.end(); + autorun = null; + }); + } -(function() { -// Ember.Logger -// Ember.watch.flushPending -// Ember.beginPropertyChanges, Ember.endPropertyChanges -// Ember.guidFor, Ember.tryFinally + function executeTimers(self) { + var now = +new Date(), + time, fns, i, l; -/** -@module ember-metal -*/ + self.run(function() { + // TODO: binary search + for (i = 0, l = timers.length; i < l; i += 2) { + time = timers[i]; + if (time > now) { break; } + } -// .......................................................... -// HELPERS -// + fns = timers.splice(0, i); -var slice = [].slice, - forEach = Ember.ArrayPolyfills.forEach; + for (i = 1, l = fns.length; i < l; i += 2) { + self.schedule(self.options.defaultQueue, null, fns[i]); + } + }); -// invokes passed params - normalizing so you can pass target/func, -// target/string or just func -function invoke(target, method, args, ignore) { + if (timers.length) { + laterTimer = window.setTimeout(function() { + executeTimers(self); + laterTimer = null; + laterTimerExpiresAt = null; + }, timers[0] - now); + laterTimerExpiresAt = timers[0]; + } + } - 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; - } + __exports__.Backburner = Backburner; + }); - return Ember.handleErrors(function() { - // IE8's Function.prototype.apply doesn't accept undefined/null arguments. - return method.apply(target || this, args || []); - }, this); -} +define("backburner/deferred_action_queues", + ["backburner/queue","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Queue = __dependency1__.Queue; + function DeferredActionQueues(queueNames, options) { + var queues = this.queues = {}; + this.queueNames = queueNames = queueNames || []; -// .......................................................... -// RUNLOOP -// + var queueName; + for (var i = 0, l = queueNames.length; i < l; i++) { + queueName = queueNames[i]; + queues[queueName] = new Queue(this, queueName, options[queueName]); + } + } -var timerMark; // used by timers... + DeferredActionQueues.prototype = { + queueNames: null, + queues: null, -/** -Ember RunLoop (Private) + schedule: function(queueName, target, method, args, onceFlag, stack) { + var queues = this.queues, + queue = queues[queueName]; -@class RunLoop -@namespace Ember -@private -@constructor -*/ -var RunLoop = function(prev) { - this._prev = prev || null; - this.onceTimers = {}; -}; + if (!queue) { throw new Error("You attempted to schedule an action in a queue (" + queueName + ") that doesn't exist"); } -RunLoop.prototype = { - /** - @method end - */ - end: function() { - this.flush(); - }, + if (onceFlag) { + return queue.pushUnique(target, method, args, stack); + } else { + return queue.push(target, method, args, stack); + } + }, - /** - @method prev - */ - prev: function() { - return this._prev; - }, + flush: function() { + var queues = this.queues, + queueNames = this.queueNames, + queueName, queue, queueItems, priorQueueNameIndex, + queueNameIndex = 0, numberOfQueues = queueNames.length; + + outerloop: + while (queueNameIndex < numberOfQueues) { + queueName = queueNames[queueNameIndex]; + queue = queues[queueName]; + queueItems = queue._queue.slice(); + queue._queue = []; + + var options = queue.options, + before = options && options.before, + after = options && options.after, + target, method, args, stack, + queueIndex = 0, numberOfQueueItems = queueItems.length; + + if (numberOfQueueItems && before) { before(); } + while (queueIndex < numberOfQueueItems) { + target = queueItems[queueIndex]; + method = queueItems[queueIndex+1]; + args = queueItems[queueIndex+2]; + stack = queueItems[queueIndex+3]; // Debugging assistance + + if (typeof method === 'string') { method = target[method]; } + + // TODO: error handling + if (args && args.length > 0) { + method.apply(target, args); + } else { + method.call(target); + } - // .......................................................... - // Delayed Actions - // + queueIndex += 4; + } + if (numberOfQueueItems && after) { after(); } - /** - @method schedule - @param {String} queueName - @param target - @param method - */ - schedule: function(queueName, target, method) { - var queues = this._queues, queue; - if (!queues) { queues = this._queues = {}; } - queue = queues[queueName]; - if (!queue) { queue = queues[queueName] = []; } + if ((priorQueueNameIndex = indexOfPriorQueueWithActions(this, queueNameIndex)) !== -1) { + queueNameIndex = priorQueueNameIndex; + continue outerloop; + } - var args = arguments.length > 3 ? slice.call(arguments, 3) : null; - queue.push({ target: target, method: method, args: args }); - return this; - }, + queueNameIndex++; + } + } + }; - /** - @method flush - @param {String} queueName - */ - flush: function(queueName) { - var queueNames, idx, len, queue, log; + function indexOfPriorQueueWithActions(daq, currentQueueIndex) { + var queueName, queue; - if (!this._queues) { return this; } // nothing to do + for (var i = 0, l = currentQueueIndex; i <= l; i++) { + queueName = daq.queueNames[i]; + queue = daq.queues[queueName]; + if (queue._queue.length) { return i; } + } - function iter(item) { - invoke(item.target, item.method, item.args); + return -1; } - function tryable() { - forEach.call(queue, iter); - } + __exports__.DeferredActionQueues = DeferredActionQueues; + }); - Ember.watch.flushPending(); // make sure all chained watchers are setup +define("backburner/queue", + ["exports"], + function(__exports__) { + "use strict"; + function Queue(daq, name, options) { + this.daq = daq; + this.name = name; + this.options = options; + this._queue = []; + } + + Queue.prototype = { + daq: null, + name: null, + options: null, + _queue: null, + + push: function(target, method, args, stack) { + var queue = this._queue; + queue.push(target, method, args, stack); + return {queue: this, target: target, method: method}; + }, - if (queueName) { - while (this._queues && (queue = this._queues[queueName])) { - this._queues[queueName] = null; + pushUnique: function(target, method, args, stack) { + var queue = this._queue, currentTarget, currentMethod, i, l; - // 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'); } + for (i = 0, l = queue.length; i < l; i += 4) { + currentTarget = queue[i]; + currentMethod = queue[i+1]; - Ember.beginPropertyChanges(); + if (currentTarget === target && currentMethod === method) { + queue[i+2] = args; // replace args + queue[i+3] = stack; // replace stack + return {queue: this, target: target, method: method}; // TODO: test this code path + } + } - Ember.tryFinally(tryable, Ember.endPropertyChanges); + this._queue.push(target, method, args, stack); + return {queue: this, target: target, method: method}; + }, - if (log) { Ember.Logger.log('End: Flush Sync Queue'); } + // TODO: remove me, only being used for Ember.run.sync + flush: function() { + var queue = this._queue, + options = this.options, + before = options && options.before, + after = options && options.after, + target, method, args, stack, i, l = queue.length; + + if (l && before) { before(); } + for (i = 0; i < l; i += 4) { + target = queue[i]; + method = queue[i+1]; + args = queue[i+2]; + stack = queue[i+3]; // Debugging assistance + + // TODO: error handling + if (args && args.length > 0) { + method.apply(target, args); + } else { + method.call(target); + } + } + if (l && after) { after(); } + // check if new items have been added + if (queue.length > l) { + this._queue = queue.slice(l); + this.flush(); } else { - forEach.call(queue, iter); + this._queue.length = 0; } - } - - } 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(); + cancel: function(actionToCancel) { + var queue = this._queue, currentTarget, currentMethod, i, l; - Ember.tryFinally(tryable, Ember.endPropertyChanges); + for (i = 0, l = queue.length; i < l; i += 4) { + currentTarget = queue[i]; + currentMethod = queue[i+1]; - if (log) { Ember.Logger.log('End: Flush Sync Queue'); } - } else { - forEach.call(queue, iter); + if (currentTarget === actionToCancel.target && currentMethod === actionToCancel.method) { + queue.splice(i, 4); + return true; } } + } + }; - // 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; - } - } + __exports__.Queue = Queue; + }); - idx++; - } - } +})(); - timerMark = null; - return this; - } +(function() { +var onBegin = function(current) { + Ember.run.currentRunLoop = current; }; -Ember.RunLoop = RunLoop; +var onEnd = function(current, next) { + Ember.run.currentRunLoop = next; +}; + +var Backburner = requireModule('backburner').Backburner, + backburner = new Backburner(['sync', 'actions', 'destroy'], { + sync: { + before: Ember.beginPropertyChanges, + after: Ember.endPropertyChanges + }, + defaultQueue: 'actions', + onBegin: onBegin, + onEnd: onEnd + }), + slice = [].slice; // .......................................................... // Ember.run - this is ideally the only public API the dev sees @@ -4539,20 +5062,76 @@ Ember.RunLoop = RunLoop; @return {Object} return value from invoking the passed function. */ Ember.run = function(target, method) { - var args = arguments; - run.begin(); + var ret; - function tryable() { - if (target || method) { - return invoke(target, method, args, 2); + if (Ember.onerror) { + try { + ret = backburner.run.apply(backburner, arguments); + } catch (e) { + Ember.onerror(e); } + } else { + ret = backburner.run.apply(backburner, arguments); + } + + return ret; +}; + +/** + + If no run-loop is present, it creates a new one. If a run loop is + present it will queue itself to run on the existing run-loops action + queue. + + Please note: This is not for normal usage, and should be used sparingly. + + If invoked when not within a run loop: + + ```javascript + Ember.run.join(function(){ + // creates a new run-loop + }); + ``` + + Alternatively, if called within an existing run loop: + + ```javascript + Ember.run(function(){ + // creates a new run-loop + Ember.run.join(function(){ + // joins with the existing run-loop, and queues for invocation on + // the existing run-loops action queue. + }); + }); + ``` + + @method join + @namespace Ember + @param {Object} [target] target of method to call + @param {Function|String} method Method to invoke. + May be a function or a string. If you pass a string + then it will be looked up on the passed target. + @param {Object} [args*] Any additional arguments you wish to pass to the method. + @return {Object} return value from invoking the passed function. Please note, + when called within an existing loop, no return value is possible. +*/ +Ember.run.join = function(target, method) { + if (!Ember.run.currentRunLoop) { + return Ember.run.apply(Ember.run, arguments); } - return Ember.tryFinally(tryable, run.end); + var args = slice.call(arguments); + args.unshift('actions'); + Ember.run.schedule.apply(Ember.run, args); }; +Ember.run.backburner = backburner; + var run = Ember.run; +Ember.run.currentRunLoop = null; + +Ember.run.queues = backburner.queueNames; /** Begins a new RunLoop. Any deferred actions invoked after the begin will @@ -4569,7 +5148,7 @@ var run = Ember.run; @return {void} */ Ember.run.begin = function() { - run.currentRunLoop = new RunLoop(run.currentRunLoop); + backburner.begin(); }; /** @@ -4587,12 +5166,7 @@ Ember.run.begin = function() { @return {void} */ Ember.run.end = function() { - Ember.assert('must have a current run loop', run.currentRunLoop); - - function tryable() { run.currentRunLoop.end(); } - function finalizer() { run.currentRunLoop = run.currentRunLoop.prev(); } - - Ember.tryFinally(tryable, finalizer); + backburner.end(); }; /** @@ -4605,7 +5179,6 @@ Ember.run.end = function() { @type Array @default ['sync', 'actions', 'destroy'] */ -Ember.run.queues = ['sync', 'actions', 'destroy']; /** Adds the passed target/method and any optional arguments to the named @@ -4615,7 +5188,7 @@ Ember.run.queues = ['sync', 'actions', 'destroy']; At the end of a RunLoop, any methods scheduled in this way will be invoked. Methods will be invoked in an order matching the named queues defined in - the `run.queues` property. + the `Ember.run.queues` property. ```javascript Ember.run.schedule('sync', this, function(){ @@ -4644,57 +5217,18 @@ Ember.run.queues = ['sync', 'actions', 'destroy']; @return {void} */ Ember.run.schedule = function(queue, target, method) { - var loop = run.autorun(); - loop.schedule.apply(loop, arguments); + checkAutoRun(); + backburner.schedule.apply(backburner, arguments); }; -var scheduledAutorun; -function autorun() { - scheduledAutorun = null; - if (run.currentRunLoop) { run.end(); } -} - // Used by global test teardown Ember.run.hasScheduledTimers = function() { - return !!(scheduledAutorun || scheduledLater); + return backburner.hasTimers(); }; // Used by global test teardown Ember.run.cancelTimers = function () { - if (scheduledAutorun) { - clearTimeout(scheduledAutorun); - scheduledAutorun = null; - } - if (scheduledLater) { - clearTimeout(scheduledLater); - scheduledLater = null; - } - timers = {}; -}; - -/** - Begins a new RunLoop if necessary and schedules a timer to flush the - RunLoop at a later time. This method is used by parts of Ember to - ensure the RunLoop always finishes. You normally do not need to call this - method directly. Instead use `Ember.run()` - - @method autorun - @example - Ember.run.autorun(); - @return {Ember.RunLoop} the new current RunLoop -*/ -Ember.run.autorun = function() { - if (!run.currentRunLoop) { - Ember.assert("You have turned on testing mode, which disabled the run-loop's autorun. You will need to wrap any code with asynchronous side-effects in an Ember.run", !Ember.testing); - - run.begin(); - - if (!scheduledAutorun) { - scheduledAutorun = setTimeout(autorun, 1); - } - } - - return run.currentRunLoop; + backburner.cancelTimers(); }; /** @@ -4714,42 +5248,9 @@ Ember.run.autorun = function() { @return {void} */ Ember.run.sync = function() { - run.autorun(); - run.currentRunLoop.flush('sync'); + backburner.currentInstance.queues.sync.flush(); }; -// .......................................................... -// TIMERS -// - -var timers = {}; // active timers... - -var scheduledLater, scheduledLaterExpires; -function invokeLaterTimers() { - scheduledLater = null; - run(function() { - var now = (+ new Date()), earliest = -1; - for (var key in timers) { - if (!timers.hasOwnProperty(key)) { continue; } - var timer = timers[key]; - if (timer && timer.expires) { - if (now >= timer.expires) { - delete timers[key]; - invoke(timer.target, timer.method, timer.args, 2); - } else { - if (earliest < 0 || (timer.expires < earliest)) { earliest = timer.expires; } - } - } - } - - // schedule next timeout to fire when the earliest timer expires - if (earliest > 0) { - scheduledLater = setTimeout(invokeLaterTimers, earliest - now); - scheduledLaterExpires = earliest; - } - }); -} - /** Invokes the passed target/method and optional arguments after a specified period if time. The last parameter of this method must always be a number @@ -4774,80 +5275,35 @@ function invokeLaterTimers() { @param {Object} [args*] Optional arguments to pass to the timeout. @param {Number} wait Number of milliseconds to wait. @return {String} a string you can use to cancel the timer in - {{#crossLink "Ember/run.cancel"}}{{/crossLink}} later. + `Ember.run.cancel` later. */ Ember.run.later = function(target, method) { - var args, expires, timer, guid, wait; - - // setTimeout compatibility... - if (arguments.length===2 && 'function' === typeof target) { - wait = method; - method = target; - target = undefined; - args = [target, method]; - } else { - args = slice.call(arguments); - wait = args.pop(); - } - - expires = (+ new Date()) + wait; - timer = { target: target, method: method, expires: expires, args: args }; - guid = Ember.guidFor(timer); - timers[guid] = timer; - - if(scheduledLater && expires < scheduledLaterExpires) { - // Cancel later timer (then reschedule earlier timer below) - clearTimeout(scheduledLater); - scheduledLater = null; - } - - if (!scheduledLater) { - // Schedule later timers to be run. - scheduledLater = setTimeout(invokeLaterTimers, wait); - scheduledLaterExpires = expires; - } - - return guid; + return backburner.later.apply(backburner, arguments); }; -function invokeOnceTimer(guid, onceTimers) { - if (onceTimers[this.tguid]) { delete onceTimers[this.tguid][this.mguid]; } - if (timers[guid]) { invoke(this.target, this.method, this.args); } - delete timers[guid]; -} - -function scheduleOnce(queue, target, method, args) { - var tguid = Ember.guidFor(target), - mguid = Ember.guidFor(method), - onceTimers = run.autorun().onceTimers, - guid = onceTimers[tguid] && onceTimers[tguid][mguid], - timer; - - if (guid && timers[guid]) { - timers[guid].args = args; // replace args - } else { - timer = { - target: target, - method: method, - args: args, - tguid: tguid, - mguid: mguid - }; - - guid = Ember.guidFor(timer); - timers[guid] = timer; - if (!onceTimers[tguid]) { onceTimers[tguid] = {}; } - onceTimers[tguid][mguid] = guid; // so it isn't scheduled more than once - - run.schedule(queue, timer, invokeOnceTimer, guid, onceTimers); - } +/** + Schedule a function to run one time during the current RunLoop. This is equivalent + to calling `scheduleOnce` with the "actions" queue. - return guid; -} + @method once + @param {Object} [target] The target of the method to invoke. + @param {Function|String} method The method to invoke. + If you pass a string it will be resolved on the + target at the time the method is invoked. + @param {Object} [args*] Optional arguments to pass to the timeout. + @return {Object} timer +*/ +Ember.run.once = function(target, method) { + checkAutoRun(); + var args = slice.call(arguments); + args.unshift('actions'); + return backburner.scheduleOnce.apply(backburner, args); +}; /** - Schedules an item to run one time during the current RunLoop. Calling - this method with the same target/method combination will have no effect. + Schedules a function to run one time in a given queue of the current RunLoop. + Calling this method with the same queue/target/method combination will have + no effect (past the initial call). Note that although you can pass optional arguments these will not be considered when looking for duplicates. New arguments will replace previous @@ -4855,47 +5311,47 @@ function scheduleOnce(queue, target, method, args) { ```javascript Ember.run(function(){ - var doFoo = function() { foo(); } - Ember.run.once(myContext, doFoo); - Ember.run.once(myContext, doFoo); - // doFoo will only be executed once at the end of the RunLoop + var sayHi = function() { console.log('hi'); } + Ember.run.scheduleOnce('afterRender', myContext, sayHi); + Ember.run.scheduleOnce('afterRender', myContext, sayHi); + // doFoo will only be executed once, in the afterRender queue of the RunLoop }); ``` - Also note that passing an anonymous function to `Ember.run.once` will + Also note that passing an anonymous function to `Ember.run.scheduleOnce` will not prevent additional calls with an identical anonymous function from scheduling the items multiple times, e.g.: ```javascript function scheduleIt() { - Ember.run.once(myContext, function() { console.log("Closure"); }); + Ember.run.scheduleOnce('actions', myContext, function() { console.log("Closure"); }); } scheduleIt(); scheduleIt(); - // "Closure" will print twice, even though we're using `Ember.run.once`, + // "Closure" will print twice, even though we're using `Ember.run.scheduleOnce`, // because the function we pass to it is anonymous and won't match the // previously scheduled operation. ``` - @method once - @param {Object} [target] target of method to invoke + Available queues, and their order, can be found at `Ember.run.queues` + + @method scheduleOnce + @param {String} [queue] The name of the queue to schedule against. Default queues are 'sync' and 'actions'. + @param {Object} [target] The target of the method to invoke. @param {Function|String} method The method to invoke. If you pass a string it will be resolved on the target at the time the method is invoked. @param {Object} [args*] Optional arguments to pass to the timeout. @return {Object} timer */ -Ember.run.once = function(target, method) { - return scheduleOnce('actions', target, method, slice.call(arguments, 2)); -}; - -Ember.run.scheduleOnce = function(queue, target, method, args) { - return scheduleOnce(queue, target, method, slice.call(arguments, 3)); +Ember.run.scheduleOnce = function(queue, target, method) { + checkAutoRun(); + return backburner.scheduleOnce.apply(backburner, arguments); }; /** - Schedules an item to run from within a separate run loop, after - control has been returned to the system. This is equivalent to calling + Schedules an item to run from within a separate run loop, after + control has been returned to the system. This is equivalent to calling `Ember.run.later` with a wait time of 1ms. ```javascript @@ -4907,7 +5363,7 @@ Ember.run.scheduleOnce = function(queue, target, method, args) { Multiple operations scheduled with `Ember.run.next` will coalesce into the same later run loop, along with any other operations scheduled by `Ember.run.later` that expire right around the same - time that `Ember.run.next` operations will fire. + time that `Ember.run.next` operations will fire. Note that there are often alternatives to using `Ember.run.next`. For instance, if you'd like to schedule an operation to happen @@ -4933,13 +5389,13 @@ Ember.run.scheduleOnce = function(queue, target, method, args) { One benefit of the above approach compared to using `Ember.run.next` is that you will be able to perform DOM/CSS operations before unprocessed - elements are rendered to the screen, which may prevent flickering or + elements are rendered to the screen, which may prevent flickering or other artifacts caused by delaying processing until after rendering. - The other major benefit to the above approach is that `Ember.run.next` - introduces an element of non-determinism, which can make things much - harder to test, due to its reliance on `setTimeout`; it's much harder - to guarantee the order of scheduled operations when they are scheduled + The other major benefit to the above approach is that `Ember.run.next` + introduces an element of non-determinism, which can make things much + harder to test, due to its reliance on `setTimeout`; it's much harder + to guarantee the order of scheduled operations when they are scheduled outside of the current run loop, i.e. with `Ember.run.next`. @method next @@ -4952,8 +5408,8 @@ Ember.run.scheduleOnce = function(queue, target, method, args) { */ Ember.run.next = function() { var args = slice.call(arguments); - args.push(1); // 1 millisecond wait - return run.later.apply(this, args); + args.push(1); + return backburner.later.apply(backburner, args); }; /** @@ -4982,17 +5438,25 @@ Ember.run.next = function() { @return {void} */ Ember.run.cancel = function(timer) { - delete timers[timer]; + return backburner.cancel(timer); }; +// Make sure it's not an autorun during testing +function checkAutoRun() { + if (!Ember.run.currentRunLoop) { + Ember.assert("You have turned on testing mode, which disabled the run-loop's autorun. You will need to wrap any code with asynchronous side-effects in an Ember.run", !Ember.testing); + } +} + })(); (function() { // Ember.Logger -// get, set, trySet -// guidFor, isArray, meta +// get +// set +// guidFor, meta // addObserver, removeObserver // Ember.run.schedule /** @@ -5018,8 +5482,21 @@ Ember.LOG_BINDINGS = false || !!Ember.ENV.LOG_BINDINGS; var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, - isGlobalPath = Ember.isGlobalPath; + IS_GLOBAL = /^([A-Z$]|([0-9][A-Z$]))/; +/** + Returns true if the provided path is global (e.g., `MyApp.fooController.bar`) + instead of local (`foo.bar.baz`). + + @method isGlobalPath + @for Ember + @private + @param {String} path + @return Boolean +*/ +var isGlobalPath = Ember.isGlobalPath = function(path) { + return IS_GLOBAL.test(path); +}; function getWithGlobals(obj, path) { return get(isGlobalPath(path) ? Ember.lookup : obj, path); @@ -5255,7 +5732,7 @@ function mixinProperties(to, from) { mixinProperties(Binding, { /** - See {{#crossLink "Ember.Binding/from"}}{{/crossLink}} + See `Ember.Binding.from`. @method from @static @@ -5266,7 +5743,7 @@ mixinProperties(Binding, { }, /** - See {{#crossLink "Ember.Binding/to"}}{{/crossLink}} + See `Ember.Binding.to`. @method to @static @@ -5283,7 +5760,7 @@ mixinProperties(Binding, { This means that if you change the "to" side directly, the "from" side may have a different value. - See {{#crossLink "Binding/oneWay"}}{{/crossLink}} + See `Binding.oneWay`. @method oneWay @param {String} from from path. @@ -5311,7 +5788,7 @@ mixinProperties(Binding, { Properties ending in a `Binding` suffix will be converted to `Ember.Binding` instances. The value of this property should be a string representing a path to another object or a custom binding instanced created using Binding helpers - (see "Customizing Your Bindings"): + (see "One Way Bindings"): ``` valueBinding: "MyApp.someController.title" @@ -5344,7 +5821,7 @@ mixinProperties(Binding, { You should consider using one way bindings anytime you have an object that may be created frequently and you do not intend to change a property; only - to monitor it for changes. (such as in the example above). + to monitor it for changes (such as in the example above). ## Adding Bindings Manually @@ -5611,7 +6088,7 @@ function addNormalizedProperty(base, key, value, meta, descs, values, concats) { } } -function mergeMixins(mixins, m, descs, values, base) { +function mergeMixins(mixins, m, descs, values, base, keys) { var mixin, props, key, concats, meta; function removeKeys(keyName) { @@ -5632,26 +6109,19 @@ function mergeMixins(mixins, m, descs, values, base) { for (key in props) { if (!props.hasOwnProperty(key)) { continue; } + keys.push(key); addNormalizedProperty(base, key, props[key], meta, descs, values, concats); } // manually copy toString() because some JS engines do not enumerate it if (props.hasOwnProperty('toString')) { base.toString = props.toString; } } else if (mixin.mixins) { - mergeMixins(mixin.mixins, m, descs, values, base); + mergeMixins(mixin.mixins, m, descs, values, base, keys); if (mixin._without) { a_forEach.call(mixin._without, removeKeys); } } } } -function writableReq(obj) { - var m = Ember.meta(obj), req = m.required; - if (!req || !m.hasOwnProperty('required')) { - req = m.required = req ? o_create(req) : {}; - } - return req; -} - var IS_BINDING = Ember.IS_BINDING = /^.+Binding$/; function detectBinding(obj, key, value, m) { @@ -5734,7 +6204,7 @@ function replaceObservers(obj, key, observer) { function applyMixin(obj, mixins, partial) { var descs = {}, values = {}, m = Ember.meta(obj), - key, value, desc; + key, value, desc, keys = []; // Go through all mixins and hashes passed in, and: // @@ -5742,10 +6212,11 @@ function applyMixin(obj, mixins, partial) { // * Set up _super wrapping if necessary // * Set up computed property descriptors // * Copying `toString` in broken browsers - mergeMixins(mixins, mixinsMeta(obj), descs, values, obj); + mergeMixins(mixins, mixinsMeta(obj), descs, values, obj, keys); - for(key in values) { - if (key === 'contructor' || !values.hasOwnProperty(key)) { continue; } + for(var i = 0, l = keys.length; i < l; i++) { + key = keys[i]; + if (key === 'constructor' || !values.hasOwnProperty(key)) { continue; } desc = descs[key]; value = values[key]; @@ -5799,7 +6270,7 @@ Ember.mixin = function(obj) { }); // Mix mixins into classes by passing them as the first arguments to - // .extend or .create. + // .extend. App.CommentView = Ember.View.extend(App.Editable, { template: Ember.Handlebars.compile('{{#if isEditing}}...{{else}}...{{/if}}') }); @@ -5818,6 +6289,12 @@ Ember.Mixin = function() { return initMixin(this, arguments); }; Mixin = Ember.Mixin; +Mixin.prototype = { + properties: null, + mixins: null, + ownerConstructor: null +}; + Mixin._apply = applyMixin; Mixin.applyPartial = function(obj) { @@ -6042,7 +6519,7 @@ Ember.alias = function(methodName) { return new Alias(methodName); }; -Ember.deprecateFunc("Ember.alias is deprecated. Please use Ember.aliasMethod or Ember.computed.alias instead.", Ember.alias); +Ember.alias = Ember.deprecateFunc("Ember.alias is deprecated. Please use Ember.aliasMethod or Ember.computed.alias instead.", Ember.alias); /** Makes a method available via an additional name. @@ -6103,6 +6580,29 @@ Ember.immediateObserver = function() { }; /** + When observers fire, they are called with the arguments `obj`, `keyName` + and `value`. In a typical observer, value is the new, post-change value. + + A `beforeObserver` fires before a property changes. The `value` argument contains + the pre-change value. + + A `beforeObserver` is an alternative form of `.observesBefore()`. + + ```javascript + App.PersonView = Ember.View.extend({ + valueWillChange: function (obj, keyName, value) { + this.changingFrom = value; + }.observesBefore('content.value'), + valueDidChange: function(obj, keyName, value) { + // only run if updating a value already in the DOM + if(this.get('state') === 'inDOM') { + var color = value > this.changingFrom ? 'green' : 'red'; + // logic + } + }.observes('content.value') + }); + ``` + @method beforeObserver @for Ember @param {Function} func @@ -6130,14 +6630,57 @@ Ember Metal })(); (function() { -define("rsvp", - [], - function() { +define("rsvp/all", + ["rsvp/defer","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var defer = __dependency1__.defer; + + function all(promises) { + var results = [], deferred = defer(), remaining = promises.length; + + if (remaining === 0) { + deferred.resolve([]); + } + + var resolver = function(index) { + return function(value) { + resolveAll(index, value); + }; + }; + + var resolveAll = function(index, value) { + results[index] = value; + if (--remaining === 0) { + deferred.resolve(results); + } + }; + + var rejectAll = function(error) { + deferred.reject(error); + }; + + for (var i = 0; i < promises.length; i++) { + if (promises[i] && typeof promises[i].then === 'function') { + promises[i].then(resolver(i), rejectAll); + } else { + resolveAll(i, promises[i]); + } + } + return deferred.promise; + } + + __exports__.all = all; + }); + +define("rsvp/async", + ["exports"], + function(__exports__) { "use strict"; var browserGlobal = (typeof window !== 'undefined') ? window : {}; - var MutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; - var RSVP, async; + var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; + var async; if (typeof process !== 'undefined' && {}.toString.call(process) === '[object process]') { @@ -6146,10 +6689,10 @@ define("rsvp", callback.call(binding); }); }; - } else if (MutationObserver) { + } else if (BrowserMutationObserver) { var queue = []; - var observer = new MutationObserver(function() { + var observer = new BrowserMutationObserver(function() { var toProcess = queue.slice(); queue = []; @@ -6180,6 +6723,47 @@ define("rsvp", }; } + + __exports__.async = async; + }); + +define("rsvp/config", + ["rsvp/async","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var async = __dependency1__.async; + + var config = {}; + config.async = async; + + __exports__.config = config; + }); + +define("rsvp/defer", + ["rsvp/promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__.Promise; + + function defer() { + var deferred = {}; + + var promise = new Promise(function(resolve, reject) { + deferred.resolve = resolve; + deferred.reject = reject; + }); + + deferred.promise = promise; + return deferred; + } + + __exports__.defer = defer; + }); + +define("rsvp/events", + ["exports"], + function(__exports__) { + "use strict"; var Event = function(type, options) { this.type = type; @@ -6274,7 +6858,148 @@ define("rsvp", } }; - var Promise = function() { + + __exports__.EventTarget = EventTarget; + }); + +define("rsvp/hash", + ["rsvp/defer","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var defer = __dependency1__.defer; + + function size(object) { + var size = 0; + + for (var prop in object) { + size++; + } + + return size; + } + + function hash(promises) { + var results = {}, deferred = defer(), remaining = size(promises); + + if (remaining === 0) { + deferred.resolve({}); + } + + var resolver = function(prop) { + return function(value) { + resolveAll(prop, value); + }; + }; + + var resolveAll = function(prop, value) { + results[prop] = value; + if (--remaining === 0) { + deferred.resolve(results); + } + }; + + var rejectAll = function(error) { + deferred.reject(error); + }; + + for (var prop in promises) { + if (promises[prop] && typeof promises[prop].then === 'function') { + promises[prop].then(resolver(prop), rejectAll); + } else { + resolveAll(prop, promises[prop]); + } + } + + return deferred.promise; + } + + __exports__.hash = hash; + }); + +define("rsvp/node", + ["rsvp/promise","rsvp/all","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var Promise = __dependency1__.Promise; + var all = __dependency2__.all; + + function makeNodeCallbackFor(resolve, reject) { + return function (error, value) { + if (error) { + reject(error); + } else if (arguments.length > 2) { + resolve(Array.prototype.slice.call(arguments, 1)); + } else { + resolve(value); + } + }; + } + + function denodeify(nodeFunc) { + return function() { + var nodeArgs = Array.prototype.slice.call(arguments), resolve, reject; + + var promise = new Promise(function(nodeResolve, nodeReject) { + resolve = nodeResolve; + reject = nodeReject; + }); + + all(nodeArgs).then(function(nodeArgs) { + nodeArgs.push(makeNodeCallbackFor(resolve, reject)); + + try { + nodeFunc.apply(this, nodeArgs); + } catch(e) { + reject(e); + } + }); + + return promise; + }; + } + + __exports__.denodeify = denodeify; + }); + +define("rsvp/promise", + ["rsvp/config","rsvp/events","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var config = __dependency1__.config; + var EventTarget = __dependency2__.EventTarget; + + function objectOrFunction(x) { + return isFunction(x) || (typeof x === "object" && x !== null); + } + + function isFunction(x){ + return typeof x === "function"; + } + + var Promise = function(resolver) { + var promise = this, + resolved = false; + + if (typeof resolver !== 'function') { + throw new TypeError('You must pass a resolver function as the sole argument to the promise constructor'); + } + + if (!(promise instanceof Promise)) { + return new Promise(resolver); + } + + var resolvePromise = function(value) { + if (resolved) { return; } + resolved = true; + resolve(promise, value); + }; + + var rejectPromise = function(value) { + if (resolved) { return; } + resolved = true; + reject(promise, value); + }; + this.on('promise:resolved', function(event) { this.trigger('success', { detail: event.detail }); }, this); @@ -6282,12 +7007,16 @@ define("rsvp", this.on('promise:failed', function(event) { this.trigger('error', { detail: event.detail }); }, this); - }; - var noop = function() {}; + try { + resolver(resolvePromise, rejectPromise); + } catch(e) { + rejectPromise(e); + } + }; var invokeCallback = function(type, promise, callback, event) { - var hasCallback = typeof callback === 'function', + var hasCallback = isFunction(callback), value, error, succeeded, failed; if (hasCallback) { @@ -6303,34 +7032,34 @@ define("rsvp", succeeded = true; } - if (value && typeof value.then === 'function') { - value.then(function(value) { - promise.resolve(value); - }, function(error) { - promise.reject(error); - }); + if (handleThenable(promise, value)) { + return; } else if (hasCallback && succeeded) { - promise.resolve(value); + resolve(promise, value); } else if (failed) { - promise.reject(error); - } else { - promise[type](value); + reject(promise, error); + } else if (type === 'resolve') { + resolve(promise, value); + } else if (type === 'reject') { + reject(promise, value); } }; Promise.prototype = { + constructor: Promise, + then: function(done, fail) { - var thenPromise = new Promise(); + var thenPromise = new Promise(function() {}); - if (this.isResolved) { - RSVP.async(function() { - invokeCallback('resolve', thenPromise, done, { detail: this.resolvedValue }); + if (this.isFulfilled) { + config.async(function() { + invokeCallback('resolve', thenPromise, done, { detail: this.fulfillmentValue }); }, this); } if (this.isRejected) { - RSVP.async(function() { - invokeCallback('reject', thenPromise, fail, { detail: this.rejectedValue }); + config.async(function() { + invokeCallback('reject', thenPromise, fail, { detail: this.rejectedReason }); }, this); } @@ -6343,75 +7072,162 @@ define("rsvp", }); return thenPromise; - }, + } + }; - resolve: function(value) { - resolve(this, value); + EventTarget.mixin(Promise.prototype); - this.resolve = noop; - this.reject = noop; - }, + function resolve(promise, value) { + if (promise === value) { + fulfill(promise, value); + } else if (!handleThenable(promise, value)) { + fulfill(promise, value); + } + } - reject: function(value) { - reject(this, value); + function handleThenable(promise, value) { + var then = null; - this.resolve = noop; - this.reject = noop; + if (objectOrFunction(value)) { + try { + then = value.then; + } catch(e) { + reject(promise, e); + return true; + } + + if (isFunction(then)) { + try { + then.call(value, function(val) { + if (value !== val) { + resolve(promise, val); + } else { + fulfill(promise, val); + } + }, function(val) { + reject(promise, val); + }); + } catch (e) { + reject(promise, e); + } + return true; + } } - }; - function resolve(promise, value) { - RSVP.async(function() { + return false; + } + + function fulfill(promise, value) { + config.async(function() { promise.trigger('promise:resolved', { detail: value }); - promise.isResolved = true; - promise.resolvedValue = value; + promise.isFulfilled = true; + promise.fulfillmentValue = value; }); } function reject(promise, value) { - RSVP.async(function() { + config.async(function() { promise.trigger('promise:failed', { detail: value }); promise.isRejected = true; - promise.rejectedValue = value; + promise.rejectedReason = value; }); } - function all(promises) { - var i, results = []; - var allPromise = new Promise(); - var remaining = promises.length; - if (remaining === 0) { - allPromise.resolve([]); - } + __exports__.Promise = Promise; + }); - var resolver = function(index) { - return function(value) { - resolve(index, value); - }; - }; +define("rsvp/reject", + ["rsvp/promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__.Promise; - var resolve = function(index, value) { - results[index] = value; - if (--remaining === 0) { - allPromise.resolve(results); - } - }; - var reject = function(error) { - allPromise.reject(error); - }; + function objectOrFunction(x) { + return typeof x === "function" || (typeof x === "object" && x !== null); + } - for (i = 0; i < remaining; i++) { - promises[i].then(resolver(i), reject); - } - return allPromise; + + function reject(reason) { + return new Promise(function (resolve, reject) { + reject(reason); + }); } - EventTarget.mixin(Promise.prototype); - RSVP = { async: async, Promise: Promise, Event: Event, EventTarget: EventTarget, all: all, raiseOnUncaughtExceptions: true }; - return RSVP; + __exports__.reject = reject; + }); + +define("rsvp/resolve", + ["rsvp/promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__.Promise; + + + function objectOrFunction(x) { + return typeof x === "function" || (typeof x === "object" && x !== null); + } + + function resolve(thenable){ + var promise = new Promise(function(resolve, reject){ + var then; + + try { + if ( objectOrFunction(thenable) ) { + then = thenable.then; + + if (typeof then === "function") { + then.call(thenable, resolve, reject); + } else { + resolve(thenable); + } + + } else { + resolve(thenable); + } + + } catch(error) { + reject(error); + } + }); + + return promise; + } + + + __exports__.resolve = resolve; + }); + +define("rsvp", + ["rsvp/events","rsvp/promise","rsvp/node","rsvp/all","rsvp/hash","rsvp/defer","rsvp/config","rsvp/resolve","rsvp/reject","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __exports__) { + "use strict"; + var EventTarget = __dependency1__.EventTarget; + var Promise = __dependency2__.Promise; + var denodeify = __dependency3__.denodeify; + var all = __dependency4__.all; + var hash = __dependency5__.hash; + var defer = __dependency6__.defer; + var config = __dependency7__.config; + var resolve = __dependency8__.resolve; + var reject = __dependency9__.reject; + + function configure(name, value) { + config[name] = value; + } + + + __exports__.Promise = Promise; + __exports__.EventTarget = EventTarget; + __exports__.all = all; + __exports__.hash = hash; + __exports__.defer = defer; + __exports__.denodeify = denodeify; + __exports__.configure = configure; + __exports__.resolve = resolve; + __exports__.reject = reject; }); })(); @@ -6588,10 +7404,6 @@ define("container", this.children = []; eachDestroyable(this, function(item) { - item.isDestroying = true; - }); - - eachDestroyable(this, function(item) { item.destroy(); }); @@ -6657,7 +7469,7 @@ define("container", var factory = factoryFor(container, fullName); var splitName = fullName.split(":"), - type = splitName[0], name = splitName[1], + type = splitName[0], value; if (option(container, fullName, 'instantiate') === false) { @@ -6708,79 +7520,6 @@ define("container", var indexOf = Ember.EnumerableUtils.indexOf; -// ........................................ -// TYPING & ARRAY MESSAGING -// - -var TYPE_MAP = {}; -var t = "Boolean Number String Function Array Date RegExp Object".split(" "); -Ember.ArrayPolyfills.forEach.call(t, function(name) { - TYPE_MAP[ "[object " + name + "]" ] = name.toLowerCase(); -}); - -var toString = Object.prototype.toString; - -/** - Returns a consistent type for the passed item. - - Use this instead of the built-in `typeof` to get the type of an item. - It will return the same result across all browsers and includes a bit - more detail. Here is what will be returned: - - | Return Value | Meaning | - |---------------|------------------------------------------------------| - | 'string' | String primitive | - | 'number' | Number primitive | - | 'boolean' | Boolean primitive | - | 'null' | Null value | - | 'undefined' | Undefined value | - | 'function' | A function | - | 'array' | An instance of Array | - | 'class' | An Ember class (created using Ember.Object.extend()) | - | 'instance' | An Ember object instance | - | 'error' | An instance of the Error object | - | 'object' | A JavaScript object not inheriting from Ember.Object | - - Examples: - - ```javascript - Ember.typeOf(); // 'undefined' - Ember.typeOf(null); // 'null' - Ember.typeOf(undefined); // 'undefined' - Ember.typeOf('michael'); // 'string' - Ember.typeOf(101); // 'number' - Ember.typeOf(true); // 'boolean' - Ember.typeOf(Ember.makeArray); // 'function' - Ember.typeOf([1,2,90]); // 'array' - Ember.typeOf(Ember.Object.extend()); // 'class' - Ember.typeOf(Ember.Object.create()); // 'instance' - Ember.typeOf(new Error('teamocil')); // 'error' - - // "normal" JavaScript object - Ember.typeOf({a: 'b'}); // 'object' - ``` - - @method typeOf - @for Ember - @param {Object} item the item to check - @return {String} the type -*/ -Ember.typeOf = function(item) { - var ret; - - ret = (item === null || item === undefined) ? String(item) : TYPE_MAP[toString.call(item)] || 'object'; - - if (ret === 'function') { - if (Ember.Object && Ember.Object.detect(item)) ret = 'class'; - } else if (ret === 'object') { - if (item instanceof Error) ret = 'error'; - else if (Ember.Object && item instanceof Ember.Object) ret = 'instance'; - else ret = 'object'; - } - - return ret; -}; - /** This will compare two javascript values of possibly different types. It will tell you which one is greater than the other by returning: @@ -7035,10 +7774,15 @@ Ember.ORDER_DEFINITION = Ember.ENV.ORDER_DEFINITION || [ */ Ember.keys = Object.keys; -if (!Ember.keys) { +if (!Ember.keys || Ember.create.isSimulated) { Ember.keys = function(obj) { var ret = []; for(var key in obj) { + // Prevents browsers that don't respect non-enumerability from + // copying internal Ember properties + if (key.substring(0,2) === '__') continue; + if (key === '_super') continue; + if (obj.hasOwnProperty(key)) { ret.push(key); } } return ret; @@ -7060,7 +7804,7 @@ var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'n @constructor */ Ember.Error = function() { - var tmp = Error.prototype.constructor.apply(this, arguments); + var tmp = Error.apply(this, arguments); // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. for (var idx = 0; idx < errorProps.length; idx++) { @@ -7139,7 +7883,8 @@ Ember.String = { ``` @method fmt - @param {Object...} [args] + @param {String} str The string to format + @param {Array} formats An array of parameters to interpolate into string. @return {String} formatted string */ fmt: function(str, formats) { @@ -7321,10 +8066,12 @@ Ember.String = { /** Returns the Capitalized form of a string - 'innerHTML'.capitalize() // 'InnerHTML' - 'action_name'.capitalize() // 'Action_name' - 'css-class-name'.capitalize() // 'Css-class-name' - 'my favorite items'.capitalize() // 'My favorite items' + ```javascript + 'innerHTML'.capitalize() // 'InnerHTML' + 'action_name'.capitalize() // 'Action_name' + 'css-class-name'.capitalize() // 'Css-class-name' + 'my favorite items'.capitalize() // 'My favorite items' + ``` @method capitalize @param {String} str @@ -7361,7 +8108,7 @@ var fmt = Ember.String.fmt, if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { /** - See {{#crossLink "Ember.String/fmt"}}{{/crossLink}} + See `Ember.String.fmt`. @method fmt @for String @@ -7371,7 +8118,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/w"}}{{/crossLink}} + See `Ember.String.w`. @method w @for String @@ -7381,7 +8128,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/loc"}}{{/crossLink}} + See `Ember.String.loc`. @method loc @for String @@ -7391,7 +8138,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/camelize"}}{{/crossLink}} + See `Ember.String.camelize`. @method camelize @for String @@ -7401,7 +8148,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/decamelize"}}{{/crossLink}} + See `Ember.String.decamelize`. @method decamelize @for String @@ -7411,7 +8158,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/dasherize"}}{{/crossLink}} + See `Ember.String.dasherize`. @method dasherize @for String @@ -7421,7 +8168,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/underscore"}}{{/crossLink}} + See `Ember.String.underscore`. @method underscore @for String @@ -7431,7 +8178,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/classify"}}{{/crossLink}} + See `Ember.String.classify`. @method classify @for String @@ -7441,7 +8188,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/capitalize"}}{{/crossLink}} + See `Ember.String.capitalize`. @method capitalize @for String @@ -7517,8 +8264,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Function) { will instead clear the cache so that it is updated when the next `get` is called on the property. - See {{#crossLink "Ember.ComputedProperty"}}{{/crossLink}}, - {{#crossLink "Ember/computed"}}{{/crossLink}} + See `Ember.ComputedProperty`, `Ember.computed`. @method property @for Function @@ -7545,7 +8291,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Function) { }); ``` - See {{#crossLink "Ember.Observable/observes"}}{{/crossLink}} + See `Ember.Observable.observes`. @method observes @for Function @@ -7572,7 +8318,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Function) { }); ``` - See {{#crossLink "Ember.Observable/observesBefore"}}{{/crossLink}} + See `Ember.Observable.observesBefore`. @method observesBefore @for Function @@ -8469,9 +9215,7 @@ Ember.Enumerable = Ember.Mixin.create({ // HELPERS // -var get = Ember.get, set = Ember.set, map = Ember.EnumerableUtils.map, cacheFor = Ember.cacheFor; - -function none(obj) { return obj===null || obj===undefined; } +var get = Ember.get, set = Ember.set, isNone = Ember.isNone, map = Ember.EnumerableUtils.map, cacheFor = Ember.cacheFor; // .......................................................... // ARRAY @@ -8494,7 +9238,7 @@ function none(obj) { return obj===null || obj===undefined; } You can use the methods defined in this module to access and modify array contents in a KVO-friendly way. You can also be notified whenever the - membership if an array changes by changing the syntax of the property to + membership of an array changes by changing the syntax of the property to `.observes('*myProperty.[]')`. To support `Ember.Array` in your own class, you must override two @@ -8511,9 +9255,6 @@ function none(obj) { return obj===null || obj===undefined; } */ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.prototype */ { - // compatibility - isSCArray: true, - /** Your array must support the `length` property. Your replace methods should set this property whenever it changes. @@ -8542,7 +9283,7 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot @method objectAt @param {Number} idx The index of the item to return. - @return {any} item at index or undefined + @return {*} item at index or undefined */ objectAt: function(idx) { if ((idx < 0) || (idx>=get(this, 'length'))) return undefined ; @@ -8621,8 +9362,8 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot slice: function(beginIndex, endIndex) { var ret = Ember.A([]); var length = get(this, 'length') ; - if (none(beginIndex)) beginIndex = 0 ; - if (none(endIndex) || (endIndex > length)) endIndex = length ; + if (isNone(beginIndex)) beginIndex = 0 ; + if (isNone(endIndex) || (endIndex > length)) endIndex = length ; if (beginIndex < 0) beginIndex = length + beginIndex; if (endIndex < 0) endIndex = length + endIndex; @@ -8782,7 +9523,7 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot @param {Number} startIdx The starting index in the array that will change. @param {Number} removeAmt The number of items that will be removed. If you pass `null` assumes 0 - @param {Number} addAmt The number of items that will be added If you + @param {Number} addAmt The number of items that will be added. If you pass `null` assumes 0. @return {Ember.Array} receiver */ @@ -8816,6 +9557,20 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot return this; }, + /** + If you are implementing an object that supports `Ember.Array`, call this + method just after the array content changes to notify any observers and + invalidate any related properties. Pass the starting index of the change + as well as a delta of the amounts to change. + + @method arrayContentDidChange + @param {Number} startIdx The starting index in the array that did change. + @param {Number} removeAmt The number of items that were removed. If you + pass `null` assumes 0 + @param {Number} addAmt The number of items that were added. If you + pass `null` assumes 0. + @return {Ember.Array} receiver + */ arrayContentDidChange: function(startIdx, removeAmt, addAmt) { // if no args are passed assume everything changes @@ -8956,8 +9711,7 @@ var get = Ember.get, set = Ember.set; @extends Ember.Mixin @since Ember 0.9 */ -Ember.Copyable = Ember.Mixin.create( -/** @scope Ember.Copyable.prototype */ { +Ember.Copyable = Ember.Mixin.create(/** @scope Ember.Copyable.prototype */ { /** Override to return a copy of the receiver. Default implementation raises @@ -9062,8 +9816,7 @@ var get = Ember.get, set = Ember.set; @extends Ember.Mixin @since Ember 0.9 */ -Ember.Freezable = Ember.Mixin.create( -/** @scope Ember.Freezable.prototype */ { +Ember.Freezable = Ember.Mixin.create(/** @scope Ember.Freezable.prototype */ { /** Set to `true` when the object is frozen. Use this property to detect @@ -9116,7 +9869,7 @@ var forEach = Ember.EnumerableUtils.forEach; To add an object to an enumerable, use the `addObject()` method. This method will only add the object to the enumerable if the object is not - already present and the object if of a type supported by the enumerable. + already present and is of a type supported by the enumerable. ```javascript set.addObject(contact); @@ -9124,8 +9877,8 @@ var forEach = Ember.EnumerableUtils.forEach; ## Removing Objects - To remove an object form an enumerable, use the `removeObject()` method. This - will only remove the object if it is already in the enumerable, otherwise + To remove an object from an enumerable, use the `removeObject()` method. This + will only remove the object if it is present in the enumerable, otherwise this method has no effect. ```javascript @@ -9152,7 +9905,7 @@ Ember.MutableEnumerable = Ember.Mixin.create(Ember.Enumerable, { already present in the collection. If the object is present, this method has no effect. - If the passed object is of a type not supported by the receiver + If the passed object is of a type not supported by the receiver, then this method should raise an exception. @method addObject @@ -9179,10 +9932,10 @@ Ember.MutableEnumerable = Ember.Mixin.create(Ember.Enumerable, { __Required.__ You must implement this method to apply this mixin. Attempts to remove the passed object from the receiver collection if the - object is in present in the collection. If the object is not present, + object is present in the collection. If the object is not present, this method has no effect. - If the passed object is of a type not supported by the receiver + If the passed object is of a type not supported by the receiver, then this method should raise an exception. @method removeObject @@ -9193,7 +9946,7 @@ Ember.MutableEnumerable = Ember.Mixin.create(Ember.Enumerable, { /** - Removes each objects in the passed enumerable from the receiver. + Removes each object in the passed enumerable from the receiver. @method removeObjects @param {Ember.Enumerable} objects the objects to remove @@ -9244,8 +9997,7 @@ var get = Ember.get, set = Ember.set; @uses Ember.Array @uses Ember.MutableEnumerable */ -Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, - /** @scope Ember.MutableArray.prototype */ { +Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable,/** @scope Ember.MutableArray.prototype */ { /** __Required.__ You must implement this method to apply this mixin. @@ -9351,8 +10103,8 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, ``` @method pushObject - @param {anything} obj object to push - @return {any} the same obj passed as param + @param {*} obj object to push + @return {*} the same obj passed as param */ pushObject: function(obj) { this.insertAt(get(this, 'length'), obj) ; @@ -9431,8 +10183,8 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, ``` @method unshiftObject - @param {anything} obj object to unshift - @return {any} the same obj passed as param + @param {*} obj object to unshift + @return {*} the same obj passed as param */ unshiftObject: function(obj) { this.insertAt(0, obj) ; @@ -9516,7 +10268,6 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, }); - })(); @@ -9564,7 +10315,7 @@ var get = Ember.get, set = Ember.set; For example: ```javascript - Ember.Object.create({ + Ember.Object.extend({ valueObserver: function() { // Executes whenever the "value" property changes }.observes('value') @@ -9583,8 +10334,8 @@ var get = Ember.get, set = Ember.set; object.addObserver('propertyKey', targetObject, targetAction) ``` - This will call the `targetAction` method on the `targetObject` to be called - whenever the value of the `propertyKey` changes. + This will call the `targetAction` method on the `targetObject` whenever + the value of the `propertyKey` changes. Note that if `propertyKey` is a computed property, the observer will be called when any of the property dependencies are changed, even if the @@ -9844,8 +10595,8 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { This is the core method used to register an observer for a property. - Once you call this method, anytime the key's value is set, your observer - will be notified. Note that the observers are triggered anytime the + Once you call this method, any time the key's value is set, your observer + will be notified. Note that the observers are triggered any time the value is set, regardless of whether it has actually changed. Your observer should be prepared to handle that. @@ -9970,11 +10721,11 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { @method incrementProperty @param {String} keyName The name of the property to increment - @param {Object} increment The amount to increment by. Defaults to 1 - @return {Object} The new property value + @param {Number} increment The amount to increment by. Defaults to 1 + @return {Number} The new property value */ incrementProperty: function(keyName, increment) { - if (!increment) { increment = 1; } + if (Ember.isNone(increment)) { increment = 1; } set(this, keyName, (get(this, keyName) || 0)+increment); return get(this, keyName); }, @@ -9989,12 +10740,12 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { @method decrementProperty @param {String} keyName The name of the property to decrement - @param {Object} increment The amount to decrement by. Defaults to 1 - @return {Object} The new property value + @param {Number} decrement The amount to decrement by. Defaults to 1 + @return {Number} The new property value */ - decrementProperty: function(keyName, increment) { - if (!increment) { increment = 1; } - set(this, keyName, (get(this, keyName) || 0)-increment); + decrementProperty: function(keyName, decrement) { + if (Ember.isNone(decrement)) { decrement = 1; } + set(this, keyName, (get(this, keyName) || 0)-decrement); return get(this, keyName); }, @@ -10003,7 +10754,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { current value. ```javascript - starship.toggleProperty('warpDriveEnaged'); + starship.toggleProperty('warpDriveEngaged'); ``` @method toggleProperty @@ -10035,7 +10786,6 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { } }); - })(); @@ -10049,6 +10799,15 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { var get = Ember.get, set = Ember.set; /** +`Ember.TargetActionSupport` is a mixin that can be included in a class +to add a `triggerAction` method with semantics similar to the Handlebars +`{{action}}` helper. In normal Ember usage, the `{{action}}` helper is +usually the best choice. This mixin is most often useful when you are +doing more complex event handling in View objects. + +See also `Ember.ViewTargetActionSupport`, which has +view-aware defaults for target and actionContext. + @class TargetActionSupport @namespace Ember @extends Ember.Mixin @@ -10056,6 +10815,7 @@ var get = Ember.get, set = Ember.set; Ember.TargetActionSupport = Ember.Mixin.create({ target: null, action: null, + actionContext: null, targetObject: Ember.computed(function() { var target = get(this, 'target'); @@ -10069,21 +10829,86 @@ Ember.TargetActionSupport = Ember.Mixin.create({ } }).property('target'), - triggerAction: function() { - var action = get(this, 'action'), - target = get(this, 'targetObject'); + actionContextObject: Ember.computed(function() { + var actionContext = get(this, 'actionContext'); + + if (Ember.typeOf(actionContext) === "string") { + var value = get(this, actionContext); + if (value === undefined) { value = get(Ember.lookup, actionContext); } + return value; + } else { + return actionContext; + } + }).property('actionContext'), + + /** + Send an "action" with an "actionContext" to a "target". The action, actionContext + and target will be retrieved from properties of the object. For example: + + ```javascript + App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { + target: Ember.computed.alias('controller'), + action: 'save', + actionContext: Ember.computed.alias('context'), + click: function(){ + this.triggerAction(); // Sends the `save` action, along with the current context + // to the current controller + } + }); + ``` + + The `target`, `action`, and `actionContext` can be provided as properties of + an optional object argument to `triggerAction` as well. + + ```javascript + App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { + click: function(){ + this.triggerAction({ + action: 'save', + target: this.get('controller'), + actionContext: this.get('context'), + }); // Sends the `save` action, along with the current context + // to the current controller + } + }); + ``` + + The `actionContext` defaults to the object you mixing `TargetActionSupport` into. + But `target` and `action` must be specified either as properties or with the argument + to `triggerAction`, or a combination: + + ```javascript + App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { + target: Ember.computed.alias('controller'), + click: function(){ + this.triggerAction({ + action: 'save' + }); // Sends the `save` action, along with a reference to `this`, + // to the current controller + } + }); + ``` + + @method triggerAction + @param opts {Hash} (optional, with the optional keys action, target and/or actionContext) + @return {Boolean} true if the action was sent successfully and did not return false + */ + triggerAction: function(opts) { + opts = opts || {}; + var action = opts['action'] || get(this, 'action'), + target = opts['target'] || get(this, 'targetObject'), + actionContext = opts['actionContext'] || get(this, 'actionContextObject') || this; if (target && action) { var ret; - if (typeof target.send === 'function') { - ret = target.send(action, this); + if (target.send) { + ret = target.send.apply(target, [action, actionContext]); } else { - if (typeof action === 'string') { - action = target[action]; - } - ret = action.call(target, this); + Ember.assert("The action '" + action + "' did not exist on " + target, typeof target[action] === 'function'); + ret = target[action].apply(target, [actionContext]); } + if (ret !== false) ret = true; return ret; @@ -10255,9 +11080,9 @@ Ember.Evented = Ember.Mixin.create({ (function() { var RSVP = requireModule("rsvp"); -RSVP.async = function(callback, binding) { +RSVP.configure('async', function(callback, binding) { Ember.run.schedule('actions', binding, callback); -}; +}); /** @module ember @@ -10279,9 +11104,22 @@ Ember.DeferredMixin = Ember.Mixin.create({ @param {Function} doneCallback a callback function to be called when done @param {Function} failCallback a callback function to be called when failed */ - then: function(doneCallback, failCallback) { - var promise = get(this, 'promise'); - return promise.then.apply(promise, arguments); + then: function(resolve, reject) { + var deferred, promise, entity; + + entity = this; + deferred = get(this, '_deferred'); + promise = deferred.promise; + + function fulfillmentHandler(fulfillment) { + if (fulfillment === promise) { + return resolve(entity); + } else { + return resolve(fulfillment); + } + } + + return promise.then(resolve && fulfillmentHandler, reject); }, /** @@ -10290,7 +11128,16 @@ Ember.DeferredMixin = Ember.Mixin.create({ @method resolve */ resolve: function(value) { - get(this, 'promise').resolve(value); + var deferred, promise; + + deferred = get(this, '_deferred'); + promise = deferred.promise; + + if (value === this){ + deferred.resolve(promise); + } else { + deferred.resolve(value); + } }, /** @@ -10299,11 +11146,11 @@ Ember.DeferredMixin = Ember.Mixin.create({ @method reject */ reject: function(value) { - get(this, 'promise').reject(value); + get(this, '_deferred').reject(value); }, - promise: Ember.computed(function() { - return new RSVP.Promise(); + _deferred: Ember.computed(function() { + return RSVP.defer(); }) }); @@ -10393,6 +11240,9 @@ function makeCtor() { for (var i = 0, l = props.length; i < l; i++) { var properties = props[i]; + + Ember.assert("Ember.Object.create no longer supports mixing in other definitions, use createWithMixins instead.", !(properties instanceof Ember.Mixin)); + for (var keyName in properties) { if (!properties.hasOwnProperty(keyName)) { continue; } @@ -10514,8 +11364,6 @@ CoreObject.PrototypeMixin = Mixin.create({ do important setup work, and you'll see strange behavior in your application. - ``` - @method init */ init: function() {}, @@ -10615,24 +11463,26 @@ CoreObject.PrototypeMixin = Mixin.create({ raised. Note that destruction is scheduled for the end of the run loop and does not - happen immediately. + happen immediately. It will set an isDestroying flag immediately. @method destroy @return {Ember.Object} receiver */ destroy: function() { - if (this._didCallDestroy) { return; } - + if (this.isDestroying) { return; } this.isDestroying = true; - this._didCallDestroy = true; - - if (this.willDestroy) { this.willDestroy(); } + schedule('actions', this, this.willDestroy); schedule('destroy', this, this._scheduledDestroy); return this; }, /** + Override to implement teardown. + */ + willDestroy: Ember.K, + + /** @private Invoked by the run loop to actually destroy the object. This is @@ -10641,10 +11491,9 @@ CoreObject.PrototypeMixin = Mixin.create({ @method _scheduledDestroy */ _scheduledDestroy: function() { + if (this.isDestroyed) { return; } destroy(this); - set(this, 'isDestroyed', true); - - if (this.didDestroy) { this.didDestroy(); } + this.isDestroyed = true; }, bind: function(to, from) { @@ -10991,7 +11840,7 @@ function findNamespaces() { for (var prop in lookup) { // These don't raise exceptions but can cause warnings - if (prop === "parent" || prop === "top" || prop === "frameElement") { continue; } + if (prop === "parent" || prop === "top" || prop === "frameElement" || prop === "webkitStorageInfo") { continue; } // get(window.globalStorage, 'isNamespace') would try to read the storage for domain isNamespace and cause exception in Firefox. // globalStorage is a storage obsoleted by the WhatWG storage specification. See https://developer.mozilla.org/en/DOM/Storage#globalStorage @@ -11135,8 +11984,7 @@ var get = Ember.get, set = Ember.set; @extends Ember.Object @uses Ember.MutableArray */ -Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray, -/** @scope Ember.ArrayProxy.prototype */ { +Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray,/** @scope Ember.ArrayProxy.prototype */ { /** The content array. Must be an object that implements `Ember.Array` and/or @@ -11412,7 +12260,6 @@ Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray, } }); - })(); @@ -11512,8 +12359,7 @@ function contentPropertyDidChange(content, contentKey) { @namespace Ember @extends Ember.Object */ -Ember.ObjectProxy = Ember.Object.extend( -/** @scope Ember.ObjectProxy.prototype */ { +Ember.ObjectProxy = Ember.Object.extend(/** @scope Ember.ObjectProxy.prototype */ { /** The object whose properties will be forwarded. @@ -11679,7 +12525,7 @@ Ember.EachProxy = Ember.Object.extend({ @method unknownProperty @param keyName {String} - @param value {anything} + @param value {*} */ unknownProperty: function(keyName, value) { var ret; @@ -11903,9 +12749,8 @@ if (ignore.length>0) { @namespace Ember @extends Ember.Mixin @uses Ember.MutableArray - @uses Ember.MutableEnumerable + @uses Ember.Observable @uses Ember.Copyable - @uses Ember.Freezable */ Ember.NativeArray = NativeArray; @@ -11952,7 +12797,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) { @submodule ember-runtime */ -var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, none = Ember.isNone, fmt = Ember.String.fmt; +var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, isNone = Ember.isNone, fmt = Ember.String.fmt; /** An unordered collection of objects. @@ -12310,7 +13155,7 @@ Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Emb // implements Ember.MutableEnumerable addObject: function(obj) { if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); - if (none(obj)) return this; // nothing to do + if (isNone(obj)) return this; // nothing to do var guid = guidFor(obj), idx = this[guid], @@ -12338,7 +13183,7 @@ Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Emb // implements Ember.MutableEnumerable removeObject: function(obj) { if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); - if (none(obj)) return this; // nothing to do + if (isNone(obj)) return this; // nothing to do var guid = guidFor(obj), idx = this[guid], @@ -12413,7 +13258,7 @@ Deferred.reopenClass({ promise: function(callback, binding) { var deferred = Deferred.create(); callback.call(binding, deferred); - return get(deferred, 'promise'); + return deferred; } }); @@ -12424,6 +13269,8 @@ Ember.Deferred = Deferred; (function() { +var forEach = Ember.ArrayPolyfills.forEach; + /** @module ember @submodule ember-runtime @@ -12456,12 +13303,10 @@ Ember.onLoad = function(name, callback) { @param object {Object} object to pass to callbacks */ Ember.runLoadHooks = function(name, object) { - var hooks; - loaded[name] = object; - if (hooks = loadHooks[name]) { - loadHooks[name].forEach(function(callback) { + if (loadHooks[name]) { + forEach.call(loadHooks[name], function(callback) { callback(object); }); } @@ -12535,6 +13380,8 @@ Ember.ControllerMixin = Ember.Mixin.create({ container: null, + parentController: null, + store: null, model: Ember.computed.alias('content'), @@ -12877,6 +13724,10 @@ var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach, }); ``` + The itemController instances will have a `parentController` property set to + either the the `parentController` property of the `ArrayController` + or to the `ArrayController` instance itself. + @class ArrayController @namespace Ember @extends Ember.ArrayProxy @@ -12916,9 +13767,9 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, }); ``` - @method - @type String - @default null + @method lookupItemController + @param {Object} object + @return {String} */ lookupItemController: function(object) { return get(this, 'itemController'); @@ -12967,8 +13818,8 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, }, init: function() { - this._super(); if (!this.get('content')) { Ember.defineProperty(this, 'content', undefined, Ember.A()); } + this._super(); this.set('_subControllers', Ember.A()); }, @@ -12987,6 +13838,7 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, } subController.set('target', this); + subController.set('parentController', get(this, 'parentController') || this); subController.set('content', object); return subController; @@ -12996,10 +13848,11 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, _resetSubControllers: function() { var subControllers = get(this, '_subControllers'); - - forEach(subControllers, function(subController) { - if (subController) { subController.destroy(); } - }); + if (subControllers) { + forEach(subControllers, function(subController) { + if (subController) { subController.destroy(); } + }); + } this.set('_subControllers', Ember.A()); } @@ -13059,7 +13912,7 @@ Ember Runtime */ var jQuery = Ember.imports.jQuery; -Ember.assert("Ember Views require jQuery 1.8, 1.9 or 2.0", jQuery && (jQuery().jquery.match(/^((1\.(8|9))|2.0)(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_JQUERY)); +Ember.assert("Ember Views require jQuery 1.8, 1.9, 1.10, or 2.0", jQuery && (jQuery().jquery.match(/^((1\.(8|9|10))|2.0)(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_JQUERY)); /** Alias for jQuery @@ -13099,7 +13952,7 @@ if (Ember.$) { @submodule ember-views */ -/*** BEGIN METAMORPH HELPERS ***/ +/* BEGIN METAMORPH HELPERS */ // Internet Explorer prior to 9 does not allow setting innerHTML if the first element // is a "zero-scope" element. This problem can be worked around by making @@ -13172,7 +14025,7 @@ var setInnerHTMLWithoutFix = function(element, html) { } }; -/*** END METAMORPH HELPERS */ +/* END METAMORPH HELPERS */ var innerHTMLTags = {}; @@ -13276,7 +14129,7 @@ Ember.RenderBuffer = function(tagName) { Ember._RenderBuffer = function(tagName) { this.tagNames = [tagName || null]; - this.buffer = []; + this.buffer = ""; }; Ember._RenderBuffer.prototype = @@ -13285,6 +14138,8 @@ Ember._RenderBuffer.prototype = // The root view's element _element: null, + _hasElement: true, + /** @private @@ -13400,7 +14255,7 @@ Ember._RenderBuffer.prototype = @chainable */ push: function(string) { - this.buffer.push(string); + this.buffer += string; return this; }, @@ -13533,7 +14388,7 @@ Ember._RenderBuffer.prototype = var tagName = this.currentTagName(); if (!tagName) { return; } - if (!this._element && this.buffer.length === 0) { + if (this._hasElement && !this._element && this.buffer.length === 0) { this._element = this.generateElement(); return; } @@ -13546,27 +14401,27 @@ Ember._RenderBuffer.prototype = style = this.elementStyle, attr, prop; - buffer.push('<' + tagName); + buffer += '<' + tagName; if (id) { - buffer.push(' id="' + this._escapeAttribute(id) + '"'); + buffer += ' id="' + this._escapeAttribute(id) + '"'; this.elementId = null; } if (classes) { - buffer.push(' class="' + this._escapeAttribute(classes.join(' ')) + '"'); + buffer += ' class="' + this._escapeAttribute(classes.join(' ')) + '"'; this.classes = null; } if (style) { - buffer.push(' style="'); + buffer += ' style="'; for (prop in style) { if (style.hasOwnProperty(prop)) { - buffer.push(prop + ':' + this._escapeAttribute(style[prop]) + ';'); + buffer += prop + ':' + this._escapeAttribute(style[prop]) + ';'; } } - buffer.push('"'); + buffer += '"'; this.elementStyle = null; } @@ -13574,7 +14429,7 @@ Ember._RenderBuffer.prototype = if (attrs) { for (attr in attrs) { if (attrs.hasOwnProperty(attr)) { - buffer.push(' ' + attr + '="' + this._escapeAttribute(attrs[attr]) + '"'); + buffer += ' ' + attr + '="' + this._escapeAttribute(attrs[attr]) + '"'; } } @@ -13587,9 +14442,9 @@ Ember._RenderBuffer.prototype = var value = props[prop]; if (value || typeof(value) === 'number') { if (value === true) { - buffer.push(' ' + prop + '="' + prop + '"'); + buffer += ' ' + prop + '="' + prop + '"'; } else { - buffer.push(' ' + prop + '="' + this._escapeAttribute(props[prop]) + '"'); + buffer += ' ' + prop + '="' + this._escapeAttribute(props[prop]) + '"'; } } } @@ -13598,12 +14453,13 @@ Ember._RenderBuffer.prototype = this.elementProperties = null; } - buffer.push('>'); + buffer += '>'; + this.buffer = buffer; }, pushClosingTag: function() { var tagName = this.tagNames.pop(); - if (tagName) { this.buffer.push('</' + tagName + '>'); } + if (tagName) { this.buffer += '</' + tagName + '>'; } }, currentTagName: function() { @@ -13687,17 +14543,20 @@ Ember._RenderBuffer.prototype = @return {String} The generated HTML */ string: function() { - if (this._element) { + if (this._hasElement && this._element) { // Firefox versions < 11 do not have support for element.outerHTML. - return this.element().outerHTML || - new XMLSerializer().serializeToString(this.element()); + var thisElement = this.element(), outerHTML = thisElement.outerHTML; + if (typeof outerHTML === 'undefined'){ + return Ember.$('<div/>').append(thisElement).html(); + } + return outerHTML; } else { return this.innerString(); } }, innerString: function() { - return this.buffer.join(''); + return this.buffer; }, _escapeAttribute: function(value) { @@ -13749,8 +14608,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; @private @extends Ember.Object */ -Ember.EventDispatcher = Ember.Object.extend( -/** @scope Ember.EventDispatcher.prototype */{ +Ember.EventDispatcher = Ember.Object.extend(/** @scope Ember.EventDispatcher.prototype */{ /** @private @@ -13782,7 +14640,7 @@ Ember.EventDispatcher = Ember.Object.extend( @method setup @param addedEvents {Hash} */ - setup: function(addedEvents) { + setup: function(addedEvents, rootElement) { var event, events = { touchstart : 'touchStart', touchmove : 'touchMove', @@ -13815,7 +14673,12 @@ Ember.EventDispatcher = Ember.Object.extend( Ember.$.extend(events, addedEvents || {}); - var rootElement = Ember.$(get(this, 'rootElement')); + + if (!Ember.isNone(rootElement)) { + set(this, 'rootElement', rootElement); + } + + rootElement = Ember.$(get(this, 'rootElement')); Ember.assert(fmt('You cannot use the same root element (%@) multiple times in an Ember.Application', [rootElement.selector || rootElement[0].tagName]), !rootElement.is('.ember-application')); Ember.assert('You cannot make a new Ember.Application using a root element that is a descendent of an existing Ember.Application', !rootElement.closest('.ember-application').length); @@ -13857,7 +14720,7 @@ Ember.EventDispatcher = Ember.Object.extend( setupHandler: function(rootElement, event, eventName) { var self = this; - rootElement.delegate('.ember-view', event + '.ember', function(evt, triggeringManager) { + rootElement.on(event + '.ember', '.ember-view', function(evt, triggeringManager) { return Ember.handleErrors(function() { var view = Ember.View.views[this.id], result = true, manager = null; @@ -13876,7 +14739,7 @@ Ember.EventDispatcher = Ember.Object.extend( }, this); }); - rootElement.delegate('[data-ember-action]', event + '.ember', function(evt) { + rootElement.on(event + '.ember', '[data-ember-action]', function(evt) { return Ember.handleErrors(function() { var actionId = Ember.$(evt.currentTarget).attr('data-ember-action'), action = Ember.Handlebars.ActionHelper.registeredActions[actionId]; @@ -13928,7 +14791,7 @@ Ember.EventDispatcher = Ember.Object.extend( destroy: function() { var rootElement = get(this, 'rootElement'); - Ember.$(rootElement).undelegate('.ember').removeClass('ember-application'); + Ember.$(rootElement).off('.ember', '**').removeClass('ember-application'); return this._super(); } }); @@ -14060,18 +14923,6 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { init: function() { this._super(); - - // Register the view for event handling. This hash is used by - // Ember.EventDispatcher to dispatch incoming events. - if (!this.isVirtual) { - Ember.assert("Attempted to register a view with an id already in use: "+this.elementId, !Ember.View.views[this.elementId]); - Ember.View.views[this.elementId] = this; - } - - this.addBeforeObserver('elementId', function() { - throw new Error("Changing a view's elementId after creation is not allowed"); - }); - this.transitionTo('preRender'); }, @@ -14101,7 +14952,7 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { concreteView: Ember.computed(function() { if (!this.isVirtual) { return this; } else { return get(this, 'parentView'); } - }).property('parentView').volatile(), + }).property('parentView'), instrumentName: 'core_view', @@ -14139,8 +14990,6 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { }, _renderToBuffer: function(parentBuffer, bufferOperation) { - Ember.run.sync(); - // If this is the top-most view, start a new buffer. Otherwise, // create a new buffer relative to the original using the // provided buffer operation (for example, `insertAfter` will @@ -14186,9 +15035,11 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { return Ember.typeOf(this[name]) === 'function' || this._super(name); }, - willDestroy: function() { + destroy: function() { var parent = this._parentView; + if (!this._super()) { return; } + // destroy the element -- this will avoid each child view destroying // the element over and over again... if (!this.removedFromDOM) { this.destroyElement(); } @@ -14198,10 +15049,9 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { // the DOM again. if (parent) { parent.removeChild(this); } - this.transitionTo('destroyed'); + this.transitionTo('destroying', false); - // next remove view from global hash - if (!this.isVirtual) delete Ember.View.views[this.elementId]; + return this; }, clearRenderedChildren: Ember.K, @@ -14211,6 +15061,68 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { destroyElement: Ember.K }); +var ViewCollection = Ember._ViewCollection = function(initialViews) { + var views = this.views = initialViews || []; + this.length = views.length; +}; + +ViewCollection.prototype = { + length: 0, + + trigger: function(eventName) { + var views = this.views, view; + for (var i = 0, l = views.length; i < l; i++) { + view = views[i]; + if (view.trigger) { view.trigger(eventName); } + } + }, + + triggerRecursively: function(eventName) { + var views = this.views; + for (var i = 0, l = views.length; i < l; i++) { + views[i].triggerRecursively(eventName); + } + }, + + invokeRecursively: function(fn) { + var views = this.views, view; + + for (var i = 0, l = views.length; i < l; i++) { + view = views[i]; + fn(view); + } + }, + + transitionTo: function(state, children) { + var views = this.views; + for (var i = 0, l = views.length; i < l; i++) { + views[i].transitionTo(state, children); + } + }, + + push: function() { + this.length += arguments.length; + var views = this.views; + return views.push.apply(views, arguments); + }, + + objectAt: function(idx) { + return this.views[idx]; + }, + + forEach: function(callback) { + var views = this.views; + return a_forEach(views, callback); + }, + + clear: function() { + this.length = 0; + this.views.length = 0; + } +}; + +var EMPTY_ARRAY = []; + /** `Ember.View` is the class in Ember responsible for encapsulating templates of HTML content, combining templates with data to render as sections of a page's @@ -14682,7 +15594,7 @@ class: eventManager: Ember.Object.create({ mouseEnter: function(event, view){ // view might be instance of either - // OutsideView or InnerView depending on + // OuterView or InnerView depending on // where on the page the user interaction occured } }) @@ -14838,14 +15750,6 @@ Ember.View = Ember.CoreView.extend( return template || get(this, 'defaultTemplate'); }).property('templateName'), - container: Ember.computed(function() { - var parentView = get(this, '_parentView'); - - if (parentView) { return get(parentView, 'container'); } - - return Ember.Container && Ember.Container.defaultContainer; - }), - /** The controller managing this view. If this property is set, it will be made available for use by the template. @@ -14883,14 +15787,11 @@ Ember.View = Ember.CoreView.extend( templateForName: function(name, type) { if (!name) { return; } - Ember.assert("templateNames are not allowed to contain periods: "+name, name.indexOf('.') === -1); - var container = get(this, 'container'); - - if (container) { - return container.lookup('template:' + name); - } + // the defaultContainer is deprecated + var container = this.container || (Ember.Container && Ember.Container.defaultContainer); + return container && container.lookup('template:' + name); }, /** @@ -14981,7 +15882,7 @@ Ember.View = Ember.CoreView.extend( */ childViews: childViewsProperty, - _childViews: [], + _childViews: EMPTY_ARRAY, // When it's a virtual view, we need to notify the parent that their // childViews will change. @@ -15279,7 +16180,7 @@ Ember.View = Ember.CoreView.extend( @param {Ember.RenderBuffer} buffer */ _applyAttributeBindings: function(buffer, attributeBindings) { - var attributeValue, elem, type; + var attributeValue, elem; a_forEach(attributeBindings, function(binding) { var split = binding.split(':'), @@ -15370,7 +16271,7 @@ Ember.View = Ember.CoreView.extend( while(--idx >= 0) { view = childViews[idx]; - callback.call(this, view, idx); + callback(this, view, idx); } return this; @@ -15384,9 +16285,9 @@ Ember.View = Ember.CoreView.extend( var len = childViews.length, view, idx; - for(idx = 0; idx < len; idx++) { + for (idx = 0; idx < len; idx++) { view = childViews[idx]; - callback.call(this, view); + callback(view); } return this; @@ -15582,13 +16483,15 @@ Ember.View = Ember.CoreView.extend( /** @private - Run this callback on the current view and recursively on child views. + Run this callback on the current view (unless includeSelf is false) and recursively on child views. @method invokeRecursively @param fn {Function} + @param includeSelf (optional, default true) */ - invokeRecursively: function(fn) { - var childViews = [this], currentViews, view; + invokeRecursively: function(fn, includeSelf) { + var childViews = (includeSelf === false) ? this._childViews : [this]; + var currentViews, view; while (childViews.length) { currentViews = childViews.slice(); @@ -15596,7 +16499,7 @@ Ember.View = Ember.CoreView.extend( for (var i=0, l=currentViews.length; i<l; i++) { view = currentViews[i]; - fn.call(view, view); + fn(view); if (view._childViews) { childViews.push.apply(childViews, view._childViews); } @@ -15621,6 +16524,19 @@ Ember.View = Ember.CoreView.extend( } }, + viewHierarchyCollection: function() { + var currentView, viewCollection = new ViewCollection([this]); + + for (var i = 0; i < viewCollection.length; i++) { + currentView = viewCollection.objectAt(i); + if (currentView._childViews) { + viewCollection.push.apply(viewCollection, currentView._childViews); + } + } + + return viewCollection; + }, + /** Destroys any existing element along with the element for any child views as well. If the view does not currently have a element, then this method @@ -15665,8 +16581,10 @@ Ember.View = Ember.CoreView.extend( @method _notifyWillDestroyElement */ _notifyWillDestroyElement: function() { - this.triggerRecursively('willClearRender'); - this.triggerRecursively('willDestroyElement'); + var viewCollection = this.viewHierarchyCollection(); + viewCollection.trigger('willClearRender'); + viewCollection.trigger('willDestroyElement'); + return viewCollection; }, _elementWillChange: Ember.beforeObserver(function() { @@ -15712,8 +16630,8 @@ Ember.View = Ember.CoreView.extend( return buffer; }, - renderToBufferIfNeeded: function () { - return this.currentState.renderToBufferIfNeeded(this, this); + renderToBufferIfNeeded: function (buffer) { + return this.currentState.renderToBufferIfNeeded(this, buffer); }, beforeRender: function(buffer) { @@ -15841,7 +16759,7 @@ Ember.View = Ember.CoreView.extend( @type Array @default [] */ - classNameBindings: [], + classNameBindings: EMPTY_ARRAY, /** A list of properties of the view to apply as attributes. If the property is @@ -15869,7 +16787,7 @@ Ember.View = Ember.CoreView.extend( @property attributeBindings */ - attributeBindings: [], + attributeBindings: EMPTY_ARRAY, // ....................................................... // CORE DISPLAY METHODS @@ -15945,13 +16863,13 @@ Ember.View = Ember.CoreView.extend( @return {Ember.View} receiver */ removeAllChildren: function() { - return this.mutateChildViews(function(view) { - this.removeChild(view); + return this.mutateChildViews(function(parentView, view) { + parentView.removeChild(view); }); }, destroyAllChildren: function() { - return this.mutateChildViews(function(view) { + return this.mutateChildViews(function(parentView, view) { view.destroy(); }); }, @@ -15979,18 +16897,16 @@ Ember.View = Ember.CoreView.extend( sure that the DOM element managed by the view can be released by the memory manager. - @method willDestroy + @method destroy */ - willDestroy: function() { - // calling this._super() will nuke computed properties and observers, - // so collect any information we need before calling super. + destroy: function() { var childViews = this._childViews, - parent = this._parentView, + // get parentView before calling super because it'll be destroyed + nonVirtualParentView = get(this, 'parentView'), + viewName = this.viewName, childLen, i; - // destroy the element -- this will avoid each child view destroying - // the element over and over again... - if (!this.removedFromDOM) { this.destroyElement(); } + if (!this._super()) { return; } childLen = childViews.length; for (i=childLen-1; i>=0; i--) { @@ -15998,27 +16914,16 @@ Ember.View = Ember.CoreView.extend( } // remove from non-virtual parent view if viewName was specified - if (this.viewName) { - var nonVirtualParentView = get(this, 'parentView'); - if (nonVirtualParentView) { - set(nonVirtualParentView, this.viewName, null); - } + if (viewName && nonVirtualParentView) { + nonVirtualParentView.set(viewName, null); } - // remove from parent if found. Don't call removeFromParent, - // as removeFromParent will try to remove the element from - // the DOM again. - if (parent) { parent.removeChild(this); } - - this.transitionTo('destroyed'); - childLen = childViews.length; for (i=childLen-1; i>=0; i--) { childViews[i].destroy(); } - // next remove view from global hash - if (!this.isVirtual) delete Ember.View.views[get(this, 'elementId')]; + return this; }, /** @@ -16039,6 +16944,7 @@ Ember.View = Ember.CoreView.extend( if (Ember.CoreView.detect(view)) { attrs = attrs || {}; attrs._parentView = this; + attrs.container = this.container; attrs.templateData = attrs.templateData || get(this, 'templateData'); view = view.create(attrs); @@ -16133,9 +17039,13 @@ Ember.View = Ember.CoreView.extend( }, transitionTo: function(state, children) { - this.currentState = this.states[state]; + var priorState = this.currentState, + currentState = this.currentState = this.states[state]; this.state = state; + if (priorState && priorState.exit) { priorState.exit(this); } + if (currentState.enter) { currentState.enter(this); } + if (children !== false) { this.forEachChildView(function(view) { view.transitionTo(state); @@ -16350,7 +17260,7 @@ Ember.View.reopenClass({ // If the value is not false, undefined, or null, return the current // value of the property. - } else if (val !== false && val !== undefined && val !== null) { + } else if (val !== false && val != null) { return val; // Nothing to display. Return null so that the old class is removed @@ -16479,15 +17389,18 @@ Ember.merge(preRender, { // created (createElement). insertElement: function(view, fn) { view.createElement(); - view.triggerRecursively('willInsertElement'); + var viewCollection = view.viewHierarchyCollection(); + + viewCollection.trigger('willInsertElement'); // after createElement, the view will be in the hasElement state. fn.call(view); - view.transitionTo('inDOM'); - view.triggerRecursively('didInsertElement'); + viewCollection.transitionTo('inDOM', false); + viewCollection.trigger('didInsertElement'); }, - renderToBufferIfNeeded: function(view) { - return view.renderToBuffer(); + renderToBufferIfNeeded: function(view, buffer) { + view.renderToBuffer(buffer); + return true; }, empty: Ember.K, @@ -16534,10 +17447,11 @@ Ember.merge(inBuffer, { // view will render that view and append the resulting // buffer into its buffer. appendChild: function(view, childView, options) { - var buffer = view.buffer; + var buffer = view.buffer, _childViews = view._childViews; childView = view.createChildView(childView, options); - view._childViews.push(childView); + if (!_childViews.length) { _childViews = view._childViews = _childViews.slice(); } + _childViews.push(childView); childView.renderToBuffer(buffer); @@ -16551,8 +17465,8 @@ Ember.merge(inBuffer, { // state back into the preRender state. destroyElement: function(view) { view.clearBuffer(); - view._notifyWillDestroyElement(); - view.transitionTo('preRender'); + var viewCollection = view._notifyWillDestroyElement(); + viewCollection.transitionTo('preRender', false); return view; }, @@ -16561,8 +17475,8 @@ Ember.merge(inBuffer, { Ember.assert("Emptying a view in the inBuffer state is not allowed and should not happen under normal circumstances. Most likely there is a bug in your application. This may be due to excessive property change notifications."); }, - renderToBufferIfNeeded: function (view) { - return view.buffer; + renderToBufferIfNeeded: function (view, buffer) { + return false; }, // It should be impossible for a rendered view to be scheduled for @@ -16681,6 +17595,23 @@ Ember.merge(hasElement, { var inDOM = Ember.View.states.inDOM = Ember.create(hasElement); Ember.merge(inDOM, { + enter: function(view) { + // Register the view for event handling. This hash is used by + // Ember.EventDispatcher to dispatch incoming events. + if (!view.isVirtual) { + Ember.assert("Attempted to register a view with an id already in use: "+view.elementId, !Ember.View.views[view.elementId]); + Ember.View.views[view.elementId] = view; + } + + view.addBeforeObserver('elementId', function() { + throw new Error("Changing a view's elementId after creation is not allowed"); + }); + }, + + exit: function(view) { + if (!this.isVirtual) delete Ember.View.views[view.elementId]; + }, + insertElement: function(view, fn) { throw "You can't insert an element into the DOM that has already been inserted"; } @@ -16696,30 +17627,30 @@ Ember.merge(inDOM, { @submodule ember-views */ -var destroyedError = "You can't call %@ on a destroyed view", fmt = Ember.String.fmt; +var destroyingError = "You can't call %@ on a view being destroyed", fmt = Ember.String.fmt; -var destroyed = Ember.View.states.destroyed = Ember.create(Ember.View.states._default); +var destroying = Ember.View.states.destroying = Ember.create(Ember.View.states._default); -Ember.merge(destroyed, { +Ember.merge(destroying, { appendChild: function() { - throw fmt(destroyedError, ['appendChild']); + throw fmt(destroyingError, ['appendChild']); }, rerender: function() { - throw fmt(destroyedError, ['rerender']); + throw fmt(destroyingError, ['rerender']); }, destroyElement: function() { - throw fmt(destroyedError, ['destroyElement']); + throw fmt(destroyingError, ['destroyElement']); }, empty: function() { - throw fmt(destroyedError, ['empty']); + throw fmt(destroyingError, ['empty']); }, setElement: function() { - throw fmt(destroyedError, ["set('element', ...)"]); + throw fmt(destroyingError, ["set('element', ...)"]); }, renderToBufferIfNeeded: function() { - throw fmt(destroyedError, ["renderToBufferIfNeeded"]); + return false; }, // Since element insertion is scheduled, don't do anything if @@ -16738,7 +17669,7 @@ Ember.View.cloneStates = function(from) { into._default = {}; into.preRender = Ember.create(into._default); - into.destroyed = Ember.create(into._default); + into.destroying = Ember.create(into._default); into.inBuffer = Ember.create(into._default); into.hasElement = Ember.create(into._default); into.inDOM = Ember.create(into.hasElement); @@ -16765,6 +17696,7 @@ var states = Ember.View.cloneStates(Ember.View.states); var get = Ember.get, set = Ember.set; var forEach = Ember.EnumerableUtils.forEach; +var ViewCollection = Ember._ViewCollection; /** A `ContainerView` is an `Ember.View` subclass that implements `Ember.MutableArray` @@ -16969,12 +17901,15 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { var currentView = get(this, 'currentView'); if (currentView) { + if (!_childViews.length) { _childViews = this._childViews = this._childViews.slice(); } _childViews.push(this.createChildView(currentView)); } }, replace: function(idx, removedCount, addedViews) { var addedCount = addedViews ? get(addedViews, 'length') : 0; + var self = this; + Ember.assert("You can't add a child to a container that is already a child of another view", Ember.A(addedViews).every(function(item) { return !get(item, '_parentView') || get(item, '_parentView') === self; })); this.arrayContentWillChange(idx, removedCount, addedCount); this.childViewsWillChange(this._childViews, idx, removedCount); @@ -16983,6 +17918,7 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { this._childViews.splice(idx, removedCount) ; } else { var args = [idx, removedCount].concat(addedViews); + if (addedViews.length && !this._childViews.length) { this._childViews = this._childViews.slice(); } this._childViews.splice.apply(this._childViews, args); } @@ -17014,7 +17950,7 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { }); }, - instrumentName: 'render.container', + instrumentName: 'container', /** @private @@ -17094,6 +18030,7 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { _currentViewDidChange: Ember.observer(function() { var currentView = get(this, 'currentView'); if (currentView) { + Ember.assert("You tried to set a current view that already has a parent. Make sure you don't have multiple outlets in the same view.", !get(currentView, '_parentView')); this.pushObject(currentView); } }, 'currentView'), @@ -17127,26 +18064,48 @@ Ember.merge(states.hasElement, { }, ensureChildrenAreInDOM: function(view) { - var childViews = view._childViews, i, len, childView, previous, buffer; + var childViews = view._childViews, i, len, childView, previous, buffer, viewCollection = new ViewCollection(); + for (i = 0, len = childViews.length; i < len; i++) { childView = childViews[i]; - buffer = childView.renderToBufferIfNeeded(); - if (buffer) { - childView.triggerRecursively('willInsertElement'); - if (previous) { - previous.domManager.after(previous, buffer.string()); - } else { - view.domManager.prepend(view, buffer.string()); - } - childView.transitionTo('inDOM'); - childView.propertyDidChange('element'); - childView.triggerRecursively('didInsertElement'); + + if (!buffer) { buffer = Ember.RenderBuffer(); buffer._hasElement = false; } + + if (childView.renderToBufferIfNeeded(buffer)) { + viewCollection.push(childView); + } else if (viewCollection.length) { + insertViewCollection(view, viewCollection, previous, buffer); + buffer = null; + previous = childView; + viewCollection.clear(); + } else { + previous = childView; } - previous = childView; + } + + if (viewCollection.length) { + insertViewCollection(view, viewCollection, previous, buffer); } } }); +function insertViewCollection(view, viewCollection, previous, buffer) { + viewCollection.triggerRecursively('willInsertElement'); + + if (previous) { + previous.domManager.after(previous, buffer.string()); + } else { + view.domManager.prepend(view, buffer.string()); + } + + viewCollection.forEach(function(v) { + v.transitionTo('inDOM'); + v.propertyDidChange('element'); + v.triggerRecursively('didInsertElement'); + }); +} + + })(); @@ -17161,7 +18120,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; /** `Ember.CollectionView` is an `Ember.View` descendent responsible for managing - a collection (an array or array-like object) by maintaing a child view object + a collection (an array or array-like object) by maintaining a child view object and associated DOM representation for each item in the array and ensuring that child views and their associated rendered HTML are updated when items in the array are added, removed, or replaced. @@ -17312,8 +18271,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; @extends Ember.ContainerView @since Ember 0.9 */ -Ember.CollectionView = Ember.ContainerView.extend( -/** @scope Ember.CollectionView.prototype */ { +Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionView.prototype */ { /** A list of items to be displayed by the `Ember.CollectionView`. @@ -17387,15 +18345,17 @@ Ember.CollectionView = Ember.ContainerView.extend( this.arrayDidChange(content, 0, null, len); }, 'content'), - willDestroy: function() { + destroy: function() { + if (!this._super()) { return; } + var content = get(this, 'content'); if (content) { content.removeArrayObserver(this); } - this._super(); - if (this._createdEmptyView) { this._createdEmptyView.destroy(); } + + return this; }, arrayWillChange: function(content, start, removedCount) { @@ -17417,11 +18377,13 @@ Ember.CollectionView = Ember.ContainerView.extend( if (removingAll) { this.currentState.empty(this); + this.invokeRecursively(function(view) { + view.removedFromDOM = true; + }, false); } for (idx = start + removedCount - 1; idx >= start; idx--) { childView = childViews[idx]; - if (removingAll) { childView.removedFromDOM = true; } childView.destroy(); } }, @@ -17435,9 +18397,10 @@ Ember.CollectionView = Ember.ContainerView.extend( This array observer is added in `contentDidChange`. @method arrayDidChange - @param {Array} addedObjects the objects that were added to the content - @param {Array} removedObjects the objects that were removed from the content - @param {Number} changeIndex the index at which the changes occurred + @param {Array} content the managed collection of objects + @param {Number} start the index at which the changes occurred + @param {Number} removed number of object removed from content + @param {Number} added number of object added to content */ arrayDidChange: function(content, start, removed, added) { var itemViewClass = get(this, 'itemViewClass'), @@ -17520,6 +18483,69 @@ Ember.CollectionView.CONTAINER_MAP = { (function() { +/** +`Ember.ViewTargetActionSupport` is a mixin that can be included in a +view class to add a `triggerAction` method with semantics similar to +the Handlebars `{{action}}` helper. It provides intelligent defaults +for the action's target: the view's controller; and the context that is +sent with the action: the view's context. + +Note: In normal Ember usage, the `{{action}}` helper is usually the best +choice. This mixin is most often useful when you are doing more complex +event handling in custom View subclasses. + +For example: + +```javascript +App.SaveButtonView = Ember.View.extend(Ember.ViewTargetActionSupport, { + action: 'save', + click: function(){ + this.triggerAction(); // Sends the `save` action, along with the current context + // to the current controller + } +}); +``` + +The `action` can be provided as properties of an optional object argument +to `triggerAction` as well. + +```javascript +App.SaveButtonView = Ember.View.extend(Ember.ViewTargetActionSupport, { + click: function(){ + this.triggerAction({ + action: 'save' + }); // Sends the `save` action, along with the current context + // to the current controller + } +}); +``` + +@class ViewTargetActionSupport +@namespace Ember +@extends Ember.TargetActionSupport +*/ +Ember.ViewTargetActionSupport = Ember.Mixin.create(Ember.TargetActionSupport, { + /** + @property target + */ + target: Ember.computed.alias('controller'), + /** + @property actionContext + */ + actionContext: Ember.computed.alias('context') +}); + +})(); + + + +(function() { + +})(); + + + +(function() { /*globals jQuery*/ /** Ember Views @@ -18012,7 +19038,8 @@ if(!Handlebars && typeof require === 'function') { Handlebars = require('handlebars'); } -Ember.assert("Ember Handlebars requires Handlebars 1.0.0-rc.3 or greater. Include a SCRIPT tag in the HTML HEAD linking to the Handlebars file before you link to Ember.", Handlebars && Handlebars.COMPILER_REVISION === 2); +Ember.assert("Ember Handlebars requires Handlebars version 1.0.0-rc.4. Include a SCRIPT tag in the HTML HEAD linking to the Handlebars file before you link to Ember.", Handlebars) +Ember.assert("Ember Handlebars requires Handlebars version 1.0.0-rc.4, COMPILER_REVISION expected: 3, got: " + Handlebars.COMPILER_REVISION + " – Please note: Builds of master may have other COMPILER_REVISION values.", Handlebars.COMPILER_REVISION === 3); /** Prepares the Handlebars templating library for use inside Ember's view @@ -18030,6 +19057,32 @@ Ember.assert("Ember Handlebars requires Handlebars 1.0.0-rc.3 or greater. Includ */ Ember.Handlebars = objectCreate(Handlebars); +function makeBindings(options) { + var hash = options.hash, + hashType = options.hashTypes; + + for (var prop in hash) { + if (hashType[prop] === 'ID') { + hash[prop + 'Binding'] = hash[prop]; + hashType[prop + 'Binding'] = 'STRING'; + delete hash[prop]; + delete hashType[prop]; + } + } +} + +Ember.Handlebars.helper = function(name, value) { + if (Ember.View.detect(value)) { + Ember.Handlebars.registerHelper(name, function(options) { + Ember.assert("You can only pass attributes as parameters (not values) to a application-defined helper", arguments.length < 2); + makeBindings(options); + return Ember.Handlebars.helpers.view.call(this, value, options); + }); + } else { + Ember.Handlebars.registerBoundHelper.apply(null, arguments); + } +} + /** @class helpers @namespace Ember.Handlebars @@ -18398,15 +19451,15 @@ Ember.Handlebars.registerHelper('helperMissing', function(path, options) { }); ``` - Which allows for template syntax such as {{concatenate prop1 prop2}} or - {{concatenate prop1 prop2 prop3}}. If any of the properties change, + Which allows for template syntax such as `{{concatenate prop1 prop2}}` or + `{{concatenate prop1 prop2 prop3}}`. If any of the properties change, the helpr will re-render. Note that dependency keys cannot be using in conjunction with multi-property helpers, since it is ambiguous which property the dependent keys would belong to. ## Use with unbound helper - The {{unbound}} helper can be used with bound helper invocations + The `{{unbound}}` helper can be used with bound helper invocations to render them in their unbound form, e.g. ```handlebars @@ -18416,6 +19469,10 @@ Ember.Handlebars.registerHelper('helperMissing', function(path, options) { In this example, if the name property changes, the helper will not re-render. + ## Use with blocks not supported + + Bound helpers do not support use with Handlebars blocks or + the addition of child views of any kind. @method registerBoundHelper @for Ember.Handlebars @@ -18439,6 +19496,8 @@ Ember.Handlebars.registerBoundHelper = function(name, fn) { pathRoot, path, loc, hashOption; + Ember.assert("registerBoundHelper-generated helpers do not support use with Handlebars blocks.", !options.fn); + // Detect bound options (e.g. countBinding="otherCount") hash.boundOptions = {}; for (hashOption in hash) { @@ -18499,6 +19558,7 @@ Ember.Handlebars.registerBoundHelper = function(name, fn) { Renders the unbound form of an otherwise bound helper function. + @method evaluateMultiPropertyBoundHelper @param {Function} fn @param {Object} context @param {Array} normalizedProperties @@ -18515,7 +19575,7 @@ function evaluateMultiPropertyBoundHelper(context, fn, normalizedProperties, opt bindView = new Ember._SimpleHandlebarsView(null, null, !hash.unescaped, data); bindView.normalizedValue = function() { - var args = [], value, boundOption; + var args = [], boundOption; // Copy over bound options. for (boundOption in boundOptions) { @@ -18560,6 +19620,7 @@ function evaluateMultiPropertyBoundHelper(context, fn, normalizedProperties, opt Renders the unbound form of an otherwise bound helper function. + @method evaluateUnboundHelper @param {Function} fn @param {Object} context @param {Array} normalizedProperties @@ -18597,7 +19658,6 @@ Ember.Handlebars.template = function(spec){ return t; }; - })(); @@ -18617,7 +19677,7 @@ var htmlSafe = Ember.String.htmlSafe; if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { /** - See {{#crossLink "Ember.String/htmlSafe"}}{{/crossLink}} + See `Ember.String.htmlSafe`. @method htmlSafe @for String @@ -18698,13 +19758,18 @@ var DOMManager = { var buffer = view.renderToBuffer(); view.invokeRecursively(function(view) { - view.propertyDidChange('element'); + view.propertyWillChange('element'); }); - view.triggerRecursively('willInsertElement'); + morph.replaceWith(buffer.string()); view.transitionTo('inDOM'); + + view.invokeRecursively(function(view) { + view.propertyDidChange('element'); + }); view.triggerRecursively('didInsertElement'); + notifyMutationListeners(); }); }, @@ -18728,7 +19793,7 @@ Ember._Metamorph = Ember.Mixin.create({ isVirtual: true, tagName: '', - instrumentName: 'render.metamorph', + instrumentName: 'metamorph', init: function() { this._super(); @@ -18813,6 +19878,8 @@ SimpleHandlebarsView.prototype = { this.morph = null; }, + propertyWillChange: Ember.K, + propertyDidChange: Ember.K, normalizedValue: function() { @@ -18863,7 +19930,7 @@ SimpleHandlebarsView.prototype = { rerender: function() { switch(this.state) { case 'preRender': - case 'destroyed': + case 'destroying': break; case 'inBuffer': throw new Ember.Error("Something you did tried to replace an {{expression}} before it was inserted into the DOM."); @@ -18894,7 +19961,7 @@ merge(states._default, { merge(states.inDOM, { rerenderIfNeeded: function(view) { - if (get(view, 'normalizedValue') !== view._lastNormalizedValue) { + if (view.normalizedValue() !== view._lastNormalizedValue) { view.rerender(); } } @@ -18915,7 +19982,7 @@ merge(states.inDOM, { @private */ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ - instrumentName: 'render.boundHandlebars', + instrumentName: 'boundHandlebars', states: states, /** @@ -18998,7 +20065,7 @@ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ */ pathRoot: null, - normalizedValue: Ember.computed(function() { + normalizedValue: function() { var path = get(this, 'path'), pathRoot = get(this, 'pathRoot'), valueNormalizer = get(this, 'valueNormalizerFunc'), @@ -19016,7 +20083,7 @@ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ } return valueNormalizer ? valueNormalizer(result) : result; - }).property('path', 'pathRoot', 'valueNormalizerFunc').volatile(), + }, rerenderIfNeeded: function() { this.currentState.rerenderIfNeeded(this); @@ -19051,7 +20118,7 @@ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ var inverseTemplate = get(this, 'inverseTemplate'), displayTemplate = get(this, 'displayTemplate'); - var result = get(this, 'normalizedValue'); + var result = this.normalizedValue(); this._lastNormalizedValue = result; // First, test the conditional to see if we should @@ -19114,6 +20181,10 @@ var forEach = Ember.ArrayPolyfills.forEach; var EmberHandlebars = Ember.Handlebars, helpers = EmberHandlebars.helpers; +function exists(value){ + return !Ember.isNone(value); +} + // Binds a property into the DOM. This will create a hook in DOM that the // KVO system will look for and update if the property changes. function bind(property, options, preserveContext, shouldDisplay, valueNormalizer, childProperties) { @@ -19292,9 +20363,7 @@ EmberHandlebars.registerHelper('bind', function(property, options) { return simpleBind.call(context, property, options); } - return bind.call(context, property, options, false, function(result) { - return !Ember.isNone(result); - }); + return bind.call(context, property, options, false, exists); }); /** @@ -19366,9 +20435,7 @@ EmberHandlebars.registerHelper('with', function(context, options) { Ember.bind(options.data.keywords, keywordName, contextPath); } - return bind.call(this, path, options, true, function(result) { - return !Ember.isNone(result); - }); + return bind.call(this, path, options, true, exists); } else { Ember.assert("You must pass exactly one argument to the with helper", arguments.length === 2); Ember.assert("You must pass a block to the with helper", options.fn && options.fn !== Handlebars.VM.noop); @@ -19520,7 +20587,7 @@ EmberHandlebars.registerHelper('unless', function(context, options) { Results in the following rendered output: ```html - <img class=":class-name-to-always-apply"> + <img class="class-name-to-always-apply"> ``` All three strategies - string return value, boolean return value, and @@ -19552,7 +20619,7 @@ EmberHandlebars.registerHelper('bindAttr', function(options) { // Handle classes differently, as we can bind multiple classes var classBindings = attrs['class']; - if (classBindings !== null && classBindings !== undefined) { + if (classBindings != null) { var classResults = EmberHandlebars.bindClasses(this, classBindings, view, dataId, options); ret.push('class="' + Handlebars.Utils.escapeExpression(classResults.join(' ')) + '"'); @@ -19859,11 +20926,8 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ }, helper: function(thisContext, path, options) { - var inverse = options.inverse, - data = options.data, - view = data.view, + var data = options.data, fn = options.fn, - hash = options.hash, newView; if ('string' === typeof path) { @@ -19877,7 +20941,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ var viewOptions = this.propertiesFromHTMLOptions(options, thisContext); var currentView = data.view; - viewOptions.templateData = options.data; + viewOptions.templateData = data; var newViewProto = newView.proto ? newView.proto() : newView; if (fn) { @@ -20004,9 +21068,8 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ {{/view}} ``` - The first argument can also be a relative path. Ember will search for the - view class starting at the `Ember.View` of the template where `{{view}}` was - used as the root object: + The first argument can also be a relative path accessible from the current + context. ```javascript MyApp = Ember.Application.create({}); @@ -20014,7 +21077,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ innerViewClass: Ember.View.extend({ classNames: ['a-custom-view-class-as-property'] }), - template: Ember.Handlebars.compile('{{#view "innerViewClass"}} hi {{/view}}') + template: Ember.Handlebars.compile('{{#view "view.innerViewClass"}} hi {{/view}}') }); MyApp.OuterView.create().appendTo('body'); @@ -20259,8 +21322,6 @@ Ember.Handlebars.registerHelper('collection', function(path, options) { } } - var tagName = hash.tagName || collectionPrototype.tagName; - if (fn) { itemHash.template = fn; delete options.fn; @@ -20282,8 +21343,6 @@ Ember.Handlebars.registerHelper('collection', function(path, options) { itemHash._context = Ember.computed.alias('content'); } - var viewString = view.toString(); - var viewOptions = Ember.Handlebars.ViewHelper.propertiesFromHTMLOptions({ data: data, hash: itemHash }, this); hash.itemViewClass = itemViewClass.extend(viewOptions); @@ -20412,6 +21471,7 @@ Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { set(controller, 'container', get(this, 'controller.container')); set(controller, '_eachView', this); set(controller, 'target', get(this, 'controller')); + set(controller, 'parentController', get(this, 'controller')); this.disableContentObservers(function() { set(this, 'content', controller); @@ -20473,14 +21533,16 @@ Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { return view; }, - willDestroy: function() { + destroy: function() { + if (!this._super()) { return; } + var arrayController = get(this, '_arrayController'); if (arrayController) { arrayController.destroy(); } - return this._super(); + return this; } }); @@ -20683,6 +21745,9 @@ GroupedEach.prototype = { {{/each}} ``` + Each itemController will receive a reference to the current controller as + a `parentController` property. + @method each @for Ember.Handlebars.helpers @param [name] {String} name for item (used with `in`) @@ -20704,6 +21769,11 @@ Ember.Handlebars.registerHelper('each', function(path, options) { options.hash.keyword = keywordName; } + if (arguments.length === 1) { + options = path; + path = 'this'; + } + options.hash.dataSourceBinding = path; // Set up emptyView as a metamorph with no tag //options.hash.emptyViewClass = Ember._MetamorphView; @@ -20797,6 +21867,7 @@ Ember.Handlebars.registerHelper('template', function(name, options) { {{partial user_info}} {{/with}} </script> + ``` The `data-template-name` attribute of a partial template is prefixed with an underscore. @@ -20821,7 +21892,7 @@ Ember.Handlebars.registerHelper('partial', function(name, options) { var view = options.data.view, underscoredName = nameParts.join("/"), template = view.templateForName(underscoredName), - deprecatedTemplate = view.templateForName(name); + deprecatedTemplate = !template && view.templateForName(name); Ember.deprecate("You tried to render the partial " + name + ", which should be at '" + underscoredName + "', but Ember found '" + name + "'. Please use a leading underscore in your partials", template); Ember.assert("Unable to find partial with name '"+name+"'.", template || deprecatedTemplate); @@ -20947,7 +22018,7 @@ var set = Ember.set, get = Ember.get; You can add a `label` tag yourself in the template where the `Ember.Checkbox` is being used. - ```html + ```handlebars <label> {{view Ember.Checkbox classNames="applicaton-specific-checkbox"}} Some Title @@ -21085,7 +22156,7 @@ var get = Ember.get, set = Ember.set; By default `Ember.TextField` provides support for `type`, `value`, `size`, `pattern`, `placeholder`, `disabled`, `maxlength` and `tabindex` attributes - on a test field. If you need to support more attributes have a look at the + on a text 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 @@ -21160,6 +22231,20 @@ Ember.TextField = Ember.View.extend(Ember.TextSupport, action: null, /** + The event that should send the action. + + Options are: + + * `enter`: the user pressed enter + * `keypress`: the user pressed a key + + @property onEvent + @type String + @default enter + */ + onEvent: 'enter', + + /** Whether they `keyUp` event that triggers an `action` to be sent continues propagating to other views. @@ -21177,19 +22262,30 @@ Ember.TextField = Ember.View.extend(Ember.TextSupport, bubbles: false, insertNewline: function(event) { - var controller = get(this, 'controller'), - action = get(this, 'action'); - - if (action) { - controller.send(action, get(this, 'value'), this); + sendAction('enter', this, event); + }, - if (!get(this, 'bubbles')) { - event.stopPropagation(); - } - } + keyPress: function(event) { + sendAction('keyPress', this, event); } }); +function sendAction(eventName, view, event) { + var action = get(view, 'action'), + on = get(view, 'onEvent'); + + if (action && on === eventName) { + var controller = get(view, 'controller'), + value = get(view, 'value'), + bubbles = get(view, 'bubbles'); + + controller.send(action, value, view); + + if (!bubbles) { + event.stopPropagation(); + } + } +} })(); @@ -21407,6 +22503,55 @@ var set = Ember.set, isArray = Ember.isArray, precompileTemplate = Ember.Handlebars.compile; +Ember.SelectOption = Ember.View.extend({ + tagName: 'option', + attributeBindings: ['value', 'selected'], + + defaultTemplate: function(context, options) { + options = { data: options.data, hash: {} }; + Ember.Handlebars.helpers.bind.call(context, "view.label", options); + }, + + init: function() { + this.labelPathDidChange(); + this.valuePathDidChange(); + + this._super(); + }, + + selected: Ember.computed(function() { + var content = get(this, 'content'), + selection = get(this, 'parentView.selection'); + if (get(this, 'parentView.multiple')) { + return selection && indexOf(selection, content.valueOf()) > -1; + } else { + // Primitives get passed through bindings as objects... since + // `new Number(4) !== 4`, we use `==` below + return content == selection; + } + }).property('content', 'parentView.selection'), + + labelPathDidChange: Ember.observer(function() { + var labelPath = get(this, 'parentView.optionLabelPath'); + + if (!labelPath) { return; } + + Ember.defineProperty(this, 'label', Ember.computed(function() { + return get(this, labelPath); + }).property(labelPath)); + }, 'parentView.optionLabelPath'), + + valuePathDidChange: Ember.observer(function() { + var valuePath = get(this, 'parentView.optionValuePath'); + + if (!valuePath) { return; } + + Ember.defineProperty(this, 'value', Ember.computed(function() { + return get(this, valuePath); + }).property(valuePath)); + }, 'parentView.optionValuePath') +}); + /** The `Ember.Select` view class renders a [select](https://developer.mozilla.org/en/HTML/Element/select) HTML element, @@ -21583,7 +22728,7 @@ var set = Ember.set, Interacting with the rendered element by selecting the first option ('Yehuda') will update the `selectedPerson` value of `App.controller` to match the content object of the newly selected `<option>`. In this - case it is the first object in the `App.content.content` + case it is the first object in the `App.controller.content` ### Supplying a Prompt @@ -21660,34 +22805,38 @@ Ember.Select = Ember.View.extend( tagName: 'select', classNames: ['ember-select'], defaultTemplate: Ember.Handlebars.template(function anonymous(Handlebars,depth0,helpers,partials,data) { -this.compilerInfo = [2,'>= 1.0.0-rc.3']; +this.compilerInfo = [3,'>= 1.0.0-rc.4']; helpers = helpers || Ember.Handlebars.helpers; data = data || {}; - var buffer = '', stack1, hashTypes, escapeExpression=this.escapeExpression, self=this; + var buffer = '', stack1, hashTypes, hashContexts, escapeExpression=this.escapeExpression, self=this; function program1(depth0,data) { - var buffer = '', hashTypes; + var buffer = '', hashTypes, hashContexts; data.buffer.push("<option value=\"\">"); hashTypes = {}; - data.buffer.push(escapeExpression(helpers._triageMustache.call(depth0, "view.prompt", {hash:{},contexts:[depth0],types:["ID"],hashTypes:hashTypes,data:data}))); + hashContexts = {}; + data.buffer.push(escapeExpression(helpers._triageMustache.call(depth0, "view.prompt", {hash:{},contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}))); data.buffer.push("</option>"); return buffer; } function program3(depth0,data) { - var hashTypes; + var hashContexts, hashTypes; + hashContexts = {'contentBinding': depth0}; hashTypes = {'contentBinding': "STRING"}; - data.buffer.push(escapeExpression(helpers.view.call(depth0, "Ember.SelectOption", {hash:{ + data.buffer.push(escapeExpression(helpers.view.call(depth0, "view.optionView", {hash:{ 'contentBinding': ("this") - },contexts:[depth0],types:["ID"],hashTypes:hashTypes,data:data}))); + },contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}))); } hashTypes = {}; - stack1 = helpers['if'].call(depth0, "view.prompt", {hash:{},inverse:self.noop,fn:self.program(1, program1, data),contexts:[depth0],types:["ID"],hashTypes:hashTypes,data:data}); + hashContexts = {}; + stack1 = helpers['if'].call(depth0, "view.prompt", {hash:{},inverse:self.noop,fn:self.program(1, program1, data),contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}); if(stack1 || stack1 === 0) { data.buffer.push(stack1); } hashTypes = {}; - stack1 = helpers.each.call(depth0, "view.content", {hash:{},inverse:self.noop,fn:self.program(3, program3, data),contexts:[depth0],types:["ID"],hashTypes:hashTypes,data:data}); + hashContexts = {}; + stack1 = helpers.each.call(depth0, "view.content", {hash:{},inverse:self.noop,fn:self.program(3, program3, data),contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}); if(stack1 || stack1 === 0) { data.buffer.push(stack1); } return buffer; @@ -21787,6 +22936,15 @@ function program3(depth0,data) { */ optionValuePath: 'content', + /** + The view class for option. + + @property optionView + @type Ember.View + @default Ember.SelectOption + */ + optionView: Ember.SelectOption, + _change: function() { if (get(this, 'multiple')) { this._changeMultiple(); @@ -21816,9 +22974,9 @@ function program3(depth0,data) { selection; if (value !== selectedValue) { - selection = content.find(function(obj) { + selection = content ? content.find(function(obj) { return value === (valuePath ? get(obj, valuePath) : obj); - }); + }) : null; this.set('selection', selection); } @@ -21840,7 +22998,7 @@ function program3(depth0,data) { content = get(this, 'content'), prompt = get(this, 'prompt'); - if (!get(content, 'length')) { return; } + if (!content || !get(content, 'length')) { return; } if (prompt && selectedIndex === 0) { set(this, 'selection', null); return; } if (prompt) { selectedIndex -= 1; } @@ -21907,60 +23065,68 @@ function program3(depth0,data) { } }); -Ember.SelectOption = Ember.View.extend({ - tagName: 'option', - attributeBindings: ['value', 'selected'], +})(); - defaultTemplate: function(context, options) { - options = { data: options.data, hash: {} }; - Ember.Handlebars.helpers.bind.call(context, "view.label", options); - }, - init: function() { - this.labelPathDidChange(); - this.valuePathDidChange(); - this._super(); - }, - - selected: Ember.computed(function() { - var content = get(this, 'content'), - selection = get(this, 'parentView.selection'); - if (get(this, 'parentView.multiple')) { - return selection && indexOf(selection, content.valueOf()) > -1; - } else { - // Primitives get passed through bindings as objects... since - // `new Number(4) !== 4`, we use `==` below - return content == selection; +(function() { +function normalizeHash(hash, hashTypes) { + for (var prop in hash) { + if (hashTypes[prop] === 'ID') { + hash[prop + 'Binding'] = hash[prop]; + delete hash[prop]; } - }).property('content', 'parentView.selection').volatile(), - - labelPathDidChange: Ember.observer(function() { - var labelPath = get(this, 'parentView.optionLabelPath'); + } +} - if (!labelPath) { return; } +/** + * `{{input}}` inserts a new instance of either Ember.TextField or + * Ember.Checkbox, depending on the `type` option passed in. If no `type` + * is supplied it defaults to Ember.TextField. + * + * @method input + * @for Ember.Handlebars.helpers + * @param {Hash} options + */ +Ember.Handlebars.registerHelper('input', function(options) { + Ember.assert('You can only pass attributes to the `input` helper, not arguments', arguments.length < 2); - Ember.defineProperty(this, 'label', Ember.computed(function() { - return get(this, labelPath); - }).property(labelPath)); - }, 'parentView.optionLabelPath'), + var hash = options.hash, + types = options.hashTypes, + inputType = hash.type, + onEvent = hash.on; - valuePathDidChange: Ember.observer(function() { - var valuePath = get(this, 'parentView.optionValuePath'); + delete hash.type; + delete hash.on; - if (!valuePath) { return; } + normalizeHash(hash, types); - Ember.defineProperty(this, 'value', Ember.computed(function() { - return get(this, valuePath); - }).property(valuePath)); - }, 'parentView.optionValuePath') + if (inputType === 'checkbox') { + return Ember.Handlebars.helpers.view.call(this, Ember.Checkbox, options); + } else { + hash.type = inputType; + hash.onEvent = onEvent || 'enter'; + return Ember.Handlebars.helpers.view.call(this, Ember.TextField, options); + } }); -})(); - +/** + * `{{textarea}}` inserts a new instance of Ember.TextArea into the template + * passing its options to `Ember.TextArea`'s `create` method. + * + * @method textarea + * @for Ember.Handlebars.helpers + * @param {Hash} options + */ +Ember.Handlebars.registerHelper('textarea', function(options) { + Ember.assert('You can only pass attributes to the `textarea` helper, not arguments', arguments.length < 2); + var hash = options.hash, + types = options.hashTypes; -(function() { + normalizeHash(hash, types); + return Ember.Handlebars.helpers.view.call(this, Ember.TextArea, options); +}); })(); @@ -22425,14 +23591,14 @@ define("route-recognizer", }, recognize: function(path) { - var states = [ this.rootState ], i, l; + var states = [ this.rootState ], + pathLen, i, l; // DEBUG GROUP path - var pathLen = path.length; - if (path.charAt(0) !== "/") { path = "/" + path; } + pathLen = path.length; if (pathLen > 1 && path.charAt(pathLen - 1) === "/") { path = path.substr(0, pathLen - 1); } @@ -22578,16 +23744,10 @@ define("router", * `{String} handler`: A handler name * `{Object} params`: A hash of recognized parameters - ## `UnresolvedHandlerInfo` - - * `{Boolean} isDynamic`: whether a handler has any dynamic segments - * `{String} name`: the name of a handler - * `{Object} context`: the active context for the handler - ## `HandlerInfo` * `{Boolean} isDynamic`: whether a handler has any dynamic segments - * `{String} name`: the original unresolved handler name + * `{String} name`: the name of a handler * `{Object} handler`: a handler object * `{Object} context`: the active context for the handler */ @@ -22623,6 +23783,21 @@ define("router", }, /** + Clears the current and target route handlers and triggers exit + on each of them starting at the leaf and traversing up through + its ancestors. + */ + reset: function() { + eachHandler(this.currentHandlerInfos || [], function(handler) { + if (handler.exit) { + handler.exit(); + } + }); + this.currentHandlerInfos = null; + this.targetHandlerInfos = null; + }, + + /** The entry point for handling a change to the URL (usually via the back and forward button). @@ -22634,8 +23809,7 @@ define("router", @return {Array} an Array of `[handler, parameter]` tuples */ handleURL: function(url) { - var results = this.recognizer.recognize(url), - objects = []; + var results = this.recognizer.recognize(url); if (!results) { throw new Error("No route matched the URL '" + url + "'"); @@ -22731,7 +23905,7 @@ define("router", toSetup = [], startIdx = handlers.length, objectsToMatch = objects.length, - object, objectChanged, handlerObj, handler, names, i, len; + object, objectChanged, handlerObj, handler, names, i; // Find out which handler to start matching at for (i=handlers.length-1; i>=0 && objectsToMatch>0; i--) { @@ -22742,11 +23916,11 @@ define("router", } if (objectsToMatch > 0) { - throw "More objects were passed than dynamic segments"; + throw "More context objects were passed than there are dynamic segments for the route: "+handlerName; } // Connect the objects to the routes - for (i=0, len=handlers.length; i<len; i++) { + for (i=0; i<handlers.length; i++) { handlerObj = handlers[i]; handler = this.getHandler(handlerObj.handler); names = handlerObj.names; @@ -22791,10 +23965,22 @@ define("router", toSetup.push({ isDynamic: !!handlerObj.names.length, - handler: handlerObj.handler, - name: handlerObj.name, + name: handlerObj.handler, + handler: handler, context: object }); + + if (i === handlers.length - 1) { + var lastHandler = toSetup[toSetup.length - 1], + additionalHandler; + + if (additionalHandler = lastHandler.handler.additionalHandler) { + handlers.push({ + handler: additionalHandler.call(lastHandler.handler), + names: [] + }); + } + } } return { params: params, toSetup: toSetup }; @@ -22803,11 +23989,13 @@ define("router", isActive: function(handlerName) { var contexts = [].slice.call(arguments, 1); - var currentHandlerInfos = this.currentHandlerInfos, + var targetHandlerInfos = this.targetHandlerInfos, found = false, names, object, handlerInfo, handlerObj; - for (var i=currentHandlerInfos.length-1; i>=0; i--) { - handlerInfo = currentHandlerInfos[i]; + if (!targetHandlerInfos) { return; } + + for (var i=targetHandlerInfos.length-1; i>=0; i--) { + handlerInfo = targetHandlerInfos[i]; if (handlerInfo.name === handlerName) { found = true; } if (found) { @@ -22898,7 +24086,10 @@ define("router", function failure(router, error) { loaded(router); var handler = router.getHandler('failure'); - if (handler && handler.setup) { handler.setup(error); } + if (handler) { + if (handler.enter) { handler.enter(); } + if (handler.setup) { handler.setup(error); } + } } /** @@ -22933,9 +24124,21 @@ define("router", */ function collectObjects(router, results, index, objects) { if (results.length === index) { - loaded(router); - setupContexts(router, objects); - return; + var lastObject = objects[objects.length - 1], + lastHandler = lastObject && lastObject.handler; + + if (lastHandler && lastHandler.additionalHandler) { + var additionalResult = { + handler: lastHandler.additionalHandler(), + params: {}, + isDynamic: false + }; + results.push(additionalResult); + } else { + loaded(router); + setupContexts(router, objects); + return; + } } var result = results[index]; @@ -22960,7 +24163,8 @@ define("router", var updatedObjects = objects.concat([{ context: value, - handler: result.handler, + name: result.handler, + handler: router.getHandler(result.handler), isDynamic: result.isDynamic }]); collectObjects(router, results, index + 1, updatedObjects); @@ -22970,8 +24174,9 @@ define("router", /** @private - Takes an Array of `UnresolvedHandlerInfo`s, resolves the handler names - into handlers, and then figures out what to do with each of the handlers. + Takes an Array of `HandlerInfo`s, figures out which ones are + exiting, entering, or changing contexts, and calls the + proper handler hooks. For example, consider the following tree of handlers. Each handler is followed by the URL segment it handles. @@ -23005,28 +24210,30 @@ define("router", 4. Triggers the `setup` callback on `about` @param {Router} router - @param {Array[UnresolvedHandlerInfo]} handlerInfos + @param {Array[HandlerInfo]} handlerInfos */ function setupContexts(router, handlerInfos) { - resolveHandlers(router, handlerInfos); - var partition = partitionHandlers(router.currentHandlerInfos || [], handlerInfos); - router.currentHandlerInfos = handlerInfos; + router.targetHandlerInfos = handlerInfos; eachHandler(partition.exited, function(handler, context) { delete handler.context; if (handler.exit) { handler.exit(); } }); - eachHandler(partition.updatedContext, function(handler, context) { + var currentHandlerInfos = partition.unchanged.slice(); + router.currentHandlerInfos = currentHandlerInfos; + + eachHandler(partition.updatedContext, function(handler, context, handlerInfo) { setContext(handler, context); if (handler.setup) { handler.setup(context); } + currentHandlerInfos.push(handlerInfo); }); var aborted = false; - eachHandler(partition.entered, function(handler, context) { + eachHandler(partition.entered, function(handler, context, handlerInfo) { if (aborted) { return; } if (handler.enter) { handler.enter(); } setContext(handler, context); @@ -23035,9 +24242,13 @@ define("router", aborted = true; } } + + if (!aborted) { + currentHandlerInfos.push(handlerInfo); + } }); - if (router.didTransition) { + if (!aborted && router.didTransition) { router.didTransition(handlerInfos); } } @@ -23057,29 +24268,7 @@ define("router", handler = handlerInfo.handler, context = handlerInfo.context; - callback(handler, context); - } - } - - /** - @private - - Updates the `handler` field in each element in an Array of - `UnresolvedHandlerInfo`s from a handler name to a resolved handler. - - When done, the Array will contain `HandlerInfo` structures. - - @param {Router} router - @param {Array[UnresolvedHandlerInfo]} handlerInfos - */ - function resolveHandlers(router, handlerInfos) { - var handlerInfo; - - for (var i=0, l=handlerInfos.length; i<l; i++) { - handlerInfo = handlerInfos[i]; - - handlerInfo.name = handlerInfo.handler; - handlerInfo.handler = router.getHandler(handlerInfo.handler); + callback(handler, context, handlerInfo); } } @@ -23105,7 +24294,7 @@ define("router", * entered: the handler was not active in the old URL, but is now active. - The PartitionedHandlers structure has three fields: + The PartitionedHandlers structure has four fields: * `updatedContext`: a list of `HandlerInfo` objects that represent handlers that remain active but have a changed @@ -23114,6 +24303,7 @@ define("router", handlers that are newly active * `exited`: a list of `HandlerInfo` objects that are no longer active. + * `unchanged`: a list of `HanderInfo` objects that remain active. @param {Array[HandlerInfo]} oldHandlers a list of the handler information for the previous URL (or `[]` if this is the @@ -23127,7 +24317,8 @@ define("router", var handlers = { updatedContext: [], exited: [], - entered: [] + entered: [], + unchanged: [] }; var handlerChanged, contextChanged, i, l; @@ -23145,6 +24336,8 @@ define("router", } else if (contextChanged || oldHandler.context !== newHandler.context) { contextChanged = true; handlers.updatedContext.push(newHandler); + } else { + handlers.unchanged.push(oldHandler); } } @@ -23164,17 +24357,24 @@ define("router", throw new Error("Could not trigger event '" + name + "'. There are no active handlers"); } + var eventWasHandled = false; + for (var i=currentHandlerInfos.length-1; i>=0; i--) { var handlerInfo = currentHandlerInfos[i], handler = handlerInfo.handler; if (handler.events && handler.events[name]) { - handler.events[name].apply(handler, args); - return; + if (handler.events[name].apply(handler, args) === true) { + eventWasHandled = true; + } else { + return; + } } } - throw new Error("Nothing handled the event '" + name + "'."); + if (!eventWasHandled) { + throw new Error("Nothing handled the event '" + name + "'."); + } } function setContext(handler, context) { @@ -23184,6 +24384,7 @@ define("router", return Router; }); + })(); @@ -23275,6 +24476,8 @@ Ember.RouterDSL = DSL; (function() { +var get = Ember.get; + /** @module ember @submodule ember-routing @@ -23284,14 +24487,14 @@ 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, DefaultController, fullName; + var controller, DefaultController, fullName, instance; if (context && Ember.isArray(context)) { DefaultController = container.resolve('controller:array'); @@ -23312,10 +24515,16 @@ Ember.generateController = function(container, controllerName, context) { return "(generated " + controllerName + " controller)"; }; - fullName = 'controller:' + controllerName; container.register(fullName, controller); - return container.lookup(fullName); + + instance = container.lookup(fullName); + + if (get(instance, 'namespace.LOG_ACTIVE_GENERATION')) { + Ember.Logger.info("generated -> " + fullName, { fullName: fullName }); + } + + return instance; }; })(); @@ -23330,20 +24539,22 @@ Ember.generateController = function(container, controllerName, context) { var Router = requireModule("router"); var get = Ember.get, set = Ember.set; +var defineProperty = Ember.defineProperty; var DefaultView = Ember._MetamorphView; function setupLocation(router) { var location = get(router, 'location'), - rootURL = get(router, 'rootURL'); + rootURL = get(router, 'rootURL'), + options = {}; + + if (typeof rootURL === 'string') { + options.rootURL = rootURL; + } if ('string' === typeof location) { - location = set(router, 'location', Ember.Location.create({ - implementation: location - })); + options.implementation = location; + location = set(router, 'location', Ember.Location.create(options)); - if (typeof rootURL === 'string') { - set(location, 'rootURL', rootURL); - } } } @@ -23389,14 +24600,12 @@ Ember.Router = Ember.Object.extend({ }, didTransition: function(infos) { - // Don't do any further action here if we redirected - for (var i=0, l=infos.length; i<l; i++) { - if (infos[i].handler.redirected) { return; } - } - var appController = this.container.lookup('controller:application'), path = routePath(infos); + if (!('currentPath' in appController)) { + defineProperty(appController, 'currentPath'); + } set(appController, 'currentPath', path); this.notifyPropertyChange('url'); @@ -23410,6 +24619,22 @@ Ember.Router = Ember.Object.extend({ this.notifyPropertyChange('url'); }, + /** + Transition to another route via the `routeTo` event which + will by default be handled by ApplicationRoute. + + @method routeTo + @param {TransitionEvent} transitionEvent + */ + routeTo: function(transitionEvent) { + var handlerInfos = this.router.currentHandlerInfos; + if (handlerInfos) { + transitionEvent.sourceRoute = handlerInfos[handlerInfos.length - 1].handler; + } + + this.send('routeTo', transitionEvent); + }, + transitionTo: function(name) { var args = [].slice.call(arguments); doTransition(this, 'transitionTo', args); @@ -23438,6 +24663,18 @@ Ember.Router = Ember.Object.extend({ return this.router.hasRoute(route); }, + /** + @private + + Resets the state of the router by clearing the current route + handlers and deactivating them. + + @method reset + */ + reset: function() { + this.router.reset(); + }, + _lookupActiveView: function(templateName) { var active = this._activeViews[templateName]; return active && active[0]; @@ -23488,6 +24725,16 @@ function getHandlerFunction(router) { container.register(routeName, DefaultRoute.extend()); handler = container.lookup(routeName); + + if (get(router, 'namespace.LOG_ACTIVE_GENERATION')) { + Ember.Logger.info("generated -> " + routeName, { fullName: routeName }); + } + } + + if (name === 'application') { + // Inject default `routeTo` handler. + handler.events = handler.events || {}; + handler.events.routeTo = handler.events.routeTo || Ember.TransitionEvent.defaultHandler; } handler.routeName = name; @@ -23579,7 +24826,8 @@ Ember.Router.reopenClass({ */ var get = Ember.get, set = Ember.set, - classify = Ember.String.classify; + classify = Ember.String.classify, + fmt = Ember.String.fmt; /** The `Ember.Route` class is used to define individual routes. Refer to @@ -23610,15 +24858,16 @@ Ember.Route = Ember.Object.extend({ }, /** - The collection of functions keyed by name available on this route as + The collection of functions, keyed by name, available on this route as action targets. These functions will be invoked when a matching `{{action}}` is triggered from within a template and the application's current route is this route. - Events can also be invoked from other parts of your application via `Route#send`. + Events can also be invoked from other parts of your application via `Route#send` + or `Controller#send`. - The context of event will be the this route. + The context of the event will be this route. @see {Ember.Route#send} @see {Handlebars.helpers.action} @@ -23646,6 +24895,17 @@ Ember.Route = Ember.Object.extend({ activate: Ember.K, /** + Transition to another route via the `routeTo` event which + will by default be handled by ApplicationRoute. + + @method routeTo + @param {TransitionEvent} transitionEvent + */ + routeTo: function(transitionEvent) { + this.router.routeTo(transitionEvent); + }, + + /** Transition into another route. Optionally supply a model for the route in question. The model will be serialized into the URL using the `serialize` hook. @@ -23654,21 +24914,38 @@ Ember.Route = Ember.Object.extend({ @param {String} name the name of the route @param {...Object} models the */ - transitionTo: function() { - if (this._checkingRedirect) { this.redirected = true; } - return this.router.transitionTo.apply(this.router, arguments); + transitionTo: function(name, context) { + var router = this.router; + + // If the transition is a no-op, just bail. + if (router.isActive.apply(router, arguments)) { + return; + } + + if (this._checkingRedirect) { this._redirected[this._redirectDepth] = true; } + return router.transitionTo.apply(router, arguments); }, /** Transition into another route while replacing the current URL if possible. Identical to `transitionTo` in all other respects. + Of the bundled location types, only `history` currently supports + this behavior. + @method replaceWith @param {String} name the name of the route @param {...Object} models the */ replaceWith: function() { - if (this._checkingRedirect) { this.redirected = true; } + var router = this.router; + + // If the transition is a no-op, just bail. + if (router.isActive.apply(router, arguments)) { + return; + } + + if (this._checkingRedirect) { this._redirected[this._redirectDepth] = true; } return this.router.replaceWith.apply(this.router, arguments); }, @@ -23677,6 +24954,15 @@ Ember.Route = Ember.Object.extend({ }, /** + @private + + Internal counter for tracking whether a route handler has + called transitionTo or replaceWith inside its redirect hook. + + */ + _redirectDepth: 0, + + /** @private This hook is the entry point for router.js @@ -23684,21 +24970,64 @@ Ember.Route = Ember.Object.extend({ @method setup */ setup: function(context) { - this.redirected = false; + // Determine if this is the top-most transition. + // If so, we'll set up a data structure to track + // whether `transitionTo` or replaceWith gets called + // inside our `redirect` hook. + // + // This is necessary because we set a flag on the route + // inside transitionTo/replaceWith to determine afterwards + // if they were called, but `setup` can be called + // recursively and we need to disambiguate where in the + // call stack the redirect happened. + + // Are we the first call to setup? If so, set up the + // redirect tracking data structure, and remember that + // we're the top-most so we can clean it up later. + var isTop; + if (!this._redirected) { + isTop = true; + this._redirected = []; + } + + // Set a flag on this route saying that we are interested in + // tracking redirects, and increment the depth count. this._checkingRedirect = true; + var depth = ++this._redirectDepth; - this.redirect(context); + // Check to see if context is set. This check preserves + // the correct arguments.length inside the `redirect` hook. + if (context === undefined) { + this.redirect(); + } else { + this.redirect(context); + } + // After the call to `redirect` returns, decrement the depth count. + this._redirectDepth--; this._checkingRedirect = false; - if (this.redirected) { return false; } - var controller = this.controllerFor(this.routeName, context); + // Save off the data structure so we can reset it on the route but + // still reference it later in this method. + var redirected = this._redirected; - if (controller) { - this.controller = controller; - set(controller, 'model', context); + // If this is the top `setup` call in the call stack, clear the + // redirect tracking data structure. + if (isTop) { this._redirected = null; } + + // If we were redirected, there is nothing left for us to do. + // Returning false tells router.js not to continue calling setup + // on any children route handlers. + if (redirected[depth]) { + return false; } + var controller = this.controllerFor(this.routeName, context); + + // Assign the route's controller so that it can more easily be + // referenced in event handlers + this.controller = controller; + if (this.setupControllers) { Ember.deprecate("Ember.Route.setupControllers is deprecated. Please use Ember.Route.setupController(controller, model) instead."); this.setupControllers(controller, context); @@ -23742,6 +25071,8 @@ Ember.Route = Ember.Object.extend({ @private Called when the context is changed by router.js. + + @method contextDidChange */ contextDidChange: function() { this.currentModel = this.context; @@ -23852,30 +25183,41 @@ Ember.Route = Ember.Object.extend({ This method is called with the controller for the current route and the model supplied by the `model` hook. - ```js - App.Router.map(function() { - this.resource('post', {path: '/posts/:post_id'}); - }); - ``` - - For the `post` route, the controller is `App.PostController`. - By default, the `setupController` hook sets the `content` property of the controller to the `model`. - If no explicit controller is defined, the route will automatically create - an appropriate controller for the model: + This means that your template will get a proxy for the model as its + context, and you can act as though the model itself was the context. + + The provided controller will be one resolved based on the name + of this route. + + If no explicit controller is defined, Ember will automatically create + an appropriate controller for the model. * if the model is an `Ember.Array` (including record arrays from Ember Data), the controller is an `Ember.ArrayController`. * otherwise, the controller is an `Ember.ObjectController`. - This means that your template will get a proxy for the model as its - context, and you can act as though the model itself was the context. + As an example, consider the router: + + ```js + App.Router.map(function() { + this.resource('post', {path: '/posts/:post_id'}); + }); + ``` + + For the `post` route, a controller named `App.PostController` would + be used if it is defined. If it is not defined, an `Ember.ObjectController` + instance would be used. @method setupController */ - setupController: Ember.K, + setupController: function(controller, context){ + if (controller && (context !== undefined)) { + set(controller, 'model', context); + } + }, /** Returns the controller for a particular route. @@ -24010,7 +25352,12 @@ Ember.Route = Ember.Object.extend({ view = container.lookup('view:' + name), template = container.lookup('template:' + name); - if (!view && !template) { return; } + if (!view && !template) { + if (get(this.router, 'namespace.LOG_VIEW_LOOKUPS')) { + Ember.Logger.info("Could not find \"" + name + "\" template or view. Nothing will be rendered", { fullName: 'template:' + name }); + } + return; + } options = normalizeOptions(this, name, template, options); view = setupView(view, container, options); @@ -24026,7 +25373,9 @@ Ember.Route = Ember.Object.extend({ }); function parentRoute(route) { - var handlerInfos = route.router.router.currentHandlerInfos; + var handlerInfos = route.router.router.targetHandlerInfos; + + if (!handlerInfos) { return; } var parent, current; @@ -24042,7 +25391,7 @@ function parentTemplate(route, isRecursive) { if (!parent) { return; } - Ember.warn("The immediate parent route did not render into the main outlet and the default 'into' option may not be expected", !isRecursive); + Ember.warn(fmt("The immediate parent route ('%@') did not render into the main outlet and the default 'into' option ('%@') may not be expected", [get(parent, 'routeName'), get(route, 'routeName')]), !isRecursive); if (template = parent.lastRenderedTemplate) { return template; @@ -24057,6 +25406,7 @@ function normalizeOptions(route, name, template, options) { options.outlet = options.outlet || 'main'; options.name = name; options.template = template; + options.LOG_VIEW_LOOKUPS = get(route.router, 'namespace.LOG_VIEW_LOOKUPS'); Ember.assert("An outlet ("+options.outlet+") was specified but this view will render at the root level.", options.outlet === 'main' || options.into); @@ -24080,9 +25430,17 @@ function normalizeOptions(route, name, template, options) { } function setupView(view, container, options) { - var defaultView = options.into ? 'view:default' : 'view:toplevel'; - - view = view || container.lookup(defaultView); + if (view) { + if (options.LOG_VIEW_LOOKUPS) { + Ember.Logger.info("Rendering " + options.name + " with " + view, { fullName: 'view:' + options.name }); + } + } else { + var defaultView = options.into ? 'view:default' : 'view:toplevel'; + view = container.lookup(defaultView); + if (options.LOG_VIEW_LOOKUPS) { + Ember.Logger.info("Rendering " + options.name + " with default view " + view, { fullName: 'view:' + options.name }); + } + } if (!get(view, 'templateName')) { set(view, 'template', options.template); @@ -24103,6 +25461,10 @@ function appendView(route, view, options) { parentView.connectOutlet(options.outlet, view); } else { var rootElement = get(route, 'router.namespace.rootElement'); + // tear down view if one is already rendered + if (route.teardownView) { + route.teardownView(); + } route.router._connectActiveView(options.name, view); route.teardownView = teardownTopLevel(view); view.appendTo(rootElement); @@ -24129,6 +25491,65 @@ function teardownView(route) { (function() { +/** +@module ember +@submodule ember-routing +*/ + + +/* + A TransitionEvent is passed as the argument for `transitionTo` + events and contains information about an attempted transition + that can be modified or decorated by leafier `transitionTo` event + handlers before the actual transition is committed by ApplicationRoute. + + @class TransitionEvent + @namespace Ember + @extends Ember.Deferred + */ +Ember.TransitionEvent = Ember.Object.extend({ + + /* + The Ember.Route method used to perform the transition. Presently, + the only valid values are 'transitionTo' and 'replaceWith'. + */ + transitionMethod: 'transitionTo', + destinationRouteName: null, + sourceRoute: null, + contexts: null, + + init: function() { + this._super(); + this.contexts = this.contexts || []; + }, + + /* + Convenience method that returns an array that can be used for + legacy `transitionTo` and `replaceWith`. + */ + transitionToArgs: function() { + return [this.destinationRouteName].concat(this.contexts); + } +}); + + +Ember.TransitionEvent.reopenClass({ + /* + This is the default transition event handler that will be injected + into ApplicationRoute. The context, like all route event handlers in + the events hash, will be an `Ember.Route`. + */ + defaultHandler: function(transitionEvent) { + var router = this.router; + router[transitionEvent.transitionMethod].apply(router, transitionEvent.transitionToArgs()); + } +}); + +})(); + + + +(function() { })(); @@ -24168,7 +25589,7 @@ Ember.onLoad('Ember.Handlebars', function() { @submodule ember-routing */ -var get = Ember.get, set = Ember.set; +var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; Ember.onLoad('Ember.Handlebars', function(Handlebars) { var resolveParams = Ember.Router.resolveParams, @@ -24194,26 +25615,13 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { routeName = fullRouteName(router, passedRouteName); - Ember.assert("The route " + passedRouteName + " was not found", router.hasRoute(routeName)); + Ember.assert(fmt("The attempt to linkTo route '%@' failed. The router did not find '%@' in its possible routes: '%@'", [passedRouteName, passedRouteName, Ember.keys(router.router.recognizer.names).join("', '")]), router.hasRoute(routeName)); var ret = [ routeName ]; return ret.concat(resolvedPaths(linkView.parameters)); } /** - 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 @@ -24224,15 +25632,23 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { currentWhen: null, title: null, activeClass: 'active', + disabledClass: 'disabled', + _isDisabled: false, replace: false, attributeBindings: ['href', 'title'], - classNameBindings: 'active', + classNameBindings: ['active', 'disabled'], // Even though this isn't a virtual view, we want to treat it as if it is // so that you can access the parent with {{view.prop}} concreteView: Ember.computed(function() { return get(this, 'parentView'); - }).property('parentView').volatile(), + }).property('parentView'), + + disabled: Ember.computed(function(key, value) { + if (value !== undefined) { this.set('_isDisabled', value); } + + return value ? this.get('disabledClass') : false; + }), active: Ember.computed(function() { var router = this.get('router'), @@ -24253,17 +25669,32 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { event.preventDefault(); if (this.bubbles === false) { event.stopPropagation(); } + + if (get(this, '_isDisabled')) { return false; } var router = this.get('router'); - if (this.get('replace')) { - router.replaceWith.apply(router, args(this, router)); + if (Ember.ENV.ENABLE_ROUTE_TO) { + + var routeArgs = args(this, router); + + router.routeTo(Ember.TransitionEvent.create({ + transitionMethod: this.get('replace') ? 'replaceWith' : 'transitionTo', + destinationRouteName: routeArgs[0], + contexts: routeArgs.slice(1) + })); } else { - router.transitionTo.apply(router, args(this, router)); + if (this.get('replace')) { + router.replaceWith.apply(router, args(this, router)); + } else { + router.transitionTo.apply(router, args(this, router)); + } } }, href: Ember.computed(function() { + if (this.get('tagName') !== 'a') { return false; } + var router = this.get('router'); return router.generate.apply(router, args(this, router)); }) @@ -24272,6 +25703,155 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { LinkView.toString = function() { return "LinkView"; }; /** + The `{{linkTo}}` helper renders a link to the supplied + `routeName` passing an optionally supplied model to the + route as its `model` context of the route. The block + for `{{linkTo}}` becomes the innerHTML of the rendered + element: + + ```handlebars + {{#linkTo photoGallery}} + Great Hamster Photos + {{/linkTo}} + ``` + + ```html + <a href="/hamster-photos"> + Great Hamster Photos + </a> + ``` + + ### Supplying a tagName + By default `{{linkTo}}` renders an `<a>` element. This can + be overridden for a single use of `{{linkTo}}` by supplying + a `tagName` option: + + ```handlebars + {{#linkTo photoGallery tagName="li"}} + Great Hamster Photos + {{/linkTo}} + ``` + + ```html + <li> + Great Hamster Photos + </li> + ``` + + To override this option for your entire application, see + "Overriding Application-wide Defaults". + + ### Handling `href` + `{{linkTo}}` will use your application's Router to + fill the element's `href` property with a url that + matches the path to the supplied `routeName` for your + routers's configured `Location` scheme, which defaults + to Ember.HashLocation. + + ### Handling current route + `{{linkTo}}` will apply a CSS class name of 'active' + when the application's current route matches + the supplied routeName. For example, if the application's + current route is 'photoGallery.recent' the following + use of `{{linkTo}}`: + + ```handlebars + {{#linkTo photoGallery.recent}} + Great Hamster Photos from the last week + {{/linkTo}} + ``` + + will result in + + ```html + <a href="/hamster-photos/this-week" class="active"> + Great Hamster Photos + </a> + ``` + + The CSS class name used for active classes can be customized + for a single use of `{{linkTo}}` by passing an `activeClass` + option: + + ```handlebars + {{#linkTo photoGallery.recent activeClass="current-url"}} + Great Hamster Photos from the last week + {{/linkTo}} + ``` + + ```html + <a href="/hamster-photos/this-week" class="current-url"> + Great Hamster Photos + </a> + ``` + + To override this option for your entire application, see + "Overriding Application-wide Defaults". + + ### Supplying a model + An optional model argument can be used for routes whose + paths contain dynamic segments. This argument will become + the model context of the linked route: + + ```javascript + App.Router.map(function(){ + this.resource("photoGallery", {path: "hamster-photos/:photo_id"}); + }) + ``` + + ```handlebars + {{#linkTo photoGallery aPhoto}} + {{aPhoto.title}} + {{/linkTo}} + ``` + + ```html + <a href="/hamster-photos/42"> + Tomster + </a> + ``` + + ### Supplying multiple models + For deep-linking to route paths that contain multiple + dynamic segments, multiple model arguments can be used. + As the router transitions through the route path, each + supplied model argument will become the context for the + route with the dynamic segments: + + ```javascript + App.Router.map(function(){ + this.resource("photoGallery", {path: "hamster-photos/:photo_id"}, function(){ + this.route("comment", {path: "comments/:comment_id"}); + }); + }); + ``` + This argument will become the model context of the linked route: + + ```handlebars + {{#linkTo photoGallery.comment aPhoto comment}} + {{comment.body}} + {{/linkTo}} + ``` + + ```html + <a href="/hamster-photos/42/comment/718"> + A+++ would snuggle again. + </a> + ``` + + ### Overriding Application-wide Defaults + ``{{linkTo}}`` creates an instance of Ember.LinkView + for rendering. To override options for your entire + application, reopen Ember.LinkView and supply the + desired values: + + ``` javascript + Ember.LinkView.reopen({ + activeClass: "is-active", + tagName: 'li' + }) + ``` + @method linkTo @for Ember.Handlebars.helpers @param {String} routeName @@ -24286,6 +25866,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { hash.namedRoute = name; hash.currentWhen = hash.currentWhen || name; + hash.disabledBinding = hash.disabledWhen; hash.parameters = { context: this, @@ -24361,13 +25942,27 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { }); ``` + You can specify the view class that the outlet uses to contain and manage the + templates rendered into it. + + ``` handlebars + {{outlet viewClass=App.SectionContainer}} + ``` + + ``` javascript + App.SectionContainer = Ember.ContainerView.extend({ + tagName: 'section', + classNames: ['special'] + }); + ``` + @method outlet @for Ember.Handlebars.helpers @param {String} property the property on the controller that holds the view for this outlet */ Handlebars.registerHelper('outlet', function(property, options) { - var outletSource; + var outletSource, outletContainerClass; if (property && property.data && property.data.isRenderData) { options = property; @@ -24379,10 +25974,12 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { outletSource = outletSource.get('_parentView'); } + outletContainerClass = options.hash.viewClass || Handlebars.OutletView; + options.data.view.set('outletSource', outletSource); options.hash.currentViewBinding = '_view.outletSource._outlets.' + property; - return Handlebars.helpers.view.call(this, Handlebars.OutletView, options); + return Handlebars.helpers.view.call(this, outletContainerClass, options); }); }); @@ -24400,15 +25997,23 @@ var get = Ember.get, set = Ember.set; Ember.onLoad('Ember.Handlebars', function(Handlebars) { /** - Renders the named template in the current context using the singleton - instance of the same-named controller. + Renders the named template in the current context with the same-named + controller. - If a view class with the same name exists, uses the view class. + If a view class with the same name exists, the view class will be used. - If a `model` is specified, it becomes the model for that controller. + The optional second argument is a property path that will be bound + to the `model` property of the controller. + + If a `model` property path is specified, then a new instance of the + controller will be created. + + If no `model` property path is provided, then the helper will use the + singleton instance of the controller. A given controller may only be used + one time in your app in this manner. The default target for `{{action}}`s in the rendered template is the - named controller. + controller. @method render @for Ember.Handlebars.helpers @@ -24488,6 +26093,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { var EmberHandlebars = Ember.Handlebars, handlebarsGet = EmberHandlebars.get, SafeString = EmberHandlebars.SafeString, + forEach = Ember.ArrayPolyfills.forEach, get = Ember.get, a_slice = Array.prototype.slice; @@ -24514,7 +26120,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { var allowed = true; - keys.forEach(function(key) { + forEach.call(keys, function(key) { if (event[key + "Key"] && allowedKeys.indexOf(key) === -1) { allowed = false; } @@ -24600,7 +26206,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { aView.appendTo('body'); ``` - Will results in the following rendered HTML + Will result in the following rendered HTML ```html <div class="ember-view"> @@ -24685,7 +26291,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { 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: + to an object, accessible in the current context: ```handlebars <script type="text/x-handlebars" data-template-name='a-template'> @@ -24887,6 +26493,28 @@ if (Ember.ENV.EXPERIMENTAL_CONTROL_HELPER) { var get = Ember.get, set = Ember.set; Ember.ControllerMixin.reopen({ + /** + Transition the application into another route. The route may + be either a single route or route path: + + ```javascript + aController.transitionToRoute('blogPosts'); + aController.transitionToRoute('blogPosts.recentEntries'); + ``` + + Optionally supply a model for the route in question. The model + will be serialized into the URL using the `serialize` hook of + the route: + + ```javascript + aController.transitionToRoute('blogPost', aPost); + ``` + + @param {String} name the name of the route + @param {...Object} models the + @for Ember.ControllerMixin + @method transitionToRoute + */ transitionToRoute: function() { // target may be either another controller or a router var target = get(this, 'target'), @@ -24894,6 +26522,11 @@ Ember.ControllerMixin.reopen({ return method.apply(target, arguments); }, + /** + @deprecated + @for Ember.ControllerMixin + @method transitionTo + */ transitionTo: function() { Ember.deprecate("transitionTo is deprecated. Please use transitionToRoute."); return this.transitionToRoute.apply(this, arguments); @@ -24906,6 +26539,11 @@ Ember.ControllerMixin.reopen({ return method.apply(target, arguments); }, + /** + @deprecated + @for Ember.ControllerMixin + @method replaceWith + */ replaceWith: function() { Ember.deprecate("replaceWith is deprecated. Please use replaceRoute."); return this.replaceRoute.apply(this, arguments); @@ -24931,6 +26569,15 @@ Ember.View.reopen({ }, connectOutlet: function(outletName, view) { + if (this._pendingDisconnections) { + delete this._pendingDisconnections[outletName]; + } + + if (this._hasEquivalentView(outletName, view)) { + view.destroy(); + return; + } + var outlets = get(this, '_outlets'), container = get(this, 'container'), router = container && container.lookup('router:main'), @@ -24943,10 +26590,30 @@ Ember.View.reopen({ } }, + _hasEquivalentView: function(outletName, view) { + var existingView = get(this, '_outlets.'+outletName); + return existingView && + existingView.prototype === view.prototype && + existingView.get('template') === view.get('template') && + existingView.get('context') === view.get('context'); + }, + disconnectOutlet: function(outletName) { + if (!this._pendingDisconnections) { + this._pendingDisconnections = {}; + } + this._pendingDisconnections[outletName] = true; + Ember.run.once(this, '_finishDisconnections'); + }, + + _finishDisconnections: function() { var outlets = get(this, '_outlets'); + var pendingDisconnections = this._pendingDisconnections; + this._pendingDisconnections = null; - set(outlets, outletName, null); + for (var outletName in pendingDisconnections) { + set(outlets, outletName, null); + } } }); @@ -25132,7 +26799,7 @@ Ember.HashLocation = Ember.Object.extend({ var self = this; var guid = Ember.guidFor(this); - Ember.$(window).bind('hashchange.ember-location-'+guid, function() { + Ember.$(window).on('hashchange.ember-location-'+guid, function() { Ember.run(function() { var path = location.hash.substr(1); if (get(self, 'lastSetURL') === path) { return; } @@ -25194,7 +26861,6 @@ Ember.HistoryLocation = Ember.Object.extend({ init: function() { set(this, 'location', get(this, 'location') || window.location); - this._initialUrl = this.getURL(); this.initState(); }, @@ -25206,8 +26872,8 @@ Ember.HistoryLocation = Ember.Object.extend({ @method initState */ initState: function() { + set(this, 'history', get(this, 'history') || window.history); this.replaceState(this.formatURL(this.getURL())); - set(this, 'history', window.history); }, /** @@ -25288,7 +26954,9 @@ Ember.HistoryLocation = Ember.Object.extend({ @param path {String} */ pushState: function(path) { - window.history.pushState({ path: path }, null, path); + get(this, 'history').pushState({ path: path }, null, path); + // used for webkit workaround + this._previousURL = this.getURL(); }, /** @@ -25300,7 +26968,9 @@ Ember.HistoryLocation = Ember.Object.extend({ @param path {String} */ replaceState: function(path) { - window.history.replaceState({ path: path }, null, path); + get(this, 'history').replaceState({ path: path }, null, path); + // used for webkit workaround + this._previousURL = this.getURL(); }, /** @@ -25316,11 +26986,11 @@ Ember.HistoryLocation = Ember.Object.extend({ var guid = Ember.guidFor(this), self = this; - Ember.$(window).bind('popstate.ember-location-'+guid, function(e) { + Ember.$(window).on('popstate.ember-location-'+guid, function(e) { // Ignore initial page load popstate event in Chrome if(!popstateFired) { popstateFired = true; - if (self.getURL() === self._initialUrl) { return; } + if (self.getURL() === self._previousURL) { return; } } callback(self.getURL()); }); @@ -25702,10 +27372,27 @@ Ember.DefaultResolver = Ember.Object.extend({ @submodule ember-application */ -var get = Ember.get, set = Ember.set, - classify = Ember.String.classify, - capitalize = Ember.String.capitalize, - decamelize = Ember.String.decamelize; +var get = Ember.get, set = Ember.set; + +function DeprecatedContainer(container) { + this._container = container; +} + +DeprecatedContainer.deprecate = function(method) { + return function() { + var container = this._container; + + Ember.deprecate('Using the defaultContainer is no longer supported. [defaultContainer#' + method + '] see: http://git.io/EKPpnA', false); + return container[method].apply(container, arguments); + }; +}; + +DeprecatedContainer.prototype = { + _container: null, + lookup: DeprecatedContainer.deprecate('lookup'), + resolve: DeprecatedContainer.deprecate('resolve'), + register: DeprecatedContainer.deprecate('register') +}; /** An instance of `Ember.Application` is the starting point for every Ember @@ -25834,17 +27521,6 @@ var get = Ember.get, set = Ember.set, 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 @@ -25966,10 +27642,10 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin This allows application developers to do: ```javascript - App = Ember.Application.create(); + var App = Ember.Application.create(); - App.Router.map(function(match) { - match("/").to("index"); + App.Router.map(function() { + this.resource('posts'); }); ``` @@ -26144,14 +27820,93 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin return this; }, - reset: function() { - get(this, '__container__').destroy(); - this.buildContainer(); + /** + Reset the application. This is typically used only in tests. It cleans up + the application in the following order: + + 1. Deactivate existing routes + 2. Destroy all objects in the container + 3. Create a new application container + 4. Re-route to the existing url - Ember.run.schedule('actions', this, function(){ - this._initialize(); - this.startRouting(); + Typical Example: + + ```javascript + + var App; + + Ember.run(function(){ + App = Ember.Application.create(); }); + + module("acceptance test", { + setup: function() { + App.reset(); + } + }); + + test("first test", function(){ + // App is freshly reset + }); + + test("first test", function(){ + // App is again freshly reset + }); + ``` + + Advanced Example: + + Occasionally you may want to prevent the app from initializing during + setup. This could enable extra configuration, or enable asserting prior + to the app becoming ready. + + ```javascript + + var App; + + Ember.run(function(){ + App = Ember.Application.create(); + }); + + module("acceptance test", { + setup: function() { + Ember.run(function() { + App.reset(); + App.deferReadiness(); + }); + } + }); + + test("first test", function(){ + ok(true, 'something before app is initialized'); + + Ember.run(function(){ + App.advanceReadiness(); + }); + ok(true, 'something after app is initialized'); + }); + ``` + + @method reset + **/ + reset: function() { + this._readinessDeferrals = 1; + + function handleReset() { + var router = this.__container__.lookup('router:main'); + router.reset(); + + Ember.run(this.__container__, 'destroy'); + + this.buildContainer(); + + Ember.run.schedule('actions', this, function(){ + this._initialize(); + this.startRouting(); + }); + } + + Ember.run.join(this, handleReset); }, /** @@ -26205,27 +27960,12 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin @method setupEventDispatcher */ setupEventDispatcher: function() { - var eventDispatcher = this.createEventDispatcher(), - customEvents = get(this, 'customEvents'); + var customEvents = get(this, 'customEvents'), + rootElement = get(this, 'rootElement'), + dispatcher = this.__container__.lookup('event_dispatcher:main'); - eventDispatcher.setup(customEvents); - }, - - /** - @private - - Create an event dispatcher for the application's `rootElement`. - - @method createEventDispatcher - */ - createEventDispatcher: function() { - var rootElement = get(this, 'rootElement'), - eventDispatcher = Ember.EventDispatcher.create({ - rootElement: rootElement - }); - - set(this, 'eventDispatcher', eventDispatcher); - return eventDispatcher; + set(this, 'eventDispatcher', dispatcher); + dispatcher.setup(customEvents, rootElement); }, /** @@ -26268,10 +28008,7 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin willDestroy: function() { Ember.BOOTED = false; - var eventDispatcher = get(this, 'eventDispatcher'); - if (eventDispatcher) { eventDispatcher.destroy(); } - - get(this, '__container__').destroy(); + this.__container__.destroy(); }, initializer: function(options) { @@ -26320,7 +28057,8 @@ Ember.Application.reopenClass({ */ buildContainer: function(namespace) { var container = new Ember.Container(); - Ember.Container.defaultContainer = Ember.Container.defaultContainer || container; + + Ember.Container.defaultContainer = new DeprecatedContainer(container); container.set = Ember.set; container.normalize = normalize; @@ -26333,13 +28071,14 @@ Ember.Application.reopenClass({ container.register('controller:object', Ember.ObjectController, { instantiate: false }); container.register('controller:array', Ember.ArrayController, { instantiate: false }); container.register('route:basic', Ember.Route, { instantiate: false }); + container.register('event_dispatcher:main', Ember.EventDispatcher); container.injection('router:main', 'namespace', 'application:main'); - container.typeInjection('controller', 'target', 'router:main'); - container.typeInjection('controller', 'namespace', 'application:main'); + container.injection('controller', 'target', 'router:main'); + container.injection('controller', 'namespace', 'application:main'); - container.typeInjection('route', 'router', 'router:main'); + container.injection('route', 'router', 'router:main'); return container; } @@ -26358,8 +28097,9 @@ Ember.Application.reopenClass({ This allows the application to register default injections in the container that could be overridden by the normal naming convention. + @method resolverFor @param {Ember.Namespace} namespace the namespace to look for classes - @return {any} the resolved value for a given lookup + @return {*} the resolved value for a given lookup */ function resolverFor(namespace) { var resolverClass = namespace.get('resolver') || Ember.DefaultResolver; @@ -26372,10 +28112,11 @@ function resolverFor(namespace) { } function normalize(fullName) { - var split = fullName.split(':'), + var split = fullName.split(':', 2), type = split[0], name = split[1]; + Ember.assert("Tried to normalize a container name without a colon (:) in it. You probably tried to lookup a name that did not contain a type, a colon, and a name. A proper lookup name would be `view:post`.", split.length === 2); if (type !== 'template') { var result = name; @@ -26453,6 +28194,31 @@ function verifyDependencies(controller) { Ember.ControllerMixin.reopen({ concatenatedProperties: ['needs'], + + /** + An array of other controller objects available inside + instances of this controller via the `controllers` + property: + + For example, when you define a controller: + + ```javascript + App.CommentsController = Ember.ArrayController.extend({ + needs: ['post'] + }); + ``` + + The application's single instance of these other + controllers are accessible by name through the + `controllers` property: + + ```javascript + this.get('controllers.post'); // instance of App.PostController + ``` + + @property {Array} needs + @default [] + */ needs: [], init: function() { @@ -26599,8 +28365,26 @@ Ember.State = Ember.Object.extend(Ember.Evented, } } - set(this, 'pathsCache', {}); - set(this, 'pathsCacheNoContext', {}); + // pathsCaches is a nested hash of the form: + // pathsCaches[stateManagerTypeGuid][path] == transitions_hash + set(this, 'pathsCaches', {}); + }, + + setPathsCache: function(stateManager, path, transitions) { + var stateManagerTypeGuid = Ember.guidFor(stateManager.constructor), + pathsCaches = get(this, 'pathsCaches'), + pathsCacheForManager = pathsCaches[stateManagerTypeGuid] || {}; + + pathsCacheForManager[path] = transitions; + pathsCaches[stateManagerTypeGuid] = pathsCacheForManager; + }, + + getPathsCache: function(stateManager, path) { + var stateManagerTypeGuid = Ember.guidFor(stateManager.constructor), + pathsCaches = get(this, 'pathsCaches'), + pathsCacheForManager = pathsCaches[stateManagerTypeGuid] || {}; + + return pathsCacheForManager[path]; }, setupChild: function(states, name, value) { @@ -27588,7 +29372,7 @@ Ember.StateManager = Ember.State.extend({ }, contextFreeTransition: function(currentState, path) { - var cache = currentState.pathsCache[path]; + var cache = currentState.getPathsCache(this, path); if (cache) { return cache; } var enterStates = this.getStatesInPath(currentState, path), @@ -27674,12 +29458,14 @@ Ember.StateManager = Ember.State.extend({ // Cache the enterStates, exitStates, and resolveState for the // current state and the `path`. - var transitions = currentState.pathsCache[path] = { + var transitions = { exitStates: exitStates, enterStates: enterStates, resolveState: resolveState }; + currentState.setPathsCache(this, path, transitions); + return transitions; }, @@ -27739,13 +29525,422 @@ Ember States })(); +(function() { +var slice = [].slice, + helpers = {}, + originalMethods = {}, + injectHelpersCallbacks = []; + +/** + @class Test + @namespace Ember +*/ +Ember.Test = { + + /** + @public + + `registerHelper` is used to register a + test helper that will be injected when + `App.injectTestHelpers` is called. + + The helper method will always be called + with the current Application as the first + parameter. + + For example: + ```javascript + Ember.Test.registerHelper('boot', function(app)) { + Ember.run(app, app.deferReadiness); + } + ``` + + This helper can later be called without arguments + because it will be called with `app` as the + first parameter. + + ```javascript + App = Ember.Application.create(); + App.injectTestHelpers(); + boot(); + ``` + + Whenever you register a helper that + performs async operations, + make sure you `return wait();` at the + end of the helper. + + If an async helper also needs to return a value, + pass it to the `wait` helper as a first argument: + `return wait(val);` + + @method registerHelper + @param name {String} + @param helperMethod {Function} + */ + registerHelper: function(name, helperMethod) { + helpers[name] = helperMethod; + }, + /** + @public + @method unregisterHelper + @param name {String} + */ + unregisterHelper: function(name) { + delete helpers[name]; + if (originalMethods[name]) { + window[name] = originalMethods[name]; + } + delete originalMethods[name]; + }, + + /** + @public + + Used to register callbacks to be fired + whenever `App.injectTestHelpers` is called + + The callback will receive the current application + as an argument. + + @method unregisterHelper + @param name {String} + */ + onInjectHelpers: function(callback) { + injectHelpersCallbacks.push(callback); + }, + + /** + @public + + This returns a thenable tailored + for testing. It catches failed + `onSuccess` callbacks and invokes + the `Ember.Test.adapter.exception` + callback in the last chained then. + + This method should be returned + by async helpers such as `wait`. + + @method promise + @param resolver {Function} + */ + promise: function(resolver) { + var promise = new Ember.RSVP.Promise(resolver); + var thenable = { + chained: false + }; + thenable.then = function(onSuccess, onFailure) { + var self = this, thenPromise, nextPromise; + thenable.chained = true; + thenPromise = promise.then(onSuccess, onFailure); + // this is to ensure all downstream fulfillment + // handlers are wrapped in the error handling + nextPromise = Ember.Test.promise(function(resolve) { + resolve(thenPromise); + }); + thenPromise.then(null, function(reason) { + // ensure this is the last promise in the chain + // if not, ignore and the exception will propagate + // this prevents the same error from being fired multiple times + if (!nextPromise.chained) { + Ember.Test.adapter.exception(reason); + } + }); + return nextPromise; + }; + return thenable; + }, + + /** + @public + + Used to allow ember-testing + to communicate with a specific + testing framework. + + You can manually set it before calling + `App.setupForTesting()`. + + Example: + 'Ember.Test.adapter = MyCustomAdapter.create()' + + If you do not set it, ember-testing + will default to `Ember.Test.QUnitAdapter`. + */ + adapter: null +}; + +function curry(app, fn) { + return function() { + var args = slice.call(arguments); + args.unshift(app); + return fn.apply(app, args); + }; +} + +Ember.Application.reopen({ + testHelpers: {}, + + setupForTesting: function() { + this.deferReadiness(); + + this.Router.reopen({ + location: 'none' + }); + + // if adapter is not manually set + // default to QUnit + if (!Ember.Test.adapter) { + Ember.Test.adapter = Ember.Test.QUnitAdapter.create(); + } + }, + + injectTestHelpers: function() { + this.testHelpers = {}; + for (var name in helpers) { + originalMethods[name] = window[name]; + this.testHelpers[name] = window[name] = curry(this, helpers[name]); + } + + for(var i = 0, l = injectHelpersCallbacks.length; i < l; i++) { + injectHelpersCallbacks[i](this); + } + }, + + removeTestHelpers: function() { + for (var name in helpers) { + window[name] = originalMethods[name]; + delete this.testHelpers[name]; + delete originalMethods[name]; + } + } +}); + +})(); + + + +(function() { +var Test = Ember.Test; + +/** + @class Adapter + @namespace Ember.Test +*/ +Test.Adapter = Ember.Object.extend({ + /** + @public + + This callback will be called + whenever an async operation + is about to start. + + Override this to call your + framework's methods + that handle async operations + + @method asyncStart + */ + asyncStart: Ember.K, + + /** + @public + + This callback will be called + whenever an async operation + has completed. + + @method asyncEnd + */ + asyncEnd: Ember.K, + + /** + @public + + Override this method with your + testing framework's false assertion + This function is called whenever + an exception occurs causing the testing + promise to fail. + + QUnit example: + + ```javascript + exception: function(error) { + ok(false, error); + } + ``` + + @method exception + @param reason {String} + */ + exception: function(error) { + setTimeout(function() { + throw error; + }); + } +}); + +/** + @class QUnitAdapter + @namespace Ember.Test +*/ +Test.QUnitAdapter = Test.Adapter.extend({ + asyncStart: function() { + stop(); + }, + asyncEnd: function() { + start(); + }, + exception: function(error) { + ok(false, error); + } +}); + +})(); + + + +(function() { +var get = Ember.get, + helper = Ember.Test.registerHelper, + pendingAjaxRequests = 0, + countAsync = 0; + + +Ember.Test.onInjectHelpers(function() { + Ember.$(document).ajaxStart(function() { + pendingAjaxRequests++; + }); + + Ember.$(document).ajaxStop(function() { + pendingAjaxRequests--; + }); +}); + + +function visit(app, url) { + Ember.run(app, app.handleURL, url); + app.__container__.lookup('router:main').location.setURL(url); + return wait(app); +} + +function click(app, selector, context) { + var $el = find(app, selector, context); + Ember.run(function() { + $el.click(); + }); + return wait(app); +} + +function fillIn(app, selector, context, text) { + var $el; + if (typeof text === 'undefined') { + text = context; + context = null; + } + $el = find(app, selector, context); + Ember.run(function() { + $el.val(text).change(); + }); + return wait(app); +} + +function find(app, selector, context) { + var $el; + context = context || get(app, 'rootElement'); + $el = app.$(selector, context); + if ($el.length === 0) { + throw("Element " + selector + " not found."); + } + return $el; +} + +function wait(app, value) { + var promise, obj = {}, helperName; + + promise = Ember.Test.promise(function(resolve) { + if (++countAsync === 1) { + Ember.Test.adapter.asyncStart(); + } + var watcher = setInterval(function() { + var routerIsLoading = app.__container__.lookup('router:main').router.isLoading; + if (routerIsLoading) { return; } + if (pendingAjaxRequests) { return; } + if (Ember.run.hasScheduledTimers() || Ember.run.currentRunLoop) { return; } + clearInterval(watcher); + if (--countAsync === 0) { + Ember.Test.adapter.asyncEnd(); + } + Ember.run(function() { + resolve(value); + }); + }, 10); + }); + + return buildChainObject(app, promise); +} + +/** + Builds an object that contains + all helper methods. This object will be + returned by helpers and then-promises. + + This allows us to chain helpers: + + ```javascript + visit('posts/new') + .click('.add-btn') + .fillIn('.title', 'Post') + .click('.submit') + .then(function() { + equal('.post-title', 'Post'); + }) + .visit('comments') + .then(function() { + equal(find('.comments'),length, 0); + }); + ``` +*/ +function buildChainObject(app, promise) { + var helperName, obj = {}; + for(helperName in app.testHelpers) { + obj[helperName] = chain(app, promise, app.testHelpers[helperName]); + } + obj.then = function(fn) { + var thenPromise = promise.then(fn); + return buildChainObject(app, thenPromise); + }; + return obj; +} + +function chain(app, promise, fn) { + return function() { + var args = arguments, chainedPromise; + chainedPromise = promise.then(function() { + return fn.apply(null, args); + }); + return buildChainObject(app, chainedPromise); + }; +} + +// expose these methods as test helpers +helper('visit', visit); +helper('click', click); +helper('fillIn', fillIn); +helper('find', find); +helper('wait', wait); })(); -// Version: v1.0.0-rc.2 -// Last commit: 656fa6e (2013-03-29 13:40:38 -0700) + (function() { + +})(); + +(function() { /** Ember @@ -27754,3 +29949,5 @@ Ember })(); + +})(); diff --git a/public/js/libs/handlebars.js b/public/js/libs/handlebars.js index 9c653ee..96d86ea 100644 --- a/public/js/libs/handlebars.js +++ b/public/js/libs/handlebars.js @@ -22,31 +22,45 @@ THE SOFTWARE. */ -// lib/handlebars/base.js - -/*jshint eqnull:true*/ -this.Handlebars = {}; +// lib/handlebars/browser-prefix.js +var Handlebars = {}; -(function(Handlebars) { +(function(Handlebars, undefined) { +; +// lib/handlebars/base.js -Handlebars.VERSION = "1.0.0-rc.3"; -Handlebars.COMPILER_REVISION = 2; +Handlebars.VERSION = "1.0.0-rc.4"; +Handlebars.COMPILER_REVISION = 3; 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' + 2: '== 1.0.0-rc.3', + 3: '>= 1.0.0-rc.4' }; Handlebars.helpers = {}; Handlebars.partials = {}; +var toString = Object.prototype.toString, + functionType = '[object Function]', + objectType = '[object Object]'; + Handlebars.registerHelper = function(name, fn, inverse) { - if(inverse) { fn.not = inverse; } - this.helpers[name] = fn; + if (toString.call(name) === objectType) { + if (inverse || fn) { throw new Handlebars.Exception('Arg not supported with multiple helpers'); } + Handlebars.Utils.extend(this.helpers, name); + } else { + if (inverse) { fn.not = inverse; } + this.helpers[name] = fn; + } }; Handlebars.registerPartial = function(name, str) { - this.partials[name] = str; + if (toString.call(name) === objectType) { + Handlebars.Utils.extend(this.partials, name); + } else { + this.partials[name] = str; + } }; Handlebars.registerHelper('helperMissing', function(arg) { @@ -57,13 +71,9 @@ Handlebars.registerHelper('helperMissing', function(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); } @@ -154,23 +164,17 @@ Handlebars.registerHelper('if', function(context, options) { }); Handlebars.registerHelper('unless', function(context, options) { - var fn = options.fn, inverse = options.inverse; - options.fn = inverse; - options.inverse = fn; - - return Handlebars.helpers['if'].call(this, context, options); + return Handlebars.helpers['if'].call(this, context, {fn: options.inverse, inverse: options.fn}); }); Handlebars.registerHelper('with', function(context, options) { - return options.fn(context); + if (!Handlebars.Utils.isEmpty(context)) return options.fn(context); }); Handlebars.registerHelper('log', function(context, options) { var level = options.data && options.data.level != null ? parseInt(options.data.level, 10) : 1; Handlebars.log(level, context); }); - -}(this.Handlebars)); ; // lib/handlebars/compiler/parser.js /* Jison generated parser */ @@ -562,90 +566,93 @@ lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_STA var YYSTATE=YY_START switch($avoiding_name_collisions) { -case 0: +case 0: yy_.yytext = "\\"; return 14; +break; +case 1: if(yy_.yytext.slice(-1) !== "\\") this.begin("mu"); if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1), this.begin("emu"); if(yy_.yytext) return 14; break; -case 1: return 14; +case 2: return 14; break; -case 2: +case 3: if(yy_.yytext.slice(-1) !== "\\") this.popState(); if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1); return 14; break; -case 3: yy_.yytext = yy_.yytext.substr(0, yy_.yyleng-4); this.popState(); return 15; +case 4: yy_.yytext = yy_.yytext.substr(0, yy_.yyleng-4); this.popState(); return 15; break; -case 4: this.begin("par"); return 24; +case 5: this.begin("par"); return 24; break; -case 5: return 16; +case 6: return 16; break; -case 6: return 20; -break; -case 7: return 19; +case 7: return 20; break; case 8: return 19; break; -case 9: return 23; +case 9: return 19; break; case 10: return 23; break; -case 11: this.popState(); this.begin('com'); +case 11: return 23; break; -case 12: yy_.yytext = yy_.yytext.substr(3,yy_.yyleng-5); this.popState(); return 15; +case 12: this.popState(); this.begin('com'); break; -case 13: return 22; +case 13: yy_.yytext = yy_.yytext.substr(3,yy_.yyleng-5); this.popState(); return 15; break; -case 14: return 36; +case 14: return 22; break; -case 15: return 35; +case 15: return 36; break; case 16: return 35; break; -case 17: return 39; +case 17: return 35; break; -case 18: /*ignore whitespace*/ +case 18: return 39; break; -case 19: this.popState(); return 18; +case 19: /*ignore whitespace*/ break; case 20: this.popState(); return 18; break; -case 21: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 30; +case 21: this.popState(); return 18; break; -case 22: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\'/g,"'"); return 30; +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; +case 23: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\'/g,"'"); return 30; break; -case 24: return 32; +case 24: yy_.yytext = yy_.yytext.substr(1); return 28; break; case 25: return 32; break; -case 26: return 31; +case 26: return 32; +break; +case 27: return 31; break; -case 27: return 35; +case 28: return 35; break; -case 28: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2); return 35; +case 29: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2); return 35; break; -case 29: return 'INVALID'; +case 30: return 'INVALID'; break; -case 30: /*ignore whitespace*/ +case 31: /*ignore whitespace*/ break; -case 31: this.popState(); return 37; +case 32: this.popState(); return 37; break; -case 32: return 5; +case 33: 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}}; +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":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,33],"inclusive":false},"emu":{"rules":[3],"inclusive":false},"com":{"rules":[4],"inclusive":false},"par":{"rules":[31,32],"inclusive":false},"INITIAL":{"rules":[0,1,2,33],"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) { @@ -656,139 +663,133 @@ Handlebars.parse = function(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 = {}; - Handlebars.AST.ProgramNode = function(statements, inverse) { - this.type = "program"; - this.statements = statements; - if(inverse) { this.inverse = new Handlebars.AST.ProgramNode(inverse); } - }; +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; +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); + 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 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); + // 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. - }; + // if a mustache is an eligible helper but not a definite + // helper, it is ambiguous, and will be resolved in a later + // pass or at runtime. +}; - Handlebars.AST.PartialNode = function(partialName, context) { - this.type = "partial"; - this.partialName = partialName; - this.context = context; - }; +Handlebars.AST.PartialNode = function(partialName, context) { + this.type = "partial"; + this.partialName = partialName; + this.context = context; +}; +Handlebars.AST.BlockNode = function(mustache, program, inverse, close) { var verifyMatch = function(open, close) { if(open.original !== close.original) { throw new Handlebars.Exception(open.original + " doesn't match " + close.original); } }; - Handlebars.AST.BlockNode = function(mustache, program, inverse, close) { - verifyMatch(mustache.id, close); - this.type = "block"; - this.mustache = mustache; - this.program = program; - this.inverse = inverse; + verifyMatch(mustache.id, close); + this.type = "block"; + this.mustache = mustache; + this.program = program; + this.inverse = inverse; - if (this.inverse && !this.program) { - this.isInverse = true; - } - }; + if (this.inverse && !this.program) { + this.isInverse = true; + } +}; - Handlebars.AST.ContentNode = function(string) { - this.type = "content"; - this.string = string; - }; +Handlebars.AST.ContentNode = function(string) { + this.type = "content"; + this.string = string; +}; - Handlebars.AST.HashNode = function(pairs) { - this.type = "hash"; - this.pairs = pairs; - }; +Handlebars.AST.HashNode = function(pairs) { + this.type = "hash"; + this.pairs = pairs; +}; - Handlebars.AST.IdNode = function(parts) { - this.type = "ID"; - this.original = parts.join("."); +Handlebars.AST.IdNode = function(parts) { + this.type = "ID"; + this.original = parts.join("."); - var dig = [], depth = 0; + var dig = [], depth = 0; - for(var i=0,l=parts.length; i<l; i++) { - var part = parts[i]; + for(var i=0,l=parts.length; i<l; i++) { + var part = parts[i]; - if (part === ".." || part === "." || part === "this") { - if (dig.length > 0) { throw new Handlebars.Exception("Invalid path: " + this.original); } - else if (part === "..") { depth++; } - else { this.isScoped = true; } - } - else { dig.push(part); } + if (part === ".." || part === "." || part === "this") { + if (dig.length > 0) { throw new Handlebars.Exception("Invalid path: " + this.original); } + else if (part === "..") { depth++; } + else { this.isScoped = true; } } + else { dig.push(part); } + } - this.parts = dig; - this.string = dig.join('.'); - this.depth = depth; - - // an ID is simple if it only has one part, and that part is not - // `..` or `this`. - this.isSimple = parts.length === 1 && !this.isScoped && depth === 0; + this.parts = dig; + this.string = dig.join('.'); + this.depth = depth; - this.stringModeValue = this.string; - }; + // 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; - Handlebars.AST.PartialNameNode = function(name) { - this.type = "PARTIAL_NAME"; - this.name = name; - }; + this.stringModeValue = this.string; +}; - Handlebars.AST.DataNode = function(id) { - this.type = "DATA"; - this.id = id; - }; +Handlebars.AST.PartialNameNode = function(name) { + this.type = "PARTIAL_NAME"; + this.name = name; +}; - Handlebars.AST.StringNode = function(string) { - this.type = "STRING"; - this.string = string; - this.stringModeValue = string; - }; +Handlebars.AST.DataNode = function(id) { + this.type = "DATA"; + this.id = id; +}; - Handlebars.AST.IntegerNode = function(integer) { - this.type = "INTEGER"; - this.integer = integer; - this.stringModeValue = Number(integer); - }; +Handlebars.AST.StringNode = function(string) { + this.type = "STRING"; + this.string = string; + this.stringModeValue = string; +}; - Handlebars.AST.BooleanNode = function(bool) { - this.type = "BOOLEAN"; - this.bool = bool; - this.stringModeValue = bool === "true"; - }; +Handlebars.AST.IntegerNode = function(integer) { + this.type = "INTEGER"; + this.integer = integer; + this.stringModeValue = Number(integer); +}; - Handlebars.AST.CommentNode = function(comment) { - this.type = "comment"; - this.comment = comment; - }; +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']; @@ -811,1273 +812,1301 @@ Handlebars.SafeString.prototype.toString = function() { return this.string.toString(); }; -(function() { - var escape = { - "&": "&", - "<": "<", - ">": ">", - '"': """, - "'": "'", - "`": "`" - }; +var escape = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + "`": "`" +}; - var badChars = /[&<>"'`]/g; - var possible = /[&<>"'`]/; +var badChars = /[&<>"'`]/g; +var possible = /[&<>"'`]/; - var escapeChar = function(chr) { - return escape[chr] || "&"; - }; +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 ""; +Handlebars.Utils = { + extend: function(obj, value) { + for(var key in value) { + if(value.hasOwnProperty(key)) { + obj[key] = value[key]; } + } + }, - if(!possible.test(string)) { return string; } - return string.replace(badChars, escapeChar); - }, + 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 ""; + } - 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; - } + // Force a string conversion as this will be done by the append regardless and + // the regex test will do this transparently behind the scenes, causing issues if + // an object's to string has escaped characters in it. + string = string.toString(); + + if(!possible.test(string)) { return string; } + return string.replace(badChars, escapeChar); + }, + + isEmpty: function(value) { + if (!value && value !== 0) { + return true; + } else if(toString.call(value) === "[object Array]" && value.length === 0) { + return true; + } else { + return false; } - }; -})();; + } +}; +; // lib/handlebars/compiler/compiler.js /*jshint eqnull:true*/ -Handlebars.Compiler = function() {}; -Handlebars.JavaScriptCompiler = function() {}; +var Compiler = Handlebars.Compiler = function() {}; +var JavaScriptCompiler = 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. +// 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, +Compiler.prototype = { + compiler: Compiler, - disassemble: function() { - var opcodes = this.opcodes, opcode, out = [], params, param; + disassemble: function() { + var opcodes = this.opcodes, opcode, out = [], params, param; - for (var i=0, l=opcodes.length; i<l; i++) { - opcode = opcodes[i]; + for (var i=0, l=opcodes.length; i<l; i++) { + opcode = opcodes[i]; - if (opcode.opcode === 'DECLARE') { - out.push("DECLARE " + opcode.name + "=" + opcode.value); - } else { - params = []; - for (var j=0; j<opcode.args.length; j++) { - param = opcode.args[j]; - if (typeof param === "string") { - param = "\"" + param.replace("\n", "\\n") + "\""; - } - params.push(param); + if (opcode.opcode === 'DECLARE') { + out.push("DECLARE " + opcode.name + "=" + opcode.value); + } else { + params = []; + for (var j=0; j<opcode.args.length; j++) { + param = opcode.args[j]; + if (typeof param === "string") { + param = "\"" + param.replace("\n", "\\n") + "\""; } - out.push(opcode.opcode + " " + params.join(" ")); + params.push(param); } + out.push(opcode.opcode + " " + params.join(" ")); } + } - return out.join("\n"); - }, - equals: function(other) { - var len = this.opcodes.length; - if (other.opcodes.length !== len) { + return out.join("\n"); + }, + equals: function(other) { + var len = this.opcodes.length; + if (other.opcodes.length !== len) { + return false; + } + + for (var i = 0; i < len; i++) { + var opcode = this.opcodes[i], + otherOpcode = other.opcodes[i]; + if (opcode.opcode !== otherOpcode.opcode || opcode.args.length !== otherOpcode.args.length) { return false; } - - for (var i = 0; i < len; i++) { - var opcode = this.opcodes[i], - otherOpcode = other.opcodes[i]; - if (opcode.opcode !== otherOpcode.opcode || opcode.args.length !== otherOpcode.args.length) { + for (var j = 0; j < opcode.args.length; j++) { + if (opcode.args[j] !== otherOpcode.args[j]) { return false; } - for (var j = 0; j < opcode.args.length; j++) { - if (opcode.args[j] !== otherOpcode.args[j]) { - return false; - } - } } - return true; - }, + } - guid: 0, - - compile: function(program, options) { - this.children = []; - this.depths = {list: []}; - this.options = options; - - // These changes will propagate to the other compiler components - var knownHelpers = this.options.knownHelpers; - this.options.knownHelpers = { - 'helperMissing': true, - 'blockHelperMissing': true, - 'each': true, - 'if': true, - 'unless': true, - 'with': true, - 'log': true - }; - if (knownHelpers) { - for (var name in knownHelpers) { - this.options.knownHelpers[name] = knownHelpers[name]; - } + len = this.children.length; + if (other.children.length !== len) { + return false; + } + for (i = 0; i < len; i++) { + if (!this.children[i].equals(other.children[i])) { + return false; } + } - return this.program(program); - }, + return true; + }, - accept: function(node) { - return this[node.type](node); - }, + guid: 0, + + compile: function(program, options) { + this.children = []; + this.depths = {list: []}; + this.options = options; + + // These changes will propagate to the other compiler components + var knownHelpers = this.options.knownHelpers; + this.options.knownHelpers = { + 'helperMissing': true, + 'blockHelperMissing': true, + 'each': true, + 'if': true, + 'unless': true, + 'with': true, + 'log': true + }; + if (knownHelpers) { + for (var name in knownHelpers) { + this.options.knownHelpers[name] = knownHelpers[name]; + } + } - program: function(program) { - var statements = program.statements, statement; - this.opcodes = []; + return this.program(program); + }, - for(var i=0, l=statements.length; i<l; i++) { - statement = statements[i]; - this[statement.type](statement); - } - this.isSimple = l === 1; + accept: function(node) { + return this[node.type](node); + }, - this.depths.list = this.depths.list.sort(function(a, b) { - return a - b; - }); + program: function(program) { + var statements = program.statements, statement; + this.opcodes = []; - return this; - }, + for(var i=0, l=statements.length; i<l; i++) { + statement = statements[i]; + this[statement.type](statement); + } + this.isSimple = l === 1; - compileProgram: function(program) { - var result = new this.compiler().compile(program, this.options); - var guid = this.guid++, depth; + this.depths.list = this.depths.list.sort(function(a, b) { + return a - b; + }); - this.usePartial = this.usePartial || result.usePartial; + return this; + }, - this.children[guid] = result; + compileProgram: function(program) { + var result = new this.compiler().compile(program, this.options); + var guid = this.guid++, depth; - for(var i=0, l=result.depths.list.length; i<l; i++) { - depth = result.depths.list[i]; + this.usePartial = this.usePartial || result.usePartial; - if(depth < 2) { continue; } - else { this.addDepth(depth - 1); } - } + this.children[guid] = result; - return guid; - }, + for(var i=0, l=result.depths.list.length; i<l; i++) { + depth = result.depths.list[i]; - block: function(block) { - var mustache = block.mustache, - program = block.program, - inverse = block.inverse; + if(depth < 2) { continue; } + else { this.addDepth(depth - 1); } + } - if (program) { - program = this.compileProgram(program); - } + return guid; + }, - if (inverse) { - inverse = this.compileProgram(inverse); - } + block: function(block) { + var mustache = block.mustache, + program = block.program, + inverse = block.inverse; - var type = this.classifyMustache(mustache); + if (program) { + program = this.compileProgram(program); + } - if (type === "helper") { - this.helperMustache(mustache, program, inverse); - } else if (type === "simple") { - this.simpleMustache(mustache); + if (inverse) { + inverse = this.compileProgram(inverse); + } - // now that the simple mustache is resolved, we need to - // evaluate it by executing `blockHelperMissing` - this.opcode('pushProgram', program); - this.opcode('pushProgram', inverse); - this.opcode('emptyHash'); - this.opcode('blockValue'); - } else { - this.ambiguousMustache(mustache, program, inverse); - - // now that the simple mustache is resolved, we need to - // evaluate it by executing `blockHelperMissing` - this.opcode('pushProgram', program); - this.opcode('pushProgram', inverse); - this.opcode('emptyHash'); - this.opcode('ambiguousBlockValue'); - } + var type = this.classifyMustache(mustache); - this.opcode('append'); - }, + if (type === "helper") { + this.helperMustache(mustache, program, inverse); + } else if (type === "simple") { + this.simpleMustache(mustache); - hash: function(hash) { - var pairs = hash.pairs, pair, val; + // now that the simple mustache is resolved, we need to + // evaluate it by executing `blockHelperMissing` + this.opcode('pushProgram', program); + this.opcode('pushProgram', inverse); + this.opcode('emptyHash'); + this.opcode('blockValue'); + } else { + this.ambiguousMustache(mustache, program, inverse); - this.opcode('pushHash'); + // now that the simple mustache is resolved, we need to + // evaluate it by executing `blockHelperMissing` + this.opcode('pushProgram', program); + this.opcode('pushProgram', inverse); + this.opcode('emptyHash'); + this.opcode('ambiguousBlockValue'); + } - for(var i=0, l=pairs.length; i<l; i++) { - pair = pairs[i]; - val = pair[1]; + this.opcode('append'); + }, - if (this.options.stringParams) { - this.opcode('pushStringParam', val.stringModeValue, val.type); - } else { - this.accept(val); - } + hash: function(hash) { + var pairs = hash.pairs, pair, val; - this.opcode('assignToHash', pair[0]); - } - this.opcode('popHash'); - }, + this.opcode('pushHash'); - partial: function(partial) { - var partialName = partial.partialName; - this.usePartial = true; + for(var i=0, l=pairs.length; i<l; i++) { + pair = pairs[i]; + val = pair[1]; - if(partial.context) { - this.ID(partial.context); + if (this.options.stringParams) { + if(val.depth) { + this.addDepth(val.depth); + } + this.opcode('getContext', val.depth || 0); + this.opcode('pushStringParam', val.stringModeValue, val.type); } else { - this.opcode('push', 'depth0'); + this.accept(val); } - this.opcode('invokePartial', partialName.name); - this.opcode('append'); - }, - - content: function(content) { - this.opcode('appendContent', content.string); - }, + this.opcode('assignToHash', pair[0]); + } + this.opcode('popHash'); + }, - mustache: function(mustache) { - var options = this.options; - var type = this.classifyMustache(mustache); + partial: function(partial) { + var partialName = partial.partialName; + this.usePartial = true; - if (type === "simple") { - this.simpleMustache(mustache); - } else if (type === "helper") { - this.helperMustache(mustache); - } else { - this.ambiguousMustache(mustache); - } + if(partial.context) { + this.ID(partial.context); + } else { + this.opcode('push', 'depth0'); + } - if(mustache.escaped && !options.noEscape) { - this.opcode('appendEscaped'); - } else { - this.opcode('append'); - } - }, + this.opcode('invokePartial', partialName.name); + this.opcode('append'); + }, - ambiguousMustache: function(mustache, program, inverse) { - var id = mustache.id, - name = id.parts[0], - isBlock = program != null || inverse != null; + content: function(content) { + this.opcode('appendContent', content.string); + }, - this.opcode('getContext', id.depth); + mustache: function(mustache) { + var options = this.options; + var type = this.classifyMustache(mustache); - this.opcode('pushProgram', program); - this.opcode('pushProgram', inverse); + if (type === "simple") { + this.simpleMustache(mustache); + } else if (type === "helper") { + this.helperMustache(mustache); + } else { + this.ambiguousMustache(mustache); + } - this.opcode('invokeAmbiguous', name, isBlock); - }, + if(mustache.escaped && !options.noEscape) { + this.opcode('appendEscaped'); + } else { + this.opcode('append'); + } + }, - simpleMustache: function(mustache) { - var id = mustache.id; + ambiguousMustache: function(mustache, program, inverse) { + var id = mustache.id, + name = id.parts[0], + isBlock = program != null || inverse != null; - if (id.type === 'DATA') { - this.DATA(id); - } else if (id.parts.length) { - this.ID(id); - } else { - // Simplified ID for `this` - this.addDepth(id.depth); - this.opcode('getContext', id.depth); - this.opcode('pushContext'); - } + this.opcode('getContext', id.depth); - this.opcode('resolvePossibleLambda'); - }, + this.opcode('pushProgram', program); + this.opcode('pushProgram', inverse); - helperMustache: function(mustache, program, inverse) { - var params = this.setupFullMustacheParams(mustache, program, inverse), - name = mustache.id.parts[0]; + this.opcode('invokeAmbiguous', name, isBlock); + }, - if (this.options.knownHelpers[name]) { - this.opcode('invokeKnownHelper', params.length, name); - } else if (this.knownHelpersOnly) { - throw new Error("You specified knownHelpersOnly, but used the unknown helper " + name); - } else { - this.opcode('invokeHelper', params.length, name); - } - }, + simpleMustache: function(mustache) { + var id = mustache.id; - ID: function(id) { + if (id.type === 'DATA') { + this.DATA(id); + } else if (id.parts.length) { + this.ID(id); + } else { + // Simplified ID for `this` this.addDepth(id.depth); this.opcode('getContext', id.depth); + this.opcode('pushContext'); + } - var name = id.parts[0]; - if (!name) { - this.opcode('pushContext'); - } else { - this.opcode('lookupOnContext', id.parts[0]); - } - - for(var i=1, l=id.parts.length; i<l; i++) { - this.opcode('lookup', id.parts[i]); - } - }, + this.opcode('resolvePossibleLambda'); + }, - DATA: function(data) { - this.options.data = true; - this.opcode('lookupData', data.id); - }, + helperMustache: function(mustache, program, inverse) { + var params = this.setupFullMustacheParams(mustache, program, inverse), + name = mustache.id.parts[0]; - STRING: function(string) { - this.opcode('pushString', string.string); - }, + if (this.options.knownHelpers[name]) { + this.opcode('invokeKnownHelper', params.length, name); + } else if (this.options.knownHelpersOnly) { + throw new Error("You specified knownHelpersOnly, but used the unknown helper " + name); + } else { + this.opcode('invokeHelper', params.length, name); + } + }, - INTEGER: function(integer) { - this.opcode('pushLiteral', integer.integer); - }, + ID: function(id) { + this.addDepth(id.depth); + this.opcode('getContext', id.depth); - BOOLEAN: function(bool) { - this.opcode('pushLiteral', bool.bool); - }, + var name = id.parts[0]; + if (!name) { + this.opcode('pushContext'); + } else { + this.opcode('lookupOnContext', id.parts[0]); + } - comment: function() {}, + for(var i=1, l=id.parts.length; i<l; i++) { + this.opcode('lookup', id.parts[i]); + } + }, - // HELPERS - opcode: function(name) { - this.opcodes.push({ opcode: name, args: [].slice.call(arguments, 1) }); - }, + DATA: function(data) { + this.options.data = true; + this.opcode('lookupData', data.id); + }, - declare: function(name, value) { - this.opcodes.push({ opcode: 'DECLARE', name: name, value: value }); - }, + STRING: function(string) { + this.opcode('pushString', string.string); + }, - addDepth: function(depth) { - if(isNaN(depth)) { throw new Error("EWOT"); } - if(depth === 0) { return; } + INTEGER: function(integer) { + this.opcode('pushLiteral', integer.integer); + }, - if(!this.depths[depth]) { - this.depths[depth] = true; - this.depths.list.push(depth); - } - }, + BOOLEAN: function(bool) { + this.opcode('pushLiteral', bool.bool); + }, - classifyMustache: function(mustache) { - var isHelper = mustache.isHelper; - var isEligible = mustache.eligibleHelper; - var options = this.options; + comment: function() {}, - // if ambiguous, we can possibly resolve the ambiguity now - if (isEligible && !isHelper) { - var name = mustache.id.parts[0]; + // HELPERS + opcode: function(name) { + this.opcodes.push({ opcode: name, args: [].slice.call(arguments, 1) }); + }, - if (options.knownHelpers[name]) { - isHelper = true; - } else if (options.knownHelpersOnly) { - isEligible = false; - } - } + declare: function(name, value) { + this.opcodes.push({ opcode: 'DECLARE', name: name, value: value }); + }, - if (isHelper) { return "helper"; } - else if (isEligible) { return "ambiguous"; } - else { return "simple"; } - }, + addDepth: function(depth) { + if(isNaN(depth)) { throw new Error("EWOT"); } + if(depth === 0) { return; } - pushParams: function(params) { - var i = params.length, param; + if(!this.depths[depth]) { + this.depths[depth] = true; + this.depths.list.push(depth); + } + }, - while(i--) { - param = params[i]; + classifyMustache: function(mustache) { + var isHelper = mustache.isHelper; + var isEligible = mustache.eligibleHelper; + var options = this.options; - if(this.options.stringParams) { - if(param.depth) { - this.addDepth(param.depth); - } + // if ambiguous, we can possibly resolve the ambiguity now + if (isEligible && !isHelper) { + var name = mustache.id.parts[0]; - this.opcode('getContext', param.depth || 0); - this.opcode('pushStringParam', param.stringModeValue, param.type); - } else { - this[param.type](param); - } + if (options.knownHelpers[name]) { + isHelper = true; + } else if (options.knownHelpersOnly) { + isEligible = false; } - }, - - setupMustacheParams: function(mustache) { - var params = mustache.params; - this.pushParams(params); + } - if(mustache.hash) { - this.hash(mustache.hash); - } else { - this.opcode('emptyHash'); - } + if (isHelper) { return "helper"; } + else if (isEligible) { return "ambiguous"; } + else { return "simple"; } + }, - return params; - }, + pushParams: function(params) { + var i = params.length, param; - // this will replace setupMustacheParams when we're done - setupFullMustacheParams: function(mustache, program, inverse) { - var params = mustache.params; - this.pushParams(params); + while(i--) { + param = params[i]; - this.opcode('pushProgram', program); - this.opcode('pushProgram', inverse); + if(this.options.stringParams) { + if(param.depth) { + this.addDepth(param.depth); + } - if(mustache.hash) { - this.hash(mustache.hash); + this.opcode('getContext', param.depth || 0); + this.opcode('pushStringParam', param.stringModeValue, param.type); } else { - this.opcode('emptyHash'); + this[param.type](param); } + } + }, + + setupMustacheParams: function(mustache) { + var params = mustache.params; + this.pushParams(params); - return params; + if(mustache.hash) { + this.hash(mustache.hash); + } else { + this.opcode('emptyHash'); } - }; - var Literal = function(value) { - this.value = value; - }; + return params; + }, - JavaScriptCompiler.prototype = { - // PUBLIC API: You can override these methods in a subclass to provide - // alternative compiled forms for name lookup and buffering semantics - nameLookup: function(parent, name /* , type*/) { - if (/^[0-9]+$/.test(name)) { - return parent + "[" + name + "]"; - } else if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) { - return parent + "." + name; - } - else { - return parent + "['" + name + "']"; - } - }, + // this will replace setupMustacheParams when we're done + setupFullMustacheParams: function(mustache, program, inverse) { + var params = mustache.params; + this.pushParams(params); - appendToBuffer: function(string) { - if (this.environment.isSimple) { - return "return " + string + ";"; - } else { - return { - appendToBuffer: true, - content: string, - toString: function() { return "buffer += " + string + ";"; } - }; - } - }, + this.opcode('pushProgram', program); + this.opcode('pushProgram', inverse); - initializeBuffer: function() { - return this.quotedString(""); - }, + if(mustache.hash) { + this.hash(mustache.hash); + } else { + this.opcode('emptyHash'); + } - namespace: "Handlebars", - // END PUBLIC API + return params; + } +}; - compile: function(environment, options, context, asObject) { - this.environment = environment; - this.options = options || {}; +var Literal = function(value) { + this.value = value; +}; - Handlebars.log(Handlebars.logger.DEBUG, this.environment.disassemble() + "\n\n"); +JavaScriptCompiler.prototype = { + // PUBLIC API: You can override these methods in a subclass to provide + // alternative compiled forms for name lookup and buffering semantics + nameLookup: function(parent, name /* , type*/) { + if (/^[0-9]+$/.test(name)) { + return parent + "[" + name + "]"; + } else if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) { + return parent + "." + name; + } + else { + return parent + "['" + name + "']"; + } + }, - this.name = this.environment.name; - this.isChild = !!context; - this.context = context || { - programs: [], - environments: [], - aliases: { } + appendToBuffer: function(string) { + if (this.environment.isSimple) { + return "return " + string + ";"; + } else { + return { + appendToBuffer: true, + content: string, + toString: function() { return "buffer += " + string + ";"; } }; + } + }, - this.preamble(); + initializeBuffer: function() { + return this.quotedString(""); + }, - this.stackSlot = 0; - this.stackVars = []; - this.registers = { list: [] }; - this.compileStack = []; - this.inlineStack = []; + namespace: "Handlebars", + // END PUBLIC API - this.compileChildren(environment, options); + compile: function(environment, options, context, asObject) { + this.environment = environment; + this.options = options || {}; - var opcodes = environment.opcodes, opcode; + Handlebars.log(Handlebars.logger.DEBUG, this.environment.disassemble() + "\n\n"); - this.i = 0; + this.name = this.environment.name; + this.isChild = !!context; + this.context = context || { + programs: [], + environments: [], + aliases: { } + }; - for(l=opcodes.length; this.i<l; this.i++) { - opcode = opcodes[this.i]; + this.preamble(); - if(opcode.opcode === 'DECLARE') { - this[opcode.name] = opcode.value; - } else { - this[opcode.opcode].apply(this, opcode.args); - } - } + this.stackSlot = 0; + this.stackVars = []; + this.registers = { list: [] }; + this.compileStack = []; + this.inlineStack = []; - return this.createFunctionContext(asObject); - }, + this.compileChildren(environment, options); - nextOpcode: function() { - var opcodes = this.environment.opcodes; - return opcodes[this.i + 1]; - }, + var opcodes = environment.opcodes, opcode; - eat: function() { - this.i = this.i + 1; - }, + this.i = 0; - preamble: function() { - var out = []; + for(l=opcodes.length; this.i<l; this.i++) { + opcode = opcodes[this.i]; - if (!this.isChild) { - var namespace = this.namespace; - var copies = "helpers = helpers || " + namespace + ".helpers;"; - if (this.environment.usePartial) { copies = copies + " partials = partials || " + namespace + ".partials;"; } - if (this.options.data) { copies = copies + " data = data || {};"; } - out.push(copies); + if(opcode.opcode === 'DECLARE') { + this[opcode.name] = opcode.value; } else { - out.push(''); + this[opcode.opcode].apply(this, opcode.args); } + } - if (!this.environment.isSimple) { - out.push(", buffer = " + this.initializeBuffer()); - } else { - out.push(""); - } + return this.createFunctionContext(asObject); + }, - // track the last context pushed into place to allow skipping the - // getContext opcode when it would be a noop - this.lastContext = 0; - this.source = out; - }, + nextOpcode: function() { + var opcodes = this.environment.opcodes; + return opcodes[this.i + 1]; + }, - createFunctionContext: function(asObject) { - var locals = this.stackVars.concat(this.registers.list); + eat: function() { + this.i = this.i + 1; + }, - if(locals.length > 0) { - this.source[1] = this.source[1] + ", " + locals.join(", "); - } + preamble: function() { + var out = []; - // 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.isChild) { + var namespace = this.namespace; + var copies = "helpers = helpers || " + namespace + ".helpers;"; + if (this.environment.usePartial) { copies = copies + " partials = partials || " + namespace + ".partials;"; } + if (this.options.data) { copies = copies + " data = data || {};"; } + out.push(copies); + } else { + out.push(''); + } - if (this.source[1]) { - this.source[1] = "var " + this.source[1].substring(2) + ";"; - } + if (!this.environment.isSimple) { + out.push(", buffer = " + this.initializeBuffer()); + } else { + out.push(""); + } - // Merge children - if (!this.isChild) { - this.source[1] += '\n' + this.context.programs.join('\n') + '\n'; - } + // track the last context pushed into place to allow skipping the + // getContext opcode when it would be a noop + this.lastContext = 0; + this.source = out; + }, - if (!this.environment.isSimple) { - this.source.push("return buffer;"); - } + createFunctionContext: function(asObject) { + var locals = this.stackVars.concat(this.registers.list); - var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"]; + if(locals.length > 0) { + this.source[1] = this.source[1] + ", " + locals.join(", "); + } - for(var i=0, l=this.environment.depths.list.length; i<l; i++) { - params.push("depth" + this.environment.depths.list[i]); + // 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]; } + } - // Perform a second pass over the output to merge content when possible - var source = this.mergeSource(); + if (this.source[1]) { + this.source[1] = "var " + this.source[1].substring(2) + ";"; + } - if (!this.isChild) { - var revision = Handlebars.COMPILER_REVISION, - versions = Handlebars.REVISION_CHANGES[revision]; - source = "this.compilerInfo = ["+revision+",'"+versions+"'];\n"+source; - } + // Merge children + if (!this.isChild) { + this.source[1] += '\n' + this.context.programs.join('\n') + '\n'; + } - if (asObject) { - params.push(source); + if (!this.environment.isSimple) { + this.source.push("return buffer;"); + } - return Function.apply(this, params); - } else { - var functionSource = 'function ' + (this.name || '') + '(' + params.join(',') + ') {\n ' + source + '}'; - Handlebars.log(Handlebars.logger.DEBUG, functionSource + "\n\n"); - return functionSource; - } - }, - mergeSource: function() { - // WARN: We are not handling the case where buffer is still populated as the source should - // not have buffer append operations as their final action. - var source = '', - buffer; - for (var i = 0, len = this.source.length; i < len; i++) { - var line = this.source[i]; - if (line.appendToBuffer) { - if (buffer) { - buffer = buffer + '\n + ' + line.content; - } else { - buffer = line.content; - } + var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"]; + + for(var i=0, l=this.environment.depths.list.length; i<l; i++) { + params.push("depth" + this.environment.depths.list[i]); + } + + // Perform a second pass over the output to merge content when possible + var source = this.mergeSource(); + + if (!this.isChild) { + var revision = Handlebars.COMPILER_REVISION, + versions = Handlebars.REVISION_CHANGES[revision]; + source = "this.compilerInfo = ["+revision+",'"+versions+"'];\n"+source; + } + + if (asObject) { + params.push(source); + + return Function.apply(this, params); + } else { + var functionSource = 'function ' + (this.name || '') + '(' + params.join(',') + ') {\n ' + source + '}'; + Handlebars.log(Handlebars.logger.DEBUG, functionSource + "\n\n"); + return functionSource; + } + }, + mergeSource: function() { + // WARN: We are not handling the case where buffer is still populated as the source should + // not have buffer append operations as their final action. + var source = '', + buffer; + for (var i = 0, len = this.source.length; i < len; i++) { + var line = this.source[i]; + if (line.appendToBuffer) { + if (buffer) { + buffer = buffer + '\n + ' + line.content; } else { - if (buffer) { - source += 'buffer += ' + buffer + ';\n '; - buffer = undefined; - } - source += line + '\n '; + buffer = line.content; + } + } else { + if (buffer) { + source += 'buffer += ' + buffer + ';\n '; + buffer = undefined; } + source += line + '\n '; } - return source; - }, - - // [blockValue] - // - // On stack, before: hash, inverse, program, value - // On stack, after: return value of blockHelperMissing - // - // The purpose of this opcode is to take a block of the form - // `{{#foo}}...{{/foo}}`, resolve the value of `foo`, and - // replace it on the stack with the result of properly - // invoking blockHelperMissing. - blockValue: function() { - this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing'; - - var params = ["depth0"]; - this.setupParams(0, params); - - this.replaceStack(function(current) { - params.splice(1, 0, current); - return "blockHelperMissing.call(" + params.join(", ") + ")"; - }); - }, + } + return source; + }, - // [ambiguousBlockValue] - // - // On stack, before: hash, inverse, program, value - // Compiler value, before: lastHelper=value of last found helper, if any - // On stack, after, if no lastHelper: same as [blockValue] - // On stack, after, if lastHelper: value - ambiguousBlockValue: function() { - this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing'; + // [blockValue] + // + // On stack, before: hash, inverse, program, value + // On stack, after: return value of blockHelperMissing + // + // The purpose of this opcode is to take a block of the form + // `{{#foo}}...{{/foo}}`, resolve the value of `foo`, and + // replace it on the stack with the result of properly + // invoking blockHelperMissing. + blockValue: function() { + this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing'; + + var params = ["depth0"]; + this.setupParams(0, params); + + this.replaceStack(function(current) { + params.splice(1, 0, current); + return "blockHelperMissing.call(" + params.join(", ") + ")"; + }); + }, - var params = ["depth0"]; - this.setupParams(0, params); + // [ambiguousBlockValue] + // + // On stack, before: hash, inverse, program, value + // Compiler value, before: lastHelper=value of last found helper, if any + // On stack, after, if no lastHelper: same as [blockValue] + // On stack, after, if lastHelper: value + ambiguousBlockValue: function() { + this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing'; - var current = this.topStack(); - params.splice(1, 0, current); + var params = ["depth0"]; + this.setupParams(0, params); - // Use the options value generated from the invocation - params[params.length-1] = 'options'; + var current = this.topStack(); + params.splice(1, 0, current); - this.source.push("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }"); - }, + // Use the options value generated from the invocation + params[params.length-1] = 'options'; - // [appendContent] - // - // On stack, before: ... - // On stack, after: ... - // - // Appends the string value of `content` to the current buffer - appendContent: function(content) { - this.source.push(this.appendToBuffer(this.quotedString(content))); - }, + this.source.push("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }"); + }, - // [append] - // - // On stack, before: value, ... - // On stack, after: ... - // - // Coerces `value` to a String and appends it to the current buffer. - // - // If `value` is truthy, or 0, it is coerced into a string and appended - // Otherwise, the empty string is appended - append: function() { - // Force anything that is inlined onto the stack so we don't have duplication - // when we examine local - this.flushInline(); - var local = this.popStack(); - this.source.push("if(" + local + " || " + local + " === 0) { " + this.appendToBuffer(local) + " }"); - if (this.environment.isSimple) { - this.source.push("else { " + this.appendToBuffer("''") + " }"); - } - }, + // [appendContent] + // + // On stack, before: ... + // On stack, after: ... + // + // Appends the string value of `content` to the current buffer + appendContent: function(content) { + this.source.push(this.appendToBuffer(this.quotedString(content))); + }, - // [appendEscaped] - // - // On stack, before: value, ... - // On stack, after: ... - // - // Escape `value` and append it to the buffer - appendEscaped: function() { - this.context.aliases.escapeExpression = 'this.escapeExpression'; + // [append] + // + // On stack, before: value, ... + // On stack, after: ... + // + // Coerces `value` to a String and appends it to the current buffer. + // + // If `value` is truthy, or 0, it is coerced into a string and appended + // Otherwise, the empty string is appended + append: function() { + // Force anything that is inlined onto the stack so we don't have duplication + // when we examine local + this.flushInline(); + var local = this.popStack(); + this.source.push("if(" + local + " || " + local + " === 0) { " + this.appendToBuffer(local) + " }"); + if (this.environment.isSimple) { + this.source.push("else { " + this.appendToBuffer("''") + " }"); + } + }, - this.source.push(this.appendToBuffer("escapeExpression(" + this.popStack() + ")")); - }, + // [appendEscaped] + // + // On stack, before: value, ... + // On stack, after: ... + // + // Escape `value` and append it to the buffer + appendEscaped: function() { + this.context.aliases.escapeExpression = 'this.escapeExpression'; - // [getContext] - // - // On stack, before: ... - // On stack, after: ... - // Compiler value, after: lastContext=depth - // - // Set the value of the `lastContext` compiler value to the depth - getContext: function(depth) { - if(this.lastContext !== depth) { - this.lastContext = depth; - } - }, + this.source.push(this.appendToBuffer("escapeExpression(" + this.popStack() + ")")); + }, - // [lookupOnContext] - // - // On stack, before: ... - // On stack, after: currentContext[name], ... - // - // Looks up the value of `name` on the current context and pushes - // it onto the stack. - lookupOnContext: function(name) { - this.push(this.nameLookup('depth' + this.lastContext, name, 'context')); - }, + // [getContext] + // + // On stack, before: ... + // On stack, after: ... + // Compiler value, after: lastContext=depth + // + // Set the value of the `lastContext` compiler value to the depth + getContext: function(depth) { + if(this.lastContext !== depth) { + this.lastContext = depth; + } + }, - // [pushContext] - // - // On stack, before: ... - // On stack, after: currentContext, ... - // - // Pushes the value of the current context onto the stack. - pushContext: function() { - this.pushStackLiteral('depth' + this.lastContext); - }, + // [lookupOnContext] + // + // On stack, before: ... + // On stack, after: currentContext[name], ... + // + // Looks up the value of `name` on the current context and pushes + // it onto the stack. + lookupOnContext: function(name) { + this.push(this.nameLookup('depth' + this.lastContext, name, 'context')); + }, - // [resolvePossibleLambda] - // - // On stack, before: value, ... - // On stack, after: resolved value, ... - // - // If the `value` is a lambda, replace it on the stack by - // the return value of the lambda - resolvePossibleLambda: function() { - this.context.aliases.functionType = '"function"'; - - this.replaceStack(function(current) { - return "typeof " + current + " === functionType ? " + current + ".apply(depth0) : " + current; - }); - }, + // [pushContext] + // + // On stack, before: ... + // On stack, after: currentContext, ... + // + // Pushes the value of the current context onto the stack. + pushContext: function() { + this.pushStackLiteral('depth' + this.lastContext); + }, - // [lookup] - // - // On stack, before: value, ... - // On stack, after: value[name], ... - // - // Replace the value on the stack with the result of looking - // up `name` on `value` - lookup: function(name) { - this.replaceStack(function(current) { - return current + " == null || " + current + " === false ? " + current + " : " + this.nameLookup(current, name, 'context'); - }); - }, + // [resolvePossibleLambda] + // + // On stack, before: value, ... + // On stack, after: resolved value, ... + // + // If the `value` is a lambda, replace it on the stack by + // the return value of the lambda + resolvePossibleLambda: function() { + this.context.aliases.functionType = '"function"'; + + this.replaceStack(function(current) { + return "typeof " + current + " === functionType ? " + current + ".apply(depth0) : " + current; + }); + }, - // [lookupData] - // - // On stack, before: ... - // On stack, after: data[id], ... - // - // Push the result of looking up `id` on the current data - lookupData: function(id) { - this.push(this.nameLookup('data', id, 'data')); - }, + // [lookup] + // + // On stack, before: value, ... + // On stack, after: value[name], ... + // + // Replace the value on the stack with the result of looking + // up `name` on `value` + lookup: function(name) { + this.replaceStack(function(current) { + return current + " == null || " + current + " === false ? " + current + " : " + this.nameLookup(current, name, 'context'); + }); + }, - // [pushStringParam] - // - // On stack, before: ... - // On stack, after: string, currentContext, ... - // - // This opcode is designed for use in string mode, which - // provides the string value of a parameter along with its - // depth rather than resolving it immediately. - pushStringParam: function(string, type) { - this.pushStackLiteral('depth' + this.lastContext); - - this.pushString(type); - - if (typeof string === 'string') { - this.pushString(string); - } else { - this.pushStackLiteral(string); - } - }, + // [lookupData] + // + // On stack, before: ... + // On stack, after: data[id], ... + // + // Push the result of looking up `id` on the current data + lookupData: function(id) { + this.push(this.nameLookup('data', id, 'data')); + }, - emptyHash: function() { - this.pushStackLiteral('{}'); + // [pushStringParam] + // + // On stack, before: ... + // On stack, after: string, currentContext, ... + // + // This opcode is designed for use in string mode, which + // provides the string value of a parameter along with its + // depth rather than resolving it immediately. + pushStringParam: function(string, type) { + this.pushStackLiteral('depth' + this.lastContext); + + this.pushString(type); + + if (typeof string === 'string') { + this.pushString(string); + } else { + this.pushStackLiteral(string); + } + }, - if (this.options.stringParams) { - this.register('hashTypes', '{}'); - } - }, - pushHash: function() { - this.hash = {values: [], types: []}; - }, - popHash: function() { - var hash = this.hash; - this.hash = undefined; + emptyHash: function() { + this.pushStackLiteral('{}'); - if (this.options.stringParams) { - this.register('hashTypes', '{' + hash.types.join(',') + '}'); - } - this.push('{\n ' + hash.values.join(',\n ') + '\n }'); - }, + if (this.options.stringParams) { + this.register('hashTypes', '{}'); + this.register('hashContexts', '{}'); + } + }, + pushHash: function() { + this.hash = {values: [], types: [], contexts: []}; + }, + popHash: function() { + var hash = this.hash; + this.hash = undefined; - // [pushString] - // - // On stack, before: ... - // On stack, after: quotedString(string), ... - // - // Push a quoted version of `string` onto the stack - pushString: function(string) { - this.pushStackLiteral(this.quotedString(string)); - }, + if (this.options.stringParams) { + this.register('hashContexts', '{' + hash.contexts.join(',') + '}'); + this.register('hashTypes', '{' + hash.types.join(',') + '}'); + } + this.push('{\n ' + hash.values.join(',\n ') + '\n }'); + }, - // [push] - // - // On stack, before: ... - // On stack, after: expr, ... - // - // Push an expression onto the stack - push: function(expr) { - this.inlineStack.push(expr); - return expr; - }, + // [pushString] + // + // On stack, before: ... + // On stack, after: quotedString(string), ... + // + // Push a quoted version of `string` onto the stack + pushString: function(string) { + this.pushStackLiteral(this.quotedString(string)); + }, - // [pushLiteral] - // - // On stack, before: ... - // On stack, after: value, ... - // - // Pushes a value onto the stack. This operation prevents - // the compiler from creating a temporary variable to hold - // it. - pushLiteral: function(value) { - this.pushStackLiteral(value); - }, + // [push] + // + // On stack, before: ... + // On stack, after: expr, ... + // + // Push an expression onto the stack + push: function(expr) { + this.inlineStack.push(expr); + return expr; + }, - // [pushProgram] - // - // On stack, before: ... - // On stack, after: program(guid), ... - // - // Push a program expression onto the stack. This takes - // a compile-time guid and converts it into a runtime-accessible - // expression. - pushProgram: function(guid) { - if (guid != null) { - this.pushStackLiteral(this.programExpression(guid)); - } else { - this.pushStackLiteral(null); - } - }, + // [pushLiteral] + // + // On stack, before: ... + // On stack, after: value, ... + // + // Pushes a value onto the stack. This operation prevents + // the compiler from creating a temporary variable to hold + // it. + pushLiteral: function(value) { + this.pushStackLiteral(value); + }, - // [invokeHelper] - // - // On stack, before: hash, inverse, program, params..., ... - // On stack, after: result of helper invocation - // - // Pops off the helper's parameters, invokes the helper, - // and pushes the helper's return value onto the stack. - // - // If the helper is not found, `helperMissing` is called. - invokeHelper: function(paramSize, name) { - this.context.aliases.helperMissing = 'helpers.helperMissing'; - - var helper = this.lastHelper = this.setupHelper(paramSize, name, true); - - this.push(helper.name); - this.replaceStack(function(name) { - return name + ' ? ' + name + '.call(' + - helper.callParams + ") " + ": helperMissing.call(" + - helper.helperMissingParams + ")"; - }); - }, + // [pushProgram] + // + // On stack, before: ... + // On stack, after: program(guid), ... + // + // Push a program expression onto the stack. This takes + // a compile-time guid and converts it into a runtime-accessible + // expression. + pushProgram: function(guid) { + if (guid != null) { + this.pushStackLiteral(this.programExpression(guid)); + } else { + this.pushStackLiteral(null); + } + }, - // [invokeKnownHelper] - // - // On stack, before: hash, inverse, program, params..., ... - // On stack, after: result of helper invocation - // - // This operation is used when the helper is known to exist, - // so a `helperMissing` fallback is not required. - invokeKnownHelper: function(paramSize, name) { - var helper = this.setupHelper(paramSize, name); - this.push(helper.name + ".call(" + helper.callParams + ")"); - }, + // [invokeHelper] + // + // On stack, before: hash, inverse, program, params..., ... + // On stack, after: result of helper invocation + // + // Pops off the helper's parameters, invokes the helper, + // and pushes the helper's return value onto the stack. + // + // If the helper is not found, `helperMissing` is called. + invokeHelper: function(paramSize, name) { + this.context.aliases.helperMissing = 'helpers.helperMissing'; + + var helper = this.lastHelper = this.setupHelper(paramSize, name, true); + + this.push(helper.name); + this.replaceStack(function(name) { + return name + ' ? ' + name + '.call(' + + helper.callParams + ") " + ": helperMissing.call(" + + helper.helperMissingParams + ")"; + }); + }, - // [invokeAmbiguous] - // - // On stack, before: hash, inverse, program, params..., ... - // On stack, after: result of disambiguation - // - // This operation is used when an expression like `{{foo}}` - // is provided, but we don't know at compile-time whether it - // is a helper or a path. - // - // This operation emits more code than the other options, - // and can be avoided by passing the `knownHelpers` and - // `knownHelpersOnly` flags at compile-time. - invokeAmbiguous: function(name, helperCall) { - this.context.aliases.functionType = '"function"'; - - this.pushStackLiteral('{}'); // Hash value - var helper = this.setupHelper(0, name, helperCall); - - var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper'); - - var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context'); - var nextStack = this.nextStack(); - - this.source.push('if (' + nextStack + ' = ' + helperName + ') { ' + nextStack + ' = ' + nextStack + '.call(' + helper.callParams + '); }'); - this.source.push('else { ' + nextStack + ' = ' + nonHelper + '; ' + nextStack + ' = typeof ' + nextStack + ' === functionType ? ' + nextStack + '.apply(depth0) : ' + nextStack + '; }'); - }, + // [invokeKnownHelper] + // + // On stack, before: hash, inverse, program, params..., ... + // On stack, after: result of helper invocation + // + // This operation is used when the helper is known to exist, + // so a `helperMissing` fallback is not required. + invokeKnownHelper: function(paramSize, name) { + var helper = this.setupHelper(paramSize, name); + this.push(helper.name + ".call(" + helper.callParams + ")"); + }, - // [invokePartial] - // - // On stack, before: context, ... - // On stack after: result of partial invocation - // - // This operation pops off a context, invokes a partial with that context, - // and pushes the result of the invocation back. - invokePartial: function(name) { - var params = [this.nameLookup('partials', name, 'partial'), "'" + name + "'", this.popStack(), "helpers", "partials"]; - - if (this.options.data) { - params.push("data"); - } + // [invokeAmbiguous] + // + // On stack, before: hash, inverse, program, params..., ... + // On stack, after: result of disambiguation + // + // This operation is used when an expression like `{{foo}}` + // is provided, but we don't know at compile-time whether it + // is a helper or a path. + // + // This operation emits more code than the other options, + // and can be avoided by passing the `knownHelpers` and + // `knownHelpersOnly` flags at compile-time. + invokeAmbiguous: function(name, helperCall) { + this.context.aliases.functionType = '"function"'; + + this.pushStackLiteral('{}'); // Hash value + var helper = this.setupHelper(0, name, helperCall); + + var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper'); + + var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context'); + var nextStack = this.nextStack(); + + this.source.push('if (' + nextStack + ' = ' + helperName + ') { ' + nextStack + ' = ' + nextStack + '.call(' + helper.callParams + '); }'); + this.source.push('else { ' + nextStack + ' = ' + nonHelper + '; ' + nextStack + ' = typeof ' + nextStack + ' === functionType ? ' + nextStack + '.apply(depth0) : ' + nextStack + '; }'); + }, - this.context.aliases.self = "this"; - this.push("self.invokePartial(" + params.join(", ") + ")"); - }, + // [invokePartial] + // + // On stack, before: context, ... + // On stack after: result of partial invocation + // + // This operation pops off a context, invokes a partial with that context, + // and pushes the result of the invocation back. + invokePartial: function(name) { + var params = [this.nameLookup('partials', name, 'partial'), "'" + name + "'", this.popStack(), "helpers", "partials"]; + + if (this.options.data) { + params.push("data"); + } - // [assignToHash] - // - // On stack, before: value, hash, ... - // On stack, after: hash, ... - // - // Pops a value and hash off the stack, assigns `hash[key] = value` - // and pushes the hash back onto the stack. - assignToHash: function(key) { - var value = this.popStack(), - type; + this.context.aliases.self = "this"; + this.push("self.invokePartial(" + params.join(", ") + ")"); + }, - if (this.options.stringParams) { - type = this.popStack(); - this.popStack(); - } + // [assignToHash] + // + // On stack, before: value, hash, ... + // On stack, after: hash, ... + // + // Pops a value and hash off the stack, assigns `hash[key] = value` + // and pushes the hash back onto the stack. + assignToHash: function(key) { + var value = this.popStack(), + context, + type; + + if (this.options.stringParams) { + type = this.popStack(); + context = this.popStack(); + } - var hash = this.hash; - if (type) { - hash.types.push("'" + key + "': " + type); - } - hash.values.push("'" + key + "': (" + value + ")"); - }, + var hash = this.hash; + if (context) { + hash.contexts.push("'" + key + "': " + context); + } + if (type) { + hash.types.push("'" + key + "': " + type); + } + hash.values.push("'" + key + "': (" + value + ")"); + }, - // HELPERS + // HELPERS - compiler: JavaScriptCompiler, + compiler: JavaScriptCompiler, - compileChildren: function(environment, options) { - var children = environment.children, child, compiler; + compileChildren: function(environment, options) { + var children = environment.children, child, compiler; - for(var i=0, l=children.length; i<l; i++) { - child = children[i]; - compiler = new this.compiler(); + for(var i=0, l=children.length; i<l; i++) { + child = children[i]; + compiler = new this.compiler(); - var index = this.matchExistingProgram(child); + var index = this.matchExistingProgram(child); - if (index == null) { - this.context.programs.push(''); // Placeholder to prevent name conflicts for nested children - index = this.context.programs.length; - child.index = index; - child.name = 'program' + index; - this.context.programs[index] = compiler.compile(child, options, this.context); - this.context.environments[index] = child; - } else { - child.index = index; - child.name = 'program' + index; - } + if (index == null) { + this.context.programs.push(''); // Placeholder to prevent name conflicts for nested children + index = this.context.programs.length; + child.index = index; + child.name = 'program' + index; + this.context.programs[index] = compiler.compile(child, options, this.context); + this.context.environments[index] = child; + } else { + child.index = index; + child.name = 'program' + index; } - }, - matchExistingProgram: function(child) { - for (var i = 0, len = this.context.environments.length; i < len; i++) { - var environment = this.context.environments[i]; - if (environment && environment.equals(child)) { - return i; - } + } + }, + matchExistingProgram: function(child) { + for (var i = 0, len = this.context.environments.length; i < len; i++) { + var environment = this.context.environments[i]; + if (environment && environment.equals(child)) { + return i; } - }, - - programExpression: function(guid) { - this.context.aliases.self = "this"; + } + }, - if(guid == null) { - return "self.noop"; - } + programExpression: function(guid) { + this.context.aliases.self = "this"; - var child = this.environment.children[guid], - depths = child.depths.list, depth; + if(guid == null) { + return "self.noop"; + } - var programParams = [child.index, child.name, "data"]; + var child = this.environment.children[guid], + depths = child.depths.list, depth; - for(var i=0, l = depths.length; i<l; i++) { - depth = depths[i]; + var programParams = [child.index, child.name, "data"]; - if(depth === 1) { programParams.push("depth0"); } - else { programParams.push("depth" + (depth - 1)); } - } + for(var i=0, l = depths.length; i<l; i++) { + depth = depths[i]; - if(depths.length === 0) { - return "self.program(" + programParams.join(", ") + ")"; - } else { - programParams.shift(); - return "self.programWithDepth(" + programParams.join(", ") + ")"; - } - }, + if(depth === 1) { programParams.push("depth0"); } + else { programParams.push("depth" + (depth - 1)); } + } - register: function(name, val) { - this.useRegister(name); - this.source.push(name + " = " + val + ";"); - }, + return (depths.length === 0 ? "self.program(" : "self.programWithDepth(") + programParams.join(", ") + ")"; + }, - useRegister: function(name) { - if(!this.registers[name]) { - this.registers[name] = true; - this.registers.list.push(name); - } - }, + register: function(name, val) { + this.useRegister(name); + this.source.push(name + " = " + val + ";"); + }, - pushStackLiteral: function(item) { - return this.push(new Literal(item)); - }, + useRegister: function(name) { + if(!this.registers[name]) { + this.registers[name] = true; + this.registers.list.push(name); + } + }, - pushStack: function(item) { - this.flushInline(); + pushStackLiteral: function(item) { + return this.push(new Literal(item)); + }, - var stack = this.incrStack(); - if (item) { - this.source.push(stack + " = " + item + ";"); - } - this.compileStack.push(stack); - return stack; - }, + pushStack: function(item) { + this.flushInline(); - replaceStack: function(callback) { - var prefix = '', - inline = this.isInline(), - stack; + var stack = this.incrStack(); + if (item) { + this.source.push(stack + " = " + item + ";"); + } + this.compileStack.push(stack); + return stack; + }, - // If we are currently inline then we want to merge the inline statement into the - // replacement statement via ',' - if (inline) { - var top = this.popStack(true); + replaceStack: function(callback) { + var prefix = '', + inline = this.isInline(), + stack; - if (top instanceof Literal) { - // Literals do not need to be inlined - stack = top.value; - } else { - // Get or create the current stack name for use by the inline - var name = this.stackSlot ? this.topStackName() : this.incrStack(); + // If we are currently inline then we want to merge the inline statement into the + // replacement statement via ',' + if (inline) { + var top = this.popStack(true); - prefix = '(' + this.push(name) + ' = ' + top + '),'; - stack = this.topStack(); - } + if (top instanceof Literal) { + // Literals do not need to be inlined + stack = top.value; } else { + // Get or create the current stack name for use by the inline + var name = this.stackSlot ? this.topStackName() : this.incrStack(); + + prefix = '(' + this.push(name) + ' = ' + top + '),'; stack = this.topStack(); } + } else { + stack = this.topStack(); + } - var item = callback.call(this, stack); - - if (inline) { - if (this.inlineStack.length || this.compileStack.length) { - this.popStack(); - } - this.push('(' + prefix + item + ')'); - } else { - // Prevent modification of the context depth variable. Through replaceStack - if (!/^stack/.test(stack)) { - stack = this.nextStack(); - } + var item = callback.call(this, stack); - this.source.push(stack + " = (" + prefix + item + ");"); + if (inline) { + if (this.inlineStack.length || this.compileStack.length) { + this.popStack(); } - return stack; - }, - - nextStack: function() { - return this.pushStack(); - }, - - incrStack: function() { - this.stackSlot++; - if(this.stackSlot > this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } - return this.topStackName(); - }, - topStackName: function() { - return "stack" + this.stackSlot; - }, - flushInline: function() { - var inlineStack = this.inlineStack; - if (inlineStack.length) { - this.inlineStack = []; - for (var i = 0, len = inlineStack.length; i < len; i++) { - var entry = inlineStack[i]; - if (entry instanceof Literal) { - this.compileStack.push(entry); - } else { - this.pushStack(entry); - } - } + this.push('(' + prefix + item + ')'); + } else { + // Prevent modification of the context depth variable. Through replaceStack + if (!/^stack/.test(stack)) { + stack = this.nextStack(); } - }, - isInline: function() { - return this.inlineStack.length; - }, - popStack: function(wrapped) { - var inline = this.isInline(), - item = (inline ? this.inlineStack : this.compileStack).pop(); + this.source.push(stack + " = (" + prefix + item + ");"); + } + return stack; + }, - if (!wrapped && (item instanceof Literal)) { - return item.value; - } else { - if (!inline) { - this.stackSlot--; + nextStack: function() { + return this.pushStack(); + }, + + incrStack: function() { + this.stackSlot++; + if(this.stackSlot > this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } + return this.topStackName(); + }, + topStackName: function() { + return "stack" + this.stackSlot; + }, + flushInline: function() { + var inlineStack = this.inlineStack; + if (inlineStack.length) { + this.inlineStack = []; + for (var i = 0, len = inlineStack.length; i < len; i++) { + var entry = inlineStack[i]; + if (entry instanceof Literal) { + this.compileStack.push(entry); + } else { + this.pushStack(entry); } - return item; } - }, + } + }, + isInline: function() { + return this.inlineStack.length; + }, - topStack: function(wrapped) { - var stack = (this.isInline() ? this.inlineStack : this.compileStack), - item = stack[stack.length - 1]; + popStack: function(wrapped) { + var inline = this.isInline(), + item = (inline ? this.inlineStack : this.compileStack).pop(); - if (!wrapped && (item instanceof Literal)) { - return item.value; - } else { - return item; + if (!wrapped && (item instanceof Literal)) { + return item.value; + } else { + if (!inline) { + this.stackSlot--; } - }, + return item; + } + }, - quotedString: function(str) { - return '"' + str - .replace(/\\/g, '\\\\') - .replace(/"/g, '\\"') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') + '"'; - }, + topStack: function(wrapped) { + var stack = (this.isInline() ? this.inlineStack : this.compileStack), + item = stack[stack.length - 1]; - setupHelper: function(paramSize, name, missingParams) { - var params = []; - this.setupParams(paramSize, params, missingParams); - var foundHelper = this.nameLookup('helpers', name, 'helper'); + if (!wrapped && (item instanceof Literal)) { + return item.value; + } else { + return item; + } + }, - return { - params: params, - name: foundHelper, - callParams: ["depth0"].concat(params).join(", "), - helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ") - }; - }, + quotedString: function(str) { + return '"' + str + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4 + .replace(/\u2029/g, '\\u2029') + '"'; + }, - // the params and contexts arguments are passed in arrays - // to fill in - setupParams: function(paramSize, params, useRegister) { - var options = [], contexts = [], types = [], param, inverse, program; + setupHelper: function(paramSize, name, missingParams) { + var params = []; + this.setupParams(paramSize, params, missingParams); + var foundHelper = this.nameLookup('helpers', name, 'helper'); - options.push("hash:" + this.popStack()); + return { + params: params, + name: foundHelper, + callParams: ["depth0"].concat(params).join(", "), + helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ") + }; + }, - inverse = this.popStack(); - program = this.popStack(); + // the params and contexts arguments are passed in arrays + // to fill in + setupParams: function(paramSize, params, useRegister) { + var options = [], contexts = [], types = [], param, inverse, program; - // 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"; - } + options.push("hash:" + this.popStack()); - if (!inverse) { - this.context.aliases.self = "this"; - inverse = "self.noop"; - } + inverse = this.popStack(); + program = this.popStack(); - options.push("inverse:" + inverse); - options.push("fn:" + program); + // 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"; } - for(var i=0; i<paramSize; i++) { - param = this.popStack(); - params.push(param); - - if(this.options.stringParams) { - types.push(this.popStack()); - contexts.push(this.popStack()); - } + if (!inverse) { + this.context.aliases.self = "this"; + inverse = "self.noop"; } - if (this.options.stringParams) { - options.push("contexts:[" + contexts.join(",") + "]"); - options.push("types:[" + types.join(",") + "]"); - options.push("hashTypes:hashTypes"); - } + options.push("inverse:" + inverse); + options.push("fn:" + program); + } - if(this.options.data) { - options.push("data:data"); - } + for(var i=0; i<paramSize; i++) { + param = this.popStack(); + params.push(param); - options = "{" + options.join(",") + "}"; - if (useRegister) { - this.register('options', options); - params.push('options'); - } else { - params.push(options); + if(this.options.stringParams) { + types.push(this.popStack()); + contexts.push(this.popStack()); } - return params.join(", "); } - }; - var reservedWords = ( - "break else new var" + - " case finally return void" + - " catch for switch while" + - " continue function this with" + - " default if throw" + - " delete in try" + - " do instanceof typeof" + - " abstract enum int short" + - " boolean export interface static" + - " byte extends long super" + - " char final native synchronized" + - " class float package throws" + - " const goto private transient" + - " debugger implements protected volatile" + - " double import public let yield" - ).split(" "); - - var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {}; - - for(var i=0, l=reservedWords.length; i<l; i++) { - compilerWords[reservedWords[i]] = true; - } + if (this.options.stringParams) { + options.push("contexts:[" + contexts.join(",") + "]"); + options.push("types:[" + types.join(",") + "]"); + options.push("hashContexts:hashContexts"); + options.push("hashTypes:hashTypes"); + } - JavaScriptCompiler.isValidJavaScriptVariableName = function(name) { - if(!JavaScriptCompiler.RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]+$/.test(name)) { - return true; + if(this.options.data) { + options.push("data:data"); } - return false; - }; -})(Handlebars.Compiler, Handlebars.JavaScriptCompiler); + options = "{" + options.join(",") + "}"; + if (useRegister) { + this.register('options', options); + params.push('options'); + } else { + params.push(options); + } + return params.join(", "); + } +}; + +var reservedWords = ( + "break else new var" + + " case finally return void" + + " catch for switch while" + + " continue function this with" + + " default if throw" + + " delete in try" + + " do instanceof typeof" + + " abstract enum int short" + + " boolean export interface static" + + " byte extends long super" + + " char final native synchronized" + + " class float package throws" + + " const goto private transient" + + " debugger implements protected volatile" + + " double import public let yield" +).split(" "); + +var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {}; + +for(var i=0, l=reservedWords.length; i<l; i++) { + compilerWords[reservedWords[i]] = true; +} + +JavaScriptCompiler.isValidJavaScriptVariableName = function(name) { + if(!JavaScriptCompiler.RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]+$/.test(name)) { + return true; + } + return false; +}; Handlebars.precompile = function(input, options) { - if (!input || (typeof input !== 'string' && input.constructor !== Handlebars.AST.ProgramNode)) { - throw new Handlebars.Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed " + input); + if (input == null || (typeof input !== 'string' && input.constructor !== Handlebars.AST.ProgramNode)) { + throw new Handlebars.Exception("You must pass a string or Handlebars AST to Handlebars.precompile. You passed " + input); } options = options || {}; @@ -2085,12 +2114,12 @@ Handlebars.precompile = function(input, options) { options.data = true; } var ast = Handlebars.parse(input); - var environment = new Handlebars.Compiler().compile(ast, options); - return new Handlebars.JavaScriptCompiler().compile(environment, options); + var environment = new Compiler().compile(ast, options); + return new JavaScriptCompiler().compile(environment, options); }; Handlebars.compile = function(input, options) { - if (!input || (typeof input !== 'string' && input.constructor !== Handlebars.AST.ProgramNode)) { + if (input == null || (typeof input !== 'string' && input.constructor !== Handlebars.AST.ProgramNode)) { throw new Handlebars.Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed " + input); } @@ -2101,8 +2130,8 @@ Handlebars.compile = function(input, options) { var compiled; function compile() { var ast = Handlebars.parse(input); - var environment = new Handlebars.Compiler().compile(ast, options); - var templateSpec = new Handlebars.JavaScriptCompiler().compile(environment, options, undefined, true); + var environment = new Compiler().compile(ast, options); + var templateSpec = new JavaScriptCompiler().compile(environment, options, undefined, true); return Handlebars.template(templateSpec); } @@ -2114,8 +2143,10 @@ Handlebars.compile = function(input, options) { return compiled.call(this, context, options); }; }; + ; // lib/handlebars/runtime.js + Handlebars.VM = { template: function(templateSpec) { // Just add water @@ -2126,13 +2157,11 @@ Handlebars.VM = { program: function(i, fn, data) { var programWrapper = this.programs[i]; if(data) { - return Handlebars.VM.program(fn, data); - } else if(programWrapper) { - return programWrapper; - } else { - programWrapper = this.programs[i] = Handlebars.VM.program(fn); - return programWrapper; + programWrapper = Handlebars.VM.program(i, fn, data); + } else if (!programWrapper) { + programWrapper = this.programs[i] = Handlebars.VM.program(i, fn); } + return programWrapper; }, programWithDepth: Handlebars.VM.programWithDepth, noop: Handlebars.VM.noop, @@ -2164,21 +2193,27 @@ Handlebars.VM = { }; }, - programWithDepth: function(fn, data, $depth) { - var args = Array.prototype.slice.call(arguments, 2); + programWithDepth: function(i, fn, data /*, $depth */) { + var args = Array.prototype.slice.call(arguments, 3); - return function(context, options) { + var program = function(context, options) { options = options || {}; return fn.apply(this, [context, options.data || data].concat(args)); }; + program.program = i; + program.depth = args.length; + return program; }, - program: function(fn, data) { - return function(context, options) { + program: function(i, fn, data) { + var program = function(context, options) { options = options || {}; return fn(context, options.data || data); }; + program.program = i; + program.depth = 0; + return program; }, noop: function() { return ""; }, invokePartial: function(partial, name, context, helpers, partials, data) { @@ -2199,3 +2234,6 @@ Handlebars.VM = { Handlebars.template = Handlebars.VM.template; ; +// lib/handlebars/browser-suffix.js +})(Handlebars); +; diff --git a/public/js/libs/new-ember-data.js b/public/js/libs/new-ember-data.js deleted file mode 100644 index e07c21b..0000000 --- a/public/js/libs/new-ember-data.js +++ /dev/null @@ -1,8431 +0,0 @@ -// Last commit: 57d6c01 (2013-03-18 11:27:29 -0700) - - -(function() { -window.DS = Ember.Namespace.create({ - // this one goes past 11 - CURRENT_API_REVISION: 12 -}); - -})(); - - - -(function() { -var DeferredMixin = Ember.DeferredMixin, // ember-runtime/mixins/deferred - Evented = Ember.Evented, // ember-runtime/mixins/evented - run = Ember.run, // ember-metal/run-loop - get = Ember.get; // ember-metal/accessors - -var LoadPromise = Ember.Mixin.create(Evented, DeferredMixin, { - init: function() { - this._super.apply(this, arguments); - this.one('didLoad', function() { - run(this, 'resolve', this); - }); - - if (get(this, 'isLoaded')) { - this.trigger('didLoad'); - } - } -}); - -DS.LoadPromise = LoadPromise; - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set; - -var LoadPromise = DS.LoadPromise; // system/mixins/load_promise - -/** - A record array is an array that contains records of a certain type. The record - array materializes records as needed when they are retrieved for the first - time. You should not create record arrays yourself. Instead, an instance of - DS.RecordArray or its subclasses will be returned by your application's store - in response to queries. -*/ - -DS.RecordArray = Ember.ArrayProxy.extend(Ember.Evented, LoadPromise, { - /** - The model type contained by this record array. - - @type DS.Model - */ - type: null, - - // The array of client ids backing the record array. When a - // record is requested from the record array, the record - // for the client id at the same index is materialized, if - // necessary, by the store. - content: null, - - isLoaded: false, - isUpdating: false, - - // The store that created this record array. - store: null, - - objectAtContent: function(index) { - var content = get(this, 'content'), - reference = content.objectAt(index), - store = get(this, 'store'); - - if (reference) { - return store.recordForReference(reference); - } - }, - - materializedObjectAt: function(index) { - var reference = get(this, 'content').objectAt(index); - if (!reference) { return; } - - if (get(this, 'store').recordIsMaterialized(reference)) { - return this.objectAt(index); - } - }, - - update: function() { - if (get(this, 'isUpdating')) { return; } - - var store = get(this, 'store'), - type = get(this, 'type'); - - store.fetchAll(type, this); - }, - - addReference: function(reference) { - get(this, 'content').addObject(reference); - }, - - removeReference: function(reference) { - get(this, 'content').removeObject(reference); - } -}); - -})(); - - - -(function() { -var get = Ember.get; - -DS.FilteredRecordArray = DS.RecordArray.extend({ - filterFunction: null, - isLoaded: true, - - replace: function() { - var type = get(this, 'type').toString(); - throw new Error("The result of a client-side filter (on " + type + ") is immutable."); - }, - - updateFilter: Ember.observer(function() { - var store = get(this, 'store'); - store.updateRecordArrayFilter(this, get(this, 'type'), get(this, 'filterFunction')); - }, 'filterFunction') -}); - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set; - -DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({ - query: null, - - replace: function() { - var type = get(this, 'type').toString(); - throw new Error("The result of a server query (on " + type + ") is immutable."); - }, - - load: function(references) { - var store = get(this, 'store'), type = get(this, 'type'); - - this.beginPropertyChanges(); - set(this, 'content', Ember.A(references)); - set(this, 'isLoaded', true); - this.endPropertyChanges(); - - var self = this; - // TODO: does triggering didLoad event should be the last action of the runLoop? - Ember.run.once(function() { - self.trigger('didLoad'); - }); - } -}); - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set; - -/** - A ManyArray is a RecordArray that represents the contents of a has-many - relationship. - - The ManyArray is instantiated lazily the first time the relationship is - requested. - - ### Inverses - - Often, the relationships in Ember Data applications will have - an inverse. For example, imagine the following models are - defined: - - App.Post = DS.Model.extend({ - comments: DS.hasMany('App.Comment') - }); - - App.Comment = DS.Model.extend({ - post: DS.belongsTo('App.Post') - }); - - If you created a new instance of `App.Post` and added - a `App.Comment` record to its `comments` has-many - relationship, you would expect the comment's `post` - property to be set to the post that contained - the has-many. - - We call the record to which a relationship belongs the - relationship's _owner_. -*/ -DS.ManyArray = DS.RecordArray.extend({ - init: function() { - this._super.apply(this, arguments); - this._changesToSync = Ember.OrderedSet.create(); - }, - - /** - @private - - The record to which this relationship belongs. - - @property {DS.Model} - */ - owner: null, - - // LOADING STATE - - isLoaded: false, - - loadingRecordsCount: function(count) { - this.loadingRecordsCount = count; - }, - - loadedRecord: function() { - this.loadingRecordsCount--; - if (this.loadingRecordsCount === 0) { - set(this, 'isLoaded', true); - this.trigger('didLoad'); - } - }, - - fetch: function() { - var references = get(this, 'content'), - store = get(this, 'store'), - type = get(this, 'type'), - owner = get(this, 'owner'); - - store.fetchUnloadedReferences(type, references, owner); - }, - - // Overrides Ember.Array's replace method to implement - replaceContent: function(index, removed, added) { - // Map the array of record objects into an array of client ids. - added = added.map(function(record) { - Ember.assert("You can only add records of " + (get(this, 'type') && get(this, 'type').toString()) + " to this relationship.", !get(this, 'type') || (get(this, 'type') === record.constructor)); - return get(record, '_reference'); - }, this); - - this._super(index, removed, added); - }, - - arrangedContentDidChange: function() { - this.fetch(); - }, - - arrayContentWillChange: function(index, removed, added) { - var owner = get(this, 'owner'), - name = get(this, 'name'); - - if (!owner._suspendedRelationships) { - // This code is the first half of code that continues inside - // of arrayContentDidChange. It gets or creates a change from - // the child object, adds the current owner as the old - // parent if this is the first time the object was removed - // from a ManyArray, and sets `newParent` to null. - // - // Later, if the object is added to another ManyArray, - // the `arrayContentDidChange` will set `newParent` on - // the change. - for (var i=index; i<index+removed; i++) { - var reference = get(this, 'content').objectAt(i); - - var change = DS.RelationshipChange.createChange(owner.get('_reference'), reference, get(this, 'store'), { - parentType: owner.constructor, - changeType: "remove", - kind: "hasMany", - key: name - }); - - this._changesToSync.add(change); - } - } - - return this._super.apply(this, arguments); - }, - - arrayContentDidChange: function(index, removed, added) { - this._super.apply(this, arguments); - - var owner = get(this, 'owner'), - name = get(this, 'name'), - store = get(this, 'store'); - - if (!owner._suspendedRelationships) { - // This code is the second half of code that started in - // `arrayContentWillChange`. It gets or creates a change - // from the child object, and adds the current owner as - // the new parent. - for (var i=index; i<index+added; i++) { - var reference = get(this, 'content').objectAt(i); - - var change = DS.RelationshipChange.createChange(owner.get('_reference'), reference, store, { - parentType: owner.constructor, - changeType: "add", - kind:"hasMany", - key: name - }); - change.hasManyName = name; - - this._changesToSync.add(change); - } - - // We wait until the array has finished being - // mutated before syncing the OneToManyChanges created - // in arrayContentWillChange, so that the array - // membership test in the sync() logic operates - // on the final results. - this._changesToSync.forEach(function(change) { - change.sync(); - }); - DS.OneToManyChange.ensureSameTransaction(this._changesToSync, store); - this._changesToSync.clear(); - } - }, - - // Create a child record within the owner - createRecord: function(hash, transaction) { - var owner = get(this, 'owner'), - store = get(owner, 'store'), - type = get(this, 'type'), - record; - - transaction = transaction || get(owner, 'transaction'); - - record = store.createRecord.call(store, type, hash, transaction); - this.pushObject(record); - - return record; - } - -}); - -})(); - - - -(function() { - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt, - removeObject = Ember.EnumerableUtils.removeObject, forEach = Ember.EnumerableUtils.forEach; - -/** - A transaction allows you to collect multiple records into a unit of work - that can be committed or rolled back as a group. - - For example, if a record has local modifications that have not yet - been saved, calling `commit()` on its transaction will cause those - modifications to be sent to the adapter to be saved. Calling - `rollback()` on its transaction would cause all of the modifications to - be discarded and the record to return to the last known state before - changes were made. - - If a newly created record's transaction is rolled back, it will - immediately transition to the deleted state. - - If you do not explicitly create a transaction, a record is assigned to - an implicit transaction called the default transaction. In these cases, - you can treat your application's instance of `DS.Store` as a transaction - and call the `commit()` and `rollback()` methods on the store itself. - - Once a record has been successfully committed or rolled back, it will - be moved back to the implicit transaction. Because it will now be in - a clean state, it can be moved to a new transaction if you wish. - - ### Creating a Transaction - - To create a new transaction, call the `transaction()` method of your - application's `DS.Store` instance: - - var transaction = App.store.transaction(); - - This will return a new instance of `DS.Transaction` with no records - yet assigned to it. - - ### Adding Existing Records - - Add records to a transaction using the `add()` method: - - record = App.store.find(App.Person, 1); - transaction.add(record); - - Note that only records whose `isDirty` flag is `false` may be added - to a transaction. Once modifications to a record have been made - (its `isDirty` flag is `true`), it is not longer able to be added to - a transaction. - - ### Creating New Records - - Because newly created records are dirty from the time they are created, - and because dirty records can not be added to a transaction, you must - use the `createRecord()` method to assign new records to a transaction. - - For example, instead of this: - - var transaction = store.transaction(); - var person = App.Person.createRecord({ name: "Steve" }); - - // won't work because person is dirty - transaction.add(person); - - Call `createRecord()` on the transaction directly: - - var transaction = store.transaction(); - transaction.createRecord(App.Person, { name: "Steve" }); - - ### Asynchronous Commits - - Typically, all of the records in a transaction will be committed - together. However, new records that have a dependency on other new - records need to wait for their parent record to be saved and assigned an - ID. In that case, the child record will continue to live in the - transaction until its parent is saved, at which time the transaction will - attempt to commit again. - - For this reason, you should not re-use transactions once you have committed - them. Always make a new transaction and move the desired records to it before - calling commit. -*/ - -var arrayDefault = function() { return []; }; - -DS.Transaction = Ember.Object.extend({ - /** - @private - - Creates the bucket data structure used to segregate records by - type. - */ - init: function() { - set(this, 'buckets', { - clean: Ember.OrderedSet.create(), - created: Ember.OrderedSet.create(), - updated: Ember.OrderedSet.create(), - deleted: Ember.OrderedSet.create(), - inflight: Ember.OrderedSet.create() - }); - - set(this, 'relationships', Ember.OrderedSet.create()); - }, - - /** - Creates a new record of the given type and assigns it to the transaction - on which the method was called. - - This is useful as only clean records can be added to a transaction and - new records created using other methods immediately become dirty. - - @param {DS.Model} type the model type to create - @param {Object} hash the data hash to assign the new record - */ - createRecord: function(type, hash) { - var store = get(this, 'store'); - - return store.createRecord(type, hash, this); - }, - - isEqualOrDefault: function(other) { - if (this === other || other === get(this, 'store.defaultTransaction')) { - return true; - } - }, - - isDefault: Ember.computed(function() { - return this === get(this, 'store.defaultTransaction'); - }), - - /** - Adds an existing record to this transaction. Only records without - modficiations (i.e., records whose `isDirty` property is `false`) - can be added to a transaction. - - @param {DS.Model} record the record to add to the transaction - */ - add: function(record) { - Ember.assert("You must pass a record into transaction.add()", record instanceof DS.Model); - - var recordTransaction = get(record, 'transaction'), - defaultTransaction = get(this, 'store.defaultTransaction'); - - // Make `add` idempotent - if (recordTransaction === this) { return; } - - // XXX it should be possible to move a dirty transaction from the default transaction - - // we could probably make this work if someone has a valid use case. Do you? - Ember.assert("Once a record has changed, you cannot move it into a different transaction", !get(record, 'isDirty')); - - Ember.assert("Models cannot belong to more than one transaction at a time.", recordTransaction === defaultTransaction); - - this.adoptRecord(record); - }, - - relationshipBecameDirty: function(relationship) { - get(this, 'relationships').add(relationship); - }, - - relationshipBecameClean: function(relationship) { - get(this, 'relationships').remove(relationship); - }, - - /** - Commits the transaction, which causes all of the modified records that - belong to the transaction to be sent to the adapter to be saved. - - Once you call `commit()` on a transaction, you should not re-use it. - - When a record is saved, it will be removed from this transaction and - moved back to the store's default transaction. - */ - commit: function() { - var store = get(this, 'store'); - var adapter = get(store, '_adapter'); - var defaultTransaction = get(store, 'defaultTransaction'); - - var iterate = function(records) { - var set = records.copy(); - set.forEach(function (record) { - record.send('willCommit'); - }); - return set; - }; - - var relationships = get(this, 'relationships'); - - var commitDetails = { - created: iterate(this.bucketForType('created')), - updated: iterate(this.bucketForType('updated')), - deleted: iterate(this.bucketForType('deleted')), - relationships: relationships - }; - - if (this === defaultTransaction) { - set(store, 'defaultTransaction', store.transaction()); - } - - this.removeCleanRecords(); - - if (!commitDetails.created.isEmpty() || !commitDetails.updated.isEmpty() || !commitDetails.deleted.isEmpty() || !relationships.isEmpty()) { - if (adapter && adapter.commit) { adapter.commit(store, commitDetails); } - else { throw fmt("Adapter is either null or does not implement `commit` method", this); } - } - - // Once we've committed the transaction, there is no need to - // keep the OneToManyChanges around. Destroy them so they - // can be garbage collected. - relationships.forEach(function(relationship) { - relationship.destroy(); - }); - }, - - /** - Rolling back a transaction resets the records that belong to - that transaction. - - Updated records have their properties reset to the last known - value from the persistence layer. Deleted records are reverted - to a clean, non-deleted state. Newly created records immediately - become deleted, and are not sent to the adapter to be persisted. - - After the transaction is rolled back, any records that belong - to it will return to the store's default transaction, and the - current transaction should not be used again. - */ - rollback: function() { - // Loop through all of the records in each of the dirty states - // and initiate a rollback on them. As a side effect of telling - // the record to roll back, it should also move itself out of - // the dirty bucket and into the clean bucket. - ['created', 'updated', 'deleted', 'inflight'].forEach(function(bucketType) { - var records = this.bucketForType(bucketType); - forEach(records, function(record) { - record.send('rollback'); - }); - records.clear(); - }, this); - - // Now that all records in the transaction are guaranteed to be - // clean, migrate them all to the store's default transaction. - this.removeCleanRecords(); - }, - - /** - @private - - Removes a record from this transaction and back to the store's - default transaction. - - Note: This method is private for now, but should probably be exposed - in the future once we have stricter error checking (for example, in the - case of the record being dirty). - - @param {DS.Model} record - */ - remove: function(record) { - var defaultTransaction = get(this, 'store.defaultTransaction'); - defaultTransaction.adoptRecord(record); - }, - - /** - @private - - Removes all of the records in the transaction's clean bucket. - */ - removeCleanRecords: function() { - var clean = this.bucketForType('clean'); - clean.forEach(function(record) { - this.remove(record); - }, this); - clean.clear(); - }, - - /** - @private - - Returns the bucket for the given bucket type. For example, you might call - `this.bucketForType('updated')` to get the `Ember.Map` that contains all - of the records that have changes pending. - - @param {String} bucketType the type of bucket - @returns Ember.Map - */ - bucketForType: function(bucketType) { - var buckets = get(this, 'buckets'); - - return get(buckets, bucketType); - }, - - /** - @private - - This method moves a record into a different transaction without the normal - checks that ensure that the user is not doing something weird, like moving - a dirty record into a new transaction. - - It is designed for internal use, such as when we are moving a clean record - into a new transaction when the transaction is committed. - - This method must not be called unless the record is clean. - - @param {DS.Model} record - */ - adoptRecord: function(record) { - var oldTransaction = get(record, 'transaction'); - - if (oldTransaction) { - oldTransaction.removeFromBucket('clean', record); - } - - this.addToBucket('clean', record); - set(record, 'transaction', this); - }, - - /** - @private - - Adds a record to the named bucket. - - @param {String} bucketType one of `clean`, `created`, `updated`, or `deleted` - */ - addToBucket: function(bucketType, record) { - this.bucketForType(bucketType).add(record); - }, - - /** - @private - - Removes a record from the named bucket. - - @param {String} bucketType one of `clean`, `created`, `updated`, or `deleted` - */ - removeFromBucket: function(bucketType, record) { - this.bucketForType(bucketType).remove(record); - }, - - /** - @private - - Called by a record's state manager to indicate that the record has entered - a dirty state. The record will be moved from the `clean` bucket and into - the appropriate dirty bucket. - - @param {String} bucketType one of `created`, `updated`, or `deleted` - */ - recordBecameDirty: function(bucketType, record) { - this.removeFromBucket('clean', record); - this.addToBucket(bucketType, record); - }, - - /** - @private - - Called by a record's state manager to indicate that the record has entered - inflight state. The record will be moved from its current dirty bucket and into - the `inflight` bucket. - - @param {String} bucketType one of `created`, `updated`, or `deleted` - */ - recordBecameInFlight: function(kind, record) { - this.removeFromBucket(kind, record); - this.addToBucket('inflight', record); - }, - - recordIsMoving: function(kind, record) { - this.removeFromBucket(kind, record); - this.addToBucket('clean', record); - }, - - /** - @private - - Called by a record's state manager to indicate that the record has entered - a clean state. The record will be moved from its current dirty or inflight bucket and into - the `clean` bucket. - - @param {String} bucketType one of `created`, `updated`, or `deleted` - */ - recordBecameClean: function(kind, record) { - this.removeFromBucket(kind, record); - this.remove(record); - } -}); - -})(); - - - -(function() { -var classify = Ember.String.classify, get = Ember.get; - -/** -@private - - The Mappable mixin is designed for classes that would like to - behave as a map for configuration purposes. - - For example, the DS.Adapter class can behave like a map, with - more semantic API, via the `map` API: - - DS.Adapter.map('App.Person', { firstName: { key: 'FIRST' } }); - - Class configuration via a map-like API has a few common requirements - that differentiate it from the standard Ember.Map implementation. - - First, values often are provided as strings that should be normalized - into classes the first time the configuration options are used. - - Second, the values configured on parent classes should also be taken - into account. - - Finally, setting the value of a key sometimes should merge with the - previous value, rather than replacing it. - - This mixin provides a instance method, `createInstanceMapFor`, that - will reify all of the configuration options set on an instance's - constructor and provide it for the instance to use. - - Classes can implement certain hooks that allow them to customize - the requirements listed above: - - * `resolveMapConflict` - called when a value is set for an existing - value - * `transformMapKey` - allows a key name (for example, a global path - to a class) to be normalized - * `transformMapValue` - allows a value (for example, a class that - should be instantiated) to be normalized - - Classes that implement this mixin should also implement a class - method built using the `generateMapFunctionFor` method: - - DS.Adapter.reopenClass({ - map: DS.Mappable.generateMapFunctionFor('attributes', function(key, newValue, map) { - var existingValue = map.get(key); - - for (var prop in newValue) { - if (!newValue.hasOwnProperty(prop)) { continue; } - existingValue[prop] = newValue[prop]; - } - }) - }); - - The function passed to `generateMapFunctionFor` is invoked every time a - new value is added to the map. -**/ - -var resolveMapConflict = function(oldValue, newValue, mappingsKey) { - return oldValue; -}; - -var transformMapKey = function(key, value) { - return key; -}; - -var transformMapValue = function(key, value) { - return value; -}; - -DS._Mappable = Ember.Mixin.create({ - createInstanceMapFor: function(mapName) { - var instanceMeta = Ember.metaPath(this, ['DS.Mappable'], true); - - instanceMeta.values = instanceMeta.values || {}; - - if (instanceMeta.values[mapName]) { return instanceMeta.values[mapName]; } - - var instanceMap = instanceMeta.values[mapName] = new Ember.Map(); - - var klass = this.constructor; - - while (klass && klass !== DS.Store) { - this._copyMap(mapName, klass, instanceMap); - klass = klass.superclass; - } - - instanceMeta.values[mapName] = instanceMap; - return instanceMap; - }, - - _copyMap: function(mapName, klass, instanceMap) { - var classMeta = Ember.metaPath(klass, ['DS.Mappable'], true); - - var classMap = classMeta[mapName]; - if (classMap) { - classMap.forEach(eachMap, this); - } - - function eachMap(key, value) { - var transformedKey = (klass.transformMapKey || transformMapKey)(key, value); - var transformedValue = (klass.transformMapValue || transformMapValue)(key, value); - - var oldValue = instanceMap.get(transformedKey); - var newValue = transformedValue; - - if (oldValue) { - newValue = (this.constructor.resolveMapConflict || resolveMapConflict)(oldValue, newValue, mapName); - } - - instanceMap.set(transformedKey, newValue); - } - } - - -}); - -DS._Mappable.generateMapFunctionFor = function(mapName, transform) { - return function(key, value) { - var meta = Ember.metaPath(this, ['DS.Mappable'], true); - var map = meta[mapName] || Ember.MapWithDefault.create({ - defaultValue: function() { return {}; } - }); - - transform.call(this, key, value, map); - - meta[mapName] = map; - }; -}; - -})(); - - - -(function() { -/*globals Ember*/ -/*jshint eqnull:true*/ -var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt, once = Ember.run.once; -var forEach = Ember.EnumerableUtils.forEach; -// These values are used in the data cache when clientIds are -// needed but the underlying data has not yet been loaded by -// the server. -var UNLOADED = 'unloaded'; -var LOADING = 'loading'; -var MATERIALIZED = { materialized: true }; -var CREATED = { created: true }; - -// Implementors Note: -// -// The variables in this file are consistently named according to the following -// scheme: -// -// * +id+ means an identifier managed by an external source, provided inside -// the data provided by that source. -// * +clientId+ means a transient numerical identifier generated at runtime by -// the data store. It is important primarily because newly created objects may -// not yet have an externally generated id. -// * +type+ means a subclass of DS.Model. - -// Used by the store to normalize IDs entering the store. Despite the fact -// that developers may provide IDs as numbers (e.g., `store.find(Person, 1)`), -// it is important that internally we use strings, since IDs may be serialized -// and lose type information. For example, Ember's router may put a record's -// ID into the URL, and if we later try to deserialize that URL and find the -// corresponding record, we will not know if it is a string or a number. -var coerceId = function(id) { - return id == null ? null : id+''; -}; - -var map = Ember.EnumerableUtils.map; - -/** - The store contains all of the data for records loaded from the server. - It is also responsible for creating instances of DS.Model that wraps - the individual data for a record, so that they can be bound to in your - Handlebars templates. - - Create a new store like this: - - MyApp.store = DS.Store.create(); - - You can retrieve DS.Model instances from the store in several ways. To retrieve - a record for a specific id, use the `find()` method: - - var record = MyApp.store.find(MyApp.Contact, 123); - - By default, the store will talk to your backend using a standard REST mechanism. - You can customize how the store talks to your backend by specifying a custom adapter: - - MyApp.store = DS.Store.create({ - adapter: 'MyApp.CustomAdapter' - }); - - You can learn more about writing a custom adapter by reading the `DS.Adapter` - documentation. -*/ -DS.Store = Ember.Object.extend(DS._Mappable, { - - /** - Many methods can be invoked without specifying which store should be used. - In those cases, the first store created will be used as the default. If - an application has multiple stores, it should specify which store to use - when performing actions, such as finding records by id. - - The init method registers this store as the default if none is specified. - */ - init: function() { - // Enforce API revisioning. See BREAKING_CHANGES.md for more. - var revision = get(this, 'revision'); - - if (revision !== DS.CURRENT_API_REVISION && !Ember.ENV.TESTING) { - throw new Error("Error: The Ember Data library has had breaking API changes since the last time you updated the library. Please review the list of breaking changes at https://github.com/emberjs/data/blob/master/BREAKING_CHANGES.md, then update your store's `revision` property to " + DS.CURRENT_API_REVISION); - } - - if (!get(DS, 'defaultStore') || get(this, 'isDefaultStore')) { - set(DS, 'defaultStore', this); - } - - // internal bookkeeping; not observable - this.typeMaps = {}; - this.recordCache = []; - this.clientIdToId = {}; - this.clientIdToType = {}; - this.clientIdToData = {}; - this.clientIdToPrematerializedData = {}; - this.recordArraysByClientId = {}; - this.relationshipChanges = {}; - this.recordReferences = {}; - - // Internally, we maintain a map of all unloaded IDs requested by - // a ManyArray. As the adapter loads data into the store, the - // store notifies any interested ManyArrays. When the ManyArray's - // total number of loading records drops to zero, it becomes - // `isLoaded` and fires a `didLoad` event. - this.loadingRecordArrays = {}; - - this._recordsToSave = Ember.OrderedSet.create(); - - set(this, 'defaultTransaction', this.transaction()); - }, - - /** - Returns a new transaction scoped to this store. This delegates - responsibility for invoking the adapter's commit mechanism to - a transaction. - - Transaction are responsible for tracking changes to records - added to them, and supporting `commit` and `rollback` - functionality. Committing a transaction invokes the store's - adapter, while rolling back a transaction reverses all - changes made to records added to the transaction. - - A store has an implicit (default) transaction, which tracks changes - made to records not explicitly added to a transaction. - - @see {DS.Transaction} - @returns DS.Transaction - */ - transaction: function() { - return DS.Transaction.create({ store: this }); - }, - - ensureSameTransaction: function(records){ - var transactions = Ember.A(); - forEach( records, function(record){ - if (record){ transactions.pushObject(get(record, 'transaction')); } - }); - - var transaction = transactions.reduce(function(prev, t) { - if (!get(t, 'isDefault')) { - if (prev === null) { return t; } - Ember.assert("All records in a changed relationship must be in the same transaction. You tried to change the relationship between records when one is in " + t + " and the other is in " + prev, t === prev); - } - - return prev; - }, null); - - if (transaction) { - forEach( records, function(record){ - if (record){ transaction.add(record); } - }); - } else { - transaction = transactions.objectAt(0); - } - return transaction; - - }, - /** - @private - - Instructs the store to materialize the data for a given record. - - To materialize a record, the store first retrieves the opaque data that was - passed to either `load()` or `loadMany()`. Then, the data and the record - are passed to the adapter's `materialize()` method, which allows the adapter - to translate arbitrary data structures from the adapter into the normalized - form the record expects. - - The adapter's `materialize()` method will invoke `materializeAttribute()`, - `materializeHasMany()` and `materializeBelongsTo()` on the record to - populate it with normalized values. - - @param {DS.Model} record - */ - materializeData: function(record) { - var clientId = get(record, 'clientId'), - cidToData = this.clientIdToData, - adapter = this.adapterForType(record.constructor), - data = cidToData[clientId]; - - cidToData[clientId] = MATERIALIZED; - - var prematerialized = this.clientIdToPrematerializedData[clientId]; - - // Ensures the record's data structures are setup - // before being populated by the adapter. - record.setupData(); - - if (data !== CREATED) { - // Instructs the adapter to extract information from the - // opaque data and materialize the record's attributes and - // relationships. - adapter.materialize(record, data, prematerialized); - } - }, - - /** - @private - - Returns true if there is already a record for this clientId. - - This is used to determine whether cleanup is required, so that - "changes" to unmaterialized records do not trigger mass - materialization. - - For example, if a parent record in a relationship with a large - number of children is deleted, we want to avoid materializing - those children. - - @param {Object} reference - @return {Boolean} - */ - recordIsMaterialized: function(reference) { - return !!this.recordCache[reference.clientId]; - }, - - /** - The adapter to use to communicate to a backend server or other persistence layer. - - This can be specified as an instance, a class, or a property path that specifies - where the adapter can be located. - - @property {DS.Adapter|String} - */ - adapter: 'DS.RESTAdapter', - - /** - @private - - Returns a JSON representation of the record using the adapter's - serialization strategy. This method exists primarily to enable - a record, which has access to its store (but not the store's - adapter) to provide a `serialize()` convenience. - - The available options are: - - * `includeId`: `true` if the record's ID should be included in - the JSON representation - - @param {DS.Model} record the record to serialize - @param {Object} options an options hash - */ - serialize: function(record, options) { - return this.adapterForType(record.constructor).serialize(record, options); - }, - - /** - @private - - This property returns the adapter, after resolving a possible - property path. - - If the supplied `adapter` was a class, or a String property - path resolved to a class, this property will instantiate the - class. - - This property is cacheable, so the same instance of a specified - adapter class should be used for the lifetime of the store. - - @returns DS.Adapter - */ - _adapter: Ember.computed(function() { - var adapter = get(this, 'adapter'); - if (typeof adapter === 'string') { - adapter = get(this, adapter, false) || get(Ember.lookup, adapter); - } - - if (DS.Adapter.detect(adapter)) { - adapter = adapter.create(); - } - - return adapter; - }).property('adapter'), - - /** - @private - - A monotonically increasing number to be used to uniquely identify - data and records. - - It starts at 1 so other parts of the code can test for truthiness - when provided a `clientId` instead of having to explicitly test - for undefined. - */ - clientIdCounter: 1, - - // ..................... - // . CREATE NEW RECORD . - // ..................... - - /** - Create a new record in the current store. The properties passed - to this method are set on the newly created record. - - Note: The third `transaction` property is for internal use only. - If you want to create a record inside of a given transaction, - use `transaction.createRecord()` instead of `store.createRecord()`. - - @param {subclass of DS.Model} type - @param {Object} properties a hash of properties to set on the - newly created record. - @returns DS.Model - */ - createRecord: function(type, properties, transaction) { - properties = properties || {}; - - // Create a new instance of the model `type` and put it - // into the specified `transaction`. If no transaction is - // specified, the default transaction will be used. - var record = type._create({ - store: this - }); - - transaction = transaction || get(this, 'defaultTransaction'); - - // adoptRecord is an internal API that allows records to move - // into a transaction without assertions designed for app - // code. It is used here to ensure that regardless of new - // restrictions on the use of the public `transaction.add()` - // API, we will always be able to insert new records into - // their transaction. - transaction.adoptRecord(record); - - // `id` is a special property that may not be a `DS.attr` - var id = properties.id; - - // If the passed properties do not include a primary key, - // give the adapter an opportunity to generate one. Typically, - // client-side ID generators will use something like uuid.js - // to avoid conflicts. - var adapter; - if (Ember.isNone(id)) { - adapter = this.adapterForType(type); - if (adapter && adapter.generateIdForRecord) { - id = coerceId(adapter.generateIdForRecord(this, record)); - properties.id = id; - } - } - - id = coerceId(id); - - // Create a new `clientId` and associate it with the - // specified (or generated) `id`. Since we don't have - // any data for the server yet (by definition), store - // the sentinel value CREATED as the data for this - // clientId. If we see this value later, we will skip - // materialization. - var clientId = this.pushData(CREATED, id, type); - - // Now that we have a clientId, attach it to the record we - // just created. - set(record, 'clientId', clientId); - - // Move the record out of its initial `empty` state into - // the `loaded` state. - record.loadedData(); - - // Make sure the data is set up so the record doesn't - // try to materialize its nonexistent data. - record.setupData(); - - // Store the record we just created in the record cache for - // this clientId. - this.recordCache[clientId] = record; - - // Set the properties specified on the record. - record.setProperties(properties); - - // Resolve record promise - Ember.run(record, 'resolve', record); - - return record; - }, - - // ................. - // . DELETE RECORD . - // ................. - - /** - For symmetry, a record can be deleted via the store. - - @param {DS.Model} record - */ - deleteRecord: function(record) { - record.deleteRecord(); - }, - - /** - For symmetry, a record can be unloaded via the store. - - @param {DS.Model} record - */ - unloadRecord: function(record) { - record.unloadRecord(); - }, - - // ................ - // . FIND RECORDS . - // ................ - - /** - This is the main entry point into finding records. The first parameter to - this method is always a subclass of `DS.Model`. - - You can use the `find` method on a subclass of `DS.Model` directly if your - application only has one store. For example, instead of - `store.find(App.Person, 1)`, you could say `App.Person.find(1)`. - - --- - - To find a record by ID, pass the `id` as the second parameter: - - store.find(App.Person, 1); - App.Person.find(1); - - If the record with that `id` had not previously been loaded, the store will - return an empty record immediately and ask the adapter to find the data by - calling the adapter's `find` method. - - The `find` method will always return the same object for a given type and - `id`. To check whether the adapter has populated a record, you can check - its `isLoaded` property. - - --- - - To find all records for a type, call `find` with no additional parameters: - - store.find(App.Person); - App.Person.find(); - - This will return a `RecordArray` representing all known records for the - given type and kick off a request to the adapter's `findAll` method to load - any additional records for the type. - - The `RecordArray` returned by `find()` is live. If any more records for the - type are added at a later time through any mechanism, it will automatically - update to reflect the change. - - --- - - To find a record by a query, call `find` with a hash as the second - parameter: - - store.find(App.Person, { page: 1 }); - App.Person.find({ page: 1 }); - - This will return a `RecordArray` immediately, but it will always be an - empty `RecordArray` at first. It will call the adapter's `findQuery` - method, which will populate the `RecordArray` once the server has returned - results. - - You can check whether a query results `RecordArray` has loaded by checking - its `isLoaded` property. - */ - find: function(type, id) { - if (id === undefined) { - return this.findAll(type); - } - - // We are passed a query instead of an id. - if (Ember.typeOf(id) === 'object') { - return this.findQuery(type, id); - } - - return this.findById(type, coerceId(id)); - }, - - /** - @private - - This method returns a record for a given type and id combination. - - If the store has never seen this combination of type and id before, it - creates a new `clientId` with the LOADING sentinel and asks the adapter to - load the data. - - If the store has seen the combination, this method delegates to - `getByReference`. - */ - findById: function(type, id) { - var clientId = this.typeMapFor(type).idToCid[id]; - - if (clientId) { - return this.findByClientId(type, clientId); - } - - clientId = this.pushData(LOADING, id, type); - - // create a new instance of the model type in the - // 'isLoading' state - var record = this.materializeRecord(type, clientId, id); - - // let the adapter set the data, possibly async - var adapter = this.adapterForType(type); - if (adapter && adapter.find) { adapter.find(this, type, id); } - else { throw "Adapter is either null or does not implement `find` method"; } - - return record; - }, - - reloadRecord: function(record) { - var type = record.constructor, - adapter = this.adapterForType(type), - id = get(record, 'id'); - - Ember.assert("You cannot update a record without an ID", id); - Ember.assert("You tried to update a record but you have no adapter (for " + type + ")", adapter); - Ember.assert("You tried to update a record but your adapter does not implement `find`", adapter.find); - - adapter.find(this, type, id); - }, - - /** - @private - - This method returns a record for a given clientId. - - If there is no record object yet for the clientId, this method materializes - a new record object. This allows adapters to eagerly load large amounts of - data into the store, and avoid incurring the cost to create the objects - until they are requested. - - Several parts of Ember Data call this method: - - * findById, if a clientId already exists for a given type and - id combination - * OneToManyChange, which is backed by clientIds, when getChild, - getOldParent or getNewParent are called - * RecordArray, which is backed by clientIds, when an object at - a particular index is looked up - - In short, it's a convenient way to get a record for a known - clientId, materializing it if necessary. - - @param {Class} type - @param {Number|String} clientId - */ - findByClientId: function(type, clientId) { - var cidToData, record, id; - - record = this.recordCache[clientId]; - - if (!record) { - // create a new instance of the model type in the - // 'isLoading' state - id = this.clientIdToId[clientId]; - record = this.materializeRecord(type, clientId, id); - - cidToData = this.clientIdToData; - - if (typeof cidToData[clientId] === 'object') { - record.loadedData(); - } - } - - return record; - }, - - /** - @private - - Given a type and array of `clientId`s, determines which of those - `clientId`s has not yet been loaded. - - In preparation for loading, this method also marks any unloaded - `clientId`s as loading. - */ - neededReferences: function(type, references) { - var neededReferences = [], - cidToData = this.clientIdToData, - reference; - - for (var i=0, l=references.length; i<l; i++) { - reference = references[i]; - - if (cidToData[reference.clientId] === UNLOADED) { - neededReferences.push(reference); - cidToData[reference.clientId] = LOADING; - } - } - - return neededReferences; - }, - - /** - @private - - This method is the entry point that relationships use to update - themselves when their underlying data changes. - - First, it determines which of its `clientId`s are still unloaded, - then converts the needed `clientId`s to IDs and invokes `findMany` - on the adapter. - */ - fetchUnloadedReferences: function(type, references, owner) { - var neededReferences = this.neededReferences(type, references); - this.fetchMany(type, neededReferences, owner); - }, - - /** - @private - - This method takes a type and list of `clientId`s, converts the - `clientId`s into IDs, and then invokes the adapter's `findMany` - method. - - It is used both by a brand new relationship (via the `findMany` - method) or when the data underlying an existing relationship - changes (via the `fetchUnloadedReferences` method). - */ - fetchMany: function(type, references, owner) { - if (!references.length) { return; } - - var ids = map(references, function(reference) { - return reference.id; - }); - - var adapter = this.adapterForType(type); - if (adapter && adapter.findMany) { adapter.findMany(this, type, ids, owner); } - else { throw "Adapter is either null or does not implement `findMany` method"; } - }, - - referenceForId: function(type, id) { - var clientId = this.clientIdForId(type, id); - return this.referenceForClientId(clientId); - }, - - referenceForClientId: function(clientId) { - var references = this.recordReferences; - - if (references[clientId]) { - return references[clientId]; - } - - var type = this.clientIdToType[clientId]; - - return references[clientId] = { - id: this.idForClientId(clientId), - clientId: clientId, - type: type - }; - }, - - recordForReference: function(reference) { - return this.findByClientId(reference.type, reference.clientId); - }, - - /** - @private - - `findMany` is the entry point that relationships use to generate a - new `ManyArray` for the list of IDs specified by the server for - the relationship. - - Its responsibilities are: - - * convert the IDs into clientIds - * determine which of the clientIds still need to be loaded - * create a new ManyArray whose content is *all* of the clientIds - * notify the ManyArray of the number of its elements that are - already loaded - * insert the unloaded clientIds into the `loadingRecordArrays` - bookkeeping structure, which will allow the `ManyArray` to know - when all of its loading elements are loaded from the server. - * ask the adapter to load the unloaded elements, by invoking - findMany with the still-unloaded IDs. - */ - findMany: function(type, ids, record, relationship) { - // 1. Convert ids to client ids - // 2. Determine which of the client ids need to be loaded - // 3. Create a new ManyArray whose content is ALL of the clientIds - // 4. Decrement the ManyArray's counter by the number of loaded clientIds - // 5. Put the ManyArray into our bookkeeping data structure, keyed on - // the needed clientIds - // 6. Ask the adapter to load the records for the unloaded clientIds (but - // convert them back to ids) - - if (!Ember.isArray(ids)) { - var adapter = this.adapterForType(type); - if (adapter && adapter.findHasMany) { adapter.findHasMany(this, record, relationship, ids); } - else if (ids !== undefined) { throw fmt("Adapter is either null or does not implement `findHasMany` method", this); } - - return this.createManyArray(type, Ember.A()); - } - - // Coerce server IDs into Record Reference - var references = map(ids, function(reference) { - if (typeof reference !== 'object' && reference !== null) { - return this.referenceForId(type, reference); - } - - return reference; - }, this); - - var neededReferences = this.neededReferences(type, references), - manyArray = this.createManyArray(type, Ember.A(references)), - loadingRecordArrays = this.loadingRecordArrays, - reference, clientId, i, l; - - // Start the decrementing counter on the ManyArray at the number of - // records we need to load from the adapter - manyArray.loadingRecordsCount(neededReferences.length); - - if (neededReferences.length) { - for (i=0, l=neededReferences.length; i<l; i++) { - reference = neededReferences[i]; - clientId = reference.clientId; - - // keep track of the record arrays that a given loading record - // is part of. This way, if the same record is in multiple - // ManyArrays, all of their loading records counters will be - // decremented when the adapter provides the data. - if (loadingRecordArrays[clientId]) { - loadingRecordArrays[clientId].push(manyArray); - } else { - this.loadingRecordArrays[clientId] = [ manyArray ]; - } - } - - this.fetchMany(type, neededReferences, record); - } else { - // all requested records are available - manyArray.set('isLoaded', true); - - Ember.run.once(function() { - manyArray.trigger('didLoad'); - }); - } - - return manyArray; - }, - - /** - @private - - This method delegates a query to the adapter. This is the one place where - adapter-level semantics are exposed to the application. - - Exposing queries this way seems preferable to creating an abstract query - language for all server-side queries, and then require all adapters to - implement them. - - @param {Class} type - @param {Object} query an opaque query to be used by the adapter - @return {DS.AdapterPopulatedRecordArray} - */ - findQuery: function(type, query) { - var array = DS.AdapterPopulatedRecordArray.create({ type: type, query: query, content: Ember.A([]), store: this }); - var adapter = this.adapterForType(type); - if (adapter && adapter.findQuery) { adapter.findQuery(this, type, query, array); } - else { throw "Adapter is either null or does not implement `findQuery` method"; } - return array; - }, - - /** - @private - - This method returns an array of all records adapter can find. - It triggers the adapter's `findAll` method to give it an opportunity to populate - the array with records of that type. - - @param {Class} type - @return {DS.AdapterPopulatedRecordArray} - */ - findAll: function(type) { - var array = this.all(type); - this.fetchAll(type, array); - return array; - }, - - /** - @private - */ - fetchAll: function(type, array) { - var sinceToken = this.typeMapFor(type).sinceToken, - adapter = this.adapterForType(type); - - set(array, 'isUpdating', true); - - if (adapter && adapter.findAll) { adapter.findAll(this, type, sinceToken); } - else { throw "Adapter is either null or does not implement `findAll` method"; } - }, - - /** - */ - sinceForType: function(type, sinceToken) { - this.typeMapFor(type).sinceToken = sinceToken; - }, - - /** - */ - didUpdateAll: function(type) { - var findAllCache = this.typeMapFor(type).findAllCache; - set(findAllCache, 'isUpdating', false); - }, - - /** - This method returns a filtered array that contains all of the known records - for a given type. - - Note that because it's just a filter, it will have any locally - created records of the type. - - Also note that multiple calls to `all` for a given type will always - return the same RecordArray. - - @param {Class} type - @return {DS.RecordArray} - */ - all: function(type) { - var typeMap = this.typeMapFor(type), - findAllCache = typeMap.findAllCache; - - if (findAllCache) { return findAllCache; } - - var array = DS.RecordArray.create({ type: type, content: Ember.A([]), store: this, isLoaded: true }); - this.registerRecordArray(array, type); - - typeMap.findAllCache = array; - return array; - }, - - /** - Takes a type and filter function, and returns a live RecordArray that - remains up to date as new records are loaded into the store or created - locally. - - The callback function takes a materialized record, and returns true - if the record should be included in the filter and false if it should - not. - - The filter function is called once on all records for the type when - it is created, and then once on each newly loaded or created record. - - If any of a record's properties change, or if it changes state, the - filter function will be invoked again to determine whether it should - still be in the array. - - Note that the existence of a filter on a type will trigger immediate - materialization of all loaded data for a given type, so you might - not want to use filters for a type if you are loading many records - into the store, many of which are not active at any given time. - - In this scenario, you might want to consider filtering the raw - data before loading it into the store. - - @param {Class} type - @param {Function} filter - - @return {DS.FilteredRecordArray} - */ - filter: function(type, query, filter) { - // allow an optional server query - if (arguments.length === 3) { - this.findQuery(type, query); - } else if (arguments.length === 2) { - filter = query; - } - - var array = DS.FilteredRecordArray.create({ type: type, content: Ember.A([]), store: this, filterFunction: filter }); - - this.registerRecordArray(array, type, filter); - - return array; - }, - - /** - This method returns if a certain record is already loaded - in the store. Use this function to know beforehand if a find() - will result in a request or that it will be a cache hit. - - @param {Class} type - @param {string} id - @return {boolean} - */ - recordIsLoaded: function(type, id) { - return !Ember.isNone(this.typeMapFor(type).idToCid[id]); - }, - - // ............ - // . UPDATING . - // ............ - - /** - @private - - If the adapter updates attributes or acknowledges creation - or deletion, the record will notify the store to update its - membership in any filters. - - To avoid thrashing, this method is invoked only once per - run loop per record. - - @param {Class} type - @param {Number|String} clientId - @param {DS.Model} record - */ - dataWasUpdated: function(type, reference, record) { - // Because data updates are invoked at the end of the run loop, - // it is possible that a record might be deleted after its data - // has been modified and this method was scheduled to be called. - // - // If that's the case, the record would have already been removed - // from all record arrays; calling updateRecordArrays would just - // add it back. If the record is deleted, just bail. It shouldn't - // give us any more trouble after this. - - if (get(record, 'isDeleted')) { return; } - - var cidToData = this.clientIdToData, - clientId = reference.clientId, - data = cidToData[clientId]; - - if (typeof data === "object") { - this.updateRecordArrays(type, clientId); - } - }, - - // ................. - // . BASIC ADAPTER . - // ................. - - scheduleSave: function(record) { - this._recordsToSave.add(record); - Ember.run.once(this, 'flushSavedRecords'); - }, - - flushSavedRecords: function() { - var created = Ember.OrderedSet.create(); - var updated = Ember.OrderedSet.create(); - var deleted = Ember.OrderedSet.create(); - - this._recordsToSave.forEach(function(record) { - if (get(record, 'isNew')) { - created.add(record); - } else if (get(record, 'isDeleted')) { - deleted.add(record); - } else { - updated.add(record); - } - }); - - this._recordsToSave.clear(); - - get(this, '_adapter').commit(this, { - created: created, - updated: updated, - deleted: deleted - }); - }, - - // .............. - // . PERSISTING . - // .............. - - /** - This method delegates committing to the store's implicit - transaction. - - Calling this method is essentially a request to persist - any changes to records that were not explicitly added to - a transaction. - */ - commit: function() { - get(this, 'defaultTransaction').commit(); - }, - - /** - Adapters should call this method if they would like to acknowledge - that all changes related to a record (other than relationship - changes) have persisted. - - Because relationship changes affect multiple records, the adapter - is responsible for acknowledging the change to the relationship - directly (using `store.didUpdateRelationship`) when all aspects - of the relationship change have persisted. - - It can be called for created, deleted or updated records. - - If the adapter supplies new data, that data will become the new - canonical data for the record. That will result in blowing away - all local changes and rematerializing the record with the new - data (the "sledgehammer" approach). - - Alternatively, if the adapter does not supply new data, the record - will collapse all local changes into its saved data. Subsequent - rollbacks of the record will roll back to this point. - - If an adapter is acknowledging receipt of a newly created record - that did not generate an id in the client, it *must* either - provide data or explicitly invoke `store.didReceiveId` with - the server-provided id. - - Note that an adapter may not supply new data when acknowledging - a deleted record. - - @see DS.Store#didUpdateRelationship - - @param {DS.Model} record the in-flight record - @param {Object} data optional data (see above) - */ - didSaveRecord: function(record, data) { - record.adapterDidCommit(); - - if (data) { - this.updateId(record, data); - this.updateRecordData(record, data); - } else { - this.didUpdateAttributes(record); - } - }, - - /** - For convenience, if an adapter is performing a bulk commit, it can also - acknowledge all of the records at once. - - If the adapter supplies an array of data, they must be in the same order as - the array of records passed in as the first parameter. - - @param {#forEach} list a list of records whose changes the - adapter is acknowledging. You can pass any object that - has an ES5-like `forEach` method, including the - `OrderedSet` objects passed into the adapter at commit - time. - @param {Array[Object]} dataList an Array of data. This - parameter must be an integer-indexed Array-like. - */ - didSaveRecords: function(list, dataList) { - var i = 0; - list.forEach(function(record) { - this.didSaveRecord(record, dataList && dataList[i++]); - }, this); - }, - - /** - This method allows the adapter to specify that a record - could not be saved because it had backend-supplied validation - errors. - - The errors object must have keys that correspond to the - attribute names. Once each of the specified attributes have - changed, the record will automatically move out of the - invalid state and be ready to commit again. - - TODO: We should probably automate the process of converting - server names to attribute names using the existing serializer - infrastructure. - - @param {DS.Model} record - @param {Object} errors - */ - recordWasInvalid: function(record, errors) { - record.adapterDidInvalidate(errors); - }, - - /** - This method allows the adapter to specify that a record - could not be saved because the server returned an unhandled - error. - - @param {DS.Model} record - */ - recordWasError: function(record) { - record.adapterDidError(); - }, - - /** - This is a lower-level API than `didSaveRecord` that allows an - adapter to acknowledge the persistence of a single attribute. - - This is useful if an adapter needs to make multiple asynchronous - calls to fully persist a record. The record will keep track of - which attributes and relationships are still outstanding and - automatically move into the `saved` state once the adapter has - acknowledged everything. - - If a value is provided, it clobbers the locally specified value. - Otherwise, the local value becomes the record's last known - saved value (which is used when rolling back a record). - - Note that the specified attributeName is the normalized name - specified in the definition of the `DS.Model`, not a key in - the server-provided data. - - Also note that the adapter is responsible for performing any - transformations on the value using the serializer API. - - @param {DS.Model} record - @param {String} attributeName - @param {Object} value - */ - didUpdateAttribute: function(record, attributeName, value) { - record.adapterDidUpdateAttribute(attributeName, value); - }, - - /** - This method allows an adapter to acknowledge persistence - of all attributes of a record but not relationships or - other factors. - - It loops through the record's defined attributes and - notifies the record that they are all acknowledged. - - This method does not take optional values, because - the adapter is unlikely to have a hash of normalized - keys and transformed values, and instead of building - one up, it should just call `didUpdateAttribute` as - needed. - - This method is intended as a middle-ground between - `didSaveRecord`, which acknowledges all changes to - a record, and `didUpdateAttribute`, which allows an - adapter fine-grained control over updates. - - @param {DS.Model} record - */ - didUpdateAttributes: function(record) { - record.eachAttribute(function(attributeName) { - this.didUpdateAttribute(record, attributeName); - }, this); - }, - - /** - This allows an adapter to acknowledge that it has saved all - necessary aspects of a relationship change. - - This is separated from acknowledging the record itself - (via `didSaveRecord`) because a relationship change can - involve as many as three separate records. Records should - only move out of the in-flight state once the server has - acknowledged all of their relationships, and this differs - based upon the adapter's semantics. - - There are three basic scenarios by which an adapter can - save a relationship. - - ### Foreign Key - - An adapter can save all relationship changes by updating - a foreign key on the child record. If it does this, it - should acknowledge the changes when the child record is - saved. - - record.eachRelationship(function(name, meta) { - if (meta.kind === 'belongsTo') { - store.didUpdateRelationship(record, name); - } - }); - - store.didSaveRecord(record, data); - - ### Embedded in Parent - - An adapter can save one-to-many relationships by embedding - IDs (or records) in the parent object. In this case, the - relationship is not considered acknowledged until both the - old parent and new parent have acknowledged the change. - - In this case, the adapter should keep track of the old - parent and new parent, and acknowledge the relationship - change once both have acknowledged. If one of the two - sides does not exist (e.g. the new parent does not exist - because of nulling out the belongs-to relationship), - the adapter should acknowledge the relationship once - the other side has acknowledged. - - ### Separate Entity - - An adapter can save relationships as separate entities - on the server. In this case, they should acknowledge - the relationship as saved once the server has - acknowledged the entity. - - @see DS.Store#didSaveRecord - - @param {DS.Model} record - @param {DS.Model} relationshipName - */ - didUpdateRelationship: function(record, relationshipName) { - var relationship = this.relationshipChangeFor(get(record, 'clientId'), relationshipName); - //TODO(Igor) - if (relationship) { relationship.adapterDidUpdate(); } - }, - - /** - This allows an adapter to acknowledge all relationship changes - for a given record. - - Like `didUpdateAttributes`, this is intended as a middle ground - between `didSaveRecord` and fine-grained control via the - `didUpdateRelationship` API. - */ - didUpdateRelationships: function(record) { - var changes = this.relationshipChangesFor(get(record, '_reference')); - - for (var name in changes) { - if (!changes.hasOwnProperty(name)) { continue; } - changes[name].adapterDidUpdate(); - } - }, - - /** - When acknowledging the creation of a locally created record, - adapters must supply an id (if they did not implement - `generateIdForRecord` to generate an id locally). - - If an adapter does not use `didSaveRecord` and supply a hash - (for example, if it needs to make multiple HTTP requests to - create and then update the record), it will need to invoke - `didReceiveId` with the backend-supplied id. - - When not using `didSaveRecord`, an adapter will need to - invoke: - - * didReceiveId (unless the id was generated locally) - * didCreateRecord - * didUpdateAttribute(s) - * didUpdateRelationship(s) - - @param {DS.Model} record - @param {Number|String} id - */ - didReceiveId: function(record, id) { - var typeMap = this.typeMapFor(record.constructor), - clientId = get(record, 'clientId'), - oldId = get(record, 'id'); - - Ember.assert("An adapter cannot assign a new id to a record that already has an id. " + record + " had id: " + oldId + " and you tried to update it with " + id + ". This likely happened because your server returned data in response to a find or update that had a different id than the one you sent.", oldId === undefined || id === oldId); - - typeMap.idToCid[id] = clientId; - this.clientIdToId[clientId] = id; - }, - - /** - @private - - This method re-indexes the data by its clientId in the store - and then notifies the record that it should rematerialize - itself. - - @param {DS.Model} record - @param {Object} data - */ - updateRecordData: function(record, data) { - var clientId = get(record, 'clientId'), - cidToData = this.clientIdToData; - - cidToData[clientId] = data; - - record.didChangeData(); - }, - - /** - @private - - If an adapter invokes `didSaveRecord` with data, this method - extracts the id from the supplied data (using the adapter's - `extractId()` method) and indexes the clientId with that id. - - @param {DS.Model} record - @param {Object} data - */ - updateId: function(record, data) { - var typeMap = this.typeMapFor(record.constructor), - clientId = get(record, 'clientId'), - oldId = get(record, 'id'), - type = record.constructor, - id = this.preprocessData(type, data); - - Ember.assert("An adapter cannot assign a new id to a record that already has an id. " + record + " had id: " + oldId + " and you tried to update it with " + id + ". This likely happened because your server returned data in response to a find or update that had a different id than the one you sent.", oldId === null || id === oldId); - - typeMap.idToCid[id] = clientId; - this.clientIdToId[clientId] = id; - this.referenceForClientId(clientId).id = id; - }, - - /** - @private - - This method receives opaque data provided by the adapter and - preprocesses it, returning an ID. - - The actual preprocessing takes place in the adapter. If you would - like to change the default behavior, you should override the - appropriate hooks in `DS.Serializer`. - - @see {DS.Serializer} - @return {String} id the id represented by the data - */ - preprocessData: function(type, data) { - return this.adapterForType(type).extractId(type, data); - }, - - // ................. - // . RECORD ARRAYS . - // ................. - - /** - @private - - Register a RecordArray for a given type to be backed by - a filter function. This will cause the array to update - automatically when records of that type change attribute - values or states. - - @param {DS.RecordArray} array - @param {Class} type - @param {Function} filter - */ - registerRecordArray: function(array, type, filter) { - var recordArrays = this.typeMapFor(type).recordArrays; - - recordArrays.push(array); - - this.updateRecordArrayFilter(array, type, filter); - }, - - /** - @private - - Create a `DS.ManyArray` for a type and list of clientIds - and index the `ManyArray` under each clientId. This allows - us to efficiently remove records from `ManyArray`s when - they are deleted. - - @param {Class} type - @param {Array} clientIds - - @return {DS.ManyArray} - */ - createManyArray: function(type, clientIds) { - var array = DS.ManyArray.create({ type: type, content: clientIds, store: this }); - - clientIds.forEach(function(clientId) { - var recordArrays = this.recordArraysForClientId(clientId); - recordArrays.add(array); - }, this); - - return array; - }, - - /** - @private - - This method is invoked if the `filterFunction` property is - changed on a `DS.FilteredRecordArray`. - - It essentially re-runs the filter from scratch. This same - method is invoked when the filter is created in th first place. - */ - updateRecordArrayFilter: function(array, type, filter) { - var typeMap = this.typeMapFor(type), - cidToData = this.clientIdToData, - clientIds = typeMap.clientIds, - clientId, data, shouldFilter, record; - - for (var i=0, l=clientIds.length; i<l; i++) { - clientId = clientIds[i]; - shouldFilter = false; - - data = cidToData[clientId]; - - if (typeof data === 'object') { - if (record = this.recordCache[clientId]) { - if (!get(record, 'isDeleted')) { shouldFilter = true; } - } else { - shouldFilter = true; - } - - if (shouldFilter) { - this.updateRecordArray(array, filter, type, clientId); - } - } - } - }, - - updateRecordArraysLater: function(type, clientId) { - Ember.run.once(this, function() { - this.updateRecordArrays(type, clientId); - }); - }, - - /** - @private - - This method is invoked whenever data is loaded into the store - by the adapter or updated by the adapter, or when an attribute - changes on a record. - - It updates all filters that a record belongs to. - - To avoid thrashing, it only runs once per run loop per record. - - @param {Class} type - @param {Number|String} clientId - */ - updateRecordArrays: function(type, clientId) { - var recordArrays = this.typeMapFor(type).recordArrays, - filter; - - recordArrays.forEach(function(array) { - filter = get(array, 'filterFunction'); - this.updateRecordArray(array, filter, type, clientId); - }, this); - - // loop through all manyArrays containing an unloaded copy of this - // clientId and notify them that the record was loaded. - var manyArrays = this.loadingRecordArrays[clientId]; - - if (manyArrays) { - for (var i=0, l=manyArrays.length; i<l; i++) { - manyArrays[i].loadedRecord(); - } - - this.loadingRecordArrays[clientId] = null; - } - }, - - /** - @private - - Update an individual filter. - - @param {DS.FilteredRecordArray} array - @param {Function} filter - @param {Class} type - @param {Number|String} clientId - */ - updateRecordArray: function(array, filter, type, clientId) { - var shouldBeInArray, record; - - if (!filter) { - shouldBeInArray = true; - } else { - record = this.findByClientId(type, clientId); - shouldBeInArray = filter(record); - } - - var content = get(array, 'content'); - - var recordArrays = this.recordArraysForClientId(clientId); - var reference = this.referenceForClientId(clientId); - - if (shouldBeInArray) { - recordArrays.add(array); - array.addReference(reference); - } else if (!shouldBeInArray) { - recordArrays.remove(array); - array.removeReference(reference); - } - }, - - /** - @private - - When a record is deleted, it is removed from all its - record arrays. - - @param {DS.Model} record - */ - removeFromRecordArrays: function(record) { - var reference = get(record, '_reference'); - var recordArrays = this.recordArraysForClientId(reference.clientId); - - recordArrays.forEach(function(array) { - array.removeReference(reference); - }); - }, - - // ............ - // . INDEXING . - // ............ - - /** - @private - - Return a list of all `DS.RecordArray`s a clientId is - part of. - - @return {Object(clientId: Ember.OrderedSet)} - */ - recordArraysForClientId: function(clientId) { - var recordArrays = get(this, 'recordArraysByClientId'); - var ret = recordArrays[clientId]; - - if (!ret) { - ret = recordArrays[clientId] = Ember.OrderedSet.create(); - } - - return ret; - }, - - typeMapFor: function(type) { - var typeMaps = get(this, 'typeMaps'); - var guidForType = Ember.guidFor(type); - - var typeMap = typeMaps[guidForType]; - - if (typeMap) { - return typeMap; - } else { - return (typeMaps[guidForType] = - { - idToCid: {}, - clientIds: [], - recordArrays: [] - }); - } - }, - - /** @private - - For a given type and id combination, returns the client id used by the store. - If no client id has been assigned yet, one will be created and returned. - - @param {DS.Model} type - @param {String|Number} id - */ - clientIdForId: function(type, id) { - id = coerceId(id); - - var clientId = this.typeMapFor(type).idToCid[id]; - if (clientId !== undefined) { return clientId; } - - return this.pushData(UNLOADED, id, type); - }, - - /** - @private - - This method works exactly like `clientIdForId`, but does not - require looking up the `typeMap` for every `clientId` and - invoking a method per `clientId`. - */ - clientIdsForIds: function(type, ids) { - var typeMap = this.typeMapFor(type), - idToClientIdMap = typeMap.idToCid; - - return map(ids, function(id) { - id = coerceId(id); - - var clientId = idToClientIdMap[id]; - if (clientId) { return clientId; } - return this.pushData(UNLOADED, id, type); - }, this); - }, - - typeForClientId: function(clientId) { - return this.clientIdToType[clientId]; - }, - - idForClientId: function(clientId) { - return this.clientIdToId[clientId]; - }, - - // ................ - // . LOADING DATA . - // ................ - - /** - Load new data into the store for a given id and type combination. - If data for that record had been loaded previously, the new information - overwrites the old. - - If the record you are loading data for has outstanding changes that have not - yet been saved, an exception will be thrown. - - @param {DS.Model} type - @param {String|Number} id - @param {Object} data the data to load - */ - load: function(type, data, prematerialized) { - var id; - - if (typeof data === 'number' || typeof data === 'string') { - id = data; - data = prematerialized; - prematerialized = null; - } - - if (prematerialized && prematerialized.id) { - id = prematerialized.id; - } else if (id === undefined) { - id = this.preprocessData(type, data); - } - - id = coerceId(id); - - var typeMap = this.typeMapFor(type), - cidToData = this.clientIdToData, - clientId = typeMap.idToCid[id], - cidToPrematerialized = this.clientIdToPrematerializedData; - - if (clientId !== undefined) { - cidToData[clientId] = data; - cidToPrematerialized[clientId] = prematerialized; - - var record = this.recordCache[clientId]; - if (record) { - once(record, 'loadedData'); - } - } else { - clientId = this.pushData(data, id, type); - cidToPrematerialized[clientId] = prematerialized; - } - - this.updateRecordArraysLater(type, clientId); - - return this.referenceForClientId(clientId); - }, - - prematerialize: function(reference, prematerialized) { - this.clientIdToPrematerializedData[reference.clientId] = prematerialized; - }, - - loadMany: function(type, ids, dataList) { - if (dataList === undefined) { - dataList = ids; - ids = map(dataList, function(data) { - return this.preprocessData(type, data); - }, this); - } - - return map(ids, function(id, i) { - return this.load(type, id, dataList[i]); - }, this); - }, - - loadHasMany: function(record, key, ids) { - record.materializeHasMany(key, ids); - - // Update any existing many arrays that use the previous IDs, - // if necessary. - record.hasManyDidChange(key); - - var relationship = record.cacheFor(key); - - // TODO (tomdale) this assumes that loadHasMany *always* means - // that the records for the provided IDs are loaded. - if (relationship) { - set(relationship, 'isLoaded', true); - relationship.trigger('didLoad'); - } - }, - - /** @private - - Stores data for the specified type and id combination and returns - the client id. - - @param {Object} data - @param {String|Number} id - @param {DS.Model} type - @returns {Number} - */ - pushData: function(data, id, type) { - var typeMap = this.typeMapFor(type); - - var idToClientIdMap = typeMap.idToCid, - clientIdToIdMap = this.clientIdToId, - clientIdToTypeMap = this.clientIdToType, - clientIds = typeMap.clientIds, - cidToData = this.clientIdToData; - - Ember.assert('The id ' + id + ' has already been used with another record of type ' + type.toString() + '.', !id || !idToClientIdMap[id]); - - var clientId = ++this.clientIdCounter; - - cidToData[clientId] = data; - clientIdToTypeMap[clientId] = type; - - // if we're creating an item, this process will be done - // later, once the object has been persisted. - if (id) { - idToClientIdMap[id] = clientId; - clientIdToIdMap[clientId] = id; - } - - clientIds.push(clientId); - - return clientId; - }, - - // .......................... - // . RECORD MATERIALIZATION . - // .......................... - - materializeRecord: function(type, clientId, id) { - var record; - - this.recordCache[clientId] = record = type._create({ - store: this, - clientId: clientId - }); - - set(record, 'id', id); - - get(this, 'defaultTransaction').adoptRecord(record); - - record.loadingData(); - return record; - }, - - dematerializeRecord: function(record) { - var id = get(record, 'id'), - clientId = get(record, 'clientId'), - type = this.typeForClientId(clientId), - typeMap = this.typeMapFor(type); - - record.updateRecordArrays(); - - delete this.recordCache[clientId]; - delete this.clientIdToId[clientId]; - delete this.clientIdToType[clientId]; - delete this.clientIdToData[clientId]; - delete this.recordArraysByClientId[clientId]; - - if (id) { delete typeMap.idToCid[id]; } - }, - - willDestroy: function() { - if (get(DS, 'defaultStore') === this) { - set(DS, 'defaultStore', null); - } - }, - - // ........................ - // . RELATIONSHIP CHANGES . - // ........................ - - addRelationshipChangeFor: function(clientReference, childKey, parentReference, parentKey, change) { - var clientId = clientReference.clientId, - parentClientId = parentReference ? parentReference.clientId : parentReference; - var key = childKey + parentKey; - var changes = this.relationshipChanges; - if (!(clientId in changes)) { - changes[clientId] = {}; - } - if (!(parentClientId in changes[clientId])) { - changes[clientId][parentClientId] = {}; - } - if (!(key in changes[clientId][parentClientId])) { - changes[clientId][parentClientId][key] = {}; - } - changes[clientId][parentClientId][key][change.changeType] = change; - }, - - removeRelationshipChangeFor: function(clientReference, childKey, parentReference, parentKey, type) { - var clientId = clientReference.clientId, - parentClientId = parentReference ? parentReference.clientId : parentReference; - var changes = this.relationshipChanges; - var key = childKey + parentKey; - if (!(clientId in changes) || !(parentClientId in changes[clientId]) || !(key in changes[clientId][parentClientId])){ - return; - } - delete changes[clientId][parentClientId][key][type]; - }, - - relationshipChangeFor: function(clientId, childKey, parentClientId, parentKey, type) { - var changes = this.relationshipChanges; - var key = childKey + parentKey; - if (!(clientId in changes) || !(parentClientId in changes[clientId])){ - return; - } - if(type){ - return changes[clientId][parentClientId][key][type]; - } - else{ - //TODO(Igor) what if both present - return changes[clientId][parentClientId][key]["add"] || changes[clientId][parentClientId][key]["remove"]; - } - }, - - relationshipChangePairsFor: function(reference){ - var toReturn = []; - - if( !reference ) { return toReturn; } - - //TODO(Igor) What about the other side - var changesObject = this.relationshipChanges[reference.clientId]; - for (var objKey in changesObject){ - if(changesObject.hasOwnProperty(objKey)){ - for (var changeKey in changesObject[objKey]){ - if(changesObject[objKey].hasOwnProperty(changeKey)){ - toReturn.push(changesObject[objKey][changeKey]); - } - } - } - } - return toReturn; - }, - - relationshipChangesFor: function(reference) { - var toReturn = []; - - if( !reference ) { return toReturn; } - - var relationshipPairs = this.relationshipChangePairsFor(reference); - forEach(relationshipPairs, function(pair){ - var addedChange = pair["add"]; - var removedChange = pair["remove"]; - if(addedChange){ - toReturn.push(addedChange); - } - if(removedChange){ - toReturn.push(removedChange); - } - }); - return toReturn; - }, - // ...................... - // . PER-TYPE ADAPTERS - // ...................... - - adapterForType: function(type) { - this._adaptersMap = this.createInstanceMapFor('adapters'); - - var adapter = this._adaptersMap.get(type); - if (adapter) { return adapter; } - - return this.get('_adapter'); - }, - - // .............................. - // . RECORD CHANGE NOTIFICATION . - // .............................. - - recordAttributeDidChange: function(reference, attributeName, newValue, oldValue) { - var record = this.recordForReference(reference), - dirtySet = new Ember.OrderedSet(), - adapter = this.adapterForType(record.constructor); - - if (adapter.dirtyRecordsForAttributeChange) { - adapter.dirtyRecordsForAttributeChange(dirtySet, record, attributeName, newValue, oldValue); - } - - dirtySet.forEach(function(record) { - record.adapterDidDirty(); - }); - }, - - recordBelongsToDidChange: function(dirtySet, child, relationship) { - var adapter = this.adapterForType(child.constructor); - - if (adapter.dirtyRecordsForBelongsToChange) { - adapter.dirtyRecordsForBelongsToChange(dirtySet, child, relationship); - } - - // adapterDidDirty is called by the RelationshipChange that created - // the dirtySet. - }, - - recordHasManyDidChange: function(dirtySet, parent, relationship) { - var adapter = this.adapterForType(parent.constructor); - - if (adapter.dirtyRecordsForHasManyChange) { - adapter.dirtyRecordsForHasManyChange(dirtySet, parent, relationship); - } - - // adapterDidDirty is called by the RelationshipChange that created - // the dirtySet. - } -}); - -DS.Store.reopenClass({ - registerAdapter: DS._Mappable.generateMapFunctionFor('adapters', function(type, adapter, map) { - map.set(type, adapter); - }), - - transformMapKey: function(key) { - if (typeof key === 'string') { - var transformedKey; - transformedKey = get(Ember.lookup, key); - Ember.assert("Could not find model at path " + key, transformedKey); - return transformedKey; - } else { - return key; - } - }, - - transformMapValue: function(key, value) { - if (Ember.Object.detect(value)) { - return value.create(); - } - - return value; - } -}); - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set, - once = Ember.run.once, arrayMap = Ember.ArrayPolyfills.map; - -/** - This file encapsulates the various states that a record can transition - through during its lifecycle. - - ### State Manager - - A record's state manager explicitly tracks what state a record is in - at any given time. For instance, if a record is newly created and has - not yet been sent to the adapter to be saved, it would be in the - `created.uncommitted` state. If a record has had local modifications - made to it that are in the process of being saved, the record would be - in the `updated.inFlight` state. (These state paths will be explained - in more detail below.) - - Events are sent by the record or its store to the record's state manager. - How the state manager reacts to these events is dependent on which state - it is in. In some states, certain events will be invalid and will cause - an exception to be raised. - - States are hierarchical. For example, a record can be in the - `deleted.start` state, then transition into the `deleted.inFlight` state. - If a child state does not implement an event handler, the state manager - will attempt to invoke the event on all parent states until the root state is - reached. The state hierarchy of a record is described in terms of a path - string. You can determine a record's current state by getting its manager's - current state path: - - record.get('stateManager.currentPath'); - //=> "created.uncommitted" - - The `DS.Model` states are themselves stateless. What we mean is that, - though each instance of a record also has a unique instance of a - `DS.StateManager`, the hierarchical states that each of *those* points - to is a shared data structure. For performance reasons, instead of each - record getting its own copy of the hierarchy of states, each state - manager points to this global, immutable shared instance. How does a - state know which record it should be acting on? We pass a reference to - the current state manager as the first parameter to every method invoked - on a state. - - The state manager passed as the first parameter is where you should stash - state about the record if needed; you should never store data on the state - object itself. If you need access to the record being acted on, you can - retrieve the state manager's `record` property. For example, if you had - an event handler `myEvent`: - - myEvent: function(manager) { - var record = manager.get('record'); - record.doSomething(); - } - - For more information about state managers in general, see the Ember.js - documentation on `Ember.StateManager`. - - ### Events, Flags, and Transitions - - A state may implement zero or more events, flags, or transitions. - - #### Events - - Events are named functions that are invoked when sent to a record. The - state manager will first look for a method with the given name on the - current state. If no method is found, it will search the current state's - parent, and then its grandparent, and so on until reaching the top of - the hierarchy. If the root is reached without an event handler being found, - an exception will be raised. This can be very helpful when debugging new - features. - - Here's an example implementation of a state with a `myEvent` event handler: - - aState: DS.State.create({ - myEvent: function(manager, param) { - console.log("Received myEvent with "+param); - } - }) - - To trigger this event: - - record.send('myEvent', 'foo'); - //=> "Received myEvent with foo" - - Note that an optional parameter can be sent to a record's `send()` method, - which will be passed as the second parameter to the event handler. - - Events should transition to a different state if appropriate. This can be - done by calling the state manager's `transitionTo()` method with a path to the - desired state. The state manager will attempt to resolve the state path - relative to the current state. If no state is found at that path, it will - attempt to resolve it relative to the current state's parent, and then its - parent, and so on until the root is reached. For example, imagine a hierarchy - like this: - - * created - * start <-- currentState - * inFlight - * updated - * inFlight - - If we are currently in the `start` state, calling - `transitionTo('inFlight')` would transition to the `created.inFlight` state, - while calling `transitionTo('updated.inFlight')` would transition to - the `updated.inFlight` state. - - Remember that *only events* should ever cause a state transition. You should - never call `transitionTo()` from outside a state's event handler. If you are - tempted to do so, create a new event and send that to the state manager. - - #### Flags - - Flags are Boolean values that can be used to introspect a record's current - state in a more user-friendly way than examining its state path. For example, - instead of doing this: - - var statePath = record.get('stateManager.currentPath'); - if (statePath === 'created.inFlight') { - doSomething(); - } - - You can say: - - if (record.get('isNew') && record.get('isSaving')) { - doSomething(); - } - - If your state does not set a value for a given flag, the value will - be inherited from its parent (or the first place in the state hierarchy - where it is defined). - - The current set of flags are defined below. If you want to add a new flag, - in addition to the area below, you will also need to declare it in the - `DS.Model` class. - - #### Transitions - - Transitions are like event handlers but are called automatically upon - entering or exiting a state. To implement a transition, just call a method - either `enter` or `exit`: - - myState: DS.State.create({ - // Gets called automatically when entering - // this state. - enter: function(manager) { - console.log("Entered myState"); - } - }) - - Note that enter and exit events are called once per transition. If the - current state changes, but changes to another child state of the parent, - the transition event on the parent will not be triggered. -*/ - -var stateProperty = Ember.computed(function(key) { - var parent = get(this, 'parentState'); - if (parent) { - return get(parent, key); - } -}).property(); - -var hasDefinedProperties = function(object) { - for (var name in object) { - if (object.hasOwnProperty(name) && object[name]) { return true; } - } - - return false; -}; - -var didChangeData = function(manager) { - var record = get(manager, 'record'); - record.materializeData(); -}; - -var willSetProperty = function(manager, context) { - context.oldValue = get(get(manager, 'record'), context.name); - - var change = DS.AttributeChange.createChange(context); - get(manager, 'record')._changesToSync[context.attributeName] = change; -}; - -var didSetProperty = function(manager, context) { - var change = get(manager, 'record')._changesToSync[context.attributeName]; - change.value = get(get(manager, 'record'), context.name); - change.sync(); -}; - -DS.State = Ember.State.extend({ - isLoaded: stateProperty, - isReloading: stateProperty, - isDirty: stateProperty, - isSaving: stateProperty, - isDeleted: stateProperty, - isError: stateProperty, - isNew: stateProperty, - isValid: stateProperty, - - // For states that are substates of a - // DirtyState (updated or created), it is - // useful to be able to determine which - // type of dirty state it is. - dirtyType: stateProperty -}); - -// Implementation notes: -// -// Each state has a boolean value for all of the following flags: -// -// * isLoaded: The record has a populated `data` property. When a -// record is loaded via `store.find`, `isLoaded` is false -// until the adapter sets it. When a record is created locally, -// its `isLoaded` property is always true. -// * isDirty: The record has local changes that have not yet been -// saved by the adapter. This includes records that have been -// created (but not yet saved) or deleted. -// * isSaving: The record's transaction has been committed, but -// the adapter has not yet acknowledged that the changes have -// been persisted to the backend. -// * isDeleted: The record was marked for deletion. When `isDeleted` -// is true and `isDirty` is true, the record is deleted locally -// but the deletion was not yet persisted. When `isSaving` is -// true, the change is in-flight. When both `isDirty` and -// `isSaving` are false, the change has persisted. -// * isError: The adapter reported that it was unable to save -// local changes to the backend. This may also result in the -// record having its `isValid` property become false if the -// adapter reported that server-side validations failed. -// * isNew: The record was created on the client and the adapter -// did not yet report that it was successfully saved. -// * isValid: No client-side validations have failed and the -// adapter did not report any server-side validation failures. - -// The dirty state is a abstract state whose functionality is -// shared between the `created` and `updated` states. -// -// The deleted state shares the `isDirty` flag with the -// subclasses of `DirtyState`, but with a very different -// implementation. -// -// Dirty states have three child states: -// -// `uncommitted`: the store has not yet handed off the record -// to be saved. -// `inFlight`: the store has handed off the record to be saved, -// but the adapter has not yet acknowledged success. -// `invalid`: the record has invalid information and cannot be -// send to the adapter yet. -var DirtyState = DS.State.extend({ - initialState: 'uncommitted', - - // FLAGS - isDirty: true, - - // SUBSTATES - - // When a record first becomes dirty, it is `uncommitted`. - // This means that there are local pending changes, but they - // have not yet begun to be saved, and are not invalid. - uncommitted: DS.State.extend({ - // TRANSITIONS - enter: function(manager) { - var dirtyType = get(this, 'dirtyType'), - record = get(manager, 'record'); - - record.withTransaction(function (t) { - t.recordBecameDirty(dirtyType, record); - }); - }, - - // EVENTS - willSetProperty: willSetProperty, - didSetProperty: didSetProperty, - - becomeDirty: Ember.K, - - willCommit: function(manager) { - manager.transitionTo('inFlight'); - }, - - becameClean: function(manager) { - var record = get(manager, 'record'), - dirtyType = get(this, 'dirtyType'); - - record.withTransaction(function(t) { - t.recordBecameClean(dirtyType, record); - }); - - manager.transitionTo('loaded.materializing'); - }, - - becameInvalid: function(manager) { - var dirtyType = get(this, 'dirtyType'), - record = get(manager, 'record'); - - record.withTransaction(function (t) { - t.recordBecameInFlight(dirtyType, record); - }); - - manager.transitionTo('invalid'); - }, - - rollback: function(manager) { - get(manager, 'record').rollback(); - } - }), - - // Once a record has been handed off to the adapter to be - // saved, it is in the 'in flight' state. Changes to the - // record cannot be made during this window. - inFlight: DS.State.extend({ - // FLAGS - isSaving: true, - - // TRANSITIONS - enter: function(manager) { - var dirtyType = get(this, 'dirtyType'), - record = get(manager, 'record'); - - record.becameInFlight(); - - record.withTransaction(function (t) { - t.recordBecameInFlight(dirtyType, record); - }); - }, - - // EVENTS - didCommit: function(manager) { - var dirtyType = get(this, 'dirtyType'), - record = get(manager, 'record'); - - record.withTransaction(function(t) { - t.recordBecameClean('inflight', record); - }); - - manager.transitionTo('saved'); - manager.send('invokeLifecycleCallbacks', dirtyType); - }, - - becameInvalid: function(manager, errors) { - var record = get(manager, 'record'); - - set(record, 'errors', errors); - - manager.transitionTo('invalid'); - manager.send('invokeLifecycleCallbacks'); - }, - - becameError: function(manager) { - manager.transitionTo('error'); - manager.send('invokeLifecycleCallbacks'); - } - }), - - // A record is in the `invalid` state when its client-side - // invalidations have failed, or if the adapter has indicated - // the the record failed server-side invalidations. - invalid: DS.State.extend({ - // FLAGS - isValid: false, - - exit: function(manager) { - var record = get(manager, 'record'); - - record.withTransaction(function (t) { - t.recordBecameClean('inflight', record); - }); - }, - - // EVENTS - deleteRecord: function(manager) { - manager.transitionTo('deleted'); - get(manager, 'record').clearRelationships(); - }, - - willSetProperty: willSetProperty, - - didSetProperty: function(manager, context) { - var record = get(manager, 'record'), - errors = get(record, 'errors'), - key = context.name; - - set(errors, key, null); - - if (!hasDefinedProperties(errors)) { - manager.send('becameValid'); - } - - didSetProperty(manager, context); - }, - - becomeDirty: Ember.K, - - rollback: function(manager) { - manager.send('becameValid'); - manager.send('rollback'); - }, - - becameValid: function(manager) { - manager.transitionTo('uncommitted'); - }, - - invokeLifecycleCallbacks: function(manager) { - var record = get(manager, 'record'); - record.trigger('becameInvalid', record); - } - }) -}); - -// The created and updated states are created outside the state -// chart so we can reopen their substates and add mixins as -// necessary. - -var createdState = DirtyState.create({ - dirtyType: 'created', - - // FLAGS - isNew: true -}); - -var updatedState = DirtyState.create({ - dirtyType: 'updated' -}); - -createdState.states.uncommitted.reopen({ - deleteRecord: function(manager) { - var record = get(manager, 'record'); - - record.withTransaction(function(t) { - t.recordIsMoving('created', record); - }); - - record.clearRelationships(); - manager.transitionTo('deleted.saved'); - } -}); - -createdState.states.uncommitted.reopen({ - rollback: function(manager) { - this._super(manager); - manager.transitionTo('deleted.saved'); - } -}); - -updatedState.states.uncommitted.reopen({ - deleteRecord: function(manager) { - var record = get(manager, 'record'); - - record.withTransaction(function(t) { - t.recordIsMoving('updated', record); - }); - - manager.transitionTo('deleted'); - get(manager, 'record').clearRelationships(); - } -}); - -var states = { - rootState: Ember.State.create({ - // FLAGS - isLoaded: false, - isReloading: false, - isDirty: false, - isSaving: false, - isDeleted: false, - isError: false, - isNew: false, - isValid: true, - - // SUBSTATES - - // A record begins its lifecycle in the `empty` state. - // If its data will come from the adapter, it will - // transition into the `loading` state. Otherwise, if - // the record is being created on the client, it will - // transition into the `created` state. - empty: DS.State.create({ - // EVENTS - loadingData: function(manager) { - manager.transitionTo('loading'); - }, - - loadedData: function(manager) { - manager.transitionTo('loaded.created'); - } - }), - - // A record enters this state when the store askes - // the adapter for its data. It remains in this state - // until the adapter provides the requested data. - // - // Usually, this process is asynchronous, using an - // XHR to retrieve the data. - loading: DS.State.create({ - // EVENTS - loadedData: didChangeData, - - materializingData: function(manager) { - manager.transitionTo('loaded.materializing.firstTime'); - } - }), - - // A record enters this state when its data is populated. - // Most of a record's lifecycle is spent inside substates - // of the `loaded` state. - loaded: DS.State.create({ - initialState: 'saved', - - // FLAGS - isLoaded: true, - - // SUBSTATES - - materializing: DS.State.create({ - // FLAGS - isLoaded: false, - - // EVENTS - willSetProperty: Ember.K, - didSetProperty: Ember.K, - - didChangeData: didChangeData, - - finishedMaterializing: function(manager) { - manager.transitionTo('loaded.saved'); - }, - - // SUBSTATES - firstTime: DS.State.create({ - exit: function(manager) { - var record = get(manager, 'record'); - - once(function() { - record.trigger('didLoad'); - }); - } - }) - }), - - reloading: DS.State.create({ - // FLAGS - isReloading: true, - - // TRANSITIONS - enter: function(manager) { - var record = get(manager, 'record'), - store = get(record, 'store'); - - store.reloadRecord(record); - }, - - exit: function(manager) { - var record = get(manager, 'record'); - - once(record, 'trigger', 'didReload'); - }, - - // EVENTS - loadedData: didChangeData, - - materializingData: function(manager) { - manager.transitionTo('loaded.materializing'); - } - }), - - // If there are no local changes to a record, it remains - // in the `saved` state. - saved: DS.State.create({ - // EVENTS - willSetProperty: willSetProperty, - didSetProperty: didSetProperty, - - didChangeData: didChangeData, - loadedData: didChangeData, - - reloadRecord: function(manager) { - manager.transitionTo('loaded.reloading'); - }, - - materializingData: function(manager) { - manager.transitionTo('loaded.materializing'); - }, - - becomeDirty: function(manager) { - manager.transitionTo('updated'); - }, - - deleteRecord: function(manager) { - manager.transitionTo('deleted'); - get(manager, 'record').clearRelationships(); - }, - - unloadRecord: function(manager) { - manager.transitionTo('deleted.saved'); - get(manager, 'record').clearRelationships(); - }, - - invokeLifecycleCallbacks: function(manager, dirtyType) { - var record = get(manager, 'record'); - if (dirtyType === 'created') { - record.trigger('didCreate', record); - } else { - record.trigger('didUpdate', record); - } - } - }), - - // A record is in this state after it has been locally - // created but before the adapter has indicated that - // it has been saved. - created: createdState, - - // A record is in this state if it has already been - // saved to the server, but there are new local changes - // that have not yet been saved. - updated: updatedState - }), - - // A record is in this state if it was deleted from the store. - deleted: DS.State.create({ - initialState: 'uncommitted', - dirtyType: 'deleted', - - // FLAGS - isDeleted: true, - isLoaded: true, - isDirty: true, - - // TRANSITIONS - setup: function(manager) { - var record = get(manager, 'record'), - store = get(record, 'store'); - - store.removeFromRecordArrays(record); - }, - - // SUBSTATES - - // When a record is deleted, it enters the `start` - // state. It will exit this state when the record's - // transaction starts to commit. - uncommitted: DS.State.create({ - // TRANSITIONS - enter: function(manager) { - var record = get(manager, 'record'); - - record.withTransaction(function(t) { - t.recordBecameDirty('deleted', record); - }); - }, - - // EVENTS - willCommit: function(manager) { - manager.transitionTo('inFlight'); - }, - - rollback: function(manager) { - get(manager, 'record').rollback(); - }, - - becomeDirty: Ember.K, - - becameClean: function(manager) { - var record = get(manager, 'record'); - - record.withTransaction(function(t) { - t.recordBecameClean('deleted', record); - }); - - manager.transitionTo('loaded.materializing'); - } - }), - - // After a record's transaction is committing, but - // before the adapter indicates that the deletion - // has saved to the server, a record is in the - // `inFlight` substate of `deleted`. - inFlight: DS.State.create({ - // FLAGS - isSaving: true, - - // TRANSITIONS - enter: function(manager) { - var record = get(manager, 'record'); - - record.becameInFlight(); - - record.withTransaction(function (t) { - t.recordBecameInFlight('deleted', record); - }); - }, - - // EVENTS - didCommit: function(manager) { - var record = get(manager, 'record'); - - record.withTransaction(function(t) { - t.recordBecameClean('inflight', record); - }); - - manager.transitionTo('saved'); - - manager.send('invokeLifecycleCallbacks'); - } - }), - - // Once the adapter indicates that the deletion has - // been saved, the record enters the `saved` substate - // of `deleted`. - saved: DS.State.create({ - // FLAGS - isDirty: false, - - setup: function(manager) { - var record = get(manager, 'record'), - store = get(record, 'store'); - - store.dematerializeRecord(record); - }, - - invokeLifecycleCallbacks: function(manager) { - var record = get(manager, 'record'); - record.trigger('didDelete', record); - } - }) - }), - - // If the adapter indicates that there was an unknown - // error saving a record, the record enters the `error` - // state. - error: DS.State.create({ - isError: true, - - // EVENTS - - invokeLifecycleCallbacks: function(manager) { - var record = get(manager, 'record'); - record.trigger('becameError', record); - } - }) - }) -}; - -DS.StateManager = Ember.StateManager.extend({ - record: null, - initialState: 'rootState', - states: states, - unhandledEvent: function(manager, originalEvent) { - var record = manager.get('record'), - contexts = [].slice.call(arguments, 2), - errorMessage; - errorMessage = "Attempted to handle event `" + originalEvent + "` "; - errorMessage += "on " + record.toString() + " while in state "; - errorMessage += get(manager, 'currentState.path') + ". Called with "; - errorMessage += arrayMap.call(contexts, function(context){ - return Ember.inspect(context); - }).join(', '); - throw new Ember.Error(errorMessage); - } -}); - -})(); - - - -(function() { -var LoadPromise = DS.LoadPromise; // system/mixins/load_promise - -var get = Ember.get, set = Ember.set, map = Ember.EnumerableUtils.map; - -var retrieveFromCurrentState = Ember.computed(function(key, value) { - return get(get(this, 'stateManager.currentState'), key); -}).property('stateManager.currentState').readOnly(); - -DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, { - isLoaded: retrieveFromCurrentState, - isReloading: retrieveFromCurrentState, - isDirty: retrieveFromCurrentState, - isSaving: retrieveFromCurrentState, - isDeleted: retrieveFromCurrentState, - isError: retrieveFromCurrentState, - isNew: retrieveFromCurrentState, - isValid: retrieveFromCurrentState, - - clientId: null, - id: null, - transaction: null, - stateManager: null, - errors: null, - - /** - Create a JSON representation of the record, using the serialization - strategy of the store's adapter. - - Available options: - - * `includeId`: `true` if the record's ID should be included in the - JSON representation. - - @param {Object} options - @returns {Object} an object whose values are primitive JSON values only - */ - serialize: function(options) { - var store = get(this, 'store'); - return store.serialize(this, options); - }, - - toJSON: function() { - var serializer = DS.JSONSerializer.create(); - return serializer.serialize(this); - }, - - didLoad: Ember.K, - didReload: Ember.K, - didUpdate: Ember.K, - didCreate: Ember.K, - didDelete: Ember.K, - becameInvalid: Ember.K, - becameError: Ember.K, - - data: Ember.computed(function() { - if (!this._data) { - this.materializeData(); - } - - return this._data; - }).property(), - - materializeData: function() { - this.send('materializingData'); - - get(this, 'store').materializeData(this); - - this.suspendRelationshipObservers(function() { - this.notifyPropertyChange('data'); - }); - }, - - _data: null, - - init: function() { - this._super(); - - var stateManager = DS.StateManager.create({ record: this }); - set(this, 'stateManager', stateManager); - - this._setup(); - - stateManager.goToState('empty'); - }, - - _setup: function() { - this._relationshipChanges = {}; - this._changesToSync = {}; - }, - - send: function(name, context) { - return get(this, 'stateManager').send(name, context); - }, - - withTransaction: function(fn) { - var transaction = get(this, 'transaction'); - if (transaction) { fn(transaction); } - }, - - loadingData: function() { - this.send('loadingData'); - }, - - loadedData: function() { - this.send('loadedData'); - }, - - didChangeData: function() { - this.send('didChangeData'); - }, - - setProperty: function(key, value, oldValue) { - this.send('setProperty', { key: key, value: value, oldValue: oldValue }); - }, - - /** - Reload the record from the adapter. - - This will only work if the record has already finished loading - and has not yet been modified (`isLoaded` but not `isDirty`, - or `isSaving`). - */ - reload: function() { - this.send('reloadRecord'); - }, - - deleteRecord: function() { - this.send('deleteRecord'); - }, - - unloadRecord: function() { - Ember.assert("You can only unload a loaded, non-dirty record.", !get(this, 'isDirty')); - - this.send('unloadRecord'); - }, - - clearRelationships: function() { - this.eachRelationship(function(name, relationship) { - if (relationship.kind === 'belongsTo') { - set(this, name, null); - } else if (relationship.kind === 'hasMany') { - get(this, name).clear(); - } - }, this); - }, - - updateRecordArrays: function() { - var store = get(this, 'store'); - if (store) { - store.dataWasUpdated(this.constructor, get(this, '_reference'), this); - } - }, - - /** - If the adapter did not return a hash in response to a commit, - merge the changed attributes and relationships into the existing - saved data. - */ - adapterDidCommit: function() { - var attributes = get(this, 'data').attributes; - - get(this.constructor, 'attributes').forEach(function(name, meta) { - attributes[name] = get(this, name); - }, this); - - this.send('didCommit'); - this.updateRecordArraysLater(); - }, - - adapterDidDirty: function() { - this.send('becomeDirty'); - this.updateRecordArraysLater(); - }, - - dataDidChange: Ember.observer(function() { - var relationships = get(this.constructor, 'relationshipsByName'); - - this.updateRecordArraysLater(); - - relationships.forEach(function(name, relationship) { - if (relationship.kind === 'hasMany') { - this.hasManyDidChange(relationship.key); - } - }, this); - - this.send('finishedMaterializing'); - }, 'data'), - - hasManyDidChange: function(key) { - var cachedValue = this.cacheFor(key); - - if (cachedValue) { - var type = get(this.constructor, 'relationshipsByName').get(key).type; - var store = get(this, 'store'); - var ids = this._data.hasMany[key] || []; - - var references = map(ids, function(id) { - // if it was already a reference, return the reference - if (typeof id === 'object') { return id; } - return store.referenceForId(type, id); - }); - - set(cachedValue, 'content', Ember.A(references)); - } - }, - - updateRecordArraysLater: function() { - Ember.run.once(this, this.updateRecordArrays); - }, - - setupData: function(prematerialized) { - this._data = { - attributes: {}, - belongsTo: {}, - hasMany: {}, - id: null - }; - }, - - materializeId: function(id) { - set(this, 'id', id); - }, - - materializeAttributes: function(attributes) { - Ember.assert("Must pass a hash of attributes to materializeAttributes", !!attributes); - this._data.attributes = attributes; - }, - - materializeAttribute: function(name, value) { - this._data.attributes[name] = value; - }, - - materializeHasMany: function(name, ids) { - this._data.hasMany[name] = ids; - }, - - materializeBelongsTo: function(name, id) { - this._data.belongsTo[name] = id; - }, - - rollback: function() { - this._setup(); - this.send('becameClean'); - - this.suspendRelationshipObservers(function() { - this.notifyPropertyChange('data'); - }); - }, - - toStringExtension: function() { - return get(this, 'id'); - }, - - /** - @private - - The goal of this method is to temporarily disable specific observers - that take action in response to application changes. - - This allows the system to make changes (such as materialization and - rollback) that should not trigger secondary behavior (such as setting an - inverse relationship or marking records as dirty). - - The specific implementation will likely change as Ember proper provides - better infrastructure for suspending groups of observers, and if Array - observation becomes more unified with regular observers. - */ - suspendRelationshipObservers: function(callback, binding) { - var observers = get(this.constructor, 'relationshipNames').belongsTo; - var self = this; - - try { - this._suspendedRelationships = true; - Ember._suspendObservers(self, observers, null, 'belongsToDidChange', function() { - Ember._suspendBeforeObservers(self, observers, null, 'belongsToWillChange', function() { - callback.call(binding || self); - }); - }); - } finally { - this._suspendedRelationships = false; - } - }, - - becameInFlight: function() { - }, - - // FOR USE BY THE BASIC ADAPTER - - save: function() { - this.get('store').scheduleSave(this); - }, - - // FOR USE DURING COMMIT PROCESS - - adapterDidUpdateAttribute: function(attributeName, value) { - - // If a value is passed in, update the internal attributes and clear - // the attribute cache so it picks up the new value. Otherwise, - // collapse the current value into the internal attributes because - // the adapter has acknowledged it. - if (value !== undefined) { - get(this, 'data.attributes')[attributeName] = value; - this.notifyPropertyChange(attributeName); - } else { - value = get(this, attributeName); - get(this, 'data.attributes')[attributeName] = value; - } - - this.updateRecordArraysLater(); - }, - - _reference: Ember.computed(function() { - return get(this, 'store').referenceForClientId(get(this, 'clientId')); - }), - - adapterDidInvalidate: function(errors) { - this.send('becameInvalid', errors); - }, - - adapterDidError: function() { - this.send('becameError'); - }, - - /** - @private - - Override the default event firing from Ember.Evented to - also call methods with the given name. - */ - trigger: function(name) { - Ember.tryInvoke(this, name, [].slice.call(arguments, 1)); - this._super.apply(this, arguments); - } -}); - -// Helper function to generate store aliases. -// This returns a function that invokes the named alias -// on the default store, but injects the class as the -// first parameter. -var storeAlias = function(methodName) { - return function() { - var store = get(DS, 'defaultStore'), - args = [].slice.call(arguments); - - args.unshift(this); - Ember.assert("Your application does not have a 'Store' property defined. Attempts to call '" + methodName + "' on model classes will fail. Please provide one as with 'YourAppName.Store = DS.Store.extend()'", !!store); - return store[methodName].apply(store, args); - }; -}; - -DS.Model.reopenClass({ - isLoaded: storeAlias('recordIsLoaded'), - find: storeAlias('find'), - all: storeAlias('all'), - query: storeAlias('findQuery'), - filter: storeAlias('filter'), - - _create: DS.Model.create, - - create: function() { - throw new Ember.Error("You should not call `create` on a model. Instead, call `createRecord` with the attributes you would like to set."); - }, - - createRecord: storeAlias('createRecord') -}); - -})(); - - - -(function() { -var get = Ember.get; -DS.Model.reopenClass({ - attributes: Ember.computed(function() { - var map = Ember.Map.create(); - - this.eachComputedProperty(function(name, meta) { - if (meta.isAttribute) { - Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('<type>')` from " + this.toString(), name !== 'id'); - - meta.name = name; - map.set(name, meta); - } - }); - - return map; - }) -}); - -var AttributeChange = DS.AttributeChange = function(options) { - this.reference = options.reference; - this.store = options.store; - this.name = options.name; - this.oldValue = options.oldValue; -}; - -AttributeChange.createChange = function(options) { - return new AttributeChange(options); -}; - -AttributeChange.prototype = { - sync: function() { - this.store.recordAttributeDidChange(this.reference, this.name, this.value, this.oldValue); - - // TODO: Use this object in the commit process - this.destroy(); - }, - - destroy: function() { - delete this.store.recordForReference(this.reference)._changesToSync[this.name]; - } -}; - -DS.Model.reopen({ - eachAttribute: function(callback, binding) { - get(this.constructor, 'attributes').forEach(function(name, meta) { - callback.call(binding, name, meta); - }, binding); - }, - - attributeWillChange: Ember.beforeObserver(function(record, key) { - var reference = get(record, '_reference'), - store = get(record, 'store'); - - record.send('willSetProperty', { reference: reference, store: store, name: key }); - }), - - attributeDidChange: Ember.observer(function(record, key) { - record.send('didSetProperty', { name: key }); - }) -}); - -function getAttr(record, options, key) { - var attributes = get(record, 'data').attributes; - var value = attributes[key]; - - if (value === undefined) { - value = options.defaultValue; - } - - return value; -} - -DS.attr = function(type, options) { - options = options || {}; - - var meta = { - type: type, - isAttribute: true, - options: options - }; - - return Ember.computed(function(key, value, oldValue) { - if (arguments.length > 1) { - Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('<type>')` from " + this.constructor.toString(), key !== 'id'); - } else { - value = getAttr(this, options, key); - } - - return value; - // `data` is never set directly. However, it may be - // invalidated from the state manager's setData - // event. - }).property('data').meta(meta); -}; - - -})(); - - - -(function() { - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set, - none = Ember.isNone; - -DS.belongsTo = function(type, options) { - Ember.assert("The first argument DS.belongsTo must be a model type or string, like DS.belongsTo(App.Person)", !!type && (typeof type === 'string' || DS.Model.detect(type))); - - options = options || {}; - - var meta = { type: type, isRelationship: true, options: options, kind: 'belongsTo' }; - - return Ember.computed(function(key, value) { - if (arguments.length === 2) { - return value === undefined ? null : value; - } - - var data = get(this, 'data').belongsTo, - store = get(this, 'store'), id; - - if (typeof type === 'string') { - type = get(this, type, false) || get(Ember.lookup, type); - } - - id = data[key]; - - if(!id) { - return null; - } else if (typeof id === 'object') { - return store.recordForReference(id); - } else { - return store.find(type, id); - } - }).property('data').meta(meta); -}; - -/** - These observers observe all `belongsTo` relationships on the record. See - `relationships/ext` to see how these observers get their dependencies. - -*/ - -DS.Model.reopen({ - /** @private */ - belongsToWillChange: Ember.beforeObserver(function(record, key) { - if (get(record, 'isLoaded')) { - var oldParent = get(record, key); - - var childReference = get(record, '_reference'), - store = get(record, 'store'); - if (oldParent){ - var change = DS.RelationshipChange.createChange(childReference, get(oldParent, '_reference'), store, { key: key, kind:"belongsTo", changeType: "remove" }); - change.sync(); - this._changesToSync[key] = change; - } - } - }), - - /** @private */ - belongsToDidChange: Ember.immediateObserver(function(record, key) { - if (get(record, 'isLoaded')) { - var newParent = get(record, key); - if(newParent){ - var childReference = get(record, '_reference'), - store = get(record, 'store'); - var change = DS.RelationshipChange.createChange(childReference, get(newParent, '_reference'), store, { key: key, kind:"belongsTo", changeType: "add" }); - change.sync(); - if(this._changesToSync[key]){ - DS.OneToManyChange.ensureSameTransaction([change, this._changesToSync[key]], store); - } - } - } - delete this._changesToSync[key]; - }) -}); - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set; -var hasRelationship = function(type, options) { - options = options || {}; - - var meta = { type: type, isRelationship: true, options: options, kind: 'hasMany' }; - - return Ember.computed(function(key, value) { - var data = get(this, 'data').hasMany, - store = get(this, 'store'), - ids, relationship; - - if (typeof type === 'string') { - type = get(this, type, false) || get(Ember.lookup, type); - } - - ids = data[key]; - relationship = store.findMany(type, ids, this, meta); - set(relationship, 'owner', this); - set(relationship, 'name', key); - - return relationship; - }).property().meta(meta); -}; - -DS.hasMany = function(type, options) { - Ember.assert("The type passed to DS.hasMany must be defined", !!type); - return hasRelationship(type, options); -}; - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set; - -/** - @private - - This file defines several extensions to the base `DS.Model` class that - add support for one-to-many relationships. -*/ - -DS.Model.reopen({ - // This Ember.js hook allows an object to be notified when a property - // is defined. - // - // In this case, we use it to be notified when an Ember Data user defines a - // belongs-to relationship. In that case, we need to set up observers for - // each one, allowing us to track relationship changes and automatically - // reflect changes in the inverse has-many array. - // - // This hook passes the class being set up, as well as the key and value - // being defined. So, for example, when the user does this: - // - // DS.Model.extend({ - // parent: DS.belongsTo(App.User) - // }); - // - // This hook would be called with "parent" as the key and the computed - // property returned by `DS.belongsTo` as the value. - didDefineProperty: function(proto, key, value) { - // Check if the value being set is a computed property. - if (value instanceof Ember.Descriptor) { - - // If it is, get the metadata for the relationship. This is - // populated by the `DS.belongsTo` helper when it is creating - // the computed property. - var meta = value.meta(); - - if (meta.isRelationship && meta.kind === 'belongsTo') { - Ember.addObserver(proto, key, null, 'belongsToDidChange'); - Ember.addBeforeObserver(proto, key, null, 'belongsToWillChange'); - } - - if (meta.isAttribute) { - Ember.addObserver(proto, key, null, 'attributeDidChange'); - Ember.addBeforeObserver(proto, key, null, 'attributeWillChange'); - } - - meta.parentType = proto.constructor; - } - } -}); - -/** - These DS.Model extensions add class methods that provide relationship - introspection abilities about relationships. - - A note about the computed properties contained here: - - **These properties are effectively sealed once called for the first time.** - To avoid repeatedly doing expensive iteration over a model's fields, these - values are computed once and then cached for the remainder of the runtime of - your application. - - If your application needs to modify a class after its initial definition - (for example, using `reopen()` to add additional attributes), make sure you - do it before using your model with the store, which uses these properties - extensively. -*/ - -DS.Model.reopenClass({ - /** - For a given relationship name, returns the model type of the relationship. - - For example, if you define a model like this: - - App.Post = DS.Model.extend({ - comments: DS.hasMany(App.Comment) - }); - - Calling `App.Post.typeForRelationship('comments')` will return `App.Comment`. - - @param {String} name the name of the relationship - @return {subclass of DS.Model} the type of the relationship, or undefined - */ - typeForRelationship: function(name) { - var relationship = get(this, 'relationshipsByName').get(name); - return relationship && relationship.type; - }, - - /** - The model's relationships as a map, keyed on the type of the - relationship. The value of each entry is an array containing a descriptor - for each relationship with that type, describing the name of the relationship - as well as the type. - - For example, given the following model definition: - - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), - posts: DS.hasMany(App.Post) - }); - - This computed property would return a map describing these - relationships, like this: - - var relationships = Ember.get(App.Blog, 'relationships'); - associatons.get(App.User); - //=> [ { name: 'users', kind: 'hasMany' }, - // { name: 'owner', kind: 'belongsTo' } ] - relationships.get(App.Post); - //=> [ { name: 'posts', kind: 'hasMany' } ] - - @type Ember.Map - @readOnly - */ - relationships: Ember.computed(function() { - var map = new Ember.MapWithDefault({ - defaultValue: function() { return []; } - }); - - // Loop through each computed property on the class - this.eachComputedProperty(function(name, meta) { - - // If the computed property is a relationship, add - // it to the map. - if (meta.isRelationship) { - if (typeof meta.type === 'string') { - meta.type = Ember.get(Ember.lookup, meta.type); - } - - var relationshipsForType = map.get(meta.type); - - relationshipsForType.push({ name: name, kind: meta.kind }); - } - }); - - return map; - }), - - /** - A hash containing lists of the model's relationships, grouped - by the relationship kind. For example, given a model with this - definition: - - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), - - posts: DS.hasMany(App.Post) - }); - - This property would contain the following: - - var relationshipNames = Ember.get(App.Blog, 'relationshipNames'); - relationshipNames.hasMany; - //=> ['users', 'posts'] - relationshipNames.belongsTo; - //=> ['owner'] - - @type Object - @readOnly - */ - relationshipNames: Ember.computed(function() { - var names = { hasMany: [], belongsTo: [] }; - - this.eachComputedProperty(function(name, meta) { - if (meta.isRelationship) { - names[meta.kind].push(name); - } - }); - - return names; - }), - - /** - An array of types directly related to a model. Each type will be - included once, regardless of the number of relationships it has with - the model. - - For example, given a model with this definition: - - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), - posts: DS.hasMany(App.Post) - }); - - This property would contain the following: - - var relatedTypes = Ember.get(App.Blog, 'relatedTypes'); - //=> [ App.User, App.Post ] - - @type Ember.Array - @readOnly - */ - relatedTypes: Ember.computed(function() { - var type, - types = Ember.A([]); - - // Loop through each computed property on the class, - // and create an array of the unique types involved - // in relationships - this.eachComputedProperty(function(name, meta) { - if (meta.isRelationship) { - type = meta.type; - - if (typeof type === 'string') { - type = get(this, type, false) || get(Ember.lookup, type); - } - - if (!types.contains(type)) { - types.push(type); - } - } - }); - - return types; - }), - - /** - A map whose keys are the relationships of a model and whose values are - relationship descriptors. - - For example, given a model with this - definition: - - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), - - posts: DS.hasMany(App.Post) - }); - - This property would contain the following: - - var relationshipsByName = Ember.get(App.Blog, 'relationshipsByName'); - relationshipsByName.get('users'); - //=> { key: 'users', kind: 'hasMany', type: App.User } - relationshipsByName.get('owner'); - //=> { key: 'owner', kind: 'belongsTo', type: App.User } - - @type Ember.Map - @readOnly - */ - relationshipsByName: Ember.computed(function() { - var map = Ember.Map.create(), type; - - this.eachComputedProperty(function(name, meta) { - if (meta.isRelationship) { - meta.key = name; - type = meta.type; - - if (typeof type === 'string') { - type = get(this, type, false) || get(Ember.lookup, type); - meta.type = type; - } - - map.set(name, meta); - } - }); - - return map; - }), - - /** - A map whose keys are the fields of the model and whose values are strings - describing the kind of the field. A model's fields are the union of all of its - attributes and relationships. - - For example: - - App.Blog = DS.Model.extend({ - users: DS.hasMany(App.User), - owner: DS.belongsTo(App.User), - - posts: DS.hasMany(App.Post), - - title: DS.attr('string') - }); - - var fields = Ember.get(App.Blog, 'fields'); - fields.forEach(function(field, kind) { - console.log(field, kind); - }); - - // prints: - // users, hasMany - // owner, belongsTo - // posts, hasMany - // title, attribute - - @type Ember.Map - @readOnly - */ - fields: Ember.computed(function() { - var map = Ember.Map.create(), type; - - this.eachComputedProperty(function(name, meta) { - if (meta.isRelationship) { - map.set(name, meta.kind); - } else if (meta.isAttribute) { - map.set(name, 'attribute'); - } - }); - - return map; - }), - - /** - Given a callback, iterates over each of the relationships in the model, - invoking the callback with the name of each relationship and its relationship - descriptor. - - @param {Function} callback the callback to invoke - @param {any} binding the value to which the callback's `this` should be bound - */ - eachRelationship: function(callback, binding) { - get(this, 'relationshipsByName').forEach(function(name, relationship) { - callback.call(binding, name, relationship); - }); - }, - - /** - Given a callback, iterates over each of the types related to a model, - invoking the callback with the related type's class. Each type will be - returned just once, regardless of how many different relationships it has - with a model. - - @param {Function} callback the callback to invoke - @param {any} binding the value to which the callback's `this` should be bound - */ - eachRelatedType: function(callback, binding) { - get(this, 'relatedTypes').forEach(function(type) { - callback.call(binding, type); - }); - } -}); - -DS.Model.reopen({ - /** - Given a callback, iterates over each of the relationships in the model, - invoking the callback with the name of each relationship and its relationship - descriptor. - - @param {Function} callback the callback to invoke - @param {any} binding the value to which the callback's `this` should be bound - */ - eachRelationship: function(callback, binding) { - this.constructor.eachRelationship(callback, binding); - } -}); - -/** - @private - - Helper method to look up the name of the inverse of a relationship. - - In a has-many relationship, there are always two sides: the `belongsTo` side - and the `hasMany` side. When one side changes, the other side should be updated - automatically. - - Given a model, the model of the inverse, and the kind of the relationship, this - helper returns the name of the relationship on the inverse. - - For example, imagine the following two associated models: - - App.Post = DS.Model.extend({ - comments: DS.hasMany('App.Comment') - }); - - App.Comment = DS.Model.extend({ - post: DS.belongsTo('App.Post') - }); - - If the `post` property of a `Comment` was modified, Ember Data would invoke - this helper like this: - - DS._inverseNameFor(App.Comment, App.Post, 'hasMany'); - //=> 'comments' - - Ember Data uses the name of the relationship returned to reflect the changed - relationship on the other side. -*/ -DS._inverseRelationshipFor = function(modelType, inverseModelType) { - var relationshipMap = get(modelType, 'relationships'), - possibleRelationships = relationshipMap.get(inverseModelType), - possible, actual, oldValue; - - if (!possibleRelationships) { return; } - if (possibleRelationships.length > 1) { return; } - return possibleRelationships[0]; -}; - -/** - @private - - Given a model and a relationship name, returns the model type of - the named relationship. - - App.Post = DS.Model.extend({ - comments: DS.hasMany('App.Comment') - }); - - DS._inverseTypeFor(App.Post, 'comments'); - //=> App.Comment - @param {DS.Model class} modelType - @param {String} relationshipName - @return {DS.Model class} -*/ -DS._inverseTypeFor = function(modelType, relationshipName) { - var relationships = get(modelType, 'relationshipsByName'), - relationship = relationships.get(relationshipName); - - if (relationship) { return relationship.type; } -}; - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set; -var forEach = Ember.EnumerableUtils.forEach; - -DS.RelationshipChange = function(options) { - this.parentReference = options.parentReference; - this.childReference = options.childReference; - this.firstRecordReference = options.firstRecordReference; - this.firstRecordKind = options.firstRecordKind; - this.firstRecordName = options.firstRecordName; - this.secondRecordReference = options.secondRecordReference; - this.secondRecordKind = options.secondRecordKind; - this.secondRecordName = options.secondRecordName; - this.store = options.store; - this.committed = {}; - this.changeType = options.changeType; -}; - -DS.RelationshipChangeAdd = function(options){ - DS.RelationshipChange.call(this, options); -}; - -DS.RelationshipChangeRemove = function(options){ - DS.RelationshipChange.call(this, options); -}; - -/** @private */ -DS.RelationshipChange.create = function(options) { - return new DS.RelationshipChange(options); -}; - -/** @private */ -DS.RelationshipChangeAdd.create = function(options) { - return new DS.RelationshipChangeAdd(options); -}; - -/** @private */ -DS.RelationshipChangeRemove.create = function(options) { - return new DS.RelationshipChangeRemove(options); -}; - -DS.OneToManyChange = {}; -DS.OneToNoneChange = {}; -DS.ManyToNoneChange = {}; -DS.OneToOneChange = {}; -DS.ManyToManyChange = {}; - -DS.RelationshipChange._createChange = function(options){ - if(options.changeType === "add"){ - return DS.RelationshipChangeAdd.create(options); - } - if(options.changeType === "remove"){ - return DS.RelationshipChangeRemove.create(options); - } -}; - - -DS.RelationshipChange.determineRelationshipType = function(recordType, knownSide){ - var knownKey = knownSide.key, key, type, otherContainerType,assoc; - var knownContainerType = knownSide.kind; - var options = recordType.metaForProperty(knownKey).options; - var otherType = DS._inverseTypeFor(recordType, knownKey); - - if(options.inverse){ - key = options.inverse; - otherContainerType = get(otherType, 'relationshipsByName').get(key).kind; - } - else if(assoc = DS._inverseRelationshipFor(otherType, recordType)){ - key = assoc.name; - otherContainerType = assoc.kind; - } - if(!key){ - return knownContainerType === "belongsTo" ? "oneToNone" : "manyToNone"; - } - else{ - if(otherContainerType === "belongsTo"){ - return knownContainerType === "belongsTo" ? "oneToOne" : "manyToOne"; - } - else{ - return knownContainerType === "belongsTo" ? "oneToMany" : "manyToMany"; - } - } - -}; - -DS.RelationshipChange.createChange = function(firstRecordReference, secondRecordReference, store, options){ - // Get the type of the child based on the child's client ID - var firstRecordType = firstRecordReference.type, key, changeType; - changeType = DS.RelationshipChange.determineRelationshipType(firstRecordType, options); - if (changeType === "oneToMany"){ - return DS.OneToManyChange.createChange(firstRecordReference, secondRecordReference, store, options); - } - else if (changeType === "manyToOne"){ - return DS.OneToManyChange.createChange(secondRecordReference, firstRecordReference, store, options); - } - else if (changeType === "oneToNone"){ - return DS.OneToNoneChange.createChange(firstRecordReference, secondRecordReference, store, options); - } - else if (changeType === "manyToNone"){ - return DS.ManyToNoneChange.createChange(firstRecordReference, secondRecordReference, store, options); - } - else if (changeType === "oneToOne"){ - return DS.OneToOneChange.createChange(firstRecordReference, secondRecordReference, store, options); - } - else if (changeType === "manyToMany"){ - return DS.ManyToManyChange.createChange(firstRecordReference, secondRecordReference, store, options); - } -}; - -/** @private */ -DS.OneToNoneChange.createChange = function(childReference, parentReference, store, options) { - var key = options.key; - var change = DS.RelationshipChange._createChange({ - parentReference: parentReference, - childReference: childReference, - firstRecordReference: childReference, - store: store, - changeType: options.changeType, - firstRecordName: key, - firstRecordKind: "belongsTo" - }); - - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); - - return change; -}; - -/** @private */ -DS.ManyToNoneChange.createChange = function(childReference, parentReference, store, options) { - var key = options.key; - var change = DS.RelationshipChange._createChange({ - parentReference: childReference, - childReference: parentReference, - secondRecordReference: childReference, - store: store, - changeType: options.changeType, - secondRecordName: options.key, - secondRecordKind: "hasMany" - }); - - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); - return change; -}; - - -/** @private */ -DS.ManyToManyChange.createChange = function(childReference, parentReference, store, options) { - // Get the type of the child based on the child's client ID - var childType = childReference.type, key; - - // If the name of the belongsTo side of the relationship is specified, - // use that - // If the type of the parent is specified, look it up on the child's type - // definition. - key = options.key; - - var change = DS.RelationshipChange._createChange({ - parentReference: parentReference, - childReference: childReference, - firstRecordReference: childReference, - secondRecordReference: parentReference, - firstRecordKind: "hasMany", - secondRecordKind: "hasMany", - store: store, - changeType: options.changeType, - firstRecordName: key - }); - - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); - - - return change; -}; - -/** @private */ -DS.OneToOneChange.createChange = function(childReference, parentReference, store, options) { - // Get the type of the child based on the child's client ID - var childType = childReference.type, key; - - // If the name of the belongsTo side of the relationship is specified, - // use that - // If the type of the parent is specified, look it up on the child's type - // definition. - if (options.parentType) { - key = inverseBelongsToName(options.parentType, childType, options.key); - //DS.OneToOneChange.maintainInvariant( options, store, childReference, key ); - } else if (options.key) { - key = options.key; - } else { - Ember.assert("You must pass either a parentType or belongsToName option to OneToManyChange.forChildAndParent", false); - } - - var change = DS.RelationshipChange._createChange({ - parentReference: parentReference, - childReference: childReference, - firstRecordReference: childReference, - secondRecordReference: parentReference, - firstRecordKind: "belongsTo", - secondRecordKind: "belongsTo", - store: store, - changeType: options.changeType, - firstRecordName: key - }); - - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); - - - return change; -}; - -DS.OneToOneChange.maintainInvariant = function(options, store, childReference, key){ - if (options.changeType === "add" && store.recordIsMaterialized(childReference)) { - var child = store.recordForReference(childReference); - var oldParent = get(child, key); - if (oldParent){ - var correspondingChange = DS.OneToOneChange.createChange(childReference, oldParent.get('_reference'), store, { - parentType: options.parentType, - hasManyName: options.hasManyName, - changeType: "remove", - key: options.key - }); - store.addRelationshipChangeFor(childReference, key, options.parentReference , null, correspondingChange); - correspondingChange.sync(); - } - } -}; - -/** @private */ -DS.OneToManyChange.createChange = function(childReference, parentReference, store, options) { - // Get the type of the child based on the child's client ID - var childType = childReference.type, key; - - // If the name of the belongsTo side of the relationship is specified, - // use that - // If the type of the parent is specified, look it up on the child's type - // definition. - if (options.parentType) { - key = inverseBelongsToName(options.parentType, childType, options.key); - DS.OneToManyChange.maintainInvariant( options, store, childReference, key ); - } else if (options.key) { - key = options.key; - } else { - Ember.assert("You must pass either a parentType or belongsToName option to OneToManyChange.forChildAndParent", false); - } - - var change = DS.RelationshipChange._createChange({ - parentReference: parentReference, - childReference: childReference, - firstRecordReference: childReference, - secondRecordReference: parentReference, - firstRecordKind: "belongsTo", - secondRecordKind: "hasMany", - store: store, - changeType: options.changeType, - firstRecordName: key - }); - - store.addRelationshipChangeFor(childReference, key, parentReference, null, change); - - - return change; -}; - - -DS.OneToManyChange.maintainInvariant = function(options, store, childReference, key){ - if (options.changeType === "add" && store.recordIsMaterialized(childReference)) { - var child = store.recordForReference(childReference); - var oldParent = get(child, key); - if (oldParent){ - var correspondingChange = DS.OneToManyChange.createChange(childReference, oldParent.get('_reference'), store, { - parentType: options.parentType, - hasManyName: options.hasManyName, - changeType: "remove", - key: options.key - }); - store.addRelationshipChangeFor(childReference, key, options.parentReference , null, correspondingChange); - correspondingChange.sync(); - } - } -}; - -DS.OneToManyChange.ensureSameTransaction = function(changes, store){ - var records = Ember.A(); - forEach(changes, function(change){ - records.addObject(change.getSecondRecord()); - records.addObject(change.getFirstRecord()); - }); - var transaction = store.ensureSameTransaction(records); - forEach(changes, function(change){ - change.transaction = transaction; - }); -}; - -DS.RelationshipChange.prototype = { - - getSecondRecordName: function() { - var name = this.secondRecordName, store = this.store, parent; - - if (!name) { - parent = this.secondRecordReference; - if (!parent) { return; } - - var childType = this.firstRecordReference.type; - var inverseType = DS._inverseTypeFor(childType, this.firstRecordName); - name = inverseHasManyName(inverseType, childType, this.firstRecordName); - this.secondRecordName = name; - } - - return name; - }, - - /** - Get the name of the relationship on the belongsTo side. - - @returns {String} - */ - getFirstRecordName: function() { - var name = this.firstRecordName, store = this.store, parent, child; - - if (!name) { - parent = this.secondRecordReference; - child = this.firstRecordReference; - if (!(child && parent)) { return; } - - name = DS._inverseRelationshipFor(child.type, parent.type).name; - - this.firstRecordName = name; - } - - return name; - }, - - /** @private */ - destroy: function() { - var childReference = this.childReference, - belongsToName = this.getFirstRecordName(), - hasManyName = this.getSecondRecordName(), - store = this.store, - child, oldParent, newParent, lastParent, transaction; - - store.removeRelationshipChangeFor(childReference, belongsToName, this.parentReference, hasManyName, this.changeType); - - if (transaction = this.transaction) { - transaction.relationshipBecameClean(this); - } - }, - - /** @private */ - getByReference: function(reference) { - var store = this.store; - - // return null or undefined if the original reference was null or undefined - if (!reference) { return reference; } - - if (store.recordIsMaterialized(reference)) { - return store.recordForReference(reference); - } - }, - - getSecondRecord: function(){ - return this.getByReference(this.secondRecordReference); - }, - - /** @private */ - getFirstRecord: function() { - return this.getByReference(this.firstRecordReference); - }, - - /** - @private - - Make sure that all three parts of the relationship change are part of - the same transaction. If any of the three records is clean and in the - default transaction, and the rest are in a different transaction, move - them all into that transaction. - */ - ensureSameTransaction: function() { - var child = this.getFirstRecord(), - parentRecord = this.getSecondRecord(); - - var transaction = this.store.ensureSameTransaction([child, parentRecord]); - - this.transaction = transaction; - return transaction; - }, - - callChangeEvents: function(){ - var hasManyName = this.getSecondRecordName(), - belongsToName = this.getFirstRecordName(), - child = this.getFirstRecord(), - parentRecord = this.getSecondRecord(); - - var dirtySet = new Ember.OrderedSet(); - - // TODO: This implementation causes a race condition in key-value - // stores. The fix involves buffering changes that happen while - // a record is loading. A similar fix is required for other parts - // of ember-data, and should be done as new infrastructure, not - // a one-off hack. [tomhuda] - if (parentRecord && get(parentRecord, 'isLoaded')) { - this.store.recordHasManyDidChange(dirtySet, parentRecord, this); - } - - if (child) { - this.store.recordBelongsToDidChange(dirtySet, child, this); - } - - dirtySet.forEach(function(record) { - record.adapterDidDirty(); - }); - }, - - coalesce: function(){ - var relationshipPairs = this.store.relationshipChangePairsFor(this.firstRecordReference); - forEach(relationshipPairs, function(pair){ - var addedChange = pair["add"]; - var removedChange = pair["remove"]; - if(addedChange && removedChange) { - addedChange.destroy(); - removedChange.destroy(); - } - }); - } -}; - -DS.RelationshipChangeAdd.prototype = Ember.create(DS.RelationshipChange.create({})); -DS.RelationshipChangeRemove.prototype = Ember.create(DS.RelationshipChange.create({})); - -DS.RelationshipChangeAdd.prototype.changeType = "add"; -DS.RelationshipChangeAdd.prototype.sync = function() { - var secondRecordName = this.getSecondRecordName(), - firstRecordName = this.getFirstRecordName(), - firstRecord = this.getFirstRecord(), - secondRecord = this.getSecondRecord(); - - //Ember.assert("You specified a hasMany (" + hasManyName + ") on " + (!belongsToName && (newParent || oldParent || this.lastParent).constructor) + " but did not specify an inverse belongsTo on " + child.constructor, belongsToName); - //Ember.assert("You specified a belongsTo (" + belongsToName + ") on " + child.constructor + " but did not specify an inverse hasMany on " + (!hasManyName && (newParent || oldParent || this.lastParentRecord).constructor), hasManyName); - - var transaction = this.ensureSameTransaction(); - transaction.relationshipBecameDirty(this); - - this.callChangeEvents(); - - if (secondRecord && firstRecord) { - if(this.secondRecordKind === "belongsTo"){ - secondRecord.suspendRelationshipObservers(function(){ - set(secondRecord, secondRecordName, firstRecord); - }); - - } - else if(this.secondRecordKind === "hasMany"){ - secondRecord.suspendRelationshipObservers(function(){ - get(secondRecord, secondRecordName).addObject(firstRecord); - }); - } - } - - if (firstRecord && secondRecord && get(firstRecord, firstRecordName) !== secondRecord) { - if(this.firstRecordKind === "belongsTo"){ - firstRecord.suspendRelationshipObservers(function(){ - set(firstRecord, firstRecordName, secondRecord); - }); - } - else if(this.firstdRecordKind === "hasMany"){ - firstRecord.suspendRelationshipObservers(function(){ - get(firstRecord, firstRecordName).addObject(secondRecord); - }); - } - } - - this.coalesce(); -}; - -DS.RelationshipChangeRemove.prototype.changeType = "remove"; -DS.RelationshipChangeRemove.prototype.sync = function() { - var secondRecordName = this.getSecondRecordName(), - firstRecordName = this.getFirstRecordName(), - firstRecord = this.getFirstRecord(), - secondRecord = this.getSecondRecord(); - - //Ember.assert("You specified a hasMany (" + hasManyName + ") on " + (!belongsToName && (newParent || oldParent || this.lastParent).constructor) + " but did not specify an inverse belongsTo on " + child.constructor, belongsToName); - //Ember.assert("You specified a belongsTo (" + belongsToName + ") on " + child.constructor + " but did not specify an inverse hasMany on " + (!hasManyName && (newParent || oldParent || this.lastParentRecord).constructor), hasManyName); - - var transaction = this.ensureSameTransaction(firstRecord, secondRecord, secondRecordName, firstRecordName); - transaction.relationshipBecameDirty(this); - - this.callChangeEvents(); - - if (secondRecord && firstRecord) { - if(this.secondRecordKind === "belongsTo"){ - secondRecord.suspendRelationshipObservers(function(){ - set(secondRecord, secondRecordName, null); - }); - } - else if(this.secondRecordKind === "hasMany"){ - secondRecord.suspendRelationshipObservers(function(){ - get(secondRecord, secondRecordName).removeObject(firstRecord); - }); - } - } - - if (firstRecord && get(firstRecord, firstRecordName)) { - if(this.firstRecordKind === "belongsTo"){ - firstRecord.suspendRelationshipObservers(function(){ - set(firstRecord, firstRecordName, null); - }); - } - else if(this.firstdRecordKind === "hasMany"){ - firstRecord.suspendRelationshipObservers(function(){ - get(firstRecord, firstRecordName).removeObject(secondRecord); - }); - } - } - - this.coalesce(); -}; - -function inverseBelongsToName(parentType, childType, hasManyName) { - // Get the options passed to the parent's DS.hasMany() - var options = parentType.metaForProperty(hasManyName).options; - var belongsToName; - - if (belongsToName = options.inverse) { - return belongsToName; - } - - return DS._inverseRelationshipFor(childType, parentType).name; -} - -function inverseHasManyName(parentType, childType, belongsToName) { - var options = childType.metaForProperty(belongsToName).options; - var hasManyName; - - if (hasManyName = options.inverse) { - return hasManyName; - } - - return DS._inverseRelationshipFor(parentType, childType).name; -} - -})(); - - - -(function() { - -})(); - - - -(function() { -var set = Ember.set; - -/** - This code registers an injection for Ember.Application. - - If an Ember.js developer defines a subclass of DS.Store on their application, - this code will automatically instantiate it and make it available on the - router. - - Additionally, after an application's controllers have been injected, they will - each have the store made available to them. - - For example, imagine an Ember.js application with the following classes: - - App.Store = DS.Store.extend({ - adapter: 'App.MyCustomAdapter' - }); - - App.PostsController = Ember.ArrayController.extend({ - // ... - }); - - When the application is initialized, `App.Store` will automatically be - instantiated, and the instance of `App.PostsController` will have its `store` - property set to that instance. - - Note that this code will only be run if the `ember-application` package is - loaded. If Ember Data is being used in an environment other than a - typical application (e.g., node.js where only `ember-runtime` is available), - this code will be ignored. -*/ - -Ember.onLoad('Ember.Application', function(Application) { - if (Application.registerInjection) { - Application.registerInjection({ - name: "store", - before: "controllers", - - // If a store subclass is defined, like App.Store, - // instantiate it and inject it into the router. - injection: function(app, stateManager, property) { - if (!stateManager) { return; } - if (property === 'Store') { - set(stateManager, 'store', app[property].create()); - } - } - }); - - Application.registerInjection({ - name: "giveStoreToControllers", - after: ['store','controllers'], - - // For each controller, set its `store` property - // to the DS.Store instance we created above. - injection: function(app, stateManager, property) { - if (!stateManager) { return; } - if (/^[A-Z].*Controller$/.test(property)) { - var controllerName = property.charAt(0).toLowerCase() + property.substr(1); - var store = stateManager.get('store'); - var controller = stateManager.get(controllerName); - if(!controller) { return; } - - controller.set('store', store); - } - } - }); - } else if (Application.initializer) { - Application.initializer({ - name: "store", - - initialize: function(container, application) { - application.register('store:main', application.Store); - - // Eagerly generate the store so defaultStore is populated. - // TODO: Do this in a finisher hook - container.lookup('store:main'); - } - }); - - Application.initializer({ - name: "injectStore", - - initialize: function(container, application) { - application.inject('controller', 'store', 'store:main'); - application.inject('route', 'store', 'store:main'); - } - }); - } -}); - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set, map = Ember.ArrayPolyfills.map, isNone = Ember.isNone; - -function mustImplement(name) { - return function() { - throw new Ember.Error("Your serializer " + this.toString() + " does not implement the required method " + name); - }; -} - -/** - A serializer is responsible for serializing and deserializing a group of - records. - - `DS.Serializer` is an abstract base class designed to help you build a - serializer that can read to and write from any serialized form. While most - applications will use `DS.JSONSerializer`, which reads and writes JSON, the - serializer architecture allows your adapter to transmit things like XML, - strings, or custom binary data. - - Typically, your application's `DS.Adapter` is responsible for both creating a - serializer as well as calling the appropriate methods when it needs to - materialize data or serialize a record. - - The serializer API is designed as a series of layered hooks that you can - override to customize any of the individual steps of serialization and - deserialization. - - The hooks are organized by the three responsibilities of the serializer: - - 1. Determining naming conventions - 2. Serializing records into a serialized form - 3. Deserializing records from a serialized form - - Because Ember Data lazily materializes records, the deserialization - step, and therefore the hooks you implement, are split into two phases: - - 1. Extraction, where the serialized forms for multiple records are - extracted from a single payload. The IDs of each record are also - extracted for indexing. - 2. Materialization, where a newly-created record has its attributes - and relationships initialized based on the serialized form loaded - by the adapter. - - Additionally, a serializer can convert values from their JavaScript - versions into their serialized versions via a declarative API. - - ## Naming Conventions - - One of the most common uses of the serializer is to map attribute names - from the serialized form to your `DS.Model`. For example, in your model, - you may have an attribute called `firstName`: - - ```javascript - App.Person = DS.Model.extend({ - firstName: DS.attr('string') - }); - ``` - - However, because the web API your adapter is communicating with is - legacy, it calls this attribute `FIRST_NAME`. - - You can determine the attribute name used in the serialized form - by implementing `keyForAttributeName`: - - ```javascript - keyForAttributeName: function(type, name) { - return name.underscore.toUpperCase(); - } - ``` - - If your attribute names are not predictable, you can re-map them - one-by-one using the adapter's `map` API: - - ```javascript - App.Adapter.map('App.Person', { - firstName: { key: '*API_USER_FIRST_NAME*' } - }); - ``` - - This API will also work for relationships and primary keys. For - example: - - ```javascript - App.Adapter.map('App.Person', { - primaryKey: '_id' - }); - ``` - - ## Serialization - - During the serialization process, a record or records are converted - from Ember.js objects into their serialized form. - - These methods are designed in layers, like a delicious 7-layer - cake (but with fewer layers). - - The main entry point for serialization is the `serialize` - method, which takes the record and options. - - The `serialize` method is responsible for: - - * turning the record's attributes (`DS.attr`) into - attributes on the JSON object. - * optionally adding the record's ID onto the hash - * adding relationships (`DS.hasMany` and `DS.belongsTo`) - to the JSON object. - - Depending on the backend, the serializer can choose - whether to include the `hasMany` or `belongsTo` - relationships on the JSON hash. - - For very custom serialization, you can implement your - own `serialize` method. In general, however, you will want - to override the hooks described below. - - ### Adding the ID - - The default `serialize` will optionally call your serializer's - `addId` method with the JSON hash it is creating, the - record's type, and the record's ID. The `serialize` method - will not call `addId` if the record's ID is undefined. - - Your adapter must specifically request ID inclusion by - passing `{ includeId: true }` as an option to `serialize`. - - NOTE: You may not want to include the ID when updating an - existing record, because your server will likely disallow - changing an ID after it is created, and the PUT request - itself will include the record's identification. - - By default, `addId` will: - - 1. Get the primary key name for the record by calling - the serializer's `primaryKey` with the record's type. - Unless you override the `primaryKey` method, this - will be `'id'`. - 2. Assign the record's ID to the primary key in the - JSON hash being built. - - If your backend expects a JSON object with the primary - key at the root, you can just override the `primaryKey` - method on your serializer subclass. - - Otherwise, you can override the `addId` method for - more specialized handling. - - ### Adding Attributes - - By default, the serializer's `serialize` method will call - `addAttributes` with the JSON object it is creating - and the record to serialize. - - The `addAttributes` method will then call `addAttribute` - in turn, with the JSON object, the record to serialize, - the attribute's name and its type. - - Finally, the `addAttribute` method will serialize the - attribute: - - 1. It will call `keyForAttributeName` to determine - the key to use in the JSON hash. - 2. It will get the value from the record. - 3. It will call `serializeValue` with the attribute's - value and attribute type to convert it into a - JSON-compatible value. For example, it will convert a - Date into a String. - - If your backend expects a JSON object with attributes as - keys at the root, you can just override the `serializeValue` - and `keyForAttributeName` methods in your serializer - subclass and let the base class do the heavy lifting. - - If you need something more specialized, you can probably - override `addAttribute` and let the default `addAttributes` - handle the nitty gritty. - - ### Adding Relationships - - By default, `serialize` will call your serializer's - `addRelationships` method with the JSON object that is - being built and the record being serialized. The default - implementation of this method is to loop over all of the - relationships defined on your record type and: - - * If the relationship is a `DS.hasMany` relationship, - call `addHasMany` with the JSON object, the record - and a description of the relationship. - * If the relationship is a `DS.belongsTo` relationship, - call `addBelongsTo` with the JSON object, the record - and a description of the relationship. - - The relationship description has the following keys: - - * `type`: the class of the associated information (the - first parameter to `DS.hasMany` or `DS.belongsTo`) - * `kind`: either `hasMany` or `belongsTo` - - The relationship description may get additional - information in the future if more capabilities or - relationship types are added. However, it will - remain backwards-compatible, so the mere existence - of new features should not break existing adapters. -*/ -DS.Serializer = Ember.Object.extend({ - init: function() { - this.mappings = Ember.Map.create(); - this.configurations = Ember.Map.create(); - this.globalConfigurations = {}; - }, - - extract: mustImplement('extract'), - extractMany: mustImplement('extractMany'), - - extractRecordRepresentation: function(loader, type, json, shouldSideload) { - var mapping = this.mappingForType(type); - var embeddedData, prematerialized = {}, reference; - - if (shouldSideload) { - reference = loader.sideload(type, json); - } else { - reference = loader.load(type, json); - } - - this.eachEmbeddedHasMany(type, function(name, relationship) { - var embeddedData = json[this.keyFor(relationship)]; - if (!isNone(embeddedData)) { - this.extractEmbeddedHasMany(loader, relationship, embeddedData, reference, prematerialized); - } - }, this); - - this.eachEmbeddedBelongsTo(type, function(name, relationship) { - var embeddedData = json[this.keyFor(relationship)]; - if (!isNone(embeddedData)) { - this.extractEmbeddedBelongsTo(loader, relationship, embeddedData, reference, prematerialized); - } - }, this); - - loader.prematerialize(reference, prematerialized); - - return reference; - }, - - extractEmbeddedHasMany: function(loader, relationship, array, parent, prematerialized) { - var references = map.call(array, function(item) { - if (!item) { return; } - - var reference = this.extractRecordRepresentation(loader, relationship.type, item, true); - - // If the embedded record should also be saved back when serializing the parent, - // make sure we set its parent since it will not have an ID. - var embeddedType = this.embeddedType(parent.type, relationship.key); - if (embeddedType === 'always') { - reference.parent = parent; - } - - return reference; - }, this); - - prematerialized[relationship.key] = references; - }, - - extractEmbeddedBelongsTo: function(loader, relationship, data, parent, prematerialized) { - var reference = this.extractRecordRepresentation(loader, relationship.type, data, true); - prematerialized[relationship.key] = reference; - - // If the embedded record should also be saved back when serializing the parent, - // make sure we set its parent since it will not have an ID. - var embeddedType = this.embeddedType(parent.type, relationship.key); - if (embeddedType === 'always') { - reference.parent = parent; - } - }, - - //....................... - //. SERIALIZATION HOOKS - //....................... - - /** - The main entry point for serializing a record. While you can consider this - a hook that can be overridden in your serializer, you will have to manually - handle serialization. For most cases, there are more granular hooks that you - can override. - - If overriding this method, these are the responsibilities that you will need - to implement yourself: - - * If the option hash contains `includeId`, add the record's ID to the serialized form. - By default, `serialize` calls `addId` if appropriate. - * Add the record's attributes to the serialized form. By default, `serialize` calls - `addAttributes`. - * Add the record's relationships to the serialized form. By default, `serialize` calls - `addRelationships`. - - @param {DS.Model} record the record to serialize - @param {Object} [options] a hash of options - @returns {any} the serialized form of the record - */ - serialize: function(record, options) { - options = options || {}; - - var serialized = this.createSerializedForm(), id; - - if (options.includeId) { - if (id = get(record, 'id')) { - this._addId(serialized, record.constructor, id); - } - } - - this.addAttributes(serialized, record); - this.addRelationships(serialized, record); - - return serialized; - }, - - /** - @private - - Given an attribute type and value, convert the value into the - serialized form using the transform registered for that type. - - @param {any} value the value to convert to the serialized form - @param {String} attributeType the registered type (e.g. `string` - or `boolean`) - @returns {any} the serialized form of the value - */ - serializeValue: function(value, attributeType) { - var transform = this.transforms ? this.transforms[attributeType] : null; - - Ember.assert("You tried to use an attribute type (" + attributeType + ") that has not been registered", transform); - return transform.serialize(value); - }, - - /** - A hook you can use to normalize IDs before adding them to the - serialized representation. - - Because the store coerces all IDs to strings for consistency, - this is the opportunity for the serializer to, for example, - convert numerical IDs back into number form. - - @param {String} id the id from the record - @returns {any} the serialized representation of the id - */ - serializeId: function(id) { - if (isNaN(id)) { return id; } - return +id; - }, - - /** - A hook you can use to change how attributes are added to the serialized - representation of a record. - - By default, `addAttributes` simply loops over all of the attributes of the - passed record, maps the attribute name to the key for the serialized form, - and invokes any registered transforms on the value. It then invokes the - more granular `addAttribute` with the key and transformed value. - - Since you can override `keyForAttributeName`, `addAttribute`, and register - custom tranforms, you should rarely need to override this hook. - - @param {any} data the serialized representation that is being built - @param {DS.Model} record the record to serialize - */ - addAttributes: function(data, record) { - record.eachAttribute(function(name, attribute) { - this._addAttribute(data, record, name, attribute.type); - }, this); - }, - - /** - A hook you can use to customize how the key/value pair is added to - the serialized data. - - @param {any} serialized the serialized form being built - @param {String} key the key to add to the serialized data - @param {any} value the value to add to the serialized data - */ - addAttribute: Ember.K, - - /** - A hook you can use to customize how the record's id is added to - the serialized data. - - The `addId` hook is called with: - - * the serialized representation being built - * the resolved primary key (taking configurations and the - `primaryKey` hook into consideration) - * the serialized id (after calling the `serializeId` hook) - - @param {any} data the serialized representation that is being built - @param {String} key the resolved primary key - @param {id} id the serialized id - */ - addId: Ember.K, - - /** - A hook you can use to change how relationships are added to the serialized - representation of a record. - - By default, `addAttributes` loops over all of the relationships of the - passed record, maps the relationship names to the key for the serialized form, - and then invokes the public `addBelongsTo` and `addHasMany` hooks. - - Since you can override `keyForBelongsTo`, `keyForHasMany`, `addBelongsTo`, - `addHasMany`, and register mappings, you should rarely need to override this - hook. - - @param {any} data the serialized representation that is being built - @param {DS.Model} record the record to serialize - */ - addRelationships: function(data, record) { - record.eachRelationship(function(name, relationship) { - if (relationship.kind === 'belongsTo') { - this._addBelongsTo(data, record, name, relationship); - } else if (relationship.kind === 'hasMany') { - this._addHasMany(data, record, name, relationship); - } - }, this); - }, - - /** - A hook you can use to add a `belongsTo` relationship to the - serialized representation. - - The specifics of this hook are very adapter-specific, so there - is no default implementation. You can see `DS.JSONSerializer` - for an example of an implementation of the `addBelongsTo` hook. - - The `belongsTo` relationship object has the following properties: - - * **type** a subclass of DS.Model that is the type of the - relationship. This is the first parameter to DS.belongsTo - * **options** the options passed to the call to DS.belongsTo - * **kind** always `belongsTo` - - Additional properties may be added in the future. - - @param {any} data the serialized representation that is being built - @param {DS.Model} record the record to serialize - @param {String} key the key for the serialized object - @param {Object} relationship an object representing the relationship - */ - addBelongsTo: Ember.K, - - /** - A hook you can use to add a `hasMany` relationship to the - serialized representation. - - The specifics of this hook are very adapter-specific, so there - is no default implementation. You may not need to implement this, - for example, if your backend only expects relationships on the - child of a one to many relationship. - - The `hasMany` relationship object has the following properties: - - * **type** a subclass of DS.Model that is the type of the - relationship. This is the first parameter to DS.hasMany - * **options** the options passed to the call to DS.hasMany - * **kind** always `hasMany` - - Additional properties may be added in the future. - - @param {any} data the serialized representation that is being built - @param {DS.Model} record the record to serialize - @param {String} key the key for the serialized object - @param {Object} relationship an object representing the relationship - */ - addHasMany: Ember.K, - - /** - NAMING CONVENTIONS - - The most commonly overridden APIs of the serializer are - the naming convention methods: - - * `keyForAttributeName`: converts a camelized attribute name - into a key in the adapter-provided data hash. For example, - if the model's attribute name was `firstName`, and the - server used underscored names, you would return `first_name`. - * `primaryKey`: returns the key that should be used to - extract the id from the adapter-provided data hash. It is - also used when serializing a record. - */ - - /** - A hook you can use in your serializer subclass to customize - how an unmapped attribute name is converted into a key. - - By default, this method returns the `name` parameter. - - For example, if the attribute names in your JSON are underscored, - you will want to convert them into JavaScript conventional - camelcase: - - ```javascript - App.MySerializer = DS.Serializer.extend({ - // ... - - keyForAttributeName: function(type, name) { - return name.camelize(); - } - }); - ``` - - @param {DS.Model subclass} type the type of the record with - the attribute name `name` - @param {String} name the attribute name to convert into a key - - @returns {String} the key - */ - keyForAttributeName: function(type, name) { - return name; - }, - - /** - A hook you can use in your serializer to specify a conventional - primary key. - - By default, this method will return the string `id`. - - In general, you should not override this hook to specify a special - primary key for an individual type; use `configure` instead. - - For example, if your primary key is always `__id__`: - - ```javascript - App.MySerializer = DS.Serializer.extend({ - // ... - primaryKey: function(type) { - return '__id__'; - } - }); - ``` - - In another example, if the primary key always includes the - underscored version of the type before the string `id`: - - ```javascript - App.MySerializer = DS.Serializer.extend({ - // ... - primaryKey: function(type) { - // If the type is `BlogPost`, this will return - // `blog_post_id`. - var typeString = type.toString().split(".")[1].underscore(); - return typeString + "_id"; - } - }); - ``` - - @param {DS.Model subclass} type - @returns {String} the primary key for the type - */ - primaryKey: function(type) { - return "id"; - }, - - /** - A hook you can use in your serializer subclass to customize - how an unmapped `belongsTo` relationship is converted into - a key. - - By default, this method calls `keyForAttributeName`, so if - your naming convention is uniform across attributes and - relationships, you can use the default here and override - just `keyForAttributeName` as needed. - - For example, if the `belongsTo` names in your JSON always - begin with `BT_` (e.g. `BT_posts`), you can strip out the - `BT_` prefix:" - - ```javascript - App.MySerializer = DS.Serializer.extend({ - // ... - keyForBelongsTo: function(type, name) { - return name.match(/^BT_(.*)$/)[1].camelize(); - } - }); - ``` - - @param {DS.Model subclass} type the type of the record with - the `belongsTo` relationship. - @param {String} name the relationship name to convert into a key - - @returns {String} the key - */ - keyForBelongsTo: function(type, name) { - return this.keyForAttributeName(type, name); - }, - - /** - A hook you can use in your serializer subclass to customize - how an unmapped `hasMany` relationship is converted into - a key. - - By default, this method calls `keyForAttributeName`, so if - your naming convention is uniform across attributes and - relationships, you can use the default here and override - just `keyForAttributeName` as needed. - - For example, if the `hasMany` names in your JSON always - begin with the "table name" for the current type (e.g. - `post_comments`), you can strip out the prefix:" - - ```javascript - App.MySerializer = DS.Serializer.extend({ - // ... - keyForHasMany: function(type, name) { - // if your App.BlogPost has many App.BlogComment, the key from - // the server would look like: `blog_post_blog_comments` - // - // 1. Convert the type into a string and underscore the - // second part (App.BlogPost -> blog_post) - // 2. Extract the part after `blog_post_` (`blog_comments`) - // 3. Underscore it, to become `blogComments` - var typeString = type.toString().split(".")[1].underscore(); - return name.match(new RegExp("^" + typeString + "_(.*)$"))[1].camelize(); - } - }); - ``` - - @param {DS.Model subclass} type the type of the record with - the `belongsTo` relationship. - @param {String} name the relationship name to convert into a key - - @returns {String} the key - */ - keyForHasMany: function(type, name) { - return this.keyForAttributeName(type, name); - }, - - //......................... - //. MATERIALIZATION HOOKS - //......................... - - materialize: function(record, serialized, prematerialized) { - var id; - if (Ember.isNone(get(record, 'id'))) { - if (prematerialized && prematerialized.hasOwnProperty('id')) { - id = prematerialized.id; - } else { - id = this.extractId(record.constructor, serialized); - } - record.materializeId(id); - } - - this.materializeAttributes(record, serialized, prematerialized); - this.materializeRelationships(record, serialized, prematerialized); - }, - - deserializeValue: function(value, attributeType) { - var transform = this.transforms ? this.transforms[attributeType] : null; - - Ember.assert("You tried to use a attribute type (" + attributeType + ") that has not been registered", transform); - return transform.deserialize(value); - }, - - materializeAttributes: function(record, serialized, prematerialized) { - record.eachAttribute(function(name, attribute) { - if (prematerialized && prematerialized.hasOwnProperty(name)) { - record.materializeAttribute(name, prematerialized[name]); - } else { - this.materializeAttribute(record, serialized, name, attribute.type); - } - }, this); - }, - - materializeAttribute: function(record, serialized, attributeName, attributeType) { - var value = this.extractAttribute(record.constructor, serialized, attributeName); - value = this.deserializeValue(value, attributeType); - - record.materializeAttribute(attributeName, value); - }, - - materializeRelationships: function(record, hash, prematerialized) { - record.eachRelationship(function(name, relationship) { - if (relationship.kind === 'hasMany') { - if (prematerialized && prematerialized.hasOwnProperty(name)) { - record.materializeHasMany(name, prematerialized[name]); - } else { - this.materializeHasMany(name, record, hash, relationship, prematerialized); - } - } else if (relationship.kind === 'belongsTo') { - if (prematerialized && prematerialized.hasOwnProperty(name)) { - record.materializeBelongsTo(name, prematerialized[name]); - } else { - this.materializeBelongsTo(name, record, hash, relationship, prematerialized); - } - } - }, this); - }, - - materializeHasMany: function(name, record, hash, relationship) { - var key = this._keyForHasMany(record.constructor, relationship.key); - record.materializeHasMany(name, this.extractHasMany(record.constructor, hash, key)); - }, - - materializeBelongsTo: function(name, record, hash, relationship) { - var key = this._keyForBelongsTo(record.constructor, relationship.key); - record.materializeBelongsTo(name, this.extractBelongsTo(record.constructor, hash, key)); - }, - - _extractEmbeddedRelationship: function(type, hash, name, relationshipType) { - var key = this['_keyFor' + relationshipType](type, name); - - if (this.embeddedType(type, name)) { - return this['extractEmbedded' + relationshipType](type, hash, key); - } - }, - - _extractEmbeddedBelongsTo: function(type, hash, name) { - return this._extractEmbeddedRelationship(type, hash, name, 'BelongsTo'); - }, - - _extractEmbeddedHasMany: function(type, hash, name) { - return this._extractEmbeddedRelationship(type, hash, name, 'HasMany'); - }, - - /** - @private - - This method is called to get the primary key for a given - type. - - If a primary key configuration exists for this type, this - method will return the configured value. Otherwise, it will - call the public `primaryKey` hook. - - @param {DS.Model subclass} type - @returns {String} the primary key for the type - */ - _primaryKey: function(type) { - var config = this.configurationForType(type), - primaryKey = config && config.primaryKey; - - if (primaryKey) { - return primaryKey; - } else { - return this.primaryKey(type); - } - }, - - /** - @private - - This method looks up the key for the attribute name and transforms the - attribute's value using registered transforms. - - Specifically: - - 1. Look up the key for the attribute name. If available, this will use - any registered mappings. Otherwise, it will invoke the public - `keyForAttributeName` hook. - 2. Get the value from the record using the `attributeName`. - 3. Transform the value using registered transforms for the `attributeType`. - 4. Invoke the public `addAttribute` hook with the hash, key, and - transformed value. - - @param {any} data the serialized representation being built - @param {DS.Model} record the record to serialize - @param {String} attributeName the name of the attribute on the record - @param {String} attributeType the type of the attribute (e.g. `string` - or `boolean`) - */ - _addAttribute: function(data, record, attributeName, attributeType) { - var key = this._keyForAttributeName(record.constructor, attributeName); - var value = get(record, attributeName); - - this.addAttribute(data, key, this.serializeValue(value, attributeType)); - }, - - /** - @private - - This method looks up the primary key for the `type` and invokes - `serializeId` on the `id`. - - It then invokes the public `addId` hook with the primary key and - the serialized id. - - @param {any} data the serialized representation that is being built - @param {Ember.Model subclass} type - @param {any} id the materialized id from the record - */ - _addId: function(hash, type, id) { - var primaryKey = this._primaryKey(type); - - this.addId(hash, primaryKey, this.serializeId(id)); - }, - - /** - @private - - This method is called to get a key used in the data from - an attribute name. It first checks for any mappings before - calling the public hook `keyForAttributeName`. - - @param {DS.Model subclass} type the type of the record with - the attribute name `name` - @param {String} name the attribute name to convert into a key - - @returns {String} the key - */ - _keyForAttributeName: function(type, name) { - return this._keyFromMappingOrHook('keyForAttributeName', type, name); - }, - - /** - @private - - This method is called to get a key used in the data from - a belongsTo relationship. It first checks for any mappings before - calling the public hook `keyForBelongsTo`. - - @param {DS.Model subclass} type the type of the record with - the `belongsTo` relationship. - @param {String} name the relationship name to convert into a key - - @returns {String} the key - */ - _keyForBelongsTo: function(type, name) { - return this._keyFromMappingOrHook('keyForBelongsTo', type, name); - }, - - keyFor: function(description) { - var type = description.parentType, - name = description.key; - - switch (description.kind) { - case 'belongsTo': - return this._keyForBelongsTo(type, name); - case 'hasMany': - return this._keyForHasMany(type, name); - } - }, - - /** - @private - - This method is called to get a key used in the data from - a hasMany relationship. It first checks for any mappings before - calling the public hook `keyForHasMany`. - - @param {DS.Model subclass} type the type of the record with - the `hasMany` relationship. - @param {String} name the relationship name to convert into a key - - @returns {String} the key - */ - _keyForHasMany: function(type, name) { - return this._keyFromMappingOrHook('keyForHasMany', type, name); - }, - /** - @private - - This method converts the relationship name to a key for serialization, - and then invokes the public `addBelongsTo` hook. - - @param {any} data the serialized representation that is being built - @param {DS.Model} record the record to serialize - @param {String} name the relationship name - @param {Object} relationship an object representing the relationship - */ - _addBelongsTo: function(data, record, name, relationship) { - var key = this._keyForBelongsTo(record.constructor, name); - this.addBelongsTo(data, record, key, relationship); - }, - - /** - @private - - This method converts the relationship name to a key for serialization, - and then invokes the public `addHasMany` hook. - - @param {any} data the serialized representation that is being built - @param {DS.Model} record the record to serialize - @param {String} name the relationship name - @param {Object} relationship an object representing the relationship - */ - _addHasMany: function(data, record, name, relationship) { - var key = this._keyForHasMany(record.constructor, name); - this.addHasMany(data, record, key, relationship); - }, - - /** - @private - - An internal method that handles checking whether a mapping - exists for a particular attribute or relationship name before - calling the public hooks. - - If a mapping is found, and the mapping has a key defined, - use that instead of invoking the hook. - - @param {String} publicMethod the public hook to invoke if - a mapping is not found (e.g. `keyForAttributeName`) - @param {DS.Model subclass} type the type of the record with - the attribute or relationship name. - @param {String} name the attribute or relationship name to - convert into a key - */ - _keyFromMappingOrHook: function(publicMethod, type, name) { - var key = this.mappingOption(type, name, 'key'); - - if (key) { - return key; - } else { - return this[publicMethod](type, name); - } - }, - - /** - TRANSFORMS - */ - - registerTransform: function(type, transform) { - this.transforms[type] = transform; - }, - - registerEnumTransform: function(type, objects) { - var transform = { - deserialize: function(deserialized) { - return Ember.A(objects).objectAt(deserialized); - }, - serialize: function(serialized) { - return Ember.EnumerableUtils.indexOf(objects, serialized); - }, - values: objects - }; - this.registerTransform(type, transform); - }, - - /** - MAPPING CONVENIENCE - */ - - map: function(type, mappings) { - this.mappings.set(type, mappings); - }, - - configure: function(type, configuration) { - if (type && !configuration) { - Ember.merge(this.globalConfigurations, type); - return; - } - - var config = Ember.create(this.globalConfigurations); - Ember.merge(config, configuration); - - this.configurations.set(type, config); - }, - - mappingForType: function(type) { - this._reifyMappings(); - return this.mappings.get(type) || {}; - }, - - configurationForType: function(type) { - this._reifyConfigurations(); - return this.configurations.get(type) || this.globalConfigurations; - }, - - _reifyMappings: function() { - if (this._didReifyMappings) { return; } - - var mappings = this.mappings, - reifiedMappings = Ember.Map.create(); - - mappings.forEach(function(key, mapping) { - if (typeof key === 'string') { - var type = Ember.get(Ember.lookup, key); - Ember.assert("Could not find model at path " + key, type); - - reifiedMappings.set(type, mapping); - } else { - reifiedMappings.set(key, mapping); - } - }); - - this.mappings = reifiedMappings; - - this._didReifyMappings = true; - }, - - _reifyConfigurations: function() { - if (this._didReifyConfigurations) { return; } - - var configurations = this.configurations, - reifiedConfigurations = Ember.Map.create(); - - configurations.forEach(function(key, mapping) { - if (typeof key === 'string' && key !== 'plurals') { - var type = Ember.get(Ember.lookup, key); - Ember.assert("Could not find model at path " + key, type); - - reifiedConfigurations.set(type, mapping); - } else { - reifiedConfigurations.set(key, mapping); - } - }); - - this.configurations = reifiedConfigurations; - - this._didReifyConfigurations = true; - }, - - mappingOption: function(type, name, option) { - var mapping = this.mappingForType(type)[name]; - - return mapping && mapping[option]; - }, - - configOption: function(type, option) { - var config = this.configurationForType(type); - - return config[option]; - }, - - // EMBEDDED HELPERS - - embeddedType: function(type, name) { - return this.mappingOption(type, name, 'embedded'); - }, - - eachEmbeddedRecord: function(record, callback, binding) { - this.eachEmbeddedBelongsToRecord(record, callback, binding); - this.eachEmbeddedHasManyRecord(record, callback, binding); - }, - - eachEmbeddedBelongsToRecord: function(record, callback, binding) { - var type = record.constructor; - - this.eachEmbeddedBelongsTo(record.constructor, function(name, relationship, embeddedType) { - var embeddedRecord = get(record, name); - if (embeddedRecord) { callback.call(binding, embeddedRecord, embeddedType); } - }); - }, - - eachEmbeddedHasManyRecord: function(record, callback, binding) { - var type = record.constructor; - - this.eachEmbeddedHasMany(record.constructor, function(name, relationship, embeddedType) { - var array = get(record, name); - for (var i=0, l=get(array, 'length'); i<l; i++) { - callback.call(binding, array.objectAt(i), embeddedType); - } - }); - }, - - eachEmbeddedHasMany: function(type, callback, binding) { - this.eachEmbeddedRelationship(type, 'hasMany', callback, binding); - }, - - eachEmbeddedBelongsTo: function(type, callback, binding) { - this.eachEmbeddedRelationship(type, 'belongsTo', callback, binding); - }, - - eachEmbeddedRelationship: function(type, kind, callback, binding) { - type.eachRelationship(function(name, relationship) { - var embeddedType = this.embeddedType(type, name); - - if (embeddedType) { - if (relationship.kind === kind) { - callback.call(binding, name, relationship, embeddedType); - } - } - }, this); - } -}); - - -})(); - - - -(function() { -var none = Ember.isNone; - -/** - DS.Transforms is a hash of transforms used by DS.Serializer. -*/ -DS.JSONTransforms = { - string: { - deserialize: function(serialized) { - return none(serialized) ? null : String(serialized); - }, - - serialize: function(deserialized) { - return none(deserialized) ? null : String(deserialized); - } - }, - - number: { - deserialize: function(serialized) { - return none(serialized) ? null : Number(serialized); - }, - - serialize: function(deserialized) { - return none(deserialized) ? null : Number(deserialized); - } - }, - - // Handles the following boolean inputs: - // "TrUe", "t", "f", "FALSE", 0, (non-zero), or boolean true/false - 'boolean': { - deserialize: function(serialized) { - var type = typeof serialized; - - if (type === "boolean") { - return serialized; - } else if (type === "string") { - return serialized.match(/^true$|^t$|^1$/i) !== null; - } else if (type === "number") { - return serialized === 1; - } else { - return false; - } - }, - - serialize: function(deserialized) { - return Boolean(deserialized); - } - }, - - date: { - deserialize: function(serialized) { - var type = typeof serialized; - var date = null; - - if (type === "string" || type === "number") { - // this is a fix for Safari 5.1.5 on Mac which does not accept timestamps as yyyy-mm-dd - if (type === "string" && serialized.search(/^\d{4}-\d{2}-\d{2}$/) !== -1) { - serialized += "T00:00:00Z"; - } - - date = new Date(serialized); - - // this is a fix for IE8 which does not accept timestamps in ISO 8601 format - if (type === "string" && isNaN(date)) { - date = new Date(Date.parse(serialized.replace(/\-/ig, '/').replace(/Z$/, '').split('.')[0])); - } - - return date; - } else if (serialized === null || serialized === undefined) { - // if the value is not present in the data, - // return undefined, not null. - return serialized; - } else { - return null; - } - }, - - serialize: function(date) { - if (date instanceof Date) { - var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; - var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; - - var pad = function(num) { - return num < 10 ? "0"+num : ""+num; - }; - - var utcYear = date.getUTCFullYear(), - utcMonth = date.getUTCMonth(), - utcDayOfMonth = date.getUTCDate(), - utcDay = date.getUTCDay(), - utcHours = date.getUTCHours(), - utcMinutes = date.getUTCMinutes(), - utcSeconds = date.getUTCSeconds(); - - - var dayOfWeek = days[utcDay]; - var dayOfMonth = pad(utcDayOfMonth); - var month = months[utcMonth]; - - return dayOfWeek + ", " + dayOfMonth + " " + month + " " + utcYear + " " + - pad(utcHours) + ":" + pad(utcMinutes) + ":" + pad(utcSeconds) + " GMT"; - } else if (date === undefined) { - return undefined; - } else { - return null; - } - } - } -}; - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set; - -var generatedId = 0; - -DS.JSONSerializer = DS.Serializer.extend({ - init: function() { - this._super(); - - if (!get(this, 'transforms')) { - this.set('transforms', DS.JSONTransforms); - } - - this.sideloadMapping = Ember.Map.create(); - - this.configure({ - meta: 'meta', - since: 'since' - }); - }, - - configure: function(type, configuration) { - if (type && !configuration) { - return this._super(type); - } - - var sideloadAs = configuration.sideloadAs; - - if (sideloadAs) { - this.sideloadMapping.set(sideloadAs, type); - - // Set a flag indicating that mappings may need to be normalized - // (i.e. converted from strings -> types) before sideloading. - // We can't do this conversion immediately here, because `configure` - // may be called before certain types have been defined. - this.sideloadMapping.normalized = false; - - delete configuration.sideloadAs; - } - - this._super.apply(this, arguments); - }, - - addId: function(data, key, id) { - data[key] = id; - }, - - /** - A hook you can use to customize how the key/value pair is added to - the serialized data. - - @param {any} hash the JSON hash being built - @param {String} key the key to add to the serialized data - @param {any} value the value to add to the serialized data - */ - addAttribute: function(hash, key, value) { - hash[key] = value; - }, - - /** - @private - - Creates an empty hash that will be filled in by the hooks called from the - `serialize()` method. - - @return {Object} - */ - createSerializedForm: function() { - return {}; - }, - - extractAttribute: function(type, hash, attributeName) { - var key = this._keyForAttributeName(type, attributeName); - return hash[key]; - }, - - extractId: function(type, hash) { - var primaryKey = this._primaryKey(type); - - if (hash.hasOwnProperty(primaryKey)) { - // Ensure that we coerce IDs to strings so that record - // IDs remain consistent between application runs; especially - // if the ID is serialized and later deserialized from the URL, - // when type information will have been lost. - return hash[primaryKey]+''; - } else { - return null; - } - }, - - extractHasMany: function(type, hash, key) { - return hash[key]; - }, - - extractBelongsTo: function(type, hash, key) { - return hash[key]; - }, - - addBelongsTo: function(hash, record, key, relationship) { - var type = record.constructor, - name = relationship.key, - value = null, - embeddedChild; - - if (this.embeddedType(type, name)) { - if (embeddedChild = get(record, name)) { - value = this.serialize(embeddedChild, { includeId: true }); - } - - hash[key] = value; - } else { - var id = get(record, relationship.key+'.id'); - if (!Ember.isNone(id)) { hash[key] = id; } - } - }, - - /** - Adds a has-many relationship to the JSON hash being built. - - The default REST semantics are to only add a has-many relationship if it - is embedded. If the relationship was initially loaded by ID, we assume that - that was done as a performance optimization, and that changes to the - has-many should be saved as foreign key changes on the child's belongs-to - relationship. - - @param {Object} hash the JSON being built - @param {DS.Model} record the record being serialized - @param {String} key the JSON key into which the serialized relationship - should be saved - @param {Object} relationship metadata about the relationship being serialized - */ - addHasMany: function(hash, record, key, relationship) { - var type = record.constructor, - name = relationship.key, - serializedHasMany = [], - manyArray, embeddedType; - - // If the has-many is not embedded, there is nothing to do. - embeddedType = this.embeddedType(type, name); - if (embeddedType !== 'always') { return; } - - // Get the DS.ManyArray for the relationship off the record - manyArray = get(record, name); - - // Build up the array of serialized records - manyArray.forEach(function (record) { - serializedHasMany.push(this.serialize(record, { includeId: true })); - }, this); - - // Set the appropriate property of the serialized JSON to the - // array of serialized embedded records - hash[key] = serializedHasMany; - }, - - // EXTRACTION - - extract: function(loader, json, type, record) { - var root = this.rootForType(type); - - this.sideload(loader, type, json, root); - this.extractMeta(loader, type, json); - - if (json[root]) { - if (record) { loader.updateId(record, json[root]); } - this.extractRecordRepresentation(loader, type, json[root]); - } - }, - - extractMany: function(loader, json, type, records) { - var root = this.rootForType(type); - root = this.pluralize(root); - - this.sideload(loader, type, json, root); - this.extractMeta(loader, type, json); - - if (json[root]) { - var objects = json[root], references = []; - if (records) { records = records.toArray(); } - - for (var i = 0; i < objects.length; i++) { - if (records) { loader.updateId(records[i], objects[i]); } - var reference = this.extractRecordRepresentation(loader, type, objects[i]); - references.push(reference); - } - - loader.populateArray(references); - } - }, - - extractMeta: function(loader, type, json) { - var meta = json[this.configOption(type, 'meta')], since; - if (!meta) { return; } - - if (since = meta[this.configOption(type, 'since')]) { - loader.sinceForType(type, since); - } - }, - - /** - @private - - Iterates over the `json` payload and attempts to load any data - included alongside `root`. - - The keys expected for sideloaded data are based upon the types related - to the root model. Recursion is used to ensure that types related to - related types can be loaded as well. Any custom keys specified by - `sideloadAs` mappings will also be respected. - - @param {DS.Store subclass} loader - @param {DS.Model subclass} type - @param {Object} json - @param {String} root - */ - sideload: function(loader, type, json, root) { - var sideloadedType; - - this.normalizeSideloadMappings(); - this.configureSideloadMappingForType(type); - - for (var prop in json) { - if (!json.hasOwnProperty(prop) || - prop === root || - prop === this.configOption(type, 'meta')) { - continue; - } - - sideloadedType = this.sideloadMapping.get(prop); - Ember.assert("Your server returned a hash with the key " + prop + - " but you have no mapping for it", - !!sideloadedType); - - this.loadValue(loader, sideloadedType, json[prop]); - } - }, - - /** - @private - - Iterates over all the `sideloadAs` mappings and converts any that are - strings to their equivalent types. - - This is an optimization used to avoid performing lookups for every - call to `sideload`. - */ - normalizeSideloadMappings: function() { - if (! this.sideloadMapping.normalized) { - this.sideloadMapping.forEach(function(key, value) { - if (typeof value === 'string') { - this.sideloadMapping.set(key, get(Ember.lookup, value)); - } - }, this); - this.sideloadMapping.normalized = true; - } - }, - - /** - @private - - Configures possible sideload mappings for the types related to a - particular model. This recursive method ensures that sideloading - works for related models as well. - - @param {DS.Model subclass} type - @param {Ember.A} configured an array of types that have already been configured - */ - configureSideloadMappingForType: function(type, configured) { - if (!configured) {configured = Ember.A([]);} - configured.pushObject(type); - - type.eachRelatedType(function(relatedType) { - if (!configured.contains(relatedType)) { - var root = this.sideloadMappingForType(relatedType); - if (!root) { - root = this.defaultSideloadRootForType(relatedType); - this.sideloadMapping.set(root, relatedType); - } - this.configureSideloadMappingForType(relatedType, configured); - } - }, this); - }, - - loadValue: function(loader, type, value) { - if (value instanceof Array) { - for (var i=0; i < value.length; i++) { - loader.sideload(type, value[i]); - } - } else { - loader.sideload(type, value); - } - }, - - // HELPERS - - // define a plurals hash in your subclass to define - // special-case pluralization - pluralize: function(name) { - var plurals = this.configurations.get('plurals'); - return (plurals && plurals[name]) || name + "s"; - }, - - // use the same plurals hash to determine - // special-case singularization - singularize: function(name) { - var plurals = this.configurations.get('plurals'); - if (plurals) { - for (var i in plurals) { - if (plurals[i] === name) { - return i; - } - } - } - if (name.lastIndexOf('s') === name.length - 1) { - return name.substring(0, name.length - 1); - } else { - return name; - } - }, - - /** - @private - - Determines the singular root name for a particular type. - - This is an underscored, lowercase version of the model name. - For example, the type `App.UserGroup` will have the root - `user_group`. - - @param {DS.Model subclass} type - @returns {String} name of the root element - */ - rootForType: function(type) { - var typeString = type.toString(); - - Ember.assert("Your model must not be anonymous. It was " + type, typeString.charAt(0) !== '('); - - // use the last part of the name as the URL - var parts = typeString.split("."); - var name = parts[parts.length - 1]; - return name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1); - }, - - /** - @private - - Determines the root name mapped to a particular sideloaded type. - - @param {DS.Model subclass} type - @returns {String} name of the root element, if any is registered - */ - sideloadMappingForType: function(type) { - this.sideloadMapping.forEach(function(key, value) { - if (type === value) { - return key; - } - }); - }, - - /** - @private - - The default root name for a particular sideloaded type. - - @param {DS.Model subclass} type - @returns {String} name of the root element - */ - defaultSideloadRootForType: function(type) { - return this.pluralize(this.rootForType(type)); - } -}); - -})(); - - - -(function() { -function loaderFor(store) { - return { - load: function(type, data, prematerialized) { - return store.load(type, data, prematerialized); - }, - - loadMany: function(type, array) { - return store.loadMany(type, array); - }, - - updateId: function(record, data) { - return store.updateId(record, data); - }, - - populateArray: Ember.K, - - sideload: function(type, data) { - return store.load(type, data); - }, - - sideloadMany: function(type, array) { - return store.loadMany(type, array); - }, - - prematerialize: function(reference, prematerialized) { - store.prematerialize(reference, prematerialized); - }, - - sinceForType: function(type, since) { - store.sinceForType(type, since); - } - }; -} - -DS.loaderFor = loaderFor; - -/** - An adapter is an object that receives requests from a store and - translates them into the appropriate action to take against your - persistence layer. The persistence layer is usually an HTTP API, but may - be anything, such as the browser's local storage. - - ### Creating an Adapter - - First, create a new subclass of `DS.Adapter`: - - App.MyAdapter = DS.Adapter.extend({ - // ...your code here - }); - - To tell your store which adapter to use, set its `adapter` property: - - App.store = DS.Store.create({ - revision: 3, - adapter: App.MyAdapter.create() - }); - - `DS.Adapter` is an abstract base class that you should override in your - application to customize it for your backend. The minimum set of methods - that you should implement is: - - * `find()` - * `createRecord()` - * `updateRecord()` - * `deleteRecord()` - - To improve the network performance of your application, you can optimize - your adapter by overriding these lower-level methods: - - * `findMany()` - * `createRecords()` - * `updateRecords()` - * `deleteRecords()` - * `commit()` -*/ - -var get = Ember.get, set = Ember.set, merge = Ember.merge; - -DS.Adapter = Ember.Object.extend(DS._Mappable, { - - init: function() { - var serializer = get(this, 'serializer'); - - if (Ember.Object.detect(serializer)) { - serializer = serializer.create(); - set(this, 'serializer', serializer); - } - - this._attributesMap = this.createInstanceMapFor('attributes'); - this._configurationsMap = this.createInstanceMapFor('configurations'); - - this._outstandingOperations = new Ember.MapWithDefault({ - defaultValue: function() { return 0; } - }); - - this._dependencies = new Ember.MapWithDefault({ - defaultValue: function() { return new Ember.OrderedSet(); } - }); - - this.registerSerializerTransforms(this.constructor, serializer, {}); - this.registerSerializerMappings(serializer); - }, - - /** - Loads a payload for a record into the store. - - This method asks the serializer to break the payload into - constituent parts, and then loads them into the store. For example, - if you have a payload that contains embedded records, they will be - extracted by the serializer and loaded into the store. - - For example: - - ```javascript - adapter.load(store, App.Person, { - id: 123, - firstName: "Yehuda", - lastName: "Katz", - occupations: [{ - id: 345, - title: "Tricycle Mechanic" - }] - }); - ``` - - This will load the payload for the `App.Person` with ID `123` and - the embedded `App.Occupation` with ID `345`. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {any} payload - */ - load: function(store, type, payload) { - var loader = loaderFor(store); - get(this, 'serializer').extractRecordRepresentation(loader, type, payload); - }, - - /** - Acknowledges that the adapter has finished creating a record. - - Your adapter should call this method from `createRecord` when - it has saved a new record to its persistent storage and received - an acknowledgement. - - If the persistent storage returns a new payload in response to the - creation, and you want to update the existing record with the - new information, pass the payload as the fourth parameter. - - For example, the `RESTAdapter` saves newly created records by - making an Ajax request. When the server returns, the adapter - calls didCreateRecord. If the server returns a response body, - it is passed as the payload. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {DS.Model} record - @param {any} payload - */ - didCreateRecord: function(store, type, record, payload) { - store.didSaveRecord(record); - - if (payload) { - var loader = DS.loaderFor(store); - - loader.load = function(type, data, prematerialized) { - store.updateId(record, data); - return store.load(type, data, prematerialized); - }; - - get(this, 'serializer').extract(loader, payload, type); - } - }, - - /** - Acknowledges that the adapter has finished creating several records. - - Your adapter should call this method from `createRecords` when it - has saved multiple created records to its persistent storage - received an acknowledgement. - - If the persistent storage returns a new payload in response to the - creation, and you want to update the existing record with the - new information, pass the payload as the fourth parameter. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {DS.Model} record - @param {any} payload - */ - didCreateRecords: function(store, type, records, payload) { - records.forEach(function(record) { - store.didSaveRecord(record); - }, this); - - if (payload) { - var loader = DS.loaderFor(store); - get(this, 'serializer').extractMany(loader, payload, type, records); - } - }, - - /** - @private - - Acknowledges that the adapter has finished updating or deleting a record. - - Your adapter should call this method from `updateRecord` or `deleteRecord` - when it has updated or deleted a record to its persistent storage and - received an acknowledgement. - - If the persistent storage returns a new payload in response to the - update or delete, and you want to update the existing record with the - new information, pass the payload as the fourth parameter. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {DS.Model} record - @param {any} payload - */ - didSaveRecord: function(store, type, record, payload) { - store.didSaveRecord(record); - - var serializer = get(this, 'serializer'), - mappings = serializer.mappingForType(type); - - serializer.eachEmbeddedRecord(record, function(embeddedRecord, embeddedType) { - if (embeddedType === 'load') { return; } - - this.didSaveRecord(store, embeddedRecord.constructor, embeddedRecord); - }, this); - - if (payload) { - var loader = DS.loaderFor(store); - serializer.extract(loader, payload, type); - } - }, - - /** - Acknowledges that the adapter has finished updating a record. - - Your adapter should call this method from `updateRecord` when it - has updated a record to its persistent storage and received an - acknowledgement. - - If the persistent storage returns a new payload in response to the - update, pass the payload as the fourth parameter. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {DS.Model} record - @param {any} payload - */ - didUpdateRecord: function() { - this.didSaveRecord.apply(this, arguments); - }, - - /** - Acknowledges that the adapter has finished deleting a record. - - Your adapter should call this method from `deleteRecord` when it - has deleted a record from its persistent storage and received an - acknowledgement. - - If the persistent storage returns a new payload in response to the - deletion, pass the payload as the fourth parameter. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {DS.Model} record - @param {any} payload - */ - didDeleteRecord: function() { - this.didSaveRecord.apply(this, arguments); - }, - - /** - Acknowledges that the adapter has finished updating or deleting - multiple records. - - Your adapter should call this method from its `updateRecords` or - `deleteRecords` when it has updated or deleted multiple records - to its persistent storage and received an acknowledgement. - - If the persistent storage returns a new payload in response to the - creation, pass the payload as the fourth parameter. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {DS.Model} records - @param {any} payload - */ - didSaveRecords: function(store, type, records, payload) { - records.forEach(function(record) { - store.didSaveRecord(record); - }, this); - - if (payload) { - var loader = DS.loaderFor(store); - get(this, 'serializer').extractMany(loader, payload, type); - } - }, - - /** - Acknowledges that the adapter has finished updating multiple records. - - Your adapter should call this method from its `updateRecords` when - it has updated multiple records to its persistent storage and - received an acknowledgement. - - If the persistent storage returns a new payload in response to the - update, pass the payload as the fourth parameter. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {DS.Model} records - @param {any} payload - */ - didUpdateRecords: function() { - this.didSaveRecords.apply(this, arguments); - }, - - /** - Acknowledges that the adapter has finished updating multiple records. - - Your adapter should call this method from its `deleteRecords` when - it has deleted multiple records to its persistent storage and - received an acknowledgement. - - If the persistent storage returns a new payload in response to the - deletion, pass the payload as the fourth parameter. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {DS.Model} records - @param {any} payload - */ - didDeleteRecords: function() { - this.didSaveRecords.apply(this, arguments); - }, - - /** - Loads the response to a request for a record by ID. - - Your adapter should call this method from its `find` method - with the response from the backend. - - You should pass the same ID to this method that was given - to your find method so that the store knows which record - to associate the new data with. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {any} payload - @param {String} id - */ - didFindRecord: function(store, type, payload, id) { - var loader = DS.loaderFor(store); - - loader.load = function(type, data, prematerialized) { - prematerialized = prematerialized || {}; - prematerialized.id = id; - - return store.load(type, data, prematerialized); - }; - - get(this, 'serializer').extract(loader, payload, type); - }, - - /** - Loads the response to a request for all records by type. - - You adapter should call this method from its `findAll` - method with the response from the backend. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {any} payload - */ - didFindAll: function(store, type, payload) { - var loader = DS.loaderFor(store), - serializer = get(this, 'serializer'); - - store.didUpdateAll(type); - - serializer.extractMany(loader, payload, type); - }, - - /** - Loads the response to a request for records by query. - - Your adapter should call this method from its `findQuery` - method with the response from the backend. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {any} payload - @param {DS.AdapterPopulatedRecordArray} recordArray - */ - didFindQuery: function(store, type, payload, recordArray) { - var loader = DS.loaderFor(store); - - loader.populateArray = function(data) { - recordArray.load(data); - }; - - get(this, 'serializer').extractMany(loader, payload, type); - }, - - /** - Loads the response to a request for many records by ID. - - You adapter should call this method from its `findMany` - method with the response from the backend. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {any} payload - */ - didFindMany: function(store, type, payload) { - var loader = DS.loaderFor(store); - - get(this, 'serializer').extractMany(loader, payload, type); - }, - - /** - Notifies the store that a request to the backend returned - an error. - - Your adapter should call this method to indicate that the - backend returned an error for a request. - - @param {DS.Store} store - @param {subclass of DS.Model} type - @param {DS.Model} record - */ - didError: function(store, type, record) { - store.recordWasError(record); - }, - - dirtyRecordsForAttributeChange: function(dirtySet, record, attributeName, newValue, oldValue) { - if (newValue !== oldValue) { - // If this record is embedded, add its parent - // to the dirty set. - this.dirtyRecordsForRecordChange(dirtySet, record); - } - }, - - dirtyRecordsForRecordChange: function(dirtySet, record) { - dirtySet.add(record); - }, - - dirtyRecordsForBelongsToChange: function(dirtySet, child) { - this.dirtyRecordsForRecordChange(dirtySet, child); - }, - - dirtyRecordsForHasManyChange: function(dirtySet, parent) { - this.dirtyRecordsForRecordChange(dirtySet, parent); - }, - - /** - @private - - This method recursively climbs the superclass hierarchy and - registers any class-registered transforms on the adapter's - serializer. - - Once it registers a transform for a given type, it ignores - subsequent transforms for the same attribute type. - - @param {Class} klass the DS.Adapter subclass to extract the - transforms from - @param {DS.Serializer} serializer the serializer to register - the transforms onto - @param {Object} seen a hash of attributes already seen - */ - registerSerializerTransforms: function(klass, serializer, seen) { - var transforms = klass._registeredTransforms, superclass, prop; - - for (prop in transforms) { - if (!transforms.hasOwnProperty(prop) || prop in seen) { continue; } - seen[prop] = true; - - serializer.registerTransform(prop, transforms[prop]); - } - - if (superclass = klass.superclass) { - this.registerSerializerTransforms(superclass, serializer, seen); - } - }, - - /** - @private - - This method recursively climbs the superclass hierarchy and - registers any class-registered mappings on the adapter's - serializer. - - @param {Class} klass the DS.Adapter subclass to extract the - transforms from - @param {DS.Serializer} serializer the serializer to register the - mappings onto - */ - registerSerializerMappings: function(serializer) { - var mappings = this._attributesMap, - configurations = this._configurationsMap; - - mappings.forEach(serializer.map, serializer); - configurations.forEach(serializer.configure, serializer); - }, - - /** - The `find()` method is invoked when the store is asked for a record that - has not previously been loaded. In response to `find()` being called, you - should query your persistence layer for a record with the given ID. Once - found, you can asynchronously call the store's `load()` method to load - the record. - - Here is an example `find` implementation: - - find: function(store, type, id) { - var url = type.url; - url = url.fmt(id); - - jQuery.getJSON(url, function(data) { - // data is a hash of key/value pairs. If your server returns a - // root, simply do something like: - // store.load(type, id, data.person) - store.load(type, id, data); - }); - } - */ - find: null, - - serializer: DS.JSONSerializer, - - registerTransform: function(attributeType, transform) { - get(this, 'serializer').registerTransform(attributeType, transform); - }, - - /** - A public method that allows you to register an enumerated - type on your adapter. This is useful if you want to utilize - a text representation of an integer value. - - Eg: Say you want to utilize "low","medium","high" text strings - in your app, but you want to persist those as 0,1,2 in your backend. - You would first register the transform on your adapter instance: - - adapter.registerEnumTransform('priority', ['low', 'medium', 'high']); - - You would then refer to the 'priority' DS.attr in your model: - App.Task = DS.Model.extend({ - priority: DS.attr('priority') - }); - - And lastly, you would set/get the text representation on your model instance, - but the transformed result will be the index number of the type. - - App: myTask.get('priority') => 'low' - Server Response / Load: { myTask: {priority: 0} } - - @param {String} type of the transform - @param {Array} array of String objects to use for the enumerated values. - This is an ordered list and the index values will be used for the transform. - */ - registerEnumTransform: function(attributeType, objects) { - get(this, 'serializer').registerEnumTransform(attributeType, objects); - }, - - /** - If the globally unique IDs for your records should be generated on the client, - implement the `generateIdForRecord()` method. This method will be invoked - each time you create a new record, and the value returned from it will be - assigned to the record's `primaryKey`. - - Most traditional REST-like HTTP APIs will not use this method. Instead, the ID - of the record will be set by the server, and your adapter will update the store - with the new ID when it calls `didCreateRecord()`. Only implement this method if - you intend to generate record IDs on the client-side. - - The `generateIdForRecord()` method will be invoked with the requesting store as - the first parameter and the newly created record as the second parameter: - - generateIdForRecord: function(store, record) { - var uuid = App.generateUUIDWithStatisticallyLowOddsOfCollision(); - return uuid; - } - */ - generateIdForRecord: null, - - materialize: function(record, data, prematerialized) { - get(this, 'serializer').materialize(record, data, prematerialized); - }, - - serialize: function(record, options) { - return get(this, 'serializer').serialize(record, options); - }, - - extractId: function(type, data) { - return get(this, 'serializer').extractId(type, data); - }, - - groupByType: function(enumerable) { - var map = Ember.MapWithDefault.create({ - defaultValue: function() { return Ember.OrderedSet.create(); } - }); - - enumerable.forEach(function(item) { - map.get(item.constructor).add(item); - }); - - return map; - }, - - commit: function(store, commitDetails) { - this.save(store, commitDetails); - }, - - save: function(store, commitDetails) { - var adapter = this; - - function filter(records) { - var filteredSet = Ember.OrderedSet.create(); - - records.forEach(function(record) { - if (adapter.shouldSave(record)) { - filteredSet.add(record); - } - }); - - return filteredSet; - } - - this.groupByType(commitDetails.created).forEach(function(type, set) { - this.createRecords(store, type, filter(set)); - }, this); - - this.groupByType(commitDetails.updated).forEach(function(type, set) { - this.updateRecords(store, type, filter(set)); - }, this); - - this.groupByType(commitDetails.deleted).forEach(function(type, set) { - this.deleteRecords(store, type, filter(set)); - }, this); - }, - - shouldSave: Ember.K, - - createRecords: function(store, type, records) { - records.forEach(function(record) { - this.createRecord(store, type, record); - }, this); - }, - - updateRecords: function(store, type, records) { - records.forEach(function(record) { - this.updateRecord(store, type, record); - }, this); - }, - - deleteRecords: function(store, type, records) { - records.forEach(function(record) { - this.deleteRecord(store, type, record); - }, this); - }, - - findMany: function(store, type, ids) { - ids.forEach(function(id) { - this.find(store, type, id); - }, this); - } -}); - -DS.Adapter.reopenClass({ - registerTransform: function(attributeType, transform) { - var registeredTransforms = this._registeredTransforms || {}; - - registeredTransforms[attributeType] = transform; - - this._registeredTransforms = registeredTransforms; - }, - - map: DS._Mappable.generateMapFunctionFor('attributes', function(key, newValue, map) { - var existingValue = map.get(key); - - merge(existingValue, newValue); - }), - - configure: DS._Mappable.generateMapFunctionFor('configurations', function(key, newValue, map) { - var existingValue = map.get(key); - - // If a mapping configuration is provided, peel it off and apply it - // using the DS.Adapter.map API. - var mappings = newValue && newValue.mappings; - if (mappings) { - this.map(key, mappings); - delete newValue.mappings; - } - - merge(existingValue, newValue); - }), - - resolveMapConflict: function(oldValue, newValue, mappingsKey) { - merge(newValue, oldValue); - - return newValue; - } -}); - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set; - -DS.FixtureSerializer = DS.Serializer.extend({ - deserializeValue: function(value, attributeType) { - return value; - }, - - serializeValue: function(value, attributeType) { - return value; - }, - - addId: function(data, key, id) { - data[key] = id; - }, - - addAttribute: function(hash, key, value) { - hash[key] = value; - }, - - addBelongsTo: function(hash, record, key, relationship) { - var id = get(record, relationship.key+'.id'); - if (!Ember.isNone(id)) { hash[key] = id; } - }, - - addHasMany: function(hash, record, key, relationship) { - var ids = get(record, relationship.key).map(function(item) { - return item.get('id'); - }); - - hash[relationship.key] = ids; - }, - - /** - @private - - Creates an empty hash that will be filled in by the hooks called from the - `serialize()` method. - - @return {Object} - */ - createSerializedForm: function() { - return {}; - }, - - extract: function(loader, fixture, type, record) { - if (record) { loader.updateId(record, fixture); } - this.extractRecordRepresentation(loader, type, fixture); - }, - - extractMany: function(loader, fixtures, type, records) { - var objects = fixtures, references = []; - if (records) { records = records.toArray(); } - - for (var i = 0; i < objects.length; i++) { - if (records) { loader.updateId(records[i], objects[i]); } - var reference = this.extractRecordRepresentation(loader, type, objects[i]); - references.push(reference); - } - - loader.populateArray(references); - }, - - extractId: function(type, hash) { - var primaryKey = this._primaryKey(type); - - if (hash.hasOwnProperty(primaryKey)) { - // Ensure that we coerce IDs to strings so that record - // IDs remain consistent between application runs; especially - // if the ID is serialized and later deserialized from the URL, - // when type information will have been lost. - return hash[primaryKey]+''; - } else { - return null; - } - }, - - extractAttribute: function(type, hash, attributeName) { - var key = this._keyForAttributeName(type, attributeName); - return hash[key]; - }, - - extractHasMany: function(type, hash, key) { - return hash[key]; - }, - - extractBelongsTo: function(type, hash, key) { - return hash[key]; - } -}); - -})(); - - - -(function() { -var get = Ember.get, fmt = Ember.String.fmt, - dump = Ember.get(window, 'JSON.stringify') || function(object) { return object.toString(); }; - -/** - `DS.FixtureAdapter` is an adapter that loads records from memory. - Its primarily used for development and testing. You can also use - `DS.FixtureAdapter` while working on the API but are not ready to - integrate yet. It is a fully functioning adapter. All CRUD methods - are implemented. You can also implement query logic that a remote - system would do. Its possible to do develop your entire application - with `DS.FixtureAdapter`. - -*/ -DS.FixtureAdapter = DS.Adapter.extend({ - - simulateRemoteResponse: true, - - latency: 50, - - serializer: DS.FixtureSerializer, - - /* - Implement this method in order to provide data associated with a type - */ - fixturesForType: function(type) { - if (type.FIXTURES) { - var fixtures = Ember.A(type.FIXTURES); - return fixtures.map(function(fixture){ - if(!fixture.id){ - throw new Error(fmt('the id property must be defined for fixture %@', [dump(fixture)])); - } - fixture.id = fixture.id + ''; - return fixture; - }); - } - return null; - }, - - /* - Implement this method in order to query fixtures data - */ - queryFixtures: function(fixtures, query, type) { - Ember.assert('Not implemented: You must override the DS.FixtureAdapter::queryFixtures method to support querying the fixture store.'); - }, - - updateFixtures: function(type, fixture) { - if(!type.FIXTURES) { - type.FIXTURES = []; - } - - var fixtures = type.FIXTURES; - - this.deleteLoadedFixture(type, fixture); - - fixtures.push(fixture); - }, - - /* - Implement this method in order to provide provide json for CRUD methods - */ - mockJSON: function(type, record) { - return this.serialize(record, { includeId: true }); - }, - - /* - Adapter methods - */ - generateIdForRecord: function(store, record) { - return Ember.guidFor(record); - }, - - find: function(store, type, id) { - var fixtures = this.fixturesForType(type), - fixture; - - Ember.warn("Unable to find fixtures for model type " + type.toString(), fixtures); - - if (fixtures) { - fixture = Ember.A(fixtures).findProperty('id', id); - } - - if (fixture) { - this.simulateRemoteCall(function() { - this.didFindRecord(store, type, fixture, id); - }, this); - } - }, - - findMany: function(store, type, ids) { - var fixtures = this.fixturesForType(type); - - Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); - - if (fixtures) { - fixtures = fixtures.filter(function(item) { - return ids.indexOf(item.id) !== -1; - }); - } - - if (fixtures) { - this.simulateRemoteCall(function() { - this.didFindMany(store, type, fixtures); - }, this); - } - }, - - findAll: function(store, type) { - var fixtures = this.fixturesForType(type); - - Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); - - this.simulateRemoteCall(function() { - this.didFindAll(store, type, fixtures); - }, this); - }, - - findQuery: function(store, type, query, array) { - var fixtures = this.fixturesForType(type); - - Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); - - fixtures = this.queryFixtures(fixtures, query, type); - - if (fixtures) { - this.simulateRemoteCall(function() { - this.didFindQuery(store, type, fixtures, array); - }, this); - } - }, - - createRecord: function(store, type, record) { - var fixture = this.mockJSON(type, record); - - this.updateFixtures(type, fixture); - - this.simulateRemoteCall(function() { - this.didCreateRecord(store, type, record, fixture); - }, this); - }, - - updateRecord: function(store, type, record) { - var fixture = this.mockJSON(type, record); - - this.updateFixtures(type, fixture); - - this.simulateRemoteCall(function() { - this.didUpdateRecord(store, type, record, fixture); - }, this); - }, - - deleteRecord: function(store, type, record) { - var fixture = this.mockJSON(type, record); - - this.deleteLoadedFixture(type, fixture); - - this.simulateRemoteCall(function() { - this.didDeleteRecord(store, type, record); - }, this); - }, - - /* - @private - */ - deleteLoadedFixture: function(type, record) { - var id = this.extractId(type, record); - - var existingFixture = this.findExistingFixture(type, record); - - if(existingFixture) { - var index = type.FIXTURES.indexOf(existingFixture); - type.FIXTURES.splice(index, 1); - return true; - } - }, - - findExistingFixture: function(type, record) { - var fixtures = this.fixturesForType(type); - var id = this.extractId(type, record); - - return this.findFixtureById(fixtures, id); - }, - - findFixtureById: function(fixtures, id) { - var adapter = this; - - return Ember.A(fixtures).find(function(r) { - if(''+get(r, 'id') === ''+id) { - return true; - } else { - return false; - } - }); - }, - - simulateRemoteCall: function(callback, context) { - if (get(this, 'simulateRemoteResponse')) { - // Schedule with setTimeout - Ember.run.later(context, callback, get(this, 'latency')); - } else { - // Asynchronous, but at the of the runloop with zero latency - Ember.run.once(context, callback); - } - } -}); - -})(); - - - -(function() { -DS.RESTSerializer = DS.JSONSerializer.extend({ - keyForAttributeName: function(type, name) { - return Ember.String.decamelize(name); - }, - - keyForBelongsTo: function(type, name) { - var key = this.keyForAttributeName(type, name); - - if (this.embeddedType(type, name)) { - return key; - } - - return key + "_id"; - }, - - keyForHasMany: function(type, name) { - var key = this.keyForAttributeName(type, name); - - if (this.embeddedType(type, name)) { - return key; - } - - return this.singularize(key) + "_ids"; - } -}); - -})(); - - - -(function() { -/*global jQuery*/ - -var get = Ember.get, set = Ember.set, merge = Ember.merge; - -/** - The REST adapter allows your store to communicate with an HTTP server by - transmitting JSON via XHR. Most Ember.js apps that consume a JSON API - should use the REST adapter. - - This adapter is designed around the idea that the JSON exchanged with - the server should be conventional. - - ## JSON Structure - - The REST adapter expects the JSON returned from your server to follow - these conventions. - - ### Object Root - - The JSON payload should be an object that contains the record inside a - root property. For example, in response to a `GET` request for - `/posts/1`, the JSON should look like this: - - ```js - { - "post": { - title: "I'm Running to Reform the W3C's Tag", - author: "Yehuda Katz" - } - } - ``` - - ### Conventional Names - - Attribute names in your JSON payload should be the underscored versions of - the attributes in your Ember.js models. - - For example, if you have a `Person` model: - - ```js - App.Person = DS.Model.extend({ - firstName: DS.attr('string'), - lastName: DS.attr('string'), - occupation: DS.attr('string') - }); - ``` - - The JSON returned should look like this: - - ```js - { - "person": { - "first_name": "Barack", - "last_name": "Obama", - "occupation": "President" - } - } - ``` -*/ -DS.RESTAdapter = DS.Adapter.extend({ - bulkCommit: false, - since: 'since', - - serializer: DS.RESTSerializer, - - init: function() { - this._super.apply(this, arguments); - }, - - shouldSave: function(record) { - var reference = get(record, '_reference'); - - return !reference.parent; - }, - - createRecord: function(store, type, record) { - var root = this.rootForType(type); - - var data = {}; - data[root] = this.serialize(record, { includeId: true }); - - this.ajax(this.buildURL(root), "POST", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didCreateRecord(store, type, record, json); - }); - }, - error: function(xhr) { - this.didError(store, type, record, xhr); - } - }); - }, - - dirtyRecordsForRecordChange: function(dirtySet, record) { - this._dirtyTree(dirtySet, record); - }, - - dirtyRecordsForHasManyChange: function(dirtySet, record, relationship) { - var embeddedType = get(this, 'serializer').embeddedType(record.constructor, relationship.secondRecordName); - - if (embeddedType === 'always') { - relationship.childReference.parent = relationship.parentReference; - this._dirtyTree(dirtySet, record); - } - }, - - _dirtyTree: function(dirtySet, record) { - dirtySet.add(record); - - get(this, 'serializer').eachEmbeddedRecord(record, function(embeddedRecord, embeddedType) { - if (embeddedType !== 'always') { return; } - if (dirtySet.has(embeddedRecord)) { return; } - this._dirtyTree(dirtySet, embeddedRecord); - }, this); - - var reference = record.get('_reference'); - - if (reference.parent) { - var store = get(record, 'store'); - var parent = store.recordForReference(reference.parent); - this._dirtyTree(dirtySet, parent); - } - }, - - createRecords: function(store, type, records) { - if (get(this, 'bulkCommit') === false) { - return this._super(store, type, records); - } - - var root = this.rootForType(type), - plural = this.pluralize(root); - - var data = {}; - data[plural] = []; - records.forEach(function(record) { - data[plural].push(this.serialize(record, { includeId: true })); - }, this); - - this.ajax(this.buildURL(root), "POST", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didCreateRecords(store, type, records, json); - }); - } - }); - }, - - updateRecord: function(store, type, record) { - var id = get(record, 'id'); - var root = this.rootForType(type); - - var data = {}; - data[root] = this.serialize(record); - - this.ajax(this.buildURL(root, id), "PUT", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didSaveRecord(store, type, record, json); - }); - }, - error: function(xhr) { - this.didError(store, type, record, xhr); - } - }); - }, - - updateRecords: function(store, type, records) { - if (get(this, 'bulkCommit') === false) { - return this._super(store, type, records); - } - - var root = this.rootForType(type), - plural = this.pluralize(root); - - var data = {}; - data[plural] = []; - records.forEach(function(record) { - data[plural].push(this.serialize(record, { includeId: true })); - }, this); - - this.ajax(this.buildURL(root, "bulk"), "PUT", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didSaveRecords(store, type, records, json); - }); - } - }); - }, - - deleteRecord: function(store, type, record) { - var id = get(record, 'id'); - var root = this.rootForType(type); - - this.ajax(this.buildURL(root, id), "DELETE", { - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didSaveRecord(store, type, record, json); - }); - } - }); - }, - - deleteRecords: function(store, type, records) { - if (get(this, 'bulkCommit') === false) { - return this._super(store, type, records); - } - - var root = this.rootForType(type), - plural = this.pluralize(root), - serializer = get(this, 'serializer'); - - var data = {}; - data[plural] = []; - records.forEach(function(record) { - data[plural].push(serializer.serializeId( get(record, 'id') )); - }); - - this.ajax(this.buildURL(root, 'bulk'), "DELETE", { - data: data, - context: this, - success: function(json) { - Ember.run(this, function(){ - this.didSaveRecords(store, type, records, json); - }); - } - }); - }, - - find: function(store, type, id) { - var root = this.rootForType(type); - - this.ajax(this.buildURL(root, id), "GET", { - success: function(json) { - Ember.run(this, function(){ - this.didFindRecord(store, type, json, id); - }); - } - }); - }, - - findAll: function(store, type, since) { - var root = this.rootForType(type); - - this.ajax(this.buildURL(root), "GET", { - data: this.sinceQuery(since), - success: function(json) { - Ember.run(this, function(){ - this.didFindAll(store, type, json); - }); - } - }); - }, - - findQuery: function(store, type, query, recordArray) { - var root = this.rootForType(type); - - this.ajax(this.buildURL(root), "GET", { - data: query, - success: function(json) { - Ember.run(this, function(){ - this.didFindQuery(store, type, json, recordArray); - }); - } - }); - }, - - findMany: function(store, type, ids, owner) { - var root = this.rootForType(type); - ids = this.serializeIds(ids); - - this.ajax(this.buildURL(root), "GET", { - data: {ids: ids}, - success: function(json) { - Ember.run(this, function(){ - this.didFindMany(store, type, json); - }); - } - }); - }, - - /** - @private - - This method serializes a list of IDs using `serializeId` - - @returns {Array} an array of serialized IDs - */ - serializeIds: function(ids) { - var serializer = get(this, 'serializer'); - - return Ember.EnumerableUtils.map(ids, function(id) { - return serializer.serializeId(id); - }); - }, - - didError: function(store, type, record, xhr) { - if (xhr.status === 422) { - var data = JSON.parse(xhr.responseText); - store.recordWasInvalid(record, data['errors']); - } else { - this._super.apply(this, arguments); - } - }, - - ajax: function(url, type, hash) { - hash.url = url; - hash.type = type; - hash.dataType = 'json'; - hash.contentType = 'application/json; charset=utf-8'; - hash.context = this; - - if (hash.data && type !== 'GET') { - hash.data = JSON.stringify(hash.data); - } - - jQuery.ajax(hash); - }, - - url: "", - - rootForType: function(type) { - var serializer = get(this, 'serializer'); - return serializer.rootForType(type); - }, - - pluralize: function(string) { - var serializer = get(this, 'serializer'); - return serializer.pluralize(string); - }, - - buildURL: function(record, suffix) { - var url = [this.url]; - - Ember.assert("Namespace URL (" + this.namespace + ") must not start with slash", !this.namespace || this.namespace.toString().charAt(0) !== "/"); - Ember.assert("Record URL (" + record + ") must not start with slash", !record || record.toString().charAt(0) !== "/"); - Ember.assert("URL suffix (" + suffix + ") must not start with slash", !suffix || suffix.toString().charAt(0) !== "/"); - - if (this.namespace !== undefined) { - url.push(this.namespace); - } - - url.push(this.pluralize(record)); - if (suffix !== undefined) { - url.push(suffix); - } - - return url.join("/"); - }, - - sinceQuery: function(since) { - var query = {}; - query[get(this, 'since')] = since; - return since ? query : null; - } -}); - - -})(); - - - -(function() { -var camelize = Ember.String.camelize, - capitalize = Ember.String.capitalize, - get = Ember.get, - map = Ember.ArrayPolyfills.map, - registeredTransforms; - -var passthruTransform = { - serialize: function(value) { return value; }, - deserialize: function(value) { return value; } -}; - -var defaultTransforms = { - string: passthruTransform, - boolean: passthruTransform, - number: passthruTransform -}; - -function camelizeKeys(json) { - var value; - - for (var prop in json) { - value = json[prop]; - delete json[prop]; - json[camelize(prop)] = value; - } -} - -function munge(json, callback) { - callback(json); -} - -function applyTransforms(json, type, transformType) { - var transforms = registeredTransforms[transformType]; - - Ember.assert("You are trying to apply the '" + transformType + "' transforms, but you didn't register any transforms with that name", transforms); - - get(type, 'attributes').forEach(function(name, attribute) { - var attributeType = attribute.type, - value = json[name]; - - var transform = transforms[attributeType] || defaultTransforms[attributeType]; - - Ember.assert("Your model specified the '" + attributeType + "' type for the '" + name + "' attribute, but no transform for that type was registered", transform); - - json[name] = transform.deserialize(value); - }); -} - -function ObjectProcessor(json, type, store) { - this.json = json; - this.type = type; - this.store = store; -} - -ObjectProcessor.prototype = { - camelizeKeys: function() { - camelizeKeys(this.json); - return this; - }, - - munge: function(callback) { - munge(this.json, callback); - return this; - }, - - applyTransforms: function(transformType) { - applyTransforms(this.json, this.type, transformType); - return this; - } -}; - -function LoadObjectProcessor() { - ObjectProcessor.apply(this, arguments); -} - -LoadObjectProcessor.prototype = Ember.create(ObjectProcessor.prototype); - -LoadObjectProcessor.prototype.load = function() { - this.store.load(this.type, {}, this.json); -}; - -function loadObjectProcessorFactory(store, type) { - return function(json) { - return new LoadObjectProcessor(json, type, store); - }; -} - -function ArrayProcessor(json, type, array, store) { - this.json = json; - this.type = type; - this.array = array; - this.store = store; -} - -ArrayProcessor.prototype = { - load: function() { - var store = this.store, - type = this.type; - - var references = this.json.map(function(object) { - return store.load(type, {}, object); - }); - - this.array.load(references); - }, - - camelizeKeys: function() { - this.json.forEach(camelizeKeys); - return this; - }, - - munge: function(callback) { - this.json.forEach(function(object) { - munge(object, callback); - }); - return this; - }, - - applyTransforms: function(transformType) { - var type = this.type; - - this.json.forEach(function(object) { - applyTransforms(object, type, transformType); - }); - - return this; - } -}; - -function arrayProcessorFactory(store, type, array) { - return function(json) { - return new ArrayProcessor(json, type, array, store); - }; -} - -var HasManyProcessor = function(json, store, record, relationship) { - this.json = json; - this.store = store; - this.record = record; - this.type = record.constructor; - this.relationship = relationship; -}; - -HasManyProcessor.prototype = Ember.create(ArrayProcessor.prototype); - -HasManyProcessor.prototype.load = function() { - var store = this.store; - var ids = map.call(this.json, function(obj) { return obj.id; }); - - store.loadMany(this.relationship.type, this.json); - store.loadHasMany(this.record, this.relationship.key, ids); -}; - -function hasManyProcessorFactory(store, record, relationship) { - return function(json) { - return new HasManyProcessor(json, store, record, relationship); - }; -} - -function CreateProcessor(record, store, type) { - this.record = record; - ObjectProcessor.call(this, record.toJSON(), type, store); -} - -CreateProcessor.prototype = Ember.create(ObjectProcessor.prototype); - -CreateProcessor.prototype.save = function() {}; - -function createProcessorFactory(store, type) { - return function(record) { - return new CreateProcessor(record, store, type); - }; -} - -DS.BasicAdapter = DS.Adapter.extend({ - find: function(store, type, id) { - var sync = type.sync; - - Ember.assert("You are trying to use the BasicAdapter to find id '" + id + "' of " + type + " but " + type + ".sync was not found", sync); - Ember.assert("The sync code on " + type + " does not implement find(), but you are trying to find id '" + id + "'.", sync.find); - - sync.find(id, loadObjectProcessorFactory(store, type)); - }, - - findQuery: function(store, type, query, recordArray) { - var sync = type.sync; - - Ember.assert("You are trying to use the BasicAdapter to query " + type + " but " + type + ".sync was not found", sync); - Ember.assert("The sync code on " + type + " does not implement query(), but you are trying to query " + type + ".", sync.query); - - sync.query(query, arrayProcessorFactory(store, type, recordArray)); - }, - - findHasMany: function(store, record, relationship, data) { - var name = capitalize(relationship.key), - sync = record.constructor.sync, - processor = hasManyProcessorFactory(store, record, relationship); - - var options = { - relationship: relationship.key, - data: data - }; - - if (sync['find'+name]) { - sync['find' + name](record, options, processor); - } else if (sync.findHasMany) { - sync.findHasMany(record, options, processor); - } else { - Ember.assert("You are trying to use the BasicAdapter to find the " + relationship.key + " has-many relationship, but " + record.constructor + ".sync did not implement findHasMany or find" + name + ".", false); - } - }, - - createRecord: function(store, type, record) { - var sync = type.sync; - - sync.createRecord(record, createProcessorFactory(store, type)); - } -}); - -DS.registerTransforms = function(kind, object) { - registeredTransforms[kind] = object; -}; - -DS.clearTransforms = function() { - registeredTransforms = {}; -}; - -DS.clearTransforms(); - -})(); - - - -(function() { - -})(); - - - -(function() { -//Copyright (C) 2011 by Living Social, Inc. - -//Permission is hereby granted, free of charge, to any person obtaining a copy of -//this software and associated documentation files (the "Software"), to deal in -//the Software without restriction, including without limitation the rights to -//use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -//of the Software, and to permit persons to whom the Software is furnished to do -//so, subject to the following conditions: - -//The above copyright notice and this permission notice shall be included in all -//copies or substantial portions of the Software. - -//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -//SOFTWARE. - -})(); - |