From b3ce14cc22319d06441666b33e844e531f484739 Mon Sep 17 00:00:00 2001 From: Piotr Sarnacki Date: Fri, 18 Oct 2013 13:19:02 +0200 Subject: [PATCH] Update ember to 1.1.beta and ember-model to newest version --- assets/scripts/vendor/ember-model.js | 824 ++- assets/scripts/vendor/ember.js | 9719 ++++++++++++++++++-------- 2 files changed, 7342 insertions(+), 3201 deletions(-) diff --git a/assets/scripts/vendor/ember-model.js b/assets/scripts/vendor/ember-model.js index 4665d5d1..e4b05107 100644 --- a/assets/scripts/vendor/ember-model.js +++ b/assets/scripts/vendor/ember-model.js @@ -1,27 +1,41 @@ (function() { +var VERSION = '0.0.10'; + +if (Ember.libraries) { + Ember.libraries.register('Ember Model', VERSION); +} + + +})(); + +(function() { + function mustImplement(message) { var fn = function() { - throw new Error(message); + var className = this.constructor.toString(); + + throw new Error(message.replace('{{className}}', className)); }; fn.isUnimplemented = true; return fn; } Ember.Adapter = Ember.Object.extend({ - find: mustImplement('Ember.Adapter subclasses must implement find'), - findQuery: mustImplement('Ember.Adapter subclasses must implement findQuery'), - findMany: mustImplement('Ember.Adapter subclasses must implement findMany'), - findAll: mustImplement('Ember.Adapter subclasses must implement findAll'), - createRecord: mustImplement('Ember.Adapter subclasses must implement createRecord'), - saveRecord: mustImplement('Ember.Adapter subclasses must implement saveRecord'), - deleteRecord: mustImplement('Ember.Adapter subclasses must implement deleteRecord'), + find: mustImplement('{{className}} must implement find'), + findQuery: mustImplement('{{className}} must implement findQuery'), + findMany: mustImplement('{{className}} must implement findMany'), + findAll: mustImplement('{{className}} must implement findAll'), + createRecord: mustImplement('{{className}} must implement createRecord'), + saveRecord: mustImplement('{{className}} must implement saveRecord'), + deleteRecord: mustImplement('{{className}} must implement deleteRecord'), load: function(record, id, data) { record.load(id, data); } }); + })(); (function() { @@ -143,12 +157,23 @@ Ember.RecordArray = Ember.ArrayProxy.extend(Ember.Evented, { }, reload: function() { - var modelClass = this.get('modelClass'); - Ember.assert("Reload can only be called on findAll RecordArrays", - modelClass && modelClass._findAllRecordArray === this); + var modelClass = this.get('modelClass'), + self = this, + promises; set(this, 'isLoaded', false); - modelClass.adapter.findAll(modelClass, this); + if (modelClass._findAllRecordArray === this) { + modelClass.adapter.findAll(modelClass, this); + } else if (this._query) { + modelClass.adapter.findQuery(modelClass, this, this._query); + } else { + promises = this.map(function(record) { + return record.reload(); + }); + Ember.RSVP.all(promises).then(function(data) { + self.notifyLoaded(); + }); + } } }); @@ -171,13 +196,13 @@ Ember.FilteredRecordArray = Ember.RecordArray.extend({ throw new Error('FilteredRecordArrays must be created with filterProperties'); } - this._registeredClientIds = Ember.A([]); - var modelClass = get(this, 'modelClass'); modelClass.registerRecordArray(this); this.registerObservers(); this.updateFilter(); + + this._super(); }, updateFilter: function() { @@ -186,20 +211,17 @@ Ember.FilteredRecordArray = Ember.RecordArray.extend({ get(this, 'modelClass').forEachCachedRecord(function(record) { if (self.filterFunction(record)) { results.push(record); - } else { - results.removeObject(record); } }); this.set('content', Ember.A(results)); }, updateFilterForRecord: function(record) { - var results = get(this, 'content'); - if (this.filterFunction(record)) { - if(!results.contains(record)) { - results.pushObject(record); - } - } else { + var results = get(this, 'content'), + filterMatches = this.filterFunction(record); + if (filterMatches && !results.contains(record)) { + results.pushObject(record); + } else if(!filterMatches) { results.removeObject(record); } }, @@ -213,26 +235,44 @@ Ember.FilteredRecordArray = Ember.RecordArray.extend({ registerObserversOnRecord: function(record) { var self = this, - filterProperties = get(this, 'filterProperties'), - clientId = record._reference.clientId; + filterProperties = get(this, 'filterProperties'); - if(!this._registeredClientIds.contains(clientId)) { - for (var i = 0, l = get(filterProperties, 'length'); i < l; i++) { - record.addObserver(filterProperties[i], self, 'updateFilterForRecord'); - } - this._registeredClientIds.pushObject(clientId); + for (var i = 0, l = get(filterProperties, 'length'); i < l; i++) { + record.addObserver(filterProperties[i], self, 'updateFilterForRecord'); } } }); + })(); (function() { -var get = Ember.get; +var get = Ember.get, set = Ember.set; Ember.ManyArray = Ember.RecordArray.extend({ _records: null, + originalContent: null, + + isDirty: function() { + var originalContent = get(this, 'originalContent'), + originalContentLength = get(originalContent, 'length'), + content = get(this, 'content'), + contentLength = get(content, 'length'); + + if (originalContentLength !== contentLength) { return true; } + + var isDirty = false; + + for (var i = 0, l = contentLength; i < l; i++) { + if (!originalContent.contains(content[i])) { + isDirty = true; + break; + } + } + + return isDirty; + }.property('content.[]', 'originalContent'), objectAtContent: function(idx) { var content = get(this, 'content'); @@ -255,6 +295,48 @@ Ember.ManyArray = Ember.RecordArray.extend({ }, this); this._super(index, removed, added); + }, + + _contentWillChange: function() { + var content = get(this, 'content'); + if (content) { + content.removeArrayObserver(this); + this._setupOriginalContent(content); + } + }.observesBefore('content'), + + _contentDidChange: function() { + var content = get(this, 'content'); + if (content) { + content.addArrayObserver(this); + this.arrayDidChange(content, 0, 0, get(content, 'length')); + } + }.observes('content'), + + arrayWillChange: function(item, idx, removedCnt, addedCnt) {}, + + arrayDidChange: function(item, idx, removedCnt, addedCnt) { + var parent = get(this, 'parent'), relationshipKey = get(this, 'relationshipKey'), + isDirty = get(this, 'isDirty'); + + if (isDirty) { + parent._relationshipBecameDirty(relationshipKey); + } else { + parent._relationshipBecameClean(relationshipKey); + } + }, + + _setupOriginalContent: function(content) { + content = content || get(this, 'content'); + if (content) { + set(this, 'originalContent', content.slice()); + } + }, + + init: function() { + this._super(); + this._setupOriginalContent(); + this._contentDidChange(); } }); @@ -268,7 +350,7 @@ Ember.HasManyArray = Ember.ManyArray.extend({ if (reference.record) { record = reference.record; } else { - record = klass.findById(reference.id); + record = klass.find(reference.id); } return record; @@ -357,36 +439,6 @@ function hasCachedValue(object, key) { } } -function extractDirty(object, attrsOrRelations, dirtyAttributes) { - var key, desc, descMeta, type, dataValue, cachedValue, isDirty, dataType; - for (var i = 0, l = attrsOrRelations.length; i < l; i++) { - key = attrsOrRelations[i]; - if (!hasCachedValue(object, key)) { continue; } - cachedValue = object.cacheFor(key); - dataValue = get(object, '_data.' + object.dataKey(key)); - desc = meta(object).descs[key]; - descMeta = desc && desc.meta(); - type = descMeta.type; - dataType = Ember.Model.dataTypes[type]; - - if (type && type.isEqual) { - isDirty = !type.isEqual(dataValue, cachedValue); - } else if (dataType && dataType.isEqual) { - isDirty = !dataType.isEqual(dataValue, cachedValue); - } else if (dataValue && cachedValue instanceof Ember.Model) { // belongsTo case - isDirty = get(cachedValue, 'isDirty'); - } else if (dataValue !== cachedValue) { - isDirty = true; - } else { - isDirty = false; - } - - if (isDirty) { - dirtyAttributes.push(key); - } - } -} - Ember.run.queues.push('data'); Ember.Model = Ember.Object.extend(Ember.Evented, { @@ -407,24 +459,20 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { return value; }, - isDirty: Ember.computed(function() { - var attributes = this.attributes, - relationships = this.relationships, - dirtyAttributes = Ember.A(); // just for removeObject + isDirty: function() { + var dirtyAttributes = get(this, '_dirtyAttributes'); + return dirtyAttributes && dirtyAttributes.length !== 0 || false; + }.property('_dirtyAttributes.length'), - extractDirty(this, attributes, dirtyAttributes); - if (relationships) { - extractDirty(this, relationships, dirtyAttributes); - } + _relationshipBecameDirty: function(name) { + var dirtyAttributes = get(this, '_dirtyAttributes'); + if (!dirtyAttributes.contains(name)) { dirtyAttributes.pushObject(name); } + }, - if (dirtyAttributes.length) { - this._dirtyAttributes = dirtyAttributes; - return true; - } else { - this._dirtyAttributes = []; - return false; - } - }).property().volatile(), + _relationshipBecameClean: function(name) { + var dirtyAttributes = get(this, '_dirtyAttributes'); + dirtyAttributes.removeObject(name); + }, dataKey: function(key) { var camelizeKeys = get(this.constructor, 'camelizeKeys'); @@ -437,11 +485,10 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { init: function() { this._createReference(); + if (!this._dirtyAttributes) { + set(this, '_dirtyAttributes', []); + } this._super(); - - this.one('didLoad', function() { - this.constructor.addToRecordArrays(this); - }); }, _createReference: function() { @@ -449,9 +496,12 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { id = this.getPrimaryKey(); if (!reference) { - reference = this.constructor._referenceForId(id); + reference = this.constructor._getOrCreateReferenceForId(id); reference.record = this; this._reference = reference; + } else if (reference.id !== id) { + reference.id = id; + this.constructor._cacheReference(reference); } if (!reference.id) { @@ -469,6 +519,27 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { var data = {}; data[get(this.constructor, 'primaryKey')] = id; set(this, '_data', Ember.merge(data, hash)); + + // eagerly load embedded data + var relationships = this.constructor._relationships || [], meta = Ember.meta(this), relationshipKey, relationship, relationshipMeta, relationshipData, relationshipType; + for (var i = 0, l = relationships.length; i < l; i++) { + relationshipKey = relationships[i]; + relationship = meta.descs[relationshipKey]; + relationshipMeta = relationship.meta(); + + if (relationshipMeta.options.embedded) { + relationshipType = relationshipMeta.type; + if (typeof relationshipType === "string") { + relationshipType = Ember.get(Ember.lookup, relationshipType); + } + + relationshipData = data[relationshipKey]; + if (relationshipData) { + relationshipType.load(relationshipData); + } + } + } + set(this, 'isLoaded', true); set(this, 'isNew', false); this._createReference(); @@ -478,13 +549,15 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { didDefineProperty: function(proto, key, value) { if (value instanceof Ember.Descriptor) { var meta = value.meta(); + var klass = proto.constructor; if (meta.isAttribute) { - proto.attributes = proto.attributes ? proto.attributes.slice() : []; - proto.attributes.push(key); + if (!klass._attributes) { klass._attributes = []; } + klass._attributes.push(key); } else if (meta.isRelationship) { - proto.relationships = proto.relationships ? proto.relationships.slice() : []; - proto.relationships.push(key); + if (!klass._relationships) { klass._relationships = []; } + klass._relationships.push(key); + meta.relationshipKey = key; } } }, @@ -506,7 +579,9 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { toJSON: function() { var key, meta, json = {}, - properties = this.attributes ? this.getProperties(this.attributes) : {}, + attributes = this.constructor.getAttributes(), + relationships = this.constructor.getRelationships(), + properties = attributes ? this.getProperties(attributes) : {}, rootKey = get(this.constructor, 'rootKey'); for (key in properties) { @@ -520,11 +595,11 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { } } - if (this.relationships) { + if (relationships) { var data, relationshipKey; - for(var i = 0; i < this.relationships.length; i++) { - key = this.relationships[i]; + for(var i = 0; i < relationships.length; i++) { + key = relationships[i]; meta = this.constructor.metaForProperty(key); relationshipKey = meta.options.key || key; @@ -570,15 +645,8 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { }, revert: function() { - if (this.get('isDirty')) { - var data = get(this, '_data') || {}, - reverts = {}; - for (var i = 0; i < this._dirtyAttributes.length; i++) { - var attr = this._dirtyAttributes[i]; - reverts[attr] = data[attr]; - } - setProperties(this, reverts); - } + this.getWithDefault('_dirtyAttributes', []).clear(); + this.notifyPropertyChange('_data'); }, didCreateRecord: function() { @@ -587,10 +655,7 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { set(this, 'isNew', false); - if (!this.constructor.recordCache) this.constructor.recordCache = {}; - this.constructor.recordCache[id] = this; - - this._copyDirtyAttributesToData(); + set(this, '_dirtyAttributes', []); this.constructor.addToRecordArrays(this); this.trigger('didCreateRecord'); this.didSaveRecord(); @@ -627,7 +692,7 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { key = dirtyAttributes[i]; data[this.dataKey(key)] = this.cacheFor(key); } - this._dirtyAttributes = []; + set(this, '_dirtyAttributes', []); }, dataDidChange: Ember.observer(function() { @@ -642,11 +707,16 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { _reloadHasManys: function() { if (!this._hasManyArrays) { return; } - - var i; - for(i = 0; i < this._hasManyArrays.length; i++) { - var array = this._hasManyArrays[i]; - set(array, 'content', this._getHasManyContent(get(array, 'key'), get(array, 'modelClass'), get(array, 'embedded'))); + var i, j; + for (i = 0; i < this._hasManyArrays.length; i++) { + var array = this._hasManyArrays[i], + hasManyContent = this._getHasManyContent(get(array, 'key'), get(array, 'modelClass'), get(array, 'embedded')); + for (j = 0; j < array.get('length'); j++) { + if (array.objectAt(j).get('isNew')) { + hasManyContent.addObject(array.objectAt(j)._reference); + } + } + set(array, 'content', hasManyContent); } }, @@ -658,12 +728,12 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { if (embedded) { primaryKey = get(type, 'primaryKey'); mapFunction = function(attrs) { - reference = type._referenceForId(attrs[primaryKey]); + reference = type._getOrCreateReferenceForId(attrs[primaryKey]); reference.data = attrs; return reference; }; } else { - mapFunction = function(id) { return type._referenceForId(id); }; + mapFunction = function(id) { return type._getOrCreateReferenceForId(id); }; } content = Ember.EnumerableUtils.map(content, mapFunction); } @@ -679,26 +749,77 @@ Ember.Model.reopenClass({ _clientIdCounter: 1, - fetch: function() { - return Ember.loadPromise(this.find.apply(this, arguments)); + getAttributes: function() { + this.proto(); // force class "compilation" if it hasn't been done. + var attributes = this._attributes || []; + if (typeof this.superclass.getAttributes === 'function') { + attributes = this.superclass.getAttributes().concat(attributes); + } + return attributes; + }, + + getRelationships: function() { + this.proto(); // force class "compilation" if it hasn't been done. + var relationships = this._relationships || []; + if (typeof this.superclass.getRelationships === 'function') { + relationships = this.superclass.getRelationships().concat(relationships); + } + return relationships; + }, + + fetch: function(id) { + if (!arguments.length) { + return this._findFetchAll(true); + } else if (Ember.isArray(id)) { + return this._findFetchMany(id, true); + } else if (typeof id === 'object') { + return this._findFetchQuery(id, true); + } else { + return this._findFetchById(id, true); + } }, find: function(id) { if (!arguments.length) { - return this.findAll(); + return this._findFetchAll(false); } else if (Ember.isArray(id)) { - return this.findMany(id); + return this._findFetchMany(id, false); } else if (typeof id === 'object') { - return this.findQuery(id); + return this._findFetchQuery(id, false); } else { - return this.findById(id); + return this._findFetchById(id, false); } }, - findMany: function(ids) { - Ember.assert("findMany requires an array", Ember.isArray(ids)); + findQuery: function(params) { + return this._findFetchQuery(params, false); + }, - var records = Ember.RecordArray.create({_ids: ids}); + fetchQuery: function(params) { + return this._findFetchQuery(params, true); + }, + + _findFetchQuery: function(params, isFetch) { + var records = Ember.RecordArray.create({modelClass: this, _query: params}); + + var promise = this.adapter.findQuery(this, records, params); + + return isFetch ? promise : records; + }, + + findMany: function(ids) { + return this._findFetchMany(ids, false); + }, + + fetchMany: function(ids) { + return this._findFetchMany(ids, true); + }, + + _findFetchMany: function(ids, isFetch) { + Ember.assert("findFetchMany requires an array", Ember.isArray(ids)); + + var records = Ember.RecordArray.create({_ids: ids, modelClass: this}), + deferred; if (!this.recordArrays) { this.recordArrays = []; } this.recordArrays.push(records); @@ -711,43 +832,97 @@ Ember.Model.reopenClass({ this._currentBatchRecordArrays = [records]; } + if (isFetch) { + deferred = Ember.Deferred.create(); + Ember.set(deferred, 'resolveWith', records); + + if (!this._currentBatchDeferreds) { this._currentBatchDeferreds = []; } + this._currentBatchDeferreds.push(deferred); + } + Ember.run.scheduleOnce('data', this, this._executeBatch); - return records; + return isFetch ? deferred : records; }, findAll: function() { - if (this._findAllRecordArray) { return this._findAllRecordArray; } + return this._findFetchAll(false); + }, + + fetchAll: function() { + return this._findFetchAll(true); + }, + + _findFetchAll: function(isFetch) { + var self = this; + + if (this._findAllRecordArray) { + if (isFetch) { + return new Ember.RSVP.Promise(function(resolve) { + resolve(self._findAllRecordArray); + }); + } else { + return this._findAllRecordArray; + } + } var records = this._findAllRecordArray = Ember.RecordArray.create({modelClass: this}); - this.adapter.findAll(this, records); + var promise = this.adapter.findAll(this, records); - return records; + // Remove the cached record array if the promise is rejected + if (promise.then) { + promise.then(null, function() { + self._findAllRecordArray = null; + return Ember.RSVP.reject.apply(null, arguments); + }); + } + + return isFetch ? promise : records; + }, + + findById: function(id) { + return this._findFetchById(id, false); + }, + + fetchById: function(id) { + return this._findFetchById(id, true); + }, + + _findFetchById: function(id, isFetch) { + var record = this.cachedRecordForId(id), + isLoaded = get(record, 'isLoaded'), + adapter = get(this, 'adapter'), + deferredOrPromise; + + if (isLoaded) { + if (isFetch) { + return new Ember.RSVP.Promise(function(resolve, reject) { + resolve(record); + }); + } else { + return record; + } + } + + deferredOrPromise = this._fetchById(record, id); + + return isFetch ? deferredOrPromise : record; }, _currentBatchIds: null, _currentBatchRecordArrays: null, - - findById: function(id) { - var record = this.cachedRecordForId(id); - - if (!get(record, 'isLoaded')) { - this._fetchById(record, id); - } - return record; - }, + _currentBatchDeferreds: null, reload: function(id) { var record = this.cachedRecordForId(id); - - this._fetchById(record, id); - - return record; + record.set('isLoaded', false); + return this._fetchById(record, id); }, _fetchById: function(record, id) { - var adapter = get(this, 'adapter'); + var adapter = get(this, 'adapter'), + deferred; if (adapter.findMany && !adapter.findMany.isUnimplemented) { if (this._currentBatchIds) { @@ -757,8 +932,17 @@ Ember.Model.reopenClass({ this._currentBatchRecordArrays = []; } + deferred = Ember.Deferred.create(); + + //Attached the record to the deferred so we can resolove it later. + Ember.set(deferred, 'resolveWith', record); + + if (!this._currentBatchDeferreds) { this._currentBatchDeferreds = []; } + this._currentBatchDeferreds.push(deferred); + Ember.run.scheduleOnce('data', this, this._executeBatch); - // TODO: return a promise here + + return deferred; } else { return adapter.find(record, id); } @@ -767,6 +951,7 @@ Ember.Model.reopenClass({ _executeBatch: function() { var batchIds = this._currentBatchIds, batchRecordArrays = this._currentBatchRecordArrays, + batchDeferreds = this._currentBatchDeferreds, self = this, requestIds = [], promise, @@ -774,41 +959,60 @@ Ember.Model.reopenClass({ this._currentBatchIds = null; this._currentBatchRecordArrays = null; + this._currentBatchDeferreds = null; + + for (i = 0; i < batchIds.length; i++) { + if (!this.cachedRecordForId(batchIds[i]).get('isLoaded')) { + requestIds.push(batchIds[i]); + } + } if (batchIds.length === 1) { promise = get(this, 'adapter').find(this.cachedRecordForId(batchIds[0]), batchIds[0]); } else { var recordArray = Ember.RecordArray.create({_ids: batchIds}); - promise = get(this, 'adapter').findMany(this, recordArray, batchIds); + if (requestIds.length === 0) { + promise = new Ember.RSVP.Promise(function(resolve, reject) { resolve(recordArray); }); + recordArray.notifyLoaded(); + } else { + promise = get(this, 'adapter').findMany(this, recordArray, requestIds); + } } promise.then(function() { for (var i = 0, l = batchRecordArrays.length; i < l; i++) { batchRecordArrays[i].loadForFindMany(self); } + + if(batchDeferreds) { + for (i = 0, l = batchDeferreds.length; i < l; i++) { + var resolveWith = Ember.get(batchDeferreds[i], 'resolveWith'); + batchDeferreds[i].resolve(resolveWith); + } + } + }).then(null, function(errorXHR) { + if (batchDeferreds) { + for (var i = 0, l = batchDeferreds.length; i < l; i++) { + batchDeferreds[i].reject(errorXHR); + } + } }); }, - findQuery: function(params) { - var records = Ember.RecordArray.create(); - - this.adapter.findQuery(this, records, params); - - return records; + getCachedReferenceRecord: function(id){ + var ref = this._getReferenceById(id); + if(ref) return ref.record; + return undefined; }, cachedRecordForId: function(id) { - if (!this.recordCache) { this.recordCache = {}; } - var record; + var record = this.getCachedReferenceRecord(id); - if (this.recordCache[id]) { - record = this.recordCache[id]; - } else { + if (!record) { var primaryKey = get(this, 'primaryKey'), - attrs = {isLoaded: false}; + attrs = {isLoaded: false}; attrs[primaryKey] = id; record = this.create(attrs); - this.recordCache[id] = record; var sideloadedData = this.sideloadedData && this.sideloadedData[id]; if (sideloadedData) { record.load(id, sideloadedData); @@ -818,6 +1022,7 @@ Ember.Model.reopenClass({ return record; }, + addToRecordArrays: function(record) { if (this._findAllRecordArray) { this._findAllRecordArray.pushObject(record); @@ -834,6 +1039,26 @@ Ember.Model.reopenClass({ } }, + unload: function (record) { + this.removeFromRecordArrays(record); + var primaryKey = record.get(get(this, 'primaryKey')); + this.removeFromCache(primaryKey); + }, + + clearCache: function () { + this.sideloadedData = undefined; + this._referenceCache = undefined; + }, + + removeFromCache: function (key) { + if (this.sideloadedData && this.sideloadedData[key]) { + delete this.sideloadedData[key]; + } + if(this._referenceCache && this._referenceCache[key]) { + delete this._referenceCache[key]; + } + }, + removeFromRecordArrays: function(record) { if (this._findAllRecordArray) { this._findAllRecordArray.removeObject(record); @@ -869,25 +1094,39 @@ Ember.Model.reopenClass({ }, forEachCachedRecord: function(callback) { - if (!this.recordCache) { return Ember.A([]); } - var ids = Object.keys(this.recordCache); + if (!this._referenceCache) { this._referenceCache = {}; } + var ids = Object.keys(this._referenceCache); ids.map(function(id) { - return this.recordCache[id]; + return this._getReferenceById(id).record; }, this).forEach(callback); }, load: function(hashes) { + if (Ember.typeOf(hashes) !== 'array') { hashes = [hashes]; } + if (!this.sideloadedData) { this.sideloadedData = {}; } + for (var i = 0, l = hashes.length; i < l; i++) { - var hash = hashes[i]; - this.sideloadedData[hash[get(this, 'primaryKey')]] = hash; + var hash = hashes[i], + primaryKey = hash[get(this, 'primaryKey')], + record = this.getCachedReferenceRecord(primaryKey); + + if (record) { + record.load(primaryKey, hash); + } else { + this.sideloadedData[primaryKey] = hash; + } } }, - _referenceForId: function(id) { - if (!this._idToReference) { this._idToReference = {}; } + _getReferenceById: function(id) { + if (!this._referenceCache) { this._referenceCache = {}; } + return this._referenceCache[id]; + }, + + _getOrCreateReferenceForId: function(id) { + var reference = this._getReferenceById(id); - var reference = this._idToReference[id]; if (!reference) { reference = this._createReference(id); } @@ -896,32 +1135,26 @@ Ember.Model.reopenClass({ }, _createReference: function(id) { - if (!this._idToReference) { this._idToReference = {}; } + if (!this._referenceCache) { this._referenceCache = {}; } - Ember.assert('The id ' + id + ' has alread been used with another record of type ' + this.toString() + '.', !id || !this._idToReference[id]); + Ember.assert('The id ' + id + ' has alread been used with another record of type ' + this.toString() + '.', !id || !this._referenceCache[id]); var reference = { id: id, clientId: this._clientIdCounter++ }; - // if we're creating an item, this process will be done - // later, once the object has been persisted. - if (id) { - this._idToReference[id] = reference; - } + this._cacheReference(reference); return reference; }, - resetData: function() { - this._idToReference = null; - this.sideloadedData = null; - this.recordCache = null; - this.recordArrays = null; - this._currentBatchIds = null; - this._hasManyArrays = null; - this._findAllRecordArray = null; + _cacheReference: function(reference) { + // if we're creating an item, this process will be done + // later, once the object has been persisted. + if (reference.id) { + this._referenceCache[reference.id] = reference; + } } }); @@ -957,7 +1190,8 @@ Ember.Model.reopen({ modelClass: type, content: this._getHasManyContent(key, type, embedded), embedded: embedded, - key: key + key: key, + relationshipKey: meta.relationshipKey }); this._registerHasManyArray(collection); @@ -971,7 +1205,8 @@ Ember.Model.reopen({ (function() { -var get = Ember.get; +var get = Ember.get, + set = Ember.set; function getType() { if (typeof this.type === "string") { @@ -986,14 +1221,32 @@ Ember.belongsTo = function(type, options) { var meta = { type: type, isRelationship: true, options: options, kind: 'belongsTo', getType: getType }, relationshipKey = options.key; - return Ember.computed(function(key, value) { + return Ember.computed(function(key, value, oldValue) { type = meta.getType(); - if (arguments.length === 2) { + var dirtyAttributes = get(this, '_dirtyAttributes'), + createdDirtyAttributes = false; + + if (!dirtyAttributes) { + dirtyAttributes = []; + createdDirtyAttributes = true; + } + + if (arguments.length > 1) { if (value) { Ember.assert(Ember.String.fmt('Attempted to set property of type: %@ with a value of type: %@', [value.constructor, type]), value instanceof type); + + if (oldValue !== value) { + dirtyAttributes.pushObject(key); + } else { + dirtyAttributes.removeObject(key); + } + + if (createdDirtyAttributes) { + set(this, '_dirtyAttributes', dirtyAttributes); + } } return value === undefined ? null : value; } else { @@ -1012,11 +1265,12 @@ Ember.Model.reopen({ } if (meta.options.embedded) { - var primaryKey = get(type, 'primaryKey'); - record = type.create({ isLoaded: false }); - record.load(idOrAttrs[primaryKey], idOrAttrs); + var primaryKey = get(type, 'primaryKey'), + id = idOrAttrs[primaryKey]; + record = type.create({ isLoaded: false, id: id }); + record.load(id, idOrAttrs); } else { - record = type.findById(idOrAttrs); + record = type.find(idOrAttrs); } return record; @@ -1032,41 +1286,15 @@ var get = Ember.get, set = Ember.set, meta = Ember.meta; -function wrapObject(value) { - if (Ember.isArray(value)) { - var clonedArray = value.slice(); - - // TODO: write test for recursive cloning - for (var i = 0, l = clonedArray.length; i < l; i++) { - clonedArray[i] = wrapObject(clonedArray[i]); - } - - return Ember.A(clonedArray); - } else if (value && value.constructor === Date) { - return new Date(value.toISOString()); - } else if (value && typeof value === "object") { - var clone = Ember.create(value), property; - - for (property in value) { - if (value.hasOwnProperty(property) && typeof value[property] === "object") { - clone[property] = wrapObject(value[property]); - } - } - return clone; - } else { - return value; - } -} - Ember.Model.dataTypes = {}; Ember.Model.dataTypes[Date] = { deserialize: function(string) { - if(!string) { return null; } + if (!string) { return null; } return new Date(string); }, serialize: function (date) { - if(!date) { return null; } + if (!date) { return null; } return date.toISOString(); }, isEqual: function(obj1, obj2) { @@ -1093,7 +1321,7 @@ function deserialize(value, type) { } else if (type && Ember.Model.dataTypes[type]) { return Ember.Model.dataTypes[type].deserialize(value); } else { - return wrapObject(value); + return value; } } @@ -1103,15 +1331,35 @@ Ember.attr = function(type, options) { var data = get(this, '_data'), dataKey = this.dataKey(key), dataValue = data && get(data, dataKey), - beingCreated = meta(this).proto === this; + beingCreated = meta(this).proto === this, + dirtyAttributes = get(this, '_dirtyAttributes'), + createdDirtyAttributes = false; + + if (!dirtyAttributes) { + dirtyAttributes = []; + createdDirtyAttributes = true; + } if (arguments.length === 2) { - if (beingCreated && !data) { - data = {}; - set(this, '_data', data); - data[dataKey] = value; + if (beingCreated) { + if (!data) { + data = {}; + set(this, '_data', data); + } + dataValue = data[dataKey] = value; } - return wrapObject(value); + + if (dataValue !== value) { + dirtyAttributes.pushObject(key); + } else { + dirtyAttributes.removeObject(key); + } + + if (createdDirtyAttributes) { + set(this, '_dirtyAttributes', dirtyAttributes); + } + + return value; } return this.getAttr(key, deserialize(dataValue, type)); @@ -1132,6 +1380,7 @@ Ember.RESTAdapter = Ember.Adapter.extend({ return this.ajax(url).then(function(data) { self.didFind(record, id, data); + return record; }); }, @@ -1148,6 +1397,7 @@ Ember.RESTAdapter = Ember.Adapter.extend({ return this.ajax(url).then(function(data) { self.didFindAll(klass, records, data); + return records; }); }, @@ -1164,6 +1414,7 @@ Ember.RESTAdapter = Ember.Adapter.extend({ return this.ajax(url, params).then(function(data) { self.didFindQuery(klass, records, params, data); + return records; }); }, @@ -1180,6 +1431,7 @@ Ember.RESTAdapter = Ember.Adapter.extend({ return this.ajax(url, record.toJSON(), "POST").then(function(data) { self.didCreateRecord(record, data); + return record; }); }, @@ -1187,7 +1439,6 @@ Ember.RESTAdapter = Ember.Adapter.extend({ var rootKey = get(record.constructor, 'rootKey'), primaryKey = get(record.constructor, 'primaryKey'), dataToLoad = rootKey ? data[rootKey] : data; - record.load(dataToLoad[primaryKey], dataToLoad); record.didCreateRecord(); }, @@ -1199,6 +1450,7 @@ Ember.RESTAdapter = Ember.Adapter.extend({ return this.ajax(url, record.toJSON(), "PUT").then(function(data) { // TODO: Some APIs may or may not return data self.didSaveRecord(record, data); + return record; }); }, @@ -1228,7 +1480,7 @@ Ember.RESTAdapter = Ember.Adapter.extend({ var urlRoot = get(klass, 'url'); if (!urlRoot) { throw new Error('Ember.RESTAdapter requires a `url` property to be specified'); } - if (id) { + if (!Ember.isEmpty(id)) { return urlRoot + "/" + id + ".json"; } else { return urlRoot + ".json"; @@ -1243,8 +1495,10 @@ Ember.RESTAdapter = Ember.Adapter.extend({ }; }, - _ajax: function(url, params, method) { - var settings = this.ajaxSettings(url, method); + _ajax: function(url, params, method, settings) { + if (!settings) { + settings = this.ajaxSettings(url, method); + } return new Ember.RSVP.Promise(function(resolve, reject) { if (params) { @@ -1261,6 +1515,11 @@ Ember.RESTAdapter = Ember.Adapter.extend({ }; settings.error = function(jqXHR, textStatus, errorThrown) { + // https://github.com/ebryn/ember-model/issues/202 + if (jqXHR) { + jqXHR.then = null; + } + Ember.run(null, reject, jqXHR); }; @@ -1304,4 +1563,123 @@ Ember.loadPromise = function(target) { }; +})(); + +(function() { + +// This is a debug adapter for the Ember Extension, don't let the fact this is called an "adapter" confuse you. +// Most copied from: https://github.com/emberjs/data/blob/master/packages/ember-data/lib/system/debug/debug_adapter.js + +if (!Ember.DataAdapter) { return; } + +var get = Ember.get, capitalize = Ember.String.capitalize, underscore = Ember.String.underscore; + +var DebugAdapter = Ember.DataAdapter.extend({ + getFilters: function() { + return [ + { name: 'isNew', desc: 'New' }, + { name: 'isModified', desc: 'Modified' }, + { name: 'isClean', desc: 'Clean' } + ]; + }, + + detect: function(klass) { + return klass !== Ember.Model && Ember.Model.detect(klass); + }, + + columnsForType: function(type) { + var columns = [], count = 0, self = this; + Ember.A(get(type.proto(), 'attributes')).forEach(function(name, meta) { + if (count++ > self.attributeLimit) { return false; } + var desc = capitalize(underscore(name).replace('_', ' ')); + columns.push({ name: name, desc: desc }); + }); + return columns; + }, + + getRecords: function(type) { + var records = []; + type.forEachCachedRecord(function(record) { records.push(record); }); + return records; + }, + + getRecordColumnValues: function(record) { + var self = this, count = 0, + columnValues = { id: get(record, 'id') }; + + record.get('attributes').forEach(function(key) { + if (count++ > self.attributeLimit) { + return false; + } + var value = get(record, key); + columnValues[key] = value; + }); + return columnValues; + }, + + getRecordKeywords: function(record) { + var keywords = [], keys = Ember.A(['id']); + record.get('attributes').forEach(function(key) { + keys.push(key); + }); + keys.forEach(function(key) { + keywords.push(get(record, key)); + }); + return keywords; + }, + + getRecordFilterValues: function(record) { + return { + isNew: record.get('isNew'), + isModified: record.get('isDirty') && !record.get('isNew'), + isClean: !record.get('isDirty') + }; + }, + + getRecordColor: function(record) { + var color = 'black'; + if (record.get('isNew')) { + color = 'green'; + } else if (record.get('isDirty')) { + color = 'blue'; + } + return color; + }, + + observeRecord: function(record, recordUpdated) { + var releaseMethods = Ember.A(), self = this, + keysToObserve = Ember.A(['id', 'isNew', 'isDirty']); + + record.get('attributes').forEach(function(key) { + keysToObserve.push(key); + }); + + keysToObserve.forEach(function(key) { + var handler = function() { + recordUpdated(self.wrapRecord(record)); + }; + Ember.addObserver(record, key, handler); + releaseMethods.push(function() { + Ember.removeObserver(record, key, handler); + }); + }); + + var release = function() { + releaseMethods.forEach(function(fn) { fn(); } ); + }; + + return release; + } +}); + +Ember.onLoad('Ember.Application', function(Application) { + Application.initializer({ + name: "dataAdapter", + + initialize: function(container, application) { + application.register('dataAdapter:main', DebugAdapter); + } + }); +}); + })(); diff --git a/assets/scripts/vendor/ember.js b/assets/scripts/vendor/ember.js index 33248fc3..9a26a814 100644 --- a/assets/scripts/vendor/ember.js +++ b/assets/scripts/vendor/ember.js @@ -1,5 +1,5 @@ -// Version: v1.0.0-rc.7-59-g4048275 -// Last commit: 4048275 (2013-08-25 00:57:38 -0400) +// Version: v1.1.0-beta.4 +// Last commit: 89513c5 (2013-10-11 15:24:11 -0700) (function() { @@ -55,7 +55,7 @@ Ember.assert = function(desc, test) { if (Ember.testing && !test) { // when testing, ensure test failures when assertions fail - throw new Error("Assertion Failed: " + desc); + throw new Ember.Error("Assertion Failed: " + desc); } }; @@ -107,7 +107,7 @@ Ember.deprecate = function(message, test) { if (arguments.length === 1) { test = false; } if (test) { return; } - if (Ember.ENV.RAISE_ON_DEPRECATION) { throw new Error(message); } + if (Ember.ENV.RAISE_ON_DEPRECATION) { throw new Ember.Error(message); } var error; @@ -138,15 +138,21 @@ Ember.deprecate = function(message, test) { /** + Alias an old, deprecated method with its new counterpart. + Display a deprecation warning with the provided message and a stack trace - (Chrome and Firefox only) when the wrapped method is called. + (Chrome and Firefox only) when the assigned method is called. Ember build tools will not remove calls to `Ember.deprecateFunc()`, though no warnings will be shown in production. + ```javascript + Ember.oldMethod = Ember.deprecateFunc("Please use the new, updated method", Ember.newMethod); + ``` + @method deprecateFunc @param {String} message A description of the deprecation. - @param {Function} func The function to be deprecated. + @param {Function} func The new function called to replace its deprecated counterpart. @return {Function} a new function that wrapped the original function with a deprecation warning */ Ember.deprecateFunc = function(message, func) { @@ -156,10 +162,22 @@ Ember.deprecateFunc = function(message, func) { }; }; + +// Inform the developer about the Ember Inspector if not installed. +if (!Ember.testing) { + if (typeof window !== 'undefined' && window.chrome && window.addEventListener) { + window.addEventListener("load", function() { + if (document.body && document.body.dataset && !document.body.dataset.emberExtension) { + Ember.debug('For more advanced debugging, install the Ember Inspector from https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi'); + } + }, false); + } +} + })(); -// Version: v1.0.0-rc.7-59-g4048275 -// Last commit: 4048275 (2013-08-25 00:57:38 -0400) +// Version: v1.1.0-beta.4 +// Last commit: 89513c5 (2013-10-11 15:24:11 -0700) (function() { @@ -225,7 +243,7 @@ var define, requireModule; @class Ember @static - @version 1.0.0-rc.7 + @version 1.1.0-beta.4 */ if ('undefined' === typeof Ember) { @@ -252,10 +270,10 @@ Ember.toString = function() { return "Ember"; }; /** @property VERSION @type String - @default '1.0.0-rc.7' + @default '1.1.0-beta.4' @final */ -Ember.VERSION = '1.0.0-rc.7'; +Ember.VERSION = '1.1.0-beta.4'; /** Standard environmental variables. You can define these in a global `ENV` @@ -280,6 +298,27 @@ Ember.ENV = Ember.ENV || ENV; Ember.config = Ember.config || {}; +/** + Hash of enabled Canary features. Add to before creating your application. + + @property FEATURES + @type Hash +*/ + +Ember.FEATURES = {}; + +/** + Test that a feature is enabled. Parsed by Ember's build tools to leave + experimental features out of beta/stable builds. + + @method isEnabled + @param {string} feature +*/ + +Ember.FEATURES.isEnabled = function(feature) { + return Ember.FEATURES[feature]; +}; + // .......................................................... // BOOTSTRAP // @@ -332,7 +371,7 @@ Ember.SHIM_ES5 = (Ember.ENV.SHIM_ES5 === false) ? false : Ember.EXTEND_PROTOTYPE Ember.LOG_VERSION = (Ember.ENV.LOG_VERSION === false) ? false : true; /** - Empty function. Useful for some operations. + Empty function. Useful for some operations. Always returns `this`. @method K @private @@ -411,11 +450,87 @@ function assertPolyfill(test, message) { @namespace Ember */ Ember.Logger = { + /** + Logs the arguments to the console. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + var foo = 1; + Ember.Logger.log('log value of foo:', foo); // "log value of foo: 1" will be printed to the console + ``` + + @method log + @for Ember.Logger + @param {*} arguments + */ log: consoleMethod('log') || Ember.K, + /** + Prints the arguments to the console with a warning icon. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + Ember.Logger.warn('Something happened!'); // "Something happened!" will be printed to the console with a warning icon. + ``` + + @method warn + @for Ember.Logger + @param {*} arguments + */ warn: consoleMethod('warn') || Ember.K, + /** + Prints the arguments to the console with an error icon, red text and a stack race. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + Ember.Logger.error('Danger! Danger!'); // "Danger! Danger!" will be printed to the console in red text. + ``` + + @method error + @for Ember.Logger + @param {*} arguments + */ error: consoleMethod('error') || Ember.K, + /** + Logs the arguments to the console. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + var foo = 1; + Ember.Logger.info('log value of foo:', foo); // "log value of foo: 1" will be printed to the console + ``` + + @method info + @for Ember.Logger + @param {*} arguments + */ info: consoleMethod('info') || Ember.K, + /** + Logs the arguments to the console in blue text. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + var foo = 1; + Ember.Logger.debug('log value of foo:', foo); // "log value of foo: 1" will be printed to the console + ``` + + @method debug + @for Ember.Logger + @param {*} arguments + */ debug: consoleMethod('debug') || consoleMethod('info') || Ember.K, + /** + + If the value passed into Ember.Logger.assert is not truthy it will throw an error with a stack trace. + + ```javascript + Ember.Logger.assert(true); // undefined + Ember.Logger.assert(true === false); // Throws an Assertion failed error. + ``` + + @method assert + @for Ember.Logger + @param {Boolean} bool Value to test + */ assert: consoleMethod('assert') || assertPolyfill }; @@ -429,6 +544,15 @@ Ember.Logger = { internals encounter an error. This is useful for specialized error handling and reporting code. + ```javascript + Ember.onerror = function(error) { + Em.$.ajax('/report-error', 'POST', { + stack: error.stack, + otherInformation: 'whatever app state you want to provide' + }); + }; + ``` + @event onerror @for Ember @param {Exception} error the error object @@ -459,6 +583,21 @@ Ember.handleErrors = function(func, context) { } }; +/** + Merge the contents of two objects together into the first object. + + ```javascript + Ember.merge({first: 'Tom'}, {last: 'Dale'}); // {first: 'Tom', last: 'Dale'} + var a = {first: 'Yehuda'}, b = {last: 'Katz'}; + Ember.merge(a, b); // a == {first: 'Yehuda', last: 'Katz'}, b == {last: 'Katz'} + ``` + + @method merge + @for Ember + @param {Object} original The object to merge into + @param {Object} updates The object to copy properties from + @return {Object} +*/ Ember.merge = function(original, updates) { for (var prop in updates) { if (!updates.hasOwnProperty(prop)) { continue; } @@ -761,6 +900,12 @@ var arrayIndexOf = isNativeFunc(Array.prototype.indexOf) ? Array.prototype.index return -1; }; +/** + Array polyfills to support ES5 features in older browsers. + + @namespace Ember + @property ArrayPolyfills +*/ Ember.ArrayPolyfills = { map: arrayMap, forEach: arrayForEach, @@ -1174,10 +1319,18 @@ function canInvoke(obj, methodName) { /** Checks to see if the `methodName` exists on the `obj`. + ```javascript + var foo = {bar: Ember.K, baz: null}; + Ember.canInvoke(foo, 'bar'); // true + Ember.canInvoke(foo, 'baz'); // false + Ember.canInvoke(foo, 'bat'); // false + ``` + @method canInvoke @for Ember @param {Object} obj The object to check for the method @param {String} methodName The method name to check for + @return {Boolean} */ Ember.canInvoke = canInvoke; @@ -1185,6 +1338,13 @@ Ember.canInvoke = canInvoke; Checks to see if the `methodName` exists on the `obj`, and if it does, invokes it with the arguments passed. + ```javascript + var d = new Date('03/15/2013'); + Ember.tryInvoke(d, 'getTime'); // 1363320000000 + Ember.tryInvoke(d, 'setFullYear', [2014]); // 1394856000000 + Ember.tryInvoke(d, 'noSuchMethod', [2014]); // undefined + ``` + @method tryInvoke @for Ember @param {Object} obj The object to check for the method @@ -1216,6 +1376,17 @@ var needsFinallyFix = (function() { Provides try { } finally { } functionality, while working around Safari's double finally bug. + ```javascript + var tryable = function() { + someResource.lock(); + runCallback(); // May throw error. + }; + var finalizer = function() { + someResource.unlock(); + }; + Ember.tryFinally(tryable, finalizer); + ``` + @method tryFinally @for Ember @param {Function} tryable The function to run the try callback @@ -1266,6 +1437,30 @@ if (needsFinallyFix) { Provides try { } catch finally { } functionality, while working around Safari's double finally bug. + ```javascript + var tryable = function() { + for (i=0, l=listeners.length; i size ? size : ends; + if (count <= 0) { count = 0; } + + chunk = args.splice(0, size); + chunk = [start, count].concat(chunk); + + start += size; + ends -= count; + + ret = ret.concat(splice.apply(array, chunk)); + } + return ret; + }, + replace: function(array, idx, amt, objects) { if (array.replace) { return array.replace(idx, amt, objects); } else { - var args = concat.apply([idx, amt], objects); - return array.splice.apply(array, args); + return utils._replace(array, idx, amt, objects); } }, @@ -2016,7 +2262,7 @@ function suspendListener(obj, eventName, target, method, callback) { Suspends multiple listeners during a callback. - + @method suspendListeners @for Ember @param obj @@ -2032,6 +2278,7 @@ function suspendListeners(obj, eventNames, target, method, callback) { } var suspendedActions = [], + actionsList = [], eventName, actions, i, l; for (i=0, l=eventNames.length; i 0 || keyName === 'length', proto = m.proto, @@ -2308,7 +2556,8 @@ var propertyWillChange = Ember.propertyWillChange = function(obj, keyName) { dependentKeysWillChange(obj, keyName, m); chainsWillChange(obj, keyName, m); notifyBeforeObservers(obj, keyName); -}; +} +Ember.propertyWillChange = propertyWillChange; /** This function is called just after an object property has changed. @@ -2316,7 +2565,7 @@ var propertyWillChange = Ember.propertyWillChange = function(obj, keyName) { 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 + manually along with `Ember.propertyWillChange()` which you should call just before the property value changes. @method propertyDidChange @@ -2325,7 +2574,7 @@ var propertyWillChange = Ember.propertyWillChange = function(obj, keyName) { @param {String} keyName The property key (or path) that will change. @return {void} */ -var propertyDidChange = Ember.propertyDidChange = function(obj, keyName) { +function propertyDidChange(obj, keyName) { var m = metaFor(obj, false), watching = m.watching[keyName] > 0 || keyName === 'length', proto = m.proto, @@ -2338,9 +2587,10 @@ var propertyDidChange = Ember.propertyDidChange = function(obj, keyName) { if (!watching && keyName !== 'length') { return; } dependentKeysDidChange(obj, keyName, m); - chainsDidChange(obj, keyName, m); + chainsDidChange(obj, keyName, m, false); notifyObservers(obj, keyName); -}; +} +Ember.propertyDidChange = propertyDidChange; var WILL_SEEN, DID_SEEN; @@ -2381,35 +2631,47 @@ function iterDeps(method, obj, depKey, seen, meta) { } } -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; } - - nodes = nodes.slice(); - - for(var i = 0, l = nodes.length; i < l; i++) { - nodes[i].willChange(arg); +function chainsWillChange(obj, keyName, m) { + if (!(m.hasOwnProperty('chainWatchers') && + m.chainWatchers[keyName])) { + return; } -}; -var chainsDidChange = function(obj, keyName, m, arg) { - if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do + var nodes = m.chainWatchers[keyName], + events = [], + i, l; - var nodes = m.chainWatchers; - - nodes = nodes[keyName]; - if (!nodes) { return; } - - nodes = nodes.slice(); - - for(var i = 0, l = nodes.length; i < l; i++) { - nodes[i].didChange(arg); + for(i = 0, l = nodes.length; i < l; i++) { + nodes[i].willChange(events); } -}; + + for (i = 0, l = events.length; i < l; i += 2) { + propertyWillChange(events[i], events[i+1]); + } +} + +function chainsDidChange(obj, keyName, m, suppressEvents) { + if (!(m.hasOwnProperty('chainWatchers') && + m.chainWatchers[keyName])) { + return; + } + + var nodes = m.chainWatchers[keyName], + events = suppressEvents ? null : [], + i, l; + + for(i = 0, l = nodes.length; i < l; i++) { + nodes[i].didChange(events); + } + + if (suppressEvents) { + return; + } + + for (i = 0, l = events.length; i < l; i += 2) { + propertyDidChange(events[i], events[i+1]); + } +} Ember.overrideChains = function(obj, keyName, m) { chainsDidChange(obj, keyName, m, true); @@ -2419,20 +2681,24 @@ Ember.overrideChains = function(obj, keyName, m) { @method beginPropertyChanges @chainable */ -var beginPropertyChanges = Ember.beginPropertyChanges = function() { +function beginPropertyChanges() { deferred++; -}; +} + +Ember.beginPropertyChanges = beginPropertyChanges; /** @method endPropertyChanges */ -var endPropertyChanges = Ember.endPropertyChanges = function() { +function endPropertyChanges() { deferred--; if (deferred<=0) { beforeObserverSet.clear(); observerSet.flush(); } -}; +} + +Ember.endPropertyChanges = endPropertyChanges; /** Make a series of property changes together in an @@ -2454,7 +2720,7 @@ Ember.changeProperties = function(cb, binding) { tryFinally(cb, endPropertyChanges, binding); }; -var notifyBeforeObservers = function(obj, keyName) { +function notifyBeforeObservers(obj, keyName) { if (obj.isDestroying) { return; } var eventName = keyName + ':before', listeners, diff; @@ -2465,9 +2731,9 @@ var notifyBeforeObservers = function(obj, keyName) { } else { sendEvent(obj, eventName, [obj, keyName]); } -}; +} -var notifyObservers = function(obj, keyName) { +function notifyObservers(obj, keyName) { if (obj.isDestroying) { return; } var eventName = keyName + ':change', listeners; @@ -2477,7 +2743,7 @@ var notifyObservers = function(obj, keyName) { } else { sendEvent(obj, eventName, [obj, keyName]); } -}; +} })(); @@ -2496,7 +2762,7 @@ var META_KEY = Ember.META_KEY, /** 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` + property is not defined but the object implements the `setUnknownProperty` method then that will be invoked as well. If you plan to run on IE8 and older browsers then you should use this @@ -2506,7 +2772,7 @@ var META_KEY = Ember.META_KEY, 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 + to respect the `setUnknownProperty` handler. Otherwise you can ignore this method. @method set @@ -2837,14 +3103,14 @@ 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. @@ -3165,6 +3431,47 @@ Ember.defineProperty = function(obj, keyName, desc, data, meta) { +(function() { +var get = Ember.get; + +/** + To get multiple properties at once, call `Ember.getProperties` + with an object followed by a list of strings or an array: + + ```javascript + Ember.getProperties(record, 'firstName', 'lastName', 'zipCode'); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } + ``` + + is equivalent to: + + ```javascript + Ember.getProperties(record, ['firstName', 'lastName', 'zipCode']); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } + ``` + + @method getProperties + @param obj + @param {String...|Array} list of keys to get + @return {Hash} +*/ +Ember.getProperties = function(obj) { + var ret = {}, + propertyNames = arguments, + i = 1; + + if (arguments.length === 2 && Ember.typeOf(arguments[1]) === 'array') { + i = 0; + propertyNames = arguments[1]; + } + for(var len = propertyNames.length; i < len; i++) { + ret[propertyNames[i]] = get(obj, propertyNames[i]); + } + return ret; +}; + +})(); + + + (function() { var changeProperties = Ember.changeProperties, set = Ember.set; @@ -3174,6 +3481,14 @@ var changeProperties = Ember.changeProperties, a single `beginPropertyChanges` and `endPropertyChanges` batch, so observers will be buffered. + ```javascript + anObject.setProperties({ + firstName: "Stanley", + lastName: "Stuart", + age: "21" + }) + ``` + @method setProperties @param self @param {Object} hash @@ -3263,8 +3578,6 @@ var metaFor = Ember.meta, // utils.js warn = Ember.warn, watchKey = Ember.watchKey, unwatchKey = Ember.unwatchKey, - propertyWillChange = Ember.propertyWillChange, - propertyDidChange = Ember.propertyDidChange, FIRST_KEY = /^([^\.\*]+)/; function firstKey(path) { @@ -3498,42 +3811,50 @@ ChainNodePrototype.unchain = function(key, path) { }; -ChainNodePrototype.willChange = function() { +ChainNodePrototype.willChange = function(events) { var chains = this._chains; if (chains) { for(var key in chains) { if (!chains.hasOwnProperty(key)) { continue; } - chains[key].willChange(); + chains[key].willChange(events); } } - if (this._parent) { this._parent.chainWillChange(this, this._key, 1); } + if (this._parent) { this._parent.chainWillChange(this, this._key, 1, events); } }; -ChainNodePrototype.chainWillChange = function(chain, path, depth) { +ChainNodePrototype.chainWillChange = function(chain, path, depth, events) { if (this._key) { path = this._key + '.' + path; } if (this._parent) { - this._parent.chainWillChange(this, path, depth+1); + this._parent.chainWillChange(this, path, depth+1, events); } else { - if (depth > 1) { propertyWillChange(this.value(), path); } + if (depth > 1) { + events.push(this.value(), path); + } path = 'this.' + path; - if (this._paths[path] > 0) { propertyWillChange(this.value(), path); } + if (this._paths[path] > 0) { + events.push(this.value(), path); + } } }; -ChainNodePrototype.chainDidChange = function(chain, path, depth) { +ChainNodePrototype.chainDidChange = function(chain, path, depth, events) { if (this._key) { path = this._key + '.' + path; } if (this._parent) { - this._parent.chainDidChange(this, path, depth+1); + this._parent.chainDidChange(this, path, depth+1, events); } else { - if (depth > 1) { propertyDidChange(this.value(), path); } + if (depth > 1) { + events.push(this.value(), path); + } path = 'this.' + path; - if (this._paths[path] > 0) { propertyDidChange(this.value(), path); } + if (this._paths[path] > 0) { + events.push(this.value(), path); + } } }; -ChainNodePrototype.didChange = function(suppressEvent) { +ChainNodePrototype.didChange = function(events) { // invalidate my own value first. if (this._watching) { var obj = this._parent.value(); @@ -3555,14 +3876,15 @@ ChainNodePrototype.didChange = function(suppressEvent) { if (chains) { for(var key in chains) { if (!chains.hasOwnProperty(key)) { continue; } - chains[key].didChange(suppressEvent); + chains[key].didChange(events); } } - if (suppressEvent) { return; } + // if no events are passed in then we only care about the above wiring update + if (events === null) { return; } // and finally tell parent about my path changing... - if (this._parent) { this._parent.chainDidChange(this, this._key, 1); } + if (this._parent) { this._parent.chainDidChange(this, this._key, 1, events); } }; Ember.finishChains = function(obj) { @@ -3571,9 +3893,10 @@ Ember.finishChains = function(obj) { if (chains.value() !== obj) { m.chains = chains = chains.copy(obj); } - chains.didChange(true); + chains.didChange(null); } }; + })(); @@ -3854,6 +4177,81 @@ function removeDependentKeys(desc, obj, keyName, meta) { // /** + A computed property transforms an objects function into a property. + + By default the function backing the computed property will only be called + once and the result will be cached. You can specify various properties + that your computed property is dependent on. This will force the cached + result to be recomputed if the dependencies are modified. + + In the following example we declare a computed property (by calling + `.property()` on the fullName function) and setup the properties + dependencies (depending on firstName and lastName). The fullName function + will be called once (regardless of how many times it is accessed) as long + as it's dependencies have not been changed. Once firstName or lastName are updated + any future calls (or anything bound) to fullName will incorporate the new + values. + + ```javascript + Person = Ember.Object.extend({ + // these will be supplied by `create` + firstName: null, + lastName: null, + + fullName: function() { + var firstName = this.get('firstName'); + var lastName = this.get('lastName'); + + return firstName + ' ' + lastName; + }.property('firstName', 'lastName') + }); + + var tom = Person.create({ + firstName: "Tom", + lastName: "Dale" + }); + + tom.get('fullName') // "Tom Dale" + ``` + + You can also define what Ember should do when setting a computed property. + If you try to set a computed property, it will be invoked with the key and + value you want to set it to. You can also accept the previous value as the + third parameter. + + ```javascript + + Person = Ember.Object.extend({ + // these will be supplied by `create` + firstName: null, + lastName: null, + + fullName: function(key, value, oldValue) { + // getter + if (arguments.length === 1) { + var firstName = this.get('firstName'); + var lastName = this.get('lastName'); + + return firstName + ' ' + lastName; + + // setter + } else { + var name = value.split(" "); + + this.set('firstName', name[0]); + this.set('lastName', name[1]); + + return value; + } + }.property('firstName', 'lastName') + }); + + var person = Person.create(); + person.set('fullName', "Peter Wagenet"); + person.get('firstName') // Peter + person.get('lastName') // Wagenet + ``` + @class ComputedProperty @namespace Ember @extends Ember.Descriptor @@ -3872,7 +4270,7 @@ ComputedProperty.prototype = new Ember.Descriptor(); var ComputedPropertyPrototype = ComputedProperty.prototype; -/* +/** Properties are cacheable by default. Computed property will automatically cache the return value of your function until one of the dependent keys changes. @@ -4014,11 +4412,37 @@ ComputedPropertyPrototype.didChange = function(obj, keyName) { function finishChains(chainNodes) { for (var i=0, l=chainNodes.length; i= 0) || + if ((concats && a_indexOf.call(concats, key) >= 0) || key === 'concatenatedProperties' || key === 'mergedProperties') { value = applyConcatenatedProperties(base, key, value, values); } else if ((mergings && a_indexOf.call(mergings, key) >= 0)) { value = applyMergedProperties(base, key, value, values); + } else if (isMethod(value)) { + value = giveMethodSuper(base, key, value, values, descs); } descs[key] = undefined; @@ -6370,6 +7082,7 @@ function mergeMixins(mixins, m, descs, values, base, keys) { if (props) { meta = Ember.meta(base); + if (base.willMergeMixin) { base.willMergeMixin(props); } concats = concatenatedMixinProperties('concatenatedProperties', props, values, base); mergings = concatenatedMixinProperties('mergedProperties', props, values, base); @@ -6543,7 +7256,7 @@ Ember.mixin = function(obj) { // Mix mixins into classes by passing them as the first arguments to // .extend. App.CommentView = Ember.View.extend(App.Editable, { - template: Ember.Handlebars.compile('{{#if isEditing}}...{{else}}...{{/if}}') + template: Ember.Handlebars.compile('{{#if view.isEditing}}...{{else}}...{{/if}}') }); commentView = App.CommentView.create(); @@ -6553,6 +7266,31 @@ Ember.mixin = function(obj) { Note that Mixins are created with `Ember.Mixin.create`, not `Ember.Mixin.extend`. + Note that mixins extend a constructor's prototype so arrays and object literals + defined as properties will be shared amongst objects that implement the mixin. + If you want to define an property in a mixin that is not shared, you can define + it either as a computed property or have it be created on initialization of the object. + + ```javascript + //filters array will be shared amongst any object implementing mixin + App.Filterable = Ember.Mixin.create({ + filters: Ember.A() + }); + + //filters will be a separate array for every object implementing the mixin + App.Filterable = Ember.Mixin.create({ + filters: Ember.computed(function(){return Ember.A();}) + }); + + //filters will be created as a separate array during the object's initialization + App.Filterable = Ember.Mixin.create({ + init: function() { + this._super(); + this.set("filters", Ember.A()); + } + }); + ``` + @class Mixin @namespace Ember */ @@ -6755,11 +7493,10 @@ Alias.prototype = new Ember.Descriptor(); @deprecated Use `Ember.aliasMethod` or `Ember.computed.alias` instead */ Ember.alias = function(methodName) { + Ember.deprecate("Ember.alias is deprecated. Please use Ember.aliasMethod or Ember.computed.alias instead."); return new Alias(methodName); }; -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. @@ -6788,6 +7525,22 @@ Ember.aliasMethod = function(methodName) { // /** + Specify a method that observes property changes. + + ```javascript + Ember.Object.extend({ + valueObserver: Ember.observer(function() { + // Executes whenever the "value" property changes + }, 'value') + }); + ``` + + In the future this method may become asynchronous. If you want to ensure + synchronous behavior, use `immediateObserver`. + + Also available as `Function.prototype.observes` if prototype extensions are + enabled. + @method observer @for Ember @param {Function} func @@ -6800,9 +7553,23 @@ Ember.observer = function(func) { return func; }; -// If observers ever become asynchronous, Ember.immediateObserver -// must remain synchronous. /** + Specify a method that observes property changes. + + ```javascript + Ember.Object.extend({ + valueObserver: Ember.immediateObserver(function() { + // Executes whenever the "value" property changes + }, 'value') + }); + ``` + + In the future, `Ember.observer` may become asynchronous. In this event, + `Ember.immediateObserver` will maintain the synchronous behavior. + + Also available as `Function.prototype.observesImmediately` if prototype extensions are + enabled. + @method immediateObserver @for Ember @param {Function} func @@ -6830,24 +7597,31 @@ Ember.immediateObserver = function() { ```javascript App.PersonView = Ember.View.extend({ + friends: [{ name: 'Tom' }, { name: 'Stefan' }, { name: 'Kris' }], - valueWillChange: function (obj, keyName) { + + valueWillChange: Ember.beforeObserver(function(obj, keyName) { this.changingFrom = obj.get(keyName); - }.observesBefore('content.value'), - valueDidChange: function(obj, keyName) { + }, 'content.value'), + + valueDidChange: Ember.observer(function(obj, keyName) { // only run if updating a value already in the DOM if (this.get('state') === 'inDOM') { - var color = obj.get(keyName) > this.changingFrom ? 'green' : 'red'; - // logic + var color = obj.get(keyName) > this.changingFrom ? 'green' : 'red'; + // logic } - }.observes('content.value'), - friendsDidChange: function(obj, keyName) { + }, 'content.value'), + + friendsDidChange: Ember.observer(function(obj, keyName) { // some logic // obj.get(keyName) returns friends array - }.observes('friends.@each.name') + }, 'friends.@each.name') }); ``` + Also available as `Function.prototype.observesBefore` if prototype extensions are + enabled. + @method beforeObserver @for Ember @param {Function} func @@ -6864,6 +7638,55 @@ Ember.beforeObserver = function(func) { +(function() { +// Provides a way to register library versions with ember. +var forEach = Ember.EnumerableUtils.forEach, + indexOf = Ember.EnumerableUtils.indexOf; + +Ember.libraries = function() { + var libraries = []; + var coreLibIndex = 0; + + var getLibrary = function(name) { + for (var i = 0; i < libraries.length; i++) { + if (libraries[i].name === name) { + return libraries[i]; + } + } + }; + + libraries.register = function(name, version) { + if (!getLibrary(name)) { + libraries.push({name: name, version: version}); + } + }; + + libraries.registerCoreLibrary = function(name, version) { + if (!getLibrary(name)) { + libraries.splice(coreLibIndex++, 0, {name: name, version: version}); + } + }; + + libraries.deRegister = function(name) { + var lib = getLibrary(name); + if (lib) libraries.splice(indexOf(libraries, lib), 1); + }; + + libraries.each = function (callback) { + forEach(libraries, function(lib) { + callback(lib.name, lib.version); + }); + }; + + return libraries; +}(); + +Ember.libraries.registerCoreLibrary('Ember', Ember.VERSION); + +})(); + + + (function() { /** Ember Metal @@ -6931,6 +7754,7 @@ define("rsvp/async", var browserGlobal = (typeof window !== 'undefined') ? window : {}; var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; var async; + var local = (typeof global !== 'undefined') ? global : this; // old node function useNextTick() { @@ -6981,7 +7805,7 @@ define("rsvp/async", function useSetTimeout() { return function(callback, arg) { - setTimeout(function() { + local.setTimeout(function() { callback(arg); }, 1); }; @@ -7364,6 +8188,10 @@ define("rsvp/promise", }); return thenPromise; + }, + + fail: function(fail) { + return this.then(null, fail); } }; @@ -7466,19 +8294,36 @@ define("rsvp/resolve", __exports__.resolve = resolve; }); +define("rsvp/rethrow", + ["exports"], + function(__exports__) { + "use strict"; + var local = (typeof global === "undefined") ? this : global; + + function rethrow(reason) { + local.setTimeout(function() { + throw reason; + }); + throw reason; + } + + + __exports__.rethrow = rethrow; + }); 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__) { + ["rsvp/events","rsvp/promise","rsvp/node","rsvp/all","rsvp/hash","rsvp/rethrow","rsvp/defer","rsvp/config","rsvp/resolve","rsvp/reject","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __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; + var rethrow = __dependency6__.rethrow; + var defer = __dependency7__.defer; + var config = __dependency8__.config; + var resolve = __dependency9__.resolve; + var reject = __dependency10__.reject; function configure(name, value) { config[name] = value; @@ -7489,25 +8334,35 @@ define("rsvp", __exports__.EventTarget = EventTarget; __exports__.all = all; __exports__.hash = hash; + __exports__.rethrow = rethrow; __exports__.defer = defer; __exports__.denodeify = denodeify; __exports__.configure = configure; __exports__.resolve = resolve; __exports__.reject = reject; }); - })(); (function() { +/** +@private +Public api for the container is still in flux. +The public api, specified on the application namespace should be considered the stable api. +// @module container +*/ + +/* + Flag to enable/disable model factory injections (disabled by default) + If model factory injections are enabled, models should not be + accessed globally (only through `container.lookupFactory('model:modelName'))`); +*/ +Ember.MODEL_FACTORY_INJECTIONS = false || !!Ember.ENV.MODEL_FACTORY_INJECTIONS; + define("container", [], function() { - /** - A safe and simple inheriting object. - - @class InheritingDict - */ + // A safe and simple inheriting object. function InheritingDict(parent) { this.parent = parent; this.dict = {}; @@ -7580,7 +8435,7 @@ define("container", @method has @param {String} key - @returns {Boolean} + @return {Boolean} */ has: function(key) { var dict = this.dict; @@ -7614,11 +8469,10 @@ define("container", } }; - /** - A lightweight container that helps to assemble and decouple components. - @class Container - */ + // A lightweight container that helps to assemble and decouple components. + // Public api for the container is still in flux. + // The public api, specified on the application namespace should be considered the stable api. function Container(parent) { this.parent = parent; this.children = []; @@ -7707,7 +8561,7 @@ define("container", to correctly inherit from the current container. @method child - @returns {Container} + @return {Container} */ child: function() { var container = new Container(this); @@ -7743,25 +8597,25 @@ define("container", ``` @method register - @param {String} type - @param {String} name + @param {String} fullName @param {Function} factory @param {Object} options */ - register: function(type, name, factory, options) { - var fullName; + register: function(fullName, factory, options) { + if (fullName.indexOf(':') === -1) { + throw new TypeError("malformed fullName, expected: `type:name` got: " + fullName + ""); + } - if (type.indexOf(':') !== -1) { - options = factory; - factory = name; - fullName = type; - } else { - Ember.deprecate('register("'+type +'", "'+ name+'") is now deprecated in-favour of register("'+type+':'+name+'");', false); - fullName = type + ":" + name; + if (factory === undefined) { + throw new TypeError('Attempting to register an unknown factory: `' + fullName + '`'); } var normalizedName = this.normalize(fullName); + if (this.cache.has(normalizedName)) { + throw new Error('Cannot re-register: `' + fullName +'`, as it has already been looked up.'); + } + this.registry.set(normalizedName, factory); this._options.set(normalizedName, options || {}); }, @@ -7777,6 +8631,7 @@ define("container", container.unregister('model:user') container.lookup('model:user') === undefined //=> true + ``` @method unregister @param {String} fullName @@ -7786,6 +8641,7 @@ define("container", this.registry.remove(normalizedName); this.cache.remove(normalizedName); + this.factoryCache.remove(normalizedName); this._options.remove(normalizedName); }, @@ -7819,7 +8675,7 @@ define("container", @method resolve @param {String} fullName - @returns {Function} fullName's factory + @return {Function} fullName's factory */ resolve: function(fullName) { return this.resolver(fullName) || this.registry.get(fullName); @@ -7850,6 +8706,17 @@ define("container", return fullName; }, + /** + @method makeToString + + @param {any} factory + @param {string} fullName + @return {function} toString function + */ + makeToString: function(factory, fullName) { + return factory.toString(); + }, + /** Given a fullName return a corresponding instance. @@ -7900,7 +8767,7 @@ define("container", var value = instantiate(this, fullName); - if (!value) { return; } + if (value === undefined) { return; } if (isSingleton(this, fullName) && options.singleton !== false) { this.cache.set(fullName, value); @@ -7978,7 +8845,7 @@ define("container", this.optionsForType(type, options); }, - /* + /** @private Used only via `injection`. @@ -8020,7 +8887,7 @@ define("container", addTypeInjection(this.typeInjections, type, property, fullName); }, - /* + /** Defines injection rules. These rules are used to inject dependencies onto objects when they @@ -8028,8 +8895,8 @@ define("container", Two forms of injections are possible: - * Injecting one fullName on another fullName - * Injecting one fullName on a type + * Injecting one fullName on another fullName + * Injecting one fullName on a type Example: @@ -8075,7 +8942,7 @@ define("container", }, - /* + /** @private Used only via `factoryInjection`. @@ -8112,7 +8979,7 @@ define("container", addTypeInjection(this.factoryTypeInjections, type, property, fullName); }, - /* + /** Defines factory injection rules. Similar to regular injection rules, but are run against factories, via @@ -8123,8 +8990,8 @@ define("container", Two forms of injections are possible: - * Injecting one fullName on another fullName - * Injecting one fullName on a type + * Injecting one fullName on another fullName + * Injecting one fullName on a type Example: @@ -8226,7 +9093,7 @@ define("container", injection = injections[i]; lookup = container.lookup(injection.fullName); - if (lookup) { + if (lookup !== undefined) { hash[injection.property] = lookup; } else { throw new Error('Attempting to inject an unknown injection: `' + injection.fullName + '`'); @@ -8256,20 +9123,27 @@ define("container", var factory = container.resolve(name); var injectedFactory; var cache = container.factoryCache; + var type = fullName.split(":")[0]; - if (!factory) { return; } + if (factory === undefined) { return; } if (cache.has(fullName)) { return cache.get(fullName); } - if (typeof factory.extend !== 'function') { + if (!factory || typeof factory.extend !== 'function' || (!Ember.MODEL_FACTORY_INJECTIONS && type === 'model')) { // TODO: think about a 'safe' merge style extension // for now just fallback to create time injection return factory; } else { - injectedFactory = factory.extend(injectionsFor(container, fullName)); - injectedFactory.reopenClass(factoryInjectionsFor(container, fullName)); + + var injections = injectionsFor(container, fullName); + var factoryInjections = factoryInjectionsFor(container, fullName); + + factoryInjections._toString = container.makeToString(factory, fullName); + + injectedFactory = factory.extend(injections); + injectedFactory.reopenClass(factoryInjections); cache.set(fullName, injectedFactory); @@ -8633,16 +9507,39 @@ Ember.ORDER_DEFINITION = Ember.ENV.ORDER_DEFINITION || [ Ember.keys = Object.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; + var prototypeProperties = [ + 'constructor', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'valueOf', + 'toLocaleString', + 'toString' + ], + pushPropertyName = function(obj, array, key) { + // Prevents browsers that don't respect non-enumerability from + // copying internal Ember properties + if (key.substring(0,2) === '__') return; + if (key === '_super') return; + if (indexOf(array, key) >= 0) return; + if (!obj.hasOwnProperty(key)) return; - if (obj.hasOwnProperty(key)) { ret.push(key); } + array.push(key); + }; + + Ember.keys = function(obj) { + var ret = [], key; + for (key in obj) { + pushPropertyName(obj, ret, key); } + + // IE8 doesn't enumerate property that named the same as prototype properties. + for (var i = 0, l = prototypeProperties.length; i < l; i++) { + key = prototypeProperties[i]; + + pushPropertyName(obj, ret, key); + } + return ret; }; } @@ -8676,557 +9573,6 @@ Ember.Error.prototype = Ember.create(Error.prototype); -(function() { -/** - Expose RSVP implementation - - Documentation can be found here: https://github.com/tildeio/rsvp.js/blob/master/README.md - - @class RSVP - @namespace Ember - @constructor -*/ -Ember.RSVP = requireModule('rsvp'); - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ - -var STRING_DASHERIZE_REGEXP = (/[ _]/g); -var STRING_DASHERIZE_CACHE = {}; -var STRING_DECAMELIZE_REGEXP = (/([a-z])([A-Z])/g); -var STRING_CAMELIZE_REGEXP = (/(\-|_|\.|\s)+(.)?/g); -var STRING_UNDERSCORE_REGEXP_1 = (/([a-z\d])([A-Z]+)/g); -var STRING_UNDERSCORE_REGEXP_2 = (/\-|\s+/g); - -/** - Defines the hash of localized strings for the current language. Used by - the `Ember.String.loc()` helper. To localize, add string values to this - hash. - - @property STRINGS - @for Ember - @type Hash -*/ -Ember.STRINGS = {}; - -/** - Defines string helper methods including string formatting and localization. - Unless `Ember.EXTEND_PROTOTYPES.String` is `false` these methods will also be - added to the `String.prototype` as well. - - @class String - @namespace Ember - @static -*/ -Ember.String = { - - /** - Apply formatting options to the string. This will look for occurrences - of "%@" in your string and substitute them with the arguments you pass into - this method. If you want to control the specific order of replacement, - you can add a number after the key as well to indicate which argument - you want to insert. - - Ordered insertions are most useful when building loc strings where values - you need to insert may appear in different orders. - - ```javascript - "Hello %@ %@".fmt('John', 'Doe'); // "Hello John Doe" - "Hello %@2, %@1".fmt('John', 'Doe'); // "Hello Doe, John" - ``` - - @method fmt - @param {String} str The string to format - @param {Array} formats An array of parameters to interpolate into string. - @return {String} formatted string - */ - fmt: function(str, formats) { - // first, replace any ORDERED replacements. - var idx = 0; // the current index for non-numerical replacements - return str.replace(/%@([0-9]+)?/g, function(s, argIndex) { - argIndex = (argIndex) ? parseInt(argIndex, 10) - 1 : idx++; - s = formats[argIndex]; - return (s === null) ? '(null)' : (s === undefined) ? '' : Ember.inspect(s); - }) ; - }, - - /** - Formats the passed string, but first looks up the string in the localized - strings hash. This is a convenient way to localize text. See - `Ember.String.fmt()` for more information on formatting. - - Note that it is traditional but not required to prefix localized string - keys with an underscore or other character so you can easily identify - localized strings. - - ```javascript - Ember.STRINGS = { - '_Hello World': 'Bonjour le monde', - '_Hello %@ %@': 'Bonjour %@ %@' - }; - - Ember.String.loc("_Hello World"); // 'Bonjour le monde'; - Ember.String.loc("_Hello %@ %@", ["John", "Smith"]); // "Bonjour John Smith"; - ``` - - @method loc - @param {String} str The string to format - @param {Array} formats Optional array of parameters to interpolate into string. - @return {String} formatted string - */ - loc: function(str, formats) { - str = Ember.STRINGS[str] || str; - return Ember.String.fmt(str, formats) ; - }, - - /** - Splits a string into separate units separated by spaces, eliminating any - empty strings in the process. This is a convenience method for split that - is mostly useful when applied to the `String.prototype`. - - ```javascript - Ember.String.w("alpha beta gamma").forEach(function(key) { - console.log(key); - }); - - // > alpha - // > beta - // > gamma - ``` - - @method w - @param {String} str The string to split - @return {String} split string - */ - w: function(str) { return str.split(/\s+/); }, - - /** - Converts a camelized string into all lower case separated by underscores. - - ```javascript - 'innerHTML'.decamelize(); // 'inner_html' - 'action_name'.decamelize(); // 'action_name' - 'css-class-name'.decamelize(); // 'css-class-name' - 'my favorite items'.decamelize(); // 'my favorite items' - ``` - - @method decamelize - @param {String} str The string to decamelize. - @return {String} the decamelized string. - */ - decamelize: function(str) { - return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); - }, - - /** - Replaces underscores or spaces with dashes. - - ```javascript - 'innerHTML'.dasherize(); // 'inner-html' - 'action_name'.dasherize(); // 'action-name' - 'css-class-name'.dasherize(); // 'css-class-name' - 'my favorite items'.dasherize(); // 'my-favorite-items' - ``` - - @method dasherize - @param {String} str The string to dasherize. - @return {String} the dasherized string. - */ - dasherize: function(str) { - var cache = STRING_DASHERIZE_CACHE, - hit = cache.hasOwnProperty(str), - ret; - - if (hit) { - return cache[str]; - } else { - ret = Ember.String.decamelize(str).replace(STRING_DASHERIZE_REGEXP,'-'); - cache[str] = ret; - } - - return ret; - }, - - /** - Returns the lowerCamelCase form of a string. - - ```javascript - 'innerHTML'.camelize(); // 'innerHTML' - 'action_name'.camelize(); // 'actionName' - 'css-class-name'.camelize(); // 'cssClassName' - 'my favorite items'.camelize(); // 'myFavoriteItems' - 'My Favorite Items'.camelize(); // 'myFavoriteItems' - ``` - - @method camelize - @param {String} str The string to camelize. - @return {String} the camelized string. - */ - camelize: function(str) { - return str.replace(STRING_CAMELIZE_REGEXP, function(match, separator, chr) { - return chr ? chr.toUpperCase() : ''; - }).replace(/^([A-Z])/, function(match, separator, chr) { - return match.toLowerCase(); - }); - }, - - /** - Returns the UpperCamelCase form of a string. - - ```javascript - 'innerHTML'.classify(); // 'InnerHTML' - 'action_name'.classify(); // 'ActionName' - 'css-class-name'.classify(); // 'CssClassName' - 'my favorite items'.classify(); // 'MyFavoriteItems' - ``` - - @method classify - @param {String} str the string to classify - @return {String} the classified string - */ - classify: function(str) { - var parts = str.split("."), - out = []; - - for (var i=0, l=parts.length; i Ember.TrackedArray instances. We use + // this to lazily recompute indexes for item property observers. + this.trackedArraysByGuid = {}; + + // This is used to coalesce item changes from property observers. + this.changedItems = {}; +} + +function ItemPropertyObserverContext (dependentArray, index, trackedArray) { + Ember.assert("Internal error: trackedArray is null or undefined", trackedArray); + + this.dependentArray = dependentArray; + this.index = index; + this.item = dependentArray.objectAt(index); + this.trackedArray = trackedArray; + this.beforeObserver = null; + this.observer = null; + + this.destroyed = false; +} + +DependentArraysObserver.prototype = { + setValue: function (newValue) { + this.instanceMeta.setValue(newValue, true); + }, + getValue: function () { + return this.instanceMeta.getValue(); + }, + + setupObservers: function (dependentArray, dependentKey) { + Ember.assert("dependent array must be an `Ember.Array`", Ember.Array.detect(dependentArray)); + + this.dependentKeysByGuid[guidFor(dependentArray)] = dependentKey; + + dependentArray.addArrayObserver(this, { + willChange: 'dependentArrayWillChange', + didChange: 'dependentArrayDidChange' + }); + + if (this.cp._itemPropertyKeys[dependentKey]) { + this.setupPropertyObservers(dependentKey, this.cp._itemPropertyKeys[dependentKey]); + } + }, + + teardownObservers: function (dependentArray, dependentKey) { + var itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || []; + + delete this.dependentKeysByGuid[guidFor(dependentArray)]; + + this.teardownPropertyObservers(dependentKey, itemPropertyKeys); + + dependentArray.removeArrayObserver(this, { + willChange: 'dependentArrayWillChange', + didChange: 'dependentArrayDidChange' + }); + }, + + setupPropertyObservers: function (dependentKey, itemPropertyKeys) { + var dependentArray = get(this.instanceMeta.context, dependentKey), + length = get(dependentArray, 'length'), + observerContexts = new Array(length); + + this.resetTransformations(dependentKey, observerContexts); + + forEach(dependentArray, function (item, index) { + var observerContext = this.createPropertyObserverContext(dependentArray, index, this.trackedArraysByGuid[dependentKey]); + observerContexts[index] = observerContext; + + forEach(itemPropertyKeys, function (propertyKey) { + addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); + addObserver(item, propertyKey, this, observerContext.observer); + }, this); + }, this); + }, + + teardownPropertyObservers: function (dependentKey, itemPropertyKeys) { + var dependentArrayObserver = this, + trackedArray = this.trackedArraysByGuid[dependentKey], + beforeObserver, + observer, + item; + + if (!trackedArray) { return; } + + trackedArray.apply(function (observerContexts, offset, operation) { + if (operation === Ember.TrackedArray.DELETE) { return; } + + forEach(observerContexts, function (observerContext) { + observerContext.destroyed = true; + beforeObserver = observerContext.beforeObserver; + observer = observerContext.observer; + item = observerContext.item; + + forEach(itemPropertyKeys, function (propertyKey) { + removeBeforeObserver(item, propertyKey, dependentArrayObserver, beforeObserver); + removeObserver(item, propertyKey, dependentArrayObserver, observer); + }); + }); + }); + }, + + createPropertyObserverContext: function (dependentArray, index, trackedArray) { + var observerContext = new ItemPropertyObserverContext(dependentArray, index, trackedArray); + + this.createPropertyObserver(observerContext); + + return observerContext; + }, + + createPropertyObserver: function (observerContext) { + var dependentArrayObserver = this; + + observerContext.beforeObserver = function (obj, keyName) { + return dependentArrayObserver.itemPropertyWillChange(obj, keyName, observerContext.dependentArray, observerContext); + }; + observerContext.observer = function (obj, keyName) { + return dependentArrayObserver.itemPropertyDidChange(obj, keyName, observerContext.dependentArray, observerContext); + }; + }, + + resetTransformations: function (dependentKey, observerContexts) { + this.trackedArraysByGuid[dependentKey] = new Ember.TrackedArray(observerContexts); + }, + + trackAdd: function (dependentKey, index, newItems) { + var trackedArray = this.trackedArraysByGuid[dependentKey]; + if (trackedArray) { + trackedArray.addItems(index, newItems); + } + }, + + trackRemove: function (dependentKey, index, removedCount) { + var trackedArray = this.trackedArraysByGuid[dependentKey]; + + if (trackedArray) { + return trackedArray.removeItems(index, removedCount); + } + + return []; + }, + + updateIndexes: function (trackedArray, array) { + var length = get(array, 'length'); + // OPTIMIZE: we could stop updating once we hit the object whose observer + // fired; ie partially apply the transformations + trackedArray.apply(function (observerContexts, offset, operation) { + // we don't even have observer contexts for removed items, even if we did, + // they no longer have any index in the array + if (operation === Ember.TrackedArray.DELETE) { return; } + if (operation === Ember.TrackedArray.RETAIN && observerContexts.length === length && offset === 0) { + // If we update many items we don't want to walk the array each time: we + // only need to update the indexes at most once per run loop. + return; + } + + forEach(observerContexts, function (context, index) { + context.index = index + offset; + }); + }); + }, + + dependentArrayWillChange: function (dependentArray, index, removedCount, addedCount) { + var removedItem = this.callbacks.removedItem, + changeMeta, + guid = guidFor(dependentArray), + dependentKey = this.dependentKeysByGuid[guid], + itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || [], + item, + itemIndex, + sliceIndex, + observerContexts; + + observerContexts = this.trackRemove(dependentKey, index, removedCount); + + function removeObservers(propertyKey) { + observerContexts[sliceIndex].destroyed = true; + removeBeforeObserver(item, propertyKey, this, observerContexts[sliceIndex].beforeObserver); + removeObserver(item, propertyKey, this, observerContexts[sliceIndex].observer); + } + + for (sliceIndex = removedCount - 1; sliceIndex >= 0; --sliceIndex) { + itemIndex = index + sliceIndex; + item = dependentArray.objectAt(itemIndex); + + forEach(itemPropertyKeys, removeObservers, this); + + changeMeta = createChangeMeta(dependentArray, item, itemIndex, this.instanceMeta.propertyName, this.cp); + this.setValue( removedItem.call( + this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); + } + }, + + dependentArrayDidChange: function (dependentArray, index, removedCount, addedCount) { + var addedItem = this.callbacks.addedItem, + guid = guidFor(dependentArray), + dependentKey = this.dependentKeysByGuid[guid], + observerContexts = new Array(addedCount), + itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey], + changeMeta, + observerContext; + + forEach(dependentArray.slice(index, index + addedCount), function (item, sliceIndex) { + if (itemPropertyKeys) { + observerContext = + observerContexts[sliceIndex] = + this.createPropertyObserverContext(dependentArray, index + sliceIndex, this.trackedArraysByGuid[dependentKey]); + forEach(itemPropertyKeys, function (propertyKey) { + addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); + addObserver(item, propertyKey, this, observerContext.observer); + }, this); + } + + changeMeta = createChangeMeta(dependentArray, item, index + sliceIndex, this.instanceMeta.propertyName, this.cp); + this.setValue( addedItem.call( + this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); + }, this); + + this.trackAdd(dependentKey, index, observerContexts); + }, + + itemPropertyWillChange: function (obj, keyName, array, observerContext) { + var guid = guidFor(obj); + + if (!this.changedItems[guid]) { + this.changedItems[guid] = { + array: array, + observerContext: observerContext, + obj: obj, + previousValues: {} + }; + } + + this.changedItems[guid].previousValues[keyName] = get(obj, keyName); + }, + + itemPropertyDidChange: function(obj, keyName, array, observerContext) { + this.flushChanges(); + }, + + flushChanges: function() { + var changedItems = this.changedItems, key, c, changeMeta; + + for (key in changedItems) { + c = changedItems[key]; + if (c.observerContext.destroyed) { continue; } + + this.updateIndexes(c.observerContext.trackedArray, c.observerContext.dependentArray); + + changeMeta = createChangeMeta(c.array, c.obj, c.observerContext.index, this.instanceMeta.propertyName, this.cp, c.previousValues); + this.setValue( + this.callbacks.removedItem.call(this.instanceMeta.context, this.getValue(), c.obj, changeMeta, this.instanceMeta.sugarMeta)); + this.setValue( + this.callbacks.addedItem.call(this.instanceMeta.context, this.getValue(), c.obj, changeMeta, this.instanceMeta.sugarMeta)); + } + this.changedItems = {}; + } +}; + +function createChangeMeta(dependentArray, item, index, propertyName, property, previousValues) { + var meta = { + arrayChanged: dependentArray, + index: index, + item: item, + propertyName: propertyName, + property: property + }; + + if (previousValues) { + // previous values only available for item property changes + meta.previousValues = previousValues; + } + + return meta; +} + +function addItems (dependentArray, callbacks, cp, propertyName, meta) { + forEach(dependentArray, function (item, index) { + meta.setValue( callbacks.addedItem.call( + this, meta.getValue(), item, createChangeMeta(dependentArray, item, index, propertyName, cp), meta.sugarMeta)); + }, this); +} + +function reset(cp, propertyName) { + var callbacks = cp._callbacks(), + meta; + + if (cp._hasInstanceMeta(this, propertyName)) { + meta = cp._instanceMeta(this, propertyName); + meta.setValue(cp.resetValue(meta.getValue())); + } else { + meta = cp._instanceMeta(this, propertyName); + } + + if (cp.options.initialize) { + cp.options.initialize.call(this, meta.getValue(), { property: cp, propertyName: propertyName }, meta.sugarMeta); + } +} + +function ReduceComputedPropertyInstanceMeta(context, propertyName, initialValue) { + this.context = context; + this.propertyName = propertyName; + this.cache = metaFor(context).cache; + + this.dependentArrays = {}; + this.sugarMeta = {}; + + this.initialValue = initialValue; +} + +ReduceComputedPropertyInstanceMeta.prototype = { + getValue: function () { + if (this.propertyName in this.cache) { + return this.cache[this.propertyName]; + } else { + return this.initialValue; + } + }, + + setValue: function(newValue, triggerObservers) { + // This lets sugars force a recomputation, handy for very simple + // implementations of eg max. + if (newValue !== undefined) { + var fireObservers = triggerObservers && (newValue !== this.cache[this.propertyName]); + + if (fireObservers) { + propertyWillChange(this.context, this.propertyName); + } + + this.cache[this.propertyName] = newValue; + + if (fireObservers) { + propertyDidChange(this.context, this.propertyName); + } + } else { + delete this.cache[this.propertyName]; + } + } +}; + +/** + A computed property whose dependent keys are arrays and which is updated with + "one at a time" semantics. + + @class ReduceComputedProperty + @namespace Ember + @extends Ember.ComputedProperty + @constructor +*/ +function ReduceComputedProperty(options) { + var cp = this; + + this.options = options; + this._instanceMetas = {}; + + this._dependentKeys = null; + // A map of dependentKey -> [itemProperty, ...] that tracks what properties of + // items in the array we must track to update this property. + this._itemPropertyKeys = {}; + this._previousItemPropertyKeys = {}; + + this.readOnly(); + this.cacheable(); + + this.recomputeOnce = function(propertyName) { + // What we really want to do is coalesce by . + // We need a form of `scheduleOnce` that accepts an arbitrary token to + // coalesce by, in addition to the target and method. + Ember.run.once(this, recompute, propertyName); + }; + var recompute = function(propertyName) { + var dependentKeys = cp._dependentKeys, + meta = cp._instanceMeta(this, propertyName), + callbacks = cp._callbacks(); + + reset.call(this, cp, propertyName); + + forEach(cp._dependentKeys, function (dependentKey) { + var dependentArray = get(this, dependentKey), + previousDependentArray = meta.dependentArrays[dependentKey]; + + if (dependentArray === previousDependentArray) { + // The array may be the same, but our item property keys may have + // changed, so we set them up again. We can't easily tell if they've + // changed: the array may be the same object, but with different + // contents. + if (cp._previousItemPropertyKeys[dependentKey]) { + delete cp._previousItemPropertyKeys[dependentKey]; + meta.dependentArraysObserver.setupPropertyObservers(dependentKey, cp._itemPropertyKeys[dependentKey]); + } + } else { + meta.dependentArrays[dependentKey] = dependentArray; + + if (previousDependentArray) { + meta.dependentArraysObserver.teardownObservers(previousDependentArray, dependentKey); + } + + if (dependentArray) { + meta.dependentArraysObserver.setupObservers(dependentArray, dependentKey); + } + } + }, this); + + forEach(cp._dependentKeys, function(dependentKey) { + var dependentArray = get(this, dependentKey); + if (dependentArray) { + addItems.call(this, dependentArray, callbacks, cp, propertyName, meta); + } + }, this); + }; + + this.func = function (propertyName) { + Ember.assert("Computed reduce values require at least one dependent key", cp._dependentKeys); + + recompute.call(this, propertyName); + + return cp._instanceMeta(this, propertyName).getValue(); + }; +} + +Ember.ReduceComputedProperty = ReduceComputedProperty; +ReduceComputedProperty.prototype = o_create(ComputedProperty.prototype); + +function defaultCallback(computedValue) { + return computedValue; +} + +ReduceComputedProperty.prototype._callbacks = function () { + if (!this.callbacks) { + var options = this.options; + this.callbacks = { + removedItem: options.removedItem || defaultCallback, + addedItem: options.addedItem || defaultCallback + }; + } + return this.callbacks; +}; + +ReduceComputedProperty.prototype._hasInstanceMeta = function (context, propertyName) { + var guid = guidFor(context), + key = guid + ':' + propertyName; + + return !!this._instanceMetas[key]; +}; + +ReduceComputedProperty.prototype._instanceMeta = function (context, propertyName) { + var guid = guidFor(context), + key = guid + ':' + propertyName, + meta = this._instanceMetas[key]; + + if (!meta) { + meta = this._instanceMetas[key] = new ReduceComputedPropertyInstanceMeta(context, propertyName, this.initialValue()); + meta.dependentArraysObserver = new DependentArraysObserver(this._callbacks(), this, meta, context, propertyName, meta.sugarMeta); + } + + return meta; +}; + +ReduceComputedProperty.prototype.initialValue = function () { + if (typeof this.options.initialValue === 'function') { + return this.options.initialValue(); + } + else { + return this.options.initialValue; + } +}; + +ReduceComputedProperty.prototype.resetValue = function (value) { + return this.initialValue(); +}; + +ReduceComputedProperty.prototype.itemPropertyKey = function (dependentArrayKey, itemPropertyKey) { + this._itemPropertyKeys[dependentArrayKey] = this._itemPropertyKeys[dependentArrayKey] || []; + this._itemPropertyKeys[dependentArrayKey].push(itemPropertyKey); +}; + +ReduceComputedProperty.prototype.clearItemPropertyKeys = function (dependentArrayKey) { + if (this._itemPropertyKeys[dependentArrayKey]) { + this._previousItemPropertyKeys[dependentArrayKey] = this._itemPropertyKeys[dependentArrayKey]; + this._itemPropertyKeys[dependentArrayKey] = []; + } +}; + +ReduceComputedProperty.prototype.property = function () { + var cp = this, + args = a_slice.call(arguments), + propertyArgs = new Ember.Set(), + match, + dependentArrayKey, + itemPropertyKey; + + forEach(a_slice.call(arguments), function (dependentKey) { + if (doubleEachPropertyPattern.test(dependentKey)) { + throw new Ember.Error("Nested @each properties not supported: " + dependentKey); + } else if (match = eachPropertyPattern.exec(dependentKey)) { + dependentArrayKey = match[1]; + itemPropertyKey = match[2]; + cp.itemPropertyKey(dependentArrayKey, itemPropertyKey); + propertyArgs.add(dependentArrayKey); + } else { + propertyArgs.add(dependentKey); + } + }); + + return ComputedProperty.prototype.property.apply(this, propertyArgs.toArray()); +}; + +/** + Creates a computed property which operates on dependent arrays and + is updated with "one at a time" semantics. When items are added or + removed from the dependent array(s) a reduce computed only operates + on the change instead of re-evaluating the entire array. + + If there are more than one arguments the first arguments are + considered to be dependent property keys. The last argument is + required to be an options object. The options object can have the + following four properties: + + `initialValue` - A value or function that will be used as the initial + value for the computed. If this property is a function the result of calling + the function will be used as the initial value. This property is required. + + `initialize` - An optional initialize function. Typically this will be used + to set up state on the instanceMeta object. + + `removedItem` - A function that is called each time an element is removed + from the array. + + `addedItem` - A function that is called each time an element is added to + the array. + + + The `initialize` function has the following signature: + + ```javascript + function (initialValue, changeMeta, instanceMeta) + ``` + + `initialValue` - The value of the `initialValue` property from the + options object. + + `changeMeta` - An object which contains meta information about the + computed. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + + The `removedItem` and `addedItem` functions both have the following signature: + + ```javascript + function (accumulatedValue, item, changeMeta, instanceMeta) + ``` + + `accumulatedValue` - The value returned from the last time + `removedItem` or `addedItem` was called or `initialValue`. + + `item` - the element added or removed from the array + + `changeMeta` - An object which contains meta information about the + change. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + - `index` the index of the added or removed item + - `item` the added or removed item: this is exactly the same as + the second arg + - `arrayChanged` the array that triggered the change. Can be + useful when depending on multiple arrays. + + For property changes triggered on an item property change (when + depKey is something like `someArray.@each.someProperty`), + `changeMeta` will also contain the following property: + + - `previousValues` an object whose keys are the properties that changed on + the item, and whose values are the item's previous values. + + `previousValues` is important Ember coalesces item property changes via + Ember.run.once. This means that by the time removedItem gets called, item has + the new values, but you may need the previous value (eg for sorting & + filtering). + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + The `removedItem` and `addedItem` functions should return the accumulated + value. It is acceptable to not return anything (ie return undefined) + to invalidate the computation. This is generally not a good idea for + arrayComputed but it's used in eg max and min. + + Note that observers will be fired if either of these functions return a value + that differs from the accumulated value. When returning an object that + mutates in response to array changes, for example an array that maps + everything from some other array (see `Ember.computed.map`), it is usually + important that the *same* array be returned to avoid accidentally triggering observers. + + Example + + ```javascript + Ember.computed.max = function (dependentKey) { + return Ember.reduceComputed.call(null, dependentKey, { + initialValue: -Infinity, + + addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + return Math.max(accumulatedValue, item); + }, + + removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + if (item < accumulatedValue) { + return accumulatedValue; + } + } + }); + }; + ``` + + @method reduceComputed + @for Ember + @param {String} [dependentKeys*] + @param {Object} options + @return {Ember.ComputedProperty} +*/ +Ember.reduceComputed = function (options) { + var args; + + if (arguments.length > 1) { + args = a_slice.call(arguments, 0, -1); + options = a_slice.call(arguments, -1)[0]; + } + + if (typeof options !== "object") { + throw new Ember.Error("Reduce Computed Property declared without an options hash"); + } + + if (!('initialValue' in options)) { + throw new Ember.Error("Reduce Computed Property declared without an initial value"); + } + + var cp = new ReduceComputedProperty(options); + + if (args) { + cp.property.apply(cp, args); + } + + return cp; +}; + +})(); + + + +(function() { +var ReduceComputedProperty = Ember.ReduceComputedProperty, + a_slice = [].slice, + o_create = Ember.create, + forEach = Ember.EnumerableUtils.forEach; + +function ArrayComputedProperty() { + var cp = this; + + ReduceComputedProperty.apply(this, arguments); + + this.func = (function(reduceFunc) { + return function (propertyName) { + if (!cp._hasInstanceMeta(this, propertyName)) { + // When we recompute an array computed property, we need already + // retrieved arrays to be updated; we can't simply empty the cache and + // hope the array is re-retrieved. + forEach(cp._dependentKeys, function(dependentKey) { + Ember.addObserver(this, dependentKey, function() { + cp.recomputeOnce.call(this, propertyName); + }); + }, this); + } + + return reduceFunc.apply(this, arguments); + }; + })(this.func); + + return this; +} +Ember.ArrayComputedProperty = ArrayComputedProperty; +ArrayComputedProperty.prototype = o_create(ReduceComputedProperty.prototype); +ArrayComputedProperty.prototype.initialValue = function () { + return Ember.A(); +}; +ArrayComputedProperty.prototype.resetValue = function (array) { + array.clear(); + return array; +}; + +/** + Creates a computed property which operates on dependent arrays and + is updated with "one at a time" semantics. When items are added or + removed from the dependent array(s) an array computed only operates + on the change instead of re-evaluating the entire array. This should + return an array, if you'd like to use "one at a time" semantics and + compute some value other then an array look at + `Ember.reduceComputed`. + + If there are more than one arguments the first arguments are + considered to be dependent property keys. The last argument is + required to be an options object. The options object can have the + following three properties. + + `initialize` - An optional initialize function. Typically this will be used + to set up state on the instanceMeta object. + + `removedItem` - A function that is called each time an element is + removed from the array. + + `addedItem` - A function that is called each time an element is + added to the array. + + + The `initialize` function has the following signature: + + ```javascript + function (array, changeMeta, instanceMeta) + ``` + + `array` - The initial value of the arrayComputed, an empty array. + + `changeMeta` - An object which contains meta information about the + computed. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + + The `removedItem` and `addedItem` functions both have the following signature: + + ```javascript + function (accumulatedValue, item, changeMeta, instanceMeta) + ``` + + `accumulatedValue` - The value returned from the last time + `removedItem` or `addedItem` was called or an empty array. + + `item` - the element added or removed from the array + + `changeMeta` - An object which contains meta information about the + change. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + - `index` the index of the added or removed item + - `item` the added or removed item: this is exactly the same as + the second arg + - `arrayChanged` the array that triggered the change. Can be + useful when depending on multiple arrays. + + For property changes triggered on an item property change (when + depKey is something like `someArray.@each.someProperty`), + `changeMeta` will also contain the following property: + + - `previousValues` an object whose keys are the properties that changed on + the item, and whose values are the item's previous values. + + `previousValues` is important Ember coalesces item property changes via + Ember.run.once. This means that by the time removedItem gets called, item has + the new values, but you may need the previous value (eg for sorting & + filtering). + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + The `removedItem` and `addedItem` functions should return the accumulated + value. It is acceptable to not return anything (ie return undefined) + to invalidate the computation. This is generally not a good idea for + arrayComputed but it's used in eg max and min. + + Example + + ```javascript + Ember.computed.map = function(dependentKey, callback) { + var options = { + addedItem: function(array, item, changeMeta, instanceMeta) { + var mapped = callback(item); + array.insertAt(changeMeta.index, mapped); + return array; + }, + removedItem: function(array, item, changeMeta, instanceMeta) { + array.removeAt(changeMeta.index, 1); + return array; + } + }; + + return Ember.arrayComputed(dependentKey, options); + }; + ``` + + @method arrayComputed + @for Ember + @param {String} [dependentKeys*] + @param {Object} options + @return {Ember.ComputedProperty} +*/ +Ember.arrayComputed = function (options) { + var args; + + if (arguments.length > 1) { + args = a_slice.call(arguments, 0, -1); + options = a_slice.call(arguments, -1)[0]; + } + + if (typeof options !== "object") { + throw new Ember.Error("Array Computed Property declared without an options hash"); + } + + var cp = new ArrayComputedProperty(options); + + if (args) { + cp.property.apply(cp, args); + } + + return cp; +}; + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ + +var get = Ember.get, + set = Ember.set, + guidFor = Ember.guidFor, + merge = Ember.merge, + a_slice = [].slice, + forEach = Ember.EnumerableUtils.forEach, + map = Ember.EnumerableUtils.map; + +/** + A computed property that calculates the maximum value in the + dependent array. This will return `-Infinity` when the dependent + array is empty. + + Example + + ```javascript + App.Person = Ember.Object.extend({ + childAges: Ember.computed.mapBy('children', 'age'), + maxChildAge: Ember.computed.max('childAges') + }); + + var lordByron = App.Person.create({children: []}); + lordByron.get('maxChildAge'); // -Infinity + lordByron.get('children').pushObject({name: 'Augusta Ada Byron', age: 7}); + lordByron.get('maxChildAge'); // 7 + lordByron.get('children').pushObjects([{name: 'Allegra Byron', age: 5}, {name: 'Elizabeth Medora Leigh', age: 8}]); + lordByron.get('maxChildAge'); // 8 + ``` + + @method computed.max + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computes the largest value in the dependentKey's array +*/ +Ember.computed.max = function (dependentKey) { + return Ember.reduceComputed.call(null, dependentKey, { + initialValue: -Infinity, + + addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + return Math.max(accumulatedValue, item); + }, + + removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + if (item < accumulatedValue) { + return accumulatedValue; + } + } + }); +}; + +/** + A computed property that calculates the minimum value in the + dependent array. This will return `Infinity` when the dependent + array is empty. + + Example + + ```javascript + App.Person = Ember.Object.extend({ + childAges: Ember.computed.mapBy('children', 'age'), + minChildAge: Ember.computed.min('childAges') + }); + + var lordByron = App.Person.create({children: []}); + lordByron.get('minChildAge'); // Infinity + lordByron.get('children').pushObject({name: 'Augusta Ada Byron', age: 7}); + lordByron.get('minChildAge'); // 7 + lordByron.get('children').pushObjects([{name: 'Allegra Byron', age: 5}, {name: 'Elizabeth Medora Leigh', age: 8}]); + lordByron.get('minChildAge'); // 5 + ``` + + @method computed.min + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computes the smallest value in the dependentKey's array +*/ +Ember.computed.min = function (dependentKey) { + return Ember.reduceComputed.call(null, dependentKey, { + initialValue: Infinity, + + addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + return Math.min(accumulatedValue, item); + }, + + removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + if (item > accumulatedValue) { + return accumulatedValue; + } + } + }); +}; + +/** + Returns an array mapped via the callback + + The callback method you provide should have the following signature: + + ```javascript + function(item); + ``` + + - `item` is the current item in the iteration. + + Example + + ```javascript + App.Hampster = Ember.Object.extend({ + excitingChores: Ember.computed.map('chores', function(chore) { + return chore.toUpperCase() + '!'; + }) + }); + + var hampster = App.Hampster.create({chores: ['cook', 'clean', 'write more unit tests']}); + hampster.get('excitingChores'); // ['COOK!', 'CLEAN!', 'WRITE MORE UNIT TESTS!'] + ``` + + @method computed.map + @for Ember + @param {String} dependentKey + @param {Function} callback + @return {Ember.ComputedProperty} an array mapped via the callback +*/ +Ember.computed.map = function(dependentKey, callback) { + var options = { + addedItem: function(array, item, changeMeta, instanceMeta) { + var mapped = callback.call(this, item); + array.insertAt(changeMeta.index, mapped); + return array; + }, + removedItem: function(array, item, changeMeta, instanceMeta) { + array.removeAt(changeMeta.index, 1); + return array; + } + }; + + return Ember.arrayComputed(dependentKey, options); +}; + +/** + Returns an array mapped to the specified key. + + Example + + ```javascript + App.Person = Ember.Object.extend({ + childAges: Ember.computed.mapBy('children', 'age') + }); + + var lordByron = App.Person.create({children: []}); + lordByron.get('childAges'); // [] + lordByron.get('children').pushObject({name: 'Augusta Ada Byron', age: 7}); + lordByron.get('childAges'); // [7] + lordByron.get('children').pushObjects([{name: 'Allegra Byron', age: 5}, {name: 'Elizabeth Medora Leigh', age: 8}]); + lordByron.get('childAges'); // [7, 5, 8] + ``` + + @method computed.mapBy + @for Ember + @param {String} dependentKey + @param {String} propertyKey + @return {Ember.ComputedProperty} an array mapped to the specified key +*/ +Ember.computed.mapBy = function(dependentKey, propertyKey) { + var callback = function(item) { return get(item, propertyKey); }; + return Ember.computed.map(dependentKey + '.@each.' + propertyKey, callback); +}; + +/** + @method computed.mapProperty + @for Ember + @deprecated Use `Ember.computed.mapBy` instead + @param dependentKey + @param propertyKey +*/ +Ember.computed.mapProperty = Ember.computed.mapBy; + +/** + Filters the array by the callback. + + The callback method you provide should have the following signature: + + ```javascript + function(item); + ``` + + - `item` is the current item in the iteration. + + Example + + ```javascript + App.Hampster = Ember.Object.extend({ + remainingChores: Ember.computed.filter('chores', function(chore) { + return !chore.done; + }) + }); + + var hampster = App.Hampster.create({chores: [ + {name: 'cook', done: true}, + {name: 'clean', done: true}, + {name: 'write more unit tests', done: false} + ]}); + hampster.get('remainingChores'); // [{name: 'write more unit tests', done: false}] + ``` + + @method computed.filter + @for Ember + @param {String} dependentKey + @param {Function} callback + @return {Ember.ComputedProperty} the filtered array +*/ +Ember.computed.filter = function(dependentKey, callback) { + var options = { + initialize: function (array, changeMeta, instanceMeta) { + instanceMeta.filteredArrayIndexes = new Ember.SubArray(); + }, + + addedItem: function(array, item, changeMeta, instanceMeta) { + var match = !!callback.call(this, item), + filterIndex = instanceMeta.filteredArrayIndexes.addItem(changeMeta.index, match); + + if (match) { + array.insertAt(filterIndex, item); + } + + return array; + }, + + removedItem: function(array, item, changeMeta, instanceMeta) { + var filterIndex = instanceMeta.filteredArrayIndexes.removeItem(changeMeta.index); + + if (filterIndex > -1) { + array.removeAt(filterIndex); + } + + return array; + } + }; + + return Ember.arrayComputed(dependentKey, options); +}; + +/** + Filters the array by the property and value + + Example + + ```javascript + App.Hampster = Ember.Object.extend({ + remainingChores: Ember.computed.filterBy('chores', 'done', false) + }); + + var hampster = App.Hampster.create({chores: [ + {name: 'cook', done: true}, + {name: 'clean', done: true}, + {name: 'write more unit tests', done: false} + ]}); + hampster.get('remainingChores'); // [{name: 'write more unit tests', done: false}] + ``` + + @method computed.filterBy + @for Ember + @param {String} dependentKey + @param {String} propertyKey + @param {String} value + @return {Ember.ComputedProperty} the filtered array +*/ +Ember.computed.filterBy = function(dependentKey, propertyKey, value) { + var callback; + + if (arguments.length === 2) { + callback = function(item) { + return get(item, propertyKey); + }; + } else { + callback = function(item) { + return get(item, propertyKey) === value; + }; + } + + return Ember.computed.filter(dependentKey + '.@each.' + propertyKey, callback); +}; + +/** + @method computed.filterProperty + @for Ember + @param dependentKey + @param propertyKey + @param value + @deprecated Use `Ember.computed.filterBy` instead +*/ +Ember.computed.filterProperty = Ember.computed.filterBy; + +/** + A computed property which returns a new array with all the unique + elements from one or more dependent arrays. + + Example + + ```javascript + App.Hampster = Ember.Object.extend({ + uniqueFruits: Ember.computed.uniq('fruits') + }); + + var hampster = App.Hampster.create({fruits: [ + 'banana', + 'grape', + 'kale', + 'banana' + ]}); + hampster.get('uniqueFruits'); // ['banana', 'grape', 'kale'] + ``` + + @method computed.uniq + @for Ember + @param {String} propertyKey* + @return {Ember.ComputedProperty} computes a new array with all the + unique elements from the dependent array +*/ +Ember.computed.uniq = function() { + var args = a_slice.call(arguments); + args.push({ + initialize: function(array, changeMeta, instanceMeta) { + instanceMeta.itemCounts = {}; + }, + + addedItem: function(array, item, changeMeta, instanceMeta) { + var guid = guidFor(item); + + if (!instanceMeta.itemCounts[guid]) { + instanceMeta.itemCounts[guid] = 1; + } else { + ++instanceMeta.itemCounts[guid]; + } + array.addObject(item); + return array; + }, + removedItem: function(array, item, _, instanceMeta) { + var guid = guidFor(item), + itemCounts = instanceMeta.itemCounts; + + if (--itemCounts[guid] === 0) { + array.removeObject(item); + } + return array; + } + }); + return Ember.arrayComputed.apply(null, args); +}; + +/** + Alias for [Ember.computed.uniq](/api/#method_computed_uniq). + + @method computed.union + @for Ember + @param {String} propertyKey* + @return {Ember.ComputedProperty} computes a new array with all the + unique elements from the dependent array +*/ +Ember.computed.union = Ember.computed.uniq; + +/** + A computed property which returns a new array with all the duplicated + elements from two or more dependeny arrays. + + Example + + ```javascript + var obj = Ember.Object.createWithMixins({ + adaFriends: ['Charles Babbage', 'John Hobhouse', 'William King', 'Mary Somerville'], + charlesFriends: ['William King', 'Mary Somerville', 'Ada Lovelace', 'George Peacock'], + friendsInCommon: Ember.computed.intersect('adaFriends', 'charlesFriends') + }); + + obj.get('friendsInCommon'); // ['William King', 'Mary Somerville'] + ``` + + @method computed.intersect + @for Ember + @param {String} propertyKey* + @return {Ember.ComputedProperty} computes a new array with all the + duplicated elements from the dependent arrays +*/ +Ember.computed.intersect = function () { + var getDependentKeyGuids = function (changeMeta) { + return map(changeMeta.property._dependentKeys, function (dependentKey) { + return guidFor(dependentKey); + }); + }; + + var args = a_slice.call(arguments); + args.push({ + initialize: function (array, changeMeta, instanceMeta) { + instanceMeta.itemCounts = {}; + }, + + addedItem: function(array, item, changeMeta, instanceMeta) { + var itemGuid = guidFor(item), + dependentGuids = getDependentKeyGuids(changeMeta), + dependentGuid = guidFor(changeMeta.arrayChanged), + numberOfDependentArrays = changeMeta.property._dependentKeys.length, + itemCounts = instanceMeta.itemCounts; + + if (!itemCounts[itemGuid]) { itemCounts[itemGuid] = {}; } + if (itemCounts[itemGuid][dependentGuid] === undefined) { itemCounts[itemGuid][dependentGuid] = 0; } + + if (++itemCounts[itemGuid][dependentGuid] === 1 && + numberOfDependentArrays === Ember.keys(itemCounts[itemGuid]).length) { + + array.addObject(item); + } + return array; + }, + removedItem: function(array, item, changeMeta, instanceMeta) { + var itemGuid = guidFor(item), + dependentGuids = getDependentKeyGuids(changeMeta), + dependentGuid = guidFor(changeMeta.arrayChanged), + numberOfDependentArrays = changeMeta.property._dependentKeys.length, + numberOfArraysItemAppearsIn, + itemCounts = instanceMeta.itemCounts; + + if (itemCounts[itemGuid][dependentGuid] === undefined) { itemCounts[itemGuid][dependentGuid] = 0; } + if (--itemCounts[itemGuid][dependentGuid] === 0) { + delete itemCounts[itemGuid][dependentGuid]; + numberOfArraysItemAppearsIn = Ember.keys(itemCounts[itemGuid]).length; + + if (numberOfArraysItemAppearsIn === 0) { + delete itemCounts[itemGuid]; + } + array.removeObject(item); + } + return array; + } + }); + return Ember.arrayComputed.apply(null, args); +}; + +/** + A computed property which returns a new array with all the + properties from the first dependent array that are not in the second + dependent array. + + Example + + ```javascript + App.Hampster = Ember.Object.extend({ + likes: ['banana', 'grape', 'kale'], + wants: Ember.computed.setDiff('likes', 'fruits') + }); + + var hampster = App.Hampster.create({fruits: [ + 'grape', + 'kale', + ]}); + hampster.get('wants'); // ['banana'] + ``` + + @method computed.setDiff + @for Ember + @param {String} setAProperty + @param {String} setBProperty + @return {Ember.ComputedProperty} computes a new array with all the + items from the first dependent array that are not in the second + dependent array +*/ +Ember.computed.setDiff = function (setAProperty, setBProperty) { + if (arguments.length !== 2) { + throw new Ember.Error("setDiff requires exactly two dependent arrays."); + } + return Ember.arrayComputed.call(null, setAProperty, setBProperty, { + addedItem: function (array, item, changeMeta, instanceMeta) { + var setA = get(this, setAProperty), + setB = get(this, setBProperty); + + if (changeMeta.arrayChanged === setA) { + if (!setB.contains(item)) { + array.addObject(item); + } + } else { + array.removeObject(item); + } + return array; + }, + + removedItem: function (array, item, changeMeta, instanceMeta) { + var setA = get(this, setAProperty), + setB = get(this, setBProperty); + + if (changeMeta.arrayChanged === setB) { + if (setA.contains(item)) { + array.addObject(item); + } + } else { + array.removeObject(item); + } + return array; + } + }); +}; + +function binarySearch(array, item, low, high) { + var mid, midItem, res, guidMid, guidItem; + + if (arguments.length < 4) { high = get(array, 'length'); } + if (arguments.length < 3) { low = 0; } + + if (low === high) { + return low; + } + + mid = low + Math.floor((high - low) / 2); + midItem = array.objectAt(mid); + + guidMid = _guidFor(midItem); + guidItem = _guidFor(item); + + if (guidMid === guidItem) { + return mid; + } + + res = this.order(midItem, item); + if (res === 0) { + res = guidMid < guidItem ? -1 : 1; + } + + + if (res < 0) { + return this.binarySearch(array, item, mid+1, high); + } else if (res > 0) { + return this.binarySearch(array, item, low, mid); + } + + return mid; + + function _guidFor(item) { + if (Ember.ObjectProxy.detectInstance(item)) { + return guidFor(get(item, 'content')); + } + return guidFor(item); + } +} + +/** + A computed property which returns a new array with all the + properties from the first dependent array sorted based on a property + or sort function. + + The callback method you provide should have the following signature: + + ```javascript + function(itemA, itemB); + ``` + + - `itemA` the first item to compare. + - `itemB` the second item to compare. + + This function should return `-1` when `itemA` should come before + `itemB`. It should return `1` when `itemA` should come after + `itemB`. If the `itemA` and `itemB` are equal this function should return `0`. + + Example + + ```javascript + var ToDoList = Ember.Object.extend({ + todosSorting: ['name'], + sortedTodos: Ember.computed.sort('todos', 'todosSorting'), + priorityTodos: Ember.computed.sort('todos', function(a, b){ + if (a.priority > b.priority) { + return 1; + } else if (a.priority < b.priority) { + return -1; + } + return 0; + }), + }); + var todoList = ToDoList.create({todos: [ + {name: 'Unit Test', priority: 2}, + {name: 'Documentation', priority: 3}, + {name: 'Release', priority: 1} + ]}); + + todoList.get('sortedTodos'); // [{name:'Documentation', priority:3}, {name:'Release', priority:1}, {name:'Unit Test', priority:2}] + todoList.get('priorityTodos'); // [{name:'Release', priority:1}, {name:'Unit Test', priority:2}, {name:'Documentation', priority:3}] + ``` + + @method computed.sort + @for Ember + @param {String} dependentKey + @param {String or Function} sortDefinition a dependent key to an + array of sort properties or a function to use when sorting + @return {Ember.ComputedProperty} computes a new sorted array based + on the sort property array or callback function +*/ +Ember.computed.sort = function (itemsKey, sortDefinition) { + Ember.assert("Ember.computed.sort requires two arguments: an array key to sort and either a sort properties key or sort function", arguments.length === 2); + + var initFn, sortPropertiesKey; + + if (typeof sortDefinition === 'function') { + initFn = function (array, changeMeta, instanceMeta) { + instanceMeta.order = sortDefinition; + instanceMeta.binarySearch = binarySearch; + }; + } else { + sortPropertiesKey = sortDefinition; + initFn = function (array, changeMeta, instanceMeta) { + function setupSortProperties() { + var sortPropertyDefinitions = get(this, sortPropertiesKey), + sortProperty, + sortProperties = instanceMeta.sortProperties = [], + sortPropertyAscending = instanceMeta.sortPropertyAscending = {}, + idx, + asc; + + Ember.assert("Cannot sort: '" + sortPropertiesKey + "' is not an array.", Ember.isArray(sortPropertyDefinitions)); + + changeMeta.property.clearItemPropertyKeys(itemsKey); + + forEach(sortPropertyDefinitions, function (sortPropertyDefinition) { + if ((idx = sortPropertyDefinition.indexOf(':')) !== -1) { + sortProperty = sortPropertyDefinition.substring(0, idx); + asc = sortPropertyDefinition.substring(idx+1).toLowerCase() !== 'desc'; + } else { + sortProperty = sortPropertyDefinition; + asc = true; + } + + sortProperties.push(sortProperty); + sortPropertyAscending[sortProperty] = asc; + changeMeta.property.itemPropertyKey(itemsKey, sortProperty); + }); + + sortPropertyDefinitions.addObserver('@each', this, updateSortPropertiesOnce); + } + + function updateSortPropertiesOnce() { + Ember.run.once(this, updateSortProperties, changeMeta.propertyName); + } + + function updateSortProperties(propertyName) { + setupSortProperties.call(this); + changeMeta.property.recomputeOnce.call(this, propertyName); + } + + Ember.addObserver(this, sortPropertiesKey, updateSortPropertiesOnce); + + setupSortProperties.call(this); + + + instanceMeta.order = function (itemA, itemB) { + var sortProperty, result, asc; + for (var i = 0; i < this.sortProperties.length; ++i) { + sortProperty = this.sortProperties[i]; + result = Ember.compare(get(itemA, sortProperty), get(itemB, sortProperty)); + + if (result !== 0) { + asc = this.sortPropertyAscending[sortProperty]; + return asc ? result : (-1 * result); + } + } + + return 0; + }; + + instanceMeta.binarySearch = binarySearch; + }; + } + + return Ember.arrayComputed.call(null, itemsKey, { + initialize: initFn, + + addedItem: function (array, item, changeMeta, instanceMeta) { + var index = instanceMeta.binarySearch(array, item); + array.insertAt(index, item); + return array; + }, + + removedItem: function (array, item, changeMeta, instanceMeta) { + var proxyProperties, index, searchItem; + + if (changeMeta.previousValues) { + proxyProperties = merge({ content: item }, changeMeta.previousValues); + + searchItem = Ember.ObjectProxy.create(proxyProperties); + } else { + searchItem = item; + } + + index = instanceMeta.binarySearch(array, searchItem); + array.removeAt(index); + return array; + } + }); +}; + +})(); + + + +(function() { +/** + Expose RSVP implementation + + Documentation can be found here: https://github.com/tildeio/rsvp.js/blob/master/README.md + + @class RSVP + @namespace Ember + @constructor +*/ +Ember.RSVP = requireModule('rsvp'); + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ + +var STRING_DASHERIZE_REGEXP = (/[ _]/g); +var STRING_DASHERIZE_CACHE = {}; +var STRING_DECAMELIZE_REGEXP = (/([a-z\d])([A-Z])/g); +var STRING_CAMELIZE_REGEXP = (/(\-|_|\.|\s)+(.)?/g); +var STRING_UNDERSCORE_REGEXP_1 = (/([a-z\d])([A-Z]+)/g); +var STRING_UNDERSCORE_REGEXP_2 = (/\-|\s+/g); + +/** + Defines the hash of localized strings for the current language. Used by + the `Ember.String.loc()` helper. To localize, add string values to this + hash. + + @property STRINGS + @for Ember + @type Hash +*/ +Ember.STRINGS = {}; + +/** + Defines string helper methods including string formatting and localization. + Unless `Ember.EXTEND_PROTOTYPES.String` is `false` these methods will also be + added to the `String.prototype` as well. + + @class String + @namespace Ember + @static +*/ +Ember.String = { + + /** + Apply formatting options to the string. This will look for occurrences + of "%@" in your string and substitute them with the arguments you pass into + this method. If you want to control the specific order of replacement, + you can add a number after the key as well to indicate which argument + you want to insert. + + Ordered insertions are most useful when building loc strings where values + you need to insert may appear in different orders. + + ```javascript + "Hello %@ %@".fmt('John', 'Doe'); // "Hello John Doe" + "Hello %@2, %@1".fmt('John', 'Doe'); // "Hello Doe, John" + ``` + + @method fmt + @param {String} str The string to format + @param {Array} formats An array of parameters to interpolate into string. + @return {String} formatted string + */ + fmt: function(str, formats) { + // first, replace any ORDERED replacements. + var idx = 0; // the current index for non-numerical replacements + return str.replace(/%@([0-9]+)?/g, function(s, argIndex) { + argIndex = (argIndex) ? parseInt(argIndex, 10) - 1 : idx++; + s = formats[argIndex]; + return (s === null) ? '(null)' : (s === undefined) ? '' : Ember.inspect(s); + }) ; + }, + + /** + Formats the passed string, but first looks up the string in the localized + strings hash. This is a convenient way to localize text. See + `Ember.String.fmt()` for more information on formatting. + + Note that it is traditional but not required to prefix localized string + keys with an underscore or other character so you can easily identify + localized strings. + + ```javascript + Ember.STRINGS = { + '_Hello World': 'Bonjour le monde', + '_Hello %@ %@': 'Bonjour %@ %@' + }; + + Ember.String.loc("_Hello World"); // 'Bonjour le monde'; + Ember.String.loc("_Hello %@ %@", ["John", "Smith"]); // "Bonjour John Smith"; + ``` + + @method loc + @param {String} str The string to format + @param {Array} formats Optional array of parameters to interpolate into string. + @return {String} formatted string + */ + loc: function(str, formats) { + str = Ember.STRINGS[str] || str; + return Ember.String.fmt(str, formats) ; + }, + + /** + Splits a string into separate units separated by spaces, eliminating any + empty strings in the process. This is a convenience method for split that + is mostly useful when applied to the `String.prototype`. + + ```javascript + Ember.String.w("alpha beta gamma").forEach(function(key) { + console.log(key); + }); + + // > alpha + // > beta + // > gamma + ``` + + @method w + @param {String} str The string to split + @return {String} split string + */ + w: function(str) { return str.split(/\s+/); }, + + /** + Converts a camelized string into all lower case separated by underscores. + + ```javascript + 'innerHTML'.decamelize(); // 'inner_html' + 'action_name'.decamelize(); // 'action_name' + 'css-class-name'.decamelize(); // 'css-class-name' + 'my favorite items'.decamelize(); // 'my favorite items' + ``` + + @method decamelize + @param {String} str The string to decamelize. + @return {String} the decamelized string. + */ + decamelize: function(str) { + return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); + }, + + /** + Replaces underscores, spaces, or camelCase with dashes. + + ```javascript + 'innerHTML'.dasherize(); // 'inner-html' + 'action_name'.dasherize(); // 'action-name' + 'css-class-name'.dasherize(); // 'css-class-name' + 'my favorite items'.dasherize(); // 'my-favorite-items' + ``` + + @method dasherize + @param {String} str The string to dasherize. + @return {String} the dasherized string. + */ + dasherize: function(str) { + var cache = STRING_DASHERIZE_CACHE, + hit = cache.hasOwnProperty(str), + ret; + + if (hit) { + return cache[str]; + } else { + ret = Ember.String.decamelize(str).replace(STRING_DASHERIZE_REGEXP,'-'); + cache[str] = ret; + } + + return ret; + }, + + /** + Returns the lowerCamelCase form of a string. + + ```javascript + 'innerHTML'.camelize(); // 'innerHTML' + 'action_name'.camelize(); // 'actionName' + 'css-class-name'.camelize(); // 'cssClassName' + 'my favorite items'.camelize(); // 'myFavoriteItems' + 'My Favorite Items'.camelize(); // 'myFavoriteItems' + ``` + + @method camelize + @param {String} str The string to camelize. + @return {String} the camelized string. + */ + camelize: function(str) { + return str.replace(STRING_CAMELIZE_REGEXP, function(match, separator, chr) { + return chr ? chr.toUpperCase() : ''; + }).replace(/^([A-Z])/, function(match, separator, chr) { + return match.toLowerCase(); + }); + }, + + /** + Returns the UpperCamelCase form of a string. + + ```javascript + 'innerHTML'.classify(); // 'InnerHTML' + 'action_name'.classify(); // 'ActionName' + 'css-class-name'.classify(); // 'CssClassName' + 'my favorite items'.classify(); // 'MyFavoriteItems' + ``` + + @method classify + @param {String} str the string to classify + @return {String} the classified string + */ + classify: function(str) { + var parts = str.split("."), + out = []; + + for (var i=0, l=parts.length; i get(this, 'length')) throw new Error(OUT_OF_RANGE_EXCEPTION) ; + if (idx > get(this, 'length')) throw new Ember.Error(OUT_OF_RANGE_EXCEPTION) ; this.replace(idx, 0, [object]) ; return this ; }, @@ -11060,7 +13586,7 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable,/** if ('number' === typeof start) { if ((start < 0) || (start >= get(this, 'length'))) { - throw new Error(OUT_OF_RANGE_EXCEPTION); + throw new Ember.Error(OUT_OF_RANGE_EXCEPTION); } // fast case @@ -11260,7 +13786,10 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable,/** @submodule ember-runtime */ -var get = Ember.get, set = Ember.set; +var get = Ember.get, + set = Ember.set, + slice = Array.prototype.slice, + getProperties = Ember.getProperties; /** ## Overview @@ -11390,15 +13919,7 @@ Ember.Observable = Ember.Mixin.create({ @return {Hash} */ getProperties: function() { - var ret = {}; - var propertyNames = arguments; - if (arguments.length === 1 && Ember.typeOf(arguments[0]) === 'array') { - propertyNames = arguments[0]; - } - for(var i = 0; i < propertyNames.length; i++) { - ret[propertyNames[i]] = get(this, propertyNames[i]); - } - return ret; + return getProperties.apply(null, [this].concat(slice.call(arguments))); }, /** @@ -11406,7 +13927,7 @@ Ember.Observable = Ember.Mixin.create({ This method is generally very similar to calling `object[key] = value` or `object.key = value`, except that it provides support for computed - properties, the `unknownProperty()` method and property observers. + properties, the `setUnknownProperty()` method and property observers. ### Computed Properties @@ -11420,9 +13941,9 @@ Ember.Observable = Ember.Mixin.create({ ### Unknown Properties If you try to set a value on a key that is undefined in the target - object, then the `unknownProperty()` handler will be called instead. This + object, then the `setUnknownProperty()` handler will be called instead. This gives you an opportunity to implement complex "virtual" properties that - are not predefined on the object. If `unknownProperty()` returns + are not predefined on the object. If `setUnknownProperty()` returns undefined, then `set()` will simply set the value on the object. ### Property Observers @@ -11878,18 +14399,29 @@ Ember.TargetActionSupport = Ember.Mixin.create({ */ 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; + var action = opts.action || get(this, 'action'), + target = opts.target || get(this, 'targetObject'), + actionContext = opts.actionContext; + + function args(options, actionName) { + var ret = []; + if (actionName) { ret.push(actionName); } + + return ret.concat(options); + } + + if (typeof actionContext === 'undefined') { + actionContext = get(this, 'actionContextObject') || this; + } if (target && action) { var ret; if (target.send) { - ret = target.send.apply(target, [action, actionContext]); + ret = target.send.apply(target, args(actionContext, action)); } else { Ember.assert("The action '" + action + "' did not exist on " + target, typeof target[action] === 'function'); - ret = target[action].apply(target, [actionContext]); + ret = target[action].apply(target, args(actionContext)); } if (ret !== false) ret = true; @@ -12024,11 +14556,6 @@ Ember.Evented = Ember.Mixin.create({ Ember.sendEvent(this, name, args); }, - fire: function(name) { - Ember.deprecate("Ember.Evented#fire() has been deprecated in favor of trigger() for compatibility with jQuery. It will be removed in 1.0. Please update your code to call trigger() instead."); - this.trigger.apply(this, arguments); - }, - /** Cancels subscription for given name, target, and method. @@ -12082,8 +14609,8 @@ Ember.DeferredMixin = Ember.Mixin.create({ Add handlers to be called when the Deferred object is resolved or rejected. @method then - @param {Function} doneCallback a callback function to be called when done - @param {Function} failCallback a callback function to be called when failed + @param {Function} resolve a callback function to be called when done + @param {Function} reject a callback function to be called when failed */ then: function(resolve, reject) { var deferred, promise, entity; @@ -12141,6 +14668,699 @@ Ember.DeferredMixin = Ember.Mixin.create({ (function() { +/** +@module ember +@submodule ember-runtime +*/ + +var get = Ember.get; + +/** + The `Ember.ActionHandler` mixin implements support for moving an `actions` + property to an `_actions` property at extend time, and adding `_actions` + to the object's mergedProperties list. + + `Ember.ActionHandler` is used internally by Ember in `Ember.View`, + `Ember.Controller`, and `Ember.Route`. + + @class ActionHandler + @namespace Ember +*/ +Ember.ActionHandler = Ember.Mixin.create({ + mergedProperties: ['_actions'], + + /** + @private + + Moves `actions` to `_actions` at extend time. Note that this currently + modifies the mixin themselves, which is technically dubious but + is practically of little consequence. This may change in the future. + + @method willMergeMixin + */ + willMergeMixin: function(props) { + if (props.actions && !props._actions) { + props._actions = Ember.merge(props._actions || {}, props.actions); + delete props.actions; + } + }, + + send: function(actionName) { + var args = [].slice.call(arguments, 1), target; + + if (this._actions && this._actions[actionName]) { + if (this._actions[actionName].apply(this, args) === true) { + // handler returned true, so this action will bubble + } else { + return; + } + } else if (this.deprecatedSend && this.deprecatedSendHandles && this.deprecatedSendHandles(actionName)) { + if (this.deprecatedSend.apply(this, [].slice.call(arguments)) === true) { + // handler return true, so this action will bubble + } else { + return; + } + } + + if (target = get(this, 'target')) { + Ember.assert("The `target` for " + this + " (" + target + ") does not have a `send` method", typeof target.send === 'function'); + target.send.apply(target, arguments); + } + } + +}); + +})(); + + + +(function() { +var set = Ember.set, get = Ember.get, + resolve = Ember.RSVP.resolve, + rethrow = Ember.RSVP.rethrow, + not = Ember.computed.not, + or = Ember.computed.or; + +/** + @module ember + @submodule ember-runtime + */ + +function installPromise(proxy, promise) { + promise.then(function(value) { + set(proxy, 'isFulfilled', true); + set(proxy, 'content', value); + + return value; + }, function(reason) { + set(proxy, 'isRejected', true); + set(proxy, 'reason', reason); + }).fail(rethrow); +} + +/** + A low level mixin making ObjectProxy, ObjectController or ArrayController's promise aware. + + ```javascript + var ObjectPromiseController = Ember.ObjectController.extend(Ember.PromiseProxyMixin); + + var controller = ObjectPromiseController.create({ + promise: $.getJSON('/some/remote/data.json') + }); + + controller.then(function(json){ + // the json + }, function(reason) { + // the reason why you have no json + }); + ``` + + the controller has bindable attributes which + track the promises life cycle + + ```javascript + controller.get('isPending') //=> true + controller.get('isSettled') //=> false + controller.get('isRejected') //=> false + controller.get('isFulfilled') //=> false + ``` + + When the the $.getJSON completes, and the promise is fulfilled + with json, the life cycle attributes will update accordingly. + + ```javascript + controller.get('isPending') //=> false + controller.get('isSettled') //=> true + controller.get('isRejected') //=> false + controller.get('isFulfilled') //=> true + ``` + + As the controller is an ObjectController, and the json now its content, + all the json properties will be available directly from the controller. + + ```javascript + // Assuming the following json: + { + firstName: 'Stefan', + lastName: 'Penner' + } + + // both properties will accessible on the controller + controller.get('firstName') //=> 'Stefan' + controller.get('lastName') //=> 'Penner' + ``` + + If the controller is backing a template, the attributes are + bindable from within that template + + ```handlebars + {{#if isPending}} + loading... + {{else}} + firstName: {{firstName}} + lastName: {{lastName}} + {{/if}} + ``` + @class Ember.PromiseProxyMixin +*/ +Ember.PromiseProxyMixin = Ember.Mixin.create({ + reason: null, + isPending: not('isSettled').readOnly(), + isSettled: or('isRejected', 'isFulfilled').readOnly(), + isRejected: false, + isFulfilled: false, + + promise: Ember.computed(function(key, promise) { + if (arguments.length === 2) { + promise = resolve(promise); + installPromise(this, promise); + return promise; + } else { + throw new Ember.Error("PromiseProxy's promise must be set"); + } + }), + + then: function(fulfill, reject) { + return get(this, 'promise').then(fulfill, reject); + } +}); + + +})(); + + + +(function() { + +})(); + + + +(function() { +var get = Ember.get, + forEach = Ember.EnumerableUtils.forEach, + RETAIN = 'r', + INSERT = 'i', + DELETE = 'd'; + +/** + An `Ember.TrackedArray` tracks array operations. It's useful when you want to + lazily compute the indexes of items in an array after they've been shifted by + subsequent operations. + + @class TrackedArray + @namespace Ember + @param {array} [items=[]] The array to be tracked. This is used just to get + the initial items for the starting state of retain:n. +*/ +Ember.TrackedArray = function (items) { + if (arguments.length < 1) { items = []; } + + var length = get(items, 'length'); + + if (length) { + this._operations = [new ArrayOperation(RETAIN, length, items)]; + } else { + this._operations = []; + } +}; + +Ember.TrackedArray.RETAIN = RETAIN; +Ember.TrackedArray.INSERT = INSERT; +Ember.TrackedArray.DELETE = DELETE; + +Ember.TrackedArray.prototype = { + + /** + Track that `newItems` were added to the tracked array at `index`. + + @method addItems + @param index + @param newItems + */ + addItems: function (index, newItems) { + var count = get(newItems, 'length'); + if (count < 1) { return; } + + var match = this._findArrayOperation(index), + arrayOperation = match.operation, + arrayOperationIndex = match.index, + arrayOperationRangeStart = match.rangeStart, + composeIndex, + splitIndex, + splitItems, + splitArrayOperation, + newArrayOperation; + + newArrayOperation = new ArrayOperation(INSERT, count, newItems); + + if (arrayOperation) { + if (!match.split) { + // insert left of arrayOperation + this._operations.splice(arrayOperationIndex, 0, newArrayOperation); + composeIndex = arrayOperationIndex; + } else { + this._split(arrayOperationIndex, index - arrayOperationRangeStart, newArrayOperation); + composeIndex = arrayOperationIndex + 1; + } + } else { + // insert at end + this._operations.push(newArrayOperation); + composeIndex = arrayOperationIndex; + } + + this._composeInsert(composeIndex); + }, + + /** + Track that `count` items were removed at `index`. + + @method removeItems + @param index + @param count + */ + removeItems: function (index, count) { + if (count < 1) { return; } + + var match = this._findArrayOperation(index), + arrayOperation = match.operation, + arrayOperationIndex = match.index, + arrayOperationRangeStart = match.rangeStart, + newArrayOperation, + composeIndex; + + newArrayOperation = new ArrayOperation(DELETE, count); + if (!match.split) { + // insert left of arrayOperation + this._operations.splice(arrayOperationIndex, 0, newArrayOperation); + composeIndex = arrayOperationIndex; + } else { + this._split(arrayOperationIndex, index - arrayOperationRangeStart, newArrayOperation); + composeIndex = arrayOperationIndex + 1; + } + + return this._composeDelete(composeIndex); + }, + + /** + Apply all operations, reducing them to retain:n, for `n`, the number of + items in the array. + + `callback` will be called for each operation and will be passed the following arguments: + * {array} items The items for the given operation + * {number} offset The computed offset of the items, ie the index in the + array of the first item for this operation. + * {string} operation The type of the operation. One of + `Ember.TrackedArray.{RETAIN, DELETE, INSERT}` + + @method apply + @param {function} callback + */ + apply: function (callback) { + var items = [], + offset = 0; + + forEach(this._operations, function (arrayOperation) { + callback(arrayOperation.items, offset, arrayOperation.type); + + if (arrayOperation.type !== DELETE) { + offset += arrayOperation.count; + items = items.concat(arrayOperation.items); + } + }); + + this._operations = [new ArrayOperation(RETAIN, items.length, items)]; + }, + + /** + Return an ArrayOperationMatch for the operation that contains the item at `index`. + + @method _findArrayOperation + + @param {number} index the index of the item whose operation information + should be returned. + @private + */ + _findArrayOperation: function (index) { + var arrayOperationIndex, + len, + split = false, + arrayOperation, + arrayOperationRangeStart, + arrayOperationRangeEnd; + + // OPTIMIZE: we could search these faster if we kept a balanced tree. + // find leftmost arrayOperation to the right of `index` + for (arrayOperationIndex = arrayOperationRangeStart = 0, len = this._operations.length; arrayOperationIndex < len; ++arrayOperationIndex) { + arrayOperation = this._operations[arrayOperationIndex]; + + if (arrayOperation.type === DELETE) { continue; } + + arrayOperationRangeEnd = arrayOperationRangeStart + arrayOperation.count - 1; + + if (index === arrayOperationRangeStart) { + break; + } else if (index > arrayOperationRangeStart && index <= arrayOperationRangeEnd) { + split = true; + break; + } else { + arrayOperationRangeStart = arrayOperationRangeEnd + 1; + } + } + + return new ArrayOperationMatch(arrayOperation, arrayOperationIndex, split, arrayOperationRangeStart); + }, + + _split: function (arrayOperationIndex, splitIndex, newArrayOperation) { + var arrayOperation = this._operations[arrayOperationIndex], + splitItems = arrayOperation.items.slice(splitIndex), + splitArrayOperation = new ArrayOperation(arrayOperation.type, splitItems.length, splitItems); + + // truncate LHS + arrayOperation.count = splitIndex; + arrayOperation.items = arrayOperation.items.slice(0, splitIndex); + + this._operations.splice(arrayOperationIndex + 1, 0, newArrayOperation, splitArrayOperation); + }, + + // see SubArray for a better implementation. + _composeInsert: function (index) { + var newArrayOperation = this._operations[index], + leftArrayOperation = this._operations[index-1], // may be undefined + rightArrayOperation = this._operations[index+1], // may be undefined + leftOp = leftArrayOperation && leftArrayOperation.type, + rightOp = rightArrayOperation && rightArrayOperation.type; + + if (leftOp === INSERT) { + // merge left + leftArrayOperation.count += newArrayOperation.count; + leftArrayOperation.items = leftArrayOperation.items.concat(newArrayOperation.items); + + if (rightOp === INSERT) { + // also merge right (we have split an insert with an insert) + leftArrayOperation.count += rightArrayOperation.count; + leftArrayOperation.items = leftArrayOperation.items.concat(rightArrayOperation.items); + this._operations.splice(index, 2); + } else { + // only merge left + this._operations.splice(index, 1); + } + } else if (rightOp === INSERT) { + // merge right + newArrayOperation.count += rightArrayOperation.count; + newArrayOperation.items = newArrayOperation.items.concat(rightArrayOperation.items); + this._operations.splice(index + 1, 1); + } + }, + + _composeDelete: function (index) { + var arrayOperation = this._operations[index], + deletesToGo = arrayOperation.count, + leftArrayOperation = this._operations[index-1], // may be undefined + leftOp = leftArrayOperation && leftArrayOperation.type, + nextArrayOperation, + nextOp, + nextCount, + removeNewAndNextOp = false, + removedItems = []; + + if (leftOp === DELETE) { + arrayOperation = leftArrayOperation; + index -= 1; + } + + for (var i = index + 1; deletesToGo > 0; ++i) { + nextArrayOperation = this._operations[i]; + nextOp = nextArrayOperation.type; + nextCount = nextArrayOperation.count; + + if (nextOp === DELETE) { + arrayOperation.count += nextCount; + continue; + } + + if (nextCount > deletesToGo) { + // d:2 {r,i}:5 we reduce the retain or insert, but it stays + removedItems = removedItems.concat(nextArrayOperation.items.splice(0, deletesToGo)); + nextArrayOperation.count -= deletesToGo; + + // In the case where we truncate the last arrayOperation, we don't need to + // remove it; also the deletesToGo reduction is not the entirety of + // nextCount + i -= 1; + nextCount = deletesToGo; + + deletesToGo = 0; + } else { + if (nextCount === deletesToGo) { + // Handle edge case of d:2 i:2 in which case both operations go away + // during composition. + removeNewAndNextOp = true; + } + removedItems = removedItems.concat(nextArrayOperation.items); + deletesToGo -= nextCount; + } + + if (nextOp === INSERT) { + // d:2 i:3 will result in delete going away + arrayOperation.count -= nextCount; + } + } + + if (arrayOperation.count > 0) { + // compose our new delete with possibly several operations to the right of + // disparate types + this._operations.splice(index+1, i-1-index); + } else { + // The delete operation can go away; it has merely reduced some other + // operation, as in d:3 i:4; it may also have eliminated that operation, + // as in d:3 i:3. + this._operations.splice(index, removeNewAndNextOp ? 2 : 1); + } + + return removedItems; + }, + + toString: function () { + var str = ""; + forEach(this._operations, function (operation) { + str += " " + operation.type + ":" + operation.count; + }); + return str.substring(1); + } +}; + +/** + Internal data structure to represent an array operation. + + @method ArrayOperation + @private + @property {string} type The type of the operation. One of + `Ember.TrackedArray.{RETAIN, INSERT, DELETE}` + @property {number} count The number of items in this operation. + @property {array} items The items of the operation, if included. RETAIN and + INSERT include their items, DELETE does not. +*/ +function ArrayOperation (operation, count, items) { + this.type = operation; // RETAIN | INSERT | DELETE + this.count = count; + this.items = items; +} + +/** + Internal data structure used to include information when looking up operations + by item index. + + @method ArrayOperationMatch + @private + @property {ArrayOperation} operation + @property {number} index The index of `operation` in the array of operations. + @property {boolean} split Whether or not the item index searched for would + require a split for a new operation type. + @property {number} rangeStart The index of the first item in the operation, + with respect to the tracked array. The index of the last item can be computed + from `rangeStart` and `operation.count`. +*/ +function ArrayOperationMatch(operation, index, split, rangeStart) { + this.operation = operation; + this.index = index; + this.split = split; + this.rangeStart = rangeStart; +} + +})(); + + + +(function() { +var get = Ember.get, + forEach = Ember.EnumerableUtils.forEach, + RETAIN = 'r', + FILTER = 'f'; + +function Operation (type, count) { + this.type = type; + this.count = count; +} + +/** + An `Ember.SubArray` tracks an array in a way similar to, but more specialized + than, `Ember.TrackedArray`. It is useful for keeping track of the indexes of + items within a filtered array. + + @class SubArray + @namespace Ember +*/ +Ember.SubArray = function (length) { + if (arguments.length < 1) { length = 0; } + + if (length > 0) { + this._operations = [new Operation(RETAIN, length)]; + } else { + this._operations = []; + } +}; + +Ember.SubArray.prototype = { + /** + Track that an item was added to the tracked array. + + @method addItem + + @param {number} index The index of the item in the tracked array. + @param {boolean} match `true` iff the item is included in the subarray. + + @return {number} The index of the item in the subarray. + */ + addItem: function(index, match) { + var returnValue = -1, + itemType = match ? RETAIN : FILTER, + self = this; + + this._findOperation(index, function(operation, operationIndex, rangeStart, rangeEnd, seenInSubArray) { + var newOperation, splitOperation; + + if (itemType === operation.type) { + ++operation.count; + } else if (index === rangeStart) { + // insert to the left of `operation` + self._operations.splice(operationIndex, 0, new Operation(itemType, 1)); + } else { + newOperation = new Operation(itemType, 1); + splitOperation = new Operation(operation.type, rangeEnd - index + 1); + operation.count = index - rangeStart; + + self._operations.splice(operationIndex + 1, 0, newOperation, splitOperation); + } + + if (match) { + if (operation.type === RETAIN) { + returnValue = seenInSubArray + (index - rangeStart); + } else { + returnValue = seenInSubArray; + } + } + + self._composeAt(operationIndex); + }, function(seenInSubArray) { + self._operations.push(new Operation(itemType, 1)); + + if (match) { + returnValue = seenInSubArray; + } + + self._composeAt(self._operations.length-1); + }); + + return returnValue; + }, + + /** + Track that an item was removed from the tracked array. + + @method removeItem + + @param {number} index The index of the item in the tracked array. + + @return {number} The index of the item in the subarray, or `-1` if the item + was not in the subarray. + */ + removeItem: function(index) { + var returnValue = -1, + self = this; + + this._findOperation(index, function (operation, operationIndex, rangeStart, rangeEnd, seenInSubArray) { + if (operation.type === RETAIN) { + returnValue = seenInSubArray + (index - rangeStart); + } + + if (operation.count > 1) { + --operation.count; + } else { + self._operations.splice(operationIndex, 1); + self._composeAt(operationIndex); + } + }, function() { + throw new Ember.Error("Can't remove an item that has never been added."); + }); + + return returnValue; + }, + + + _findOperation: function (index, foundCallback, notFoundCallback) { + var operationIndex, + len, + operation, + rangeStart, + rangeEnd, + seenInSubArray = 0; + + // OPTIMIZE: change to balanced tree + // find leftmost operation to the right of `index` + for (operationIndex = rangeStart = 0, len = this._operations.length; operationIndex < len; rangeStart = rangeEnd + 1, ++operationIndex) { + operation = this._operations[operationIndex]; + rangeEnd = rangeStart + operation.count - 1; + + if (index >= rangeStart && index <= rangeEnd) { + foundCallback(operation, operationIndex, rangeStart, rangeEnd, seenInSubArray); + return; + } else if (operation.type === RETAIN) { + seenInSubArray += operation.count; + } + } + + notFoundCallback(seenInSubArray); + }, + + _composeAt: function(index) { + var op = this._operations[index], + otherOp; + + if (!op) { + // Composing out of bounds is a no-op, as when removing the last operation + // in the list. + return; + } + + if (index > 0) { + otherOp = this._operations[index-1]; + if (otherOp.type === op.type) { + op.count += otherOp.count; + this._operations.splice(index-1, 1); + --index; + } + } + + if (index < this._operations.length-1) { + otherOp = this._operations[index+1]; + if (otherOp.type === op.type) { + op.count += otherOp.count; + this._operations.splice(index+1, 1); + } + } + } +}; })(); @@ -12225,7 +15445,11 @@ function makeCtor() { 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 (Ember.typeOf(properties) !== 'object') { continue; } + + var keyNames = Ember.keys(properties); + for (var j = 0, ll = keyNames.length; j < ll; j++) { + var keyName = keyNames[j]; if (!properties.hasOwnProperty(keyName)) { continue; } var value = properties[keyName], @@ -12245,6 +15469,7 @@ function makeCtor() { Ember.assert("Ember.Object.create no longer supports defining computed properties.", !(value instanceof Ember.ComputedProperty)); Ember.assert("Ember.Object.create no longer supports defining methods that call _super.", !(typeof value === 'function' && value.toString().indexOf('._super') !== -1)); + Ember.assert("`actions` must be provided at extend time, not at create time, when Ember.ActionHandler is used (i.e. views, controllers & routes).", !((keyName === 'actions') && Ember.ActionHandler.detect(this))); if (concatenatedProperties && indexOf(concatenatedProperties, keyName) >= 0) { var baseValue = this[keyName]; @@ -12275,9 +15500,9 @@ function makeCtor() { } } finishPartial(this, m); + this.init.apply(this, arguments); m.proto = proto; finishChains(this); - this.init.apply(this, arguments); sendEvent(this, "init"); }; @@ -12410,7 +15635,10 @@ CoreObject.PrototypeMixin = Mixin.create({ are also concatenated, in addition to `classNames`. This feature is available for you to use throughout the Ember object model, - although typical app developers are likely to use it infrequently. + although typical app developers are likely to use it infrequently. Since + it changes expectations about behavior of properties, you should properly + document its usage in each individual concatenated property (to not + mislead your users to think they can override the property in a subclass). @property concatenatedProperties @type Array @@ -12550,6 +15778,86 @@ var ClassMixin = Mixin.create({ isMethod: false, + /** + Creates a new subclass. + + ```javascript + App.Person = Ember.Object.extend({ + say: function(thing) { + alert(thing); + } + }); + ``` + + This defines a new subclass of Ember.Object: `App.Person`. It contains one method: `say()`. + + You can also create a subclass from any existing class by calling its `extend()` method. For example, you might want to create a subclass of Ember's built-in `Ember.View` class: + + ```javascript + App.PersonView = Ember.View.extend({ + tagName: 'li', + classNameBindings: ['isAdministrator'] + }); + ``` + + When defining a subclass, you can override methods but still access the implementation of your parent class by calling the special `_super()` method: + + ```javascript + App.Person = Ember.Object.extend({ + say: function(thing) { + var name = this.get('name'); + alert(name + ' says: ' + thing); + } + }); + + App.Soldier = App.Person.extend({ + say: function(thing) { + this._super(thing + ", sir!"); + }, + march: function(numberOfHours) { + alert(this.get('name') + ' marches for ' + numberOfHours + ' hours.') + } + }); + + var yehuda = App.Soldier.create({ + name: "Yehuda Katz" + }); + + yehuda.say("Yes"); // alerts "Yehuda Katz says: Yes, sir!" + ``` + + The `create()` on line #17 creates an *instance* of the `App.Soldier` class. The `extend()` on line #8 creates a *subclass* of `App.Person`. Any instance of the `App.Person` class will *not* have the `march()` method. + + You can also pass `Ember.Mixin` classes to add additional properties to the subclass. + + ```javascript + App.Person = Ember.Object.extend({ + say: function(thing) { + alert(this.get('name') + ' says: ' + thing); + } + }); + + App.SingingMixin = Ember.Mixin.create({ + sing: function(thing){ + alert(this.get('name') + ' sings: la la la ' + thing); + } + }); + + App.BroadwayStar = App.Person.extend(App.SingingMixin, { + dance: function() { + alert(this.get('name') + ' dances: tap tap tap tap '); + } + }); + ``` + + The `App.BroadwayStar` class contains three methods: `say()`, `sing()`, and `dance()`. + + @method extend + @static + + @param {Ember.Mixin} [mixins]* One or more Ember.Mixin classes + @param {Object} [arguments]* Object containing values to use within the new class + */ extend: function() { var Class = makeCtor(), proto; Class.ClassMixin = Mixin.create(this.ClassMixin); @@ -12629,12 +15937,98 @@ var ClassMixin = Mixin.create({ return new C(); }, + /** + + Augments a constructor's prototype with additional + properties and functions: + + ```javascript + MyObject = Ember.Object.extend({ + name: 'an object' + }); + + o = MyObject.create(); + o.get('name'); // 'an object' + + MyObject.reopen({ + say: function(msg){ + console.log(msg); + } + }) + + o2 = MyObject.create(); + o2.say("hello"); // logs "hello" + + o.say("goodbye"); // logs "goodbye" + ``` + + To add functions and properties to the constructor itself, + see `reopenClass` + + @method reopen + */ reopen: function() { this.willReopen(); reopen.apply(this.PrototypeMixin, arguments); return this; }, + /** + Augments a constructor's own properties and functions: + + ```javascript + MyObject = Ember.Object.extend({ + name: 'an object' + }); + + + MyObject.reopenClass({ + canBuild: false + }); + + MyObject.canBuild; // false + o = MyObject.create(); + ``` + + In other words, this creates static properties and functions for the class. These are only available on the class + and not on any instance of that class. + + ```javascript + App.Person = Ember.Object.extend({ + name : "", + sayHello : function(){ + alert("Hello. My name is " + this.get('name')); + } + }); + + App.Person.reopenClass({ + species : "Homo sapiens", + createPerson: function(newPersonsName){ + return App.Person.create({ + name:newPersonsName + }); + } + }); + + var tom = App.Person.create({ + name : "Tom Dale" + }); + var yehuda = App.Person.createPerson("Yehuda Katz"); + + tom.sayHello(); // "Hello. My name is Tom Dale" + yehuda.sayHello(); // "Hello. My name is Yehuda Katz" + alert(App.Person.species); // "Homo sapiens" + ``` + + Note that `species` and `createPerson` are *not* valid on the `tom` and `yehuda` + variables. They are only valid on `App.Person`. + + To add functions and properties to instances of + a constructor by extending the constructor's prototype + see `reopen` + + @method reopenClass + */ reopenClass: function() { reopen.apply(this.ClassMixin, arguments); applyMixin(this, arguments, false); @@ -12913,6 +16307,8 @@ function classToString() { if (this[NAME_KEY]) { ret = this[NAME_KEY]; + } else if (this._toString) { + ret = this._toString; } else { var str = superClassString(this); if (str) { @@ -13191,7 +16587,7 @@ Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray,/** @scope Ember.Array }, _insertAt: function(idx, object) { - if (idx > get(this, 'content.length')) throw new Error(OUT_OF_RANGE_EXCEPTION); + if (idx > get(this, 'content.length')) throw new Ember.Error(OUT_OF_RANGE_EXCEPTION); this._replace(idx, 0, [object]); return this; }, @@ -13211,7 +16607,7 @@ Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray,/** @scope Ember.Array indices = [], i; if ((start < 0) || (start >= get(this, 'length'))) { - throw new Error(OUT_OF_RANGE_EXCEPTION); + throw new Ember.Error(OUT_OF_RANGE_EXCEPTION); } if (len === undefined) len = 1; @@ -13309,7 +16705,9 @@ var get = Ember.get, removeBeforeObserver = Ember.removeBeforeObserver, removeObserver = Ember.removeObserver, propertyWillChange = Ember.propertyWillChange, - propertyDidChange = Ember.propertyDidChange; + propertyDidChange = Ember.propertyDidChange, + meta = Ember.meta, + defineProperty = Ember.defineProperty; function contentPropertyWillChange(content, contentKey) { var key = contentKey.slice(8); // remove "content." @@ -13427,6 +16825,14 @@ Ember.ObjectProxy = Ember.Object.extend(/** @scope Ember.ObjectProxy.prototype * }, setUnknownProperty: function (key, value) { + var m = meta(this); + if (m.proto === this) { + // if marked as prototype then just defineProperty + // rather than delegate + defineProperty(this, key, null, value); + return value; + } + var content = get(this, 'content'); Ember.assert(fmt("Cannot delegate set('%@', %@) to the 'content' property of object proxy %@: its 'content' is undefined.", [key, value, this]), content); return set(content, key, value); @@ -13434,25 +16840,6 @@ Ember.ObjectProxy = Ember.Object.extend(/** @scope Ember.ObjectProxy.prototype * }); -Ember.ObjectProxy.reopenClass({ - create: function () { - var mixin, prototype, i, l, properties, keyName; - if (arguments.length) { - prototype = this.proto(); - for (i = 0, l = arguments.length; i < l; i++) { - properties = arguments[i]; - for (keyName in properties) { - if (!properties.hasOwnProperty(keyName) || keyName in prototype) { continue; } - if (!mixin) mixin = {}; - mixin[keyName] = null; - } - } - if (mixin) this._initMixins([mixin]); - } - return this._super.apply(this, arguments); - } -}); - })(); @@ -13465,7 +16852,8 @@ Ember.ObjectProxy.reopenClass({ var set = Ember.set, get = Ember.get, guidFor = Ember.guidFor; -var forEach = Ember.EnumerableUtils.forEach; +var forEach = Ember.EnumerableUtils.forEach, + indexOf = Ember.ArrayPolyfills.indexOf; var EachArray = Ember.Object.extend(Ember.Array, { @@ -13523,7 +16911,7 @@ function removeObserverForContentKey(content, keyName, proxy, idx, loc) { guid = guidFor(item); indicies = objects[guid]; - indicies[indicies.indexOf(loc)] = null; + indicies[indexOf.call(indicies, loc)] = null; } } } @@ -13672,7 +17060,7 @@ Ember.EachProxy = Ember.Object.extend({ */ -var get = Ember.get, set = Ember.set; +var get = Ember.get, set = Ember.set, replace = Ember.EnumerableUtils._replace; // Add Ember.Array to Array.prototype. Remove methods with native // implementations and supply some more optimized versions of generic methods @@ -13694,7 +17082,7 @@ var NativeArray = Ember.Mixin.create(Ember.MutableArray, Ember.Observable, Ember // primitive for array support. replace: function(idx, amt, objects) { - if (this.isFrozen) throw Ember.FROZEN_ERROR ; + if (this.isFrozen) throw Ember.FROZEN_ERROR; // if we replaced exactly the same number of items, then pass only the // replaced range. Otherwise, pass the full remaining array length @@ -13703,14 +17091,13 @@ var NativeArray = Ember.Mixin.create(Ember.MutableArray, Ember.Observable, Ember this.arrayContentWillChange(idx, amt, len); if (!objects || objects.length === 0) { - this.splice(idx, amt) ; + this.splice(idx, amt); } else { - var args = [idx, amt].concat(objects) ; - this.splice.apply(this,args) ; + replace(this, idx, amt, objects); } this.arrayContentDidChange(idx, amt, len); - return this ; + return this; }, // If you ask for an unknown property, then try to collect the value @@ -13787,7 +17174,26 @@ Ember.NativeArray = NativeArray; /** Creates an `Ember.NativeArray` from an Array like object. - Does not modify the original object. + Does not modify the original object. Ember.A is not needed if + `Ember.EXTEND_PROTOTYPES` is `true` (the default value). However, + it is recommended that you use Ember.A when creating addons for + ember or when you can not garentee that `Ember.EXTEND_PROTOTYPES` + will be `true`. + + Example + + ```js + var Pagination = Ember.CollectionView.extend({ + tagName: 'ul', + classNames: ['pagination'], + init: function() { + this._super(); + if (!this.get('content')) { + this.set('content', Ember.A([])); + } + } + }); + ``` @method A @for Ember @@ -13800,7 +17206,17 @@ Ember.A = function(arr) { /** Activates the mixin on the Array.prototype if not already applied. Calling - this method more than once is safe. + this method more than once is safe. This will be called when ember is loaded + unless you have `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Array` + set to `false`. + + Example + + ```js + if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) { + Ember.NativeArray.activate(); + } + ``` @method activate @for Ember.NativeArray @@ -13895,8 +17311,8 @@ var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, isNone = Ember.is When using `Ember.Set`, you can observe the `"[]"` property to be alerted whenever the content changes. You can also add an enumerable observer to the set to be notified of specific objects that are added and - removed from the set. See `Ember.Enumerable` for more information on - enumerables. + removed from the set. See [Ember.Enumerable](/api/classes/Ember.Enumerable.html) + for more information on enumerables. This is often unhelpful. If you are filtering sets of objects, for instance, it is very inefficient to re-filter all of the items each time the set @@ -13958,7 +17374,7 @@ Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Emb @return {Ember.Set} An empty Set */ clear: function() { - if (this.isFrozen) { throw new Error(Ember.FROZEN_ERROR); } + if (this.isFrozen) { throw new Ember.Error(Ember.FROZEN_ERROR); } var len = get(this, 'length'); if (len === 0) { return this; } @@ -14068,7 +17484,7 @@ Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Emb @return {Object} The removed object from the set or null. */ pop: function() { - if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); + if (get(this, 'isFrozen')) throw new Ember.Error(Ember.FROZEN_ERROR); var obj = this.length > 0 ? this[this.length-1] : null; this.remove(obj); return obj; @@ -14185,7 +17601,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 (get(this, 'isFrozen')) throw new Ember.Error(Ember.FROZEN_ERROR); if (isNone(obj)) return this; // nothing to do var guid = guidFor(obj), @@ -14213,7 +17629,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 (get(this, 'isFrozen')) throw new Ember.Error(Ember.FROZEN_ERROR); if (isNone(obj)) return this; // nothing to do var guid = guidFor(obj), @@ -14311,6 +17727,20 @@ var loadHooks = Ember.ENV.EMBER_LOAD_HOOKS || {}; var loaded = {}; /** + +Detects when a specific package of Ember (e.g. 'Ember.Handlebars') +has fully loaded and is available for extension. + +The provided `callback` will be called with the `name` passed +resolved from a string into the object: + +```javascript +Ember.onLoad('Ember.Handlebars' function(hbars){ + hbars.registerHelper(...); +}); +``` + + @method onLoad @for Ember @param name {String} name of hook @@ -14328,6 +17758,10 @@ Ember.onLoad = function(name, callback) { }; /** + +Called when an Ember.js package (e.g Ember.Handlebars) has finished +loading. Triggers any callbacks registered for this event. + @method runLoadHooks @for Ember @param name {String} name of hook @@ -14366,39 +17800,18 @@ var get = Ember.get; compose Ember's controller layer: `Ember.Controller`, `Ember.ArrayController`, and `Ember.ObjectController`. - Within an `Ember.Router`-managed application single shared instaces of every - Controller object in your application's namespace will be added to the - application's `Ember.Router` instance. See `Ember.Application#initialize` - for additional information. - - ## Views - - By default a controller instance will be the rendering context - for its associated `Ember.View.` This connection is made during calls to - `Ember.ControllerMixin#connectOutlet`. - - Within the view's template, the `Ember.View` instance can be accessed - through the controller with `{{view}}`. - - ## Target Forwarding - - By default a controller will target your application's `Ember.Router` - instance. Calls to `{{action}}` within the template of a controller's view - are forwarded to the router. See `Ember.Handlebars.helpers.action` for - additional information. - @class ControllerMixin @namespace Ember */ -Ember.ControllerMixin = Ember.Mixin.create({ +Ember.ControllerMixin = Ember.Mixin.create(Ember.ActionHandler, { /* ducktype as a controller */ isController: true, /** - The object to which events from the view should be sent. + The object to which actions from the view should be sent. For example, when a Handlebars template uses the `{{action}}` helper, - it will attempt to send the event to the view's controller's `target`. + it will attempt to send the action to the view's controller's `target`. By default, a controller's `target` is set to the router after it is instantiated by `Ember.Application#initialize`. @@ -14416,16 +17829,16 @@ Ember.ControllerMixin = Ember.Mixin.create({ model: Ember.computed.alias('content'), - send: function(actionName) { - var args = [].slice.call(arguments, 1), target; + deprecatedSendHandles: function(actionName) { + return !!this[actionName]; + }, - if (this[actionName]) { - Ember.assert("The controller " + this + " does not have the action " + actionName, typeof this[actionName] === 'function'); - this[actionName].apply(this, args); - } else if (target = get(this, 'target')) { - Ember.assert("The target for controller " + this + " (" + target + ") did not define a `send` method", typeof target.send === 'function'); - target.send.apply(target, arguments); - } + deprecatedSend: function(actionName) { + var args = [].slice.call(arguments, 1); + Ember.assert('' + this + " has the action " + actionName + " but it is not a function", typeof this[actionName] === 'function'); + Ember.deprecate('Action handlers implemented directly on controllers are deprecated in favor of action handlers on an `actions` object (' + actionName + ' on ' + this + ')', false); + this[actionName].apply(this, args); + return; } }); @@ -14474,6 +17887,29 @@ var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; songsController.get('firstObject'); // {trackNumber: 1, title: 'Dear Prudence'} ``` + If you add or remove the properties to sort by or change the sort direction the content + sort order will be automatically updated. + + ```javascript + songsController.set('sortProperties', ['title']); + songsController.get('firstObject'); // {trackNumber: 2, title: 'Back in the U.S.S.R.'} + + songsController.toggleProperty('sortAscending'); + songsController.get('firstObject'); // {trackNumber: 4, title: 'Ob-La-Di, Ob-La-Da'} + ``` + + SortableMixin works by sorting the arrangedContent array, which is the array that + arrayProxy displays. Due to the fact that the underlying 'content' array is not changed, that + array will not display the sorted list: + + ```javascript + songsController.get('content').get('firstObject'); // Returns the unsorted original content + songsController.get('firstObject'); // Returns the sorted content. + ``` + + Although the sorted content can also be accessed through the arrangedContent property, + it is preferable to use the proxied class and not the arrangedContent array directly. + @class SortableMixin @namespace Ember @uses Ember.MutableEnumerable @@ -14483,6 +17919,9 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { /** Specifies which properties dictate the arrangedContent's sort order. + When specifying multiple properties the sorting will use properties + from the `sortProperties` array prioritized from first to last. + @property {Array} sortProperties */ sortProperties: null, @@ -14496,7 +17935,7 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { /** The function used to compare two values. You can override this if you - want to do custom comparisons.Functions must be of the type expected by + want to do custom comparisons. Functions must be of the type expected by Array#sort, i.e. return 0 if the two parameters are equal, return a negative value if the first parameter is smaller than the second or @@ -14553,6 +17992,13 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { isSorted: Ember.computed.bool('sortProperties'), + /** + Overrides the default arrangedContent from arrayProxy in order to sort by sortFunction. + Also sets up observers for each sortProperty on each item in the content Array. + + @property arrangedContent + */ + arrangedContent: Ember.computed('content', 'sortProperties.@each', function(key, value) { var content = get(this, 'content'), isSorted = get(this, 'isSorted'), @@ -14870,11 +18316,15 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, }, init: function() { - if (!this.get('content')) { Ember.defineProperty(this, 'content', undefined, Ember.A()); } this._super(); + this.set('_subControllers', Ember.A()); }, + content: Ember.computed(function () { + return Ember.A(); + }), + controllerAt: function(idx, object, controllerClass) { var container = get(this, 'container'), subControllers = get(this, '_subControllers'), @@ -14886,7 +18336,7 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, fullName = "controller:" + controllerClass; if (!container.has(fullName)) { - throw new Error('Could not resolve itemController: "' + controllerClass + '"'); + throw new Ember.Error('Could not resolve itemController: "' + controllerClass + '"'); } subController = container.lookupFactory(fullName).create({ @@ -14925,9 +18375,11 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, */ /** - `Ember.ObjectController` is part of Ember's Controller layer. + `Ember.ObjectController` is part of Ember's Controller layer. It is intended + to wrap a single object, proxying unhandled attempts to `get` and `set` to the underlying + content object, and to forward unhandled action attempts to its `target`. - `Ember.ObjectController` derives its functionality from its superclass + `Ember.ObjectController` derives this functionality from its superclass `Ember.ObjectProxy` and the `Ember.ControllerMixin` mixin. @class ObjectController @@ -15206,14 +18658,32 @@ function escapeAttribute(value) { return string.replace(BAD_CHARS_REGEXP, escapeChar); } +// IE 6/7 have bugs arond setting names on inputs during creation. +// From http://msdn.microsoft.com/en-us/library/ie/ms536389(v=vs.85).aspx: +// "To include the NAME attribute at run time on objects created with the createElement method, use the eTag." +var canSetNameOnInputs = (function() { + var div = document.createElement('div'), + el = document.createElement('input'); + + el.setAttribute('name', 'foo'); + div.appendChild(el); + + return !!div.innerHTML.match('foo'); +})(); + /** `Ember.RenderBuffer` gathers information regarding the a view and generates the final representation. `Ember.RenderBuffer` will generate HTML which can be pushed to the DOM. + ```javascript + var buffer = Ember.RenderBuffer('div'); + ``` + @class RenderBuffer @namespace Ember @constructor + @param {String} tagName tag name (such as 'div' or 'p') used for the buffer */ Ember.RenderBuffer = function(tagName) { return new Ember._RenderBuffer(tagName); @@ -15560,14 +19030,22 @@ Ember._RenderBuffer.prototype = generateElement: function() { var tagName = this.tagNames.pop(), // pop since we don't need to close - element = document.createElement(tagName), - $element = Ember.$(element), id = this.elementId, classes = this.classes, attrs = this.elementAttributes, props = this.elementProperties, style = this.elementStyle, - styleBuffer = '', attr, prop; + styleBuffer = '', attr, prop, tagString; + + if (attrs && attrs.name && !canSetNameOnInputs) { + // IE allows passing a tag to createElement. See note on `canSetNameOnInputs` above as well. + tagString = '<'+stripTagName(tagName)+' name="'+escapeAttribute(attrs.name)+'">'; + } else { + tagString = tagName; + } + + var element = document.createElement(tagString), + $element = Ember.$(element); if (id) { $element.attr('id', id); @@ -15962,6 +19440,7 @@ var get = Ember.get, set = Ember.set; var guidFor = Ember.guidFor; var a_forEach = Ember.EnumerableUtils.forEach; var a_addObject = Ember.EnumerableUtils.addObject; +var meta = Ember.meta; var childViewsProperty = Ember.computed(function() { var childViews = this._childViews, ret = Ember.A(), view = this; @@ -15982,7 +19461,7 @@ var childViewsProperty = Ember.computed(function() { Ember.deprecate("Manipulating an Ember.ContainerView through its childViews property is deprecated. Please use the ContainerView instance itself as an Ember.MutableArray."); return view.replace(idx, removedCount, addedViews); } - throw new Error("childViews is immutable"); + throw new Ember.Error("childViews is immutable"); }; return ret; @@ -16002,7 +19481,13 @@ Ember.warn("The VIEW_PRESERVES_CONTEXT flag has been removed and the functionali Ember.TEMPLATES = {}; /** - `Ember.CoreView` is + `Ember.CoreView` is an abstract class that exists to give view-like behavior + to both Ember's main view class `Ember.View` and other classes like + `Ember._SimpleMetamorphView` that don't need the fully functionaltiy of + `Ember.View`. + + Unless you have specific needs for `CoreView`, you will use `Ember.View` + in your applications. @class CoreView @namespace Ember @@ -16010,7 +19495,7 @@ Ember.TEMPLATES = {}; @uses Ember.Evented */ -Ember.CoreView = Ember.Object.extend(Ember.Evented, { +Ember.CoreView = Ember.Object.extend(Ember.Evented, Ember.ActionHandler, { isView: true, states: states, @@ -16125,6 +19610,18 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { } }, + deprecatedSendHandles: function(actionName) { + return !!this[actionName]; + }, + + deprecatedSend: function(actionName) { + var args = [].slice.call(arguments, 1); + Ember.assert('' + this + " has the action " + actionName + " but it is not a function", typeof this[actionName] === 'function'); + Ember.deprecate('Action handlers implemented directly on views are deprecated in favor of action handlers on an `actions` object (' + actionName + ' on ' + this + ')', false); + this[actionName].apply(this, args); + return; + }, + has: function(name) { return Ember.typeOf(this[name]) === 'function' || this._super(name); }, @@ -16226,7 +19723,7 @@ var EMPTY_ARRAY = []; The default HTML tag name used for a view's DOM representation is `div`. This can be customized by setting the `tagName` property. The following view -class: + class: ```javascript ParagraphView = Ember.View.extend({ @@ -16400,8 +19897,8 @@ class: will be removed. Both `classNames` and `classNameBindings` are concatenated properties. See - `Ember.Object` documentation for more information about concatenated - properties. + [Ember.Object](/api/classes/Ember.Object.html) documentation for more + information about concatenated properties. ## HTML Attributes @@ -16461,7 +19958,7 @@ class: Updates to the the property of an attribute binding will result in automatic update of the HTML attribute in the view's rendered HTML representation. - `attributeBindings` is a concatenated property. See `Ember.Object` + `attributeBindings` is a concatenated property. See [Ember.Object](/api/classes/Ember.Object.html) documentation for more information about concatenated properties. ## Templates @@ -16504,9 +20001,6 @@ class: Using a value for `templateName` that does not have a Handlebars template with a matching `data-template-name` attribute will throw an error. - Assigning a value to both `template` and `templateName` properties will throw - an error. - For views classes that may have a template later defined (e.g. as the block portion of a `{{view}}` Handlebars helper call in another template or in a subclass), you can provide a `defaultTemplate` property set to compiled @@ -16612,7 +20106,8 @@ class: ``` - See `Handlebars.helpers.yield` for more information. + See [Ember.Handlebars.helpers.yield](/api/classes/Ember.Handlebars.helpers.html#method_yield) + for more information. ## Responding to Browser Events @@ -16709,7 +20204,7 @@ class: ### Handlebars `{{action}}` Helper - See `Handlebars.helpers.action`. + See [Handlebars.helpers.action](/api/classes/Ember.Handlebars.helpers.html#method_action). ### Event Names @@ -16764,8 +20259,8 @@ class: ## Handlebars `{{view}}` Helper Other `Ember.View` instances can be included as part of a view's template by - using the `{{view}}` Handlebars helper. See `Handlebars.helpers.view` for - additional information. + using the `{{view}}` Handlebars helper. See [Ember.Handlebars.helpers.view](/api/classes/Ember.Handlebars.helpers.html#method_view) + for additional information. @class View @namespace Ember @@ -17694,12 +21189,6 @@ Ember.View = Ember.CoreView.extend( return viewCollection; }, - _elementWillChange: Ember.beforeObserver(function() { - this.forEachChildView(function(view) { - Ember.propertyWillChange(view, 'element'); - }); - }, 'element'), - /** @private @@ -17711,7 +21200,7 @@ Ember.View = Ember.CoreView.extend( */ _elementDidChange: Ember.observer(function() { this.forEachChildView(function(view) { - Ember.propertyDidChange(view, 'element'); + delete meta(view).cache.element; }); }, 'element'), @@ -17806,7 +21295,7 @@ Ember.View = Ember.CoreView.extend( visually challenged users navigate rich web applications. The full list of valid WAI-ARIA roles is available at: - http://www.w3.org/TR/wai-aria/roles#roles_categorization + [http://www.w3.org/TR/wai-aria/roles#roles_categorization](http://www.w3.org/TR/wai-aria/roles#roles_categorization) @property ariaRole @type String @@ -18038,6 +21527,10 @@ Ember.View = Ember.CoreView.extend( @return {Ember.View} new instance */ createChildView: function(view, attrs) { + if (!view) { + throw new TypeError("createChildViews first argument must exist"); + } + if (view.isView && view._parentView === this && view.container === this.container) { return view; } @@ -18155,6 +21648,7 @@ Ember.View = Ember.CoreView.extend( if (priorState && priorState.exit) { priorState.exit(this); } if (currentState.enter) { currentState.enter(this); } + if (state === 'inDOM') { delete Ember.meta(this).cache.element; } if (children !== false) { this.forEachChildView(function(view) { @@ -18186,6 +21680,10 @@ Ember.View = Ember.CoreView.extend( target = null; } + if (!root || typeof root !== 'object') { + return; + } + var view = this, stateCheckedObserver = function() { view.currentState.invokeObserver(this, observer); @@ -18281,7 +21779,7 @@ Ember.View.reopenClass({ Parse a path and return an object which holds the parsed properties. - For example a path like "content.isEnabled:enabled:disabled" wil return the + For example a path like "content.isEnabled:enabled:disabled" will return the following object: ```javascript @@ -18502,10 +22000,18 @@ Ember.merge(preRender, { var viewCollection = view.viewHierarchyCollection(); viewCollection.trigger('willInsertElement'); - // after createElement, the view will be in the hasElement state. + fn.call(view); - viewCollection.transitionTo('inDOM', false); - viewCollection.trigger('didInsertElement'); + + // We transition to `inDOM` if the element exists in the DOM + var element = view.get('element'); + while (element = element.parentNode) { + if (element === document) { + viewCollection.transitionTo('inDOM', false); + viewCollection.trigger('didInsertElement'); + } + } + }, renderToBufferIfNeeded: function(view, buffer) { @@ -18714,7 +22220,7 @@ Ember.merge(inDOM, { } view.addBeforeObserver('elementId', function() { - throw new Error("Changing a view's elementId after creation is not allowed"); + throw new Ember.Error("Changing a view's elementId after creation is not allowed"); }); }, @@ -18954,30 +22460,6 @@ var ViewCollection = Ember._ViewCollection; or layout being rendered. The HTML contents of a `Ember.ContainerView`'s DOM representation will only be the rendered HTML of its child views. - ## Binding a View to Display - - If you would like to display a single view in your ContainerView, you can set - its `currentView` property. When the `currentView` property is set to a view - instance, it will be added to the ContainerView. If the `currentView` property - is later changed to a different view, the new view will replace the old view. - If `currentView` is set to `null`, the last `currentView` will be removed. - - This functionality is useful for cases where you want to bind the display of - a ContainerView to a controller or state manager. For example, you can bind - the `currentView` of a container to a controller like this: - - ```javascript - App.appController = Ember.Object.create({ - view: Ember.View.create({ - templateName: 'person_template' - }) - }); - ``` - - ```handlebars - {{view Ember.ContainerView currentViewBinding="App.appController.view"}} - ``` - @class ContainerView @namespace Ember @extends Ember.View @@ -19044,7 +22526,7 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { length: Ember.computed(function () { return this._childViews.length; - }), + }).volatile(), /** @private @@ -19162,7 +22644,7 @@ Ember.merge(states._default, { Ember.merge(states.inBuffer, { childViewsDidChange: function(parentView, views, start, added) { - throw new Error('You cannot modify child views while in the inBuffer state'); + throw new Ember.Error('You cannot modify child views while in the inBuffer state'); } }); @@ -19374,11 +22856,6 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; manipulated. Instead, add, remove, replace items from its `content` property. This will trigger appropriate changes to its rendered HTML. - ## Use in templates via the `{{collection}}` `Ember.Handlebars` helper - - `Ember.Handlebars` provides a helper specifically for adding - `CollectionView`s to templates. See `Ember.Handlebars.collection` for more - details @class CollectionView @namespace Ember @@ -19423,12 +22900,25 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie */ itemViewClass: Ember.View, + /** + Setup a CollectionView + + @method init + */ init: function() { var ret = this._super(); this._contentDidChange(); return ret; }, + /** + @private + + Invoked when the content property is about to change. Notifies observers that the + entire array content will change. + + @method _contentWillChange + */ _contentWillChange: Ember.beforeObserver(function() { var content = this.get('content'); @@ -19459,10 +22949,22 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie this.arrayDidChange(content, 0, null, len); }, 'content'), + /** + @private + + Ensure that the content implements Ember.Array + + @method _assertArrayLike + */ _assertArrayLike: function(content) { Ember.assert(fmt("an Ember.CollectionView's content must implement Ember.Array. You passed %@", [content]), Ember.Array.detect(content)); }, + /** + Removes the content and content observers. + + @method destroy + */ destroy: function() { if (!this._super()) { return; } @@ -19476,6 +22978,19 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie return this; }, + /** + Called when a mutation to the underlying content array will occur. + + This method will remove any views that are no longer in the underlying + content array. + + Invokes whenever the content array itself will change. + + @method arrayWillChange + @param {Array} content the managed collection of objects + @param {Number} start the index at which the changes will occurr + @param {Number} removed number of object to be removed from content + */ arrayWillChange: function(content, start, removedCount) { // If the contents were empty before and this template collection has an // empty view remove it now. @@ -19566,6 +23081,21 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie this.replace(start, 0, addedViews); }, + /** + Instantiates a view to be added to the childViews array during view + initialization. You generally will not call this method directly unless + you are overriding `createChildViews()`. Note that this method will + automatically configure the correct settings on the new view instance to + act as a child of the parent. + + The tag name for the view will be set to the tagName of the viewClass + passed in. + + @method createChildView + @param {Class} viewClass + @param {Hash} [attrs] Attributes to add + @return {Ember.View} new instance + */ createChildView: function(view, attrs) { view = this._super(view, attrs); @@ -19606,7 +23136,9 @@ Ember.CollectionView.CONTAINER_MAP = { (function() { -var get = Ember.get, set = Ember.set, isNone = Ember.isNone; +var get = Ember.get, set = Ember.set, isNone = Ember.isNone, + a_slice = Array.prototype.slice; + /** @module ember @@ -19634,7 +23166,7 @@ var get = Ember.get, set = Ember.set, isNone = Ember.isNone; ```html

{{person.title}}

- +

{{person.signature}}

``` @@ -19656,15 +23188,18 @@ var get = Ember.get, set = Ember.set, isNone = Ember.isNone; If you want to customize the component, in order to handle events or actions, you implement a subclass of `Ember.Component` named after the name of the - component. + component. Note that `Component` needs to be appended to the name of + your subclass like `AppProfileComponent`. For example, you could implement the action `hello` for the `app-profile` component: - ```js + ```javascript App.AppProfileComponent = Ember.Component.extend({ - hello: function(name) { - console.log("Hello", name) + actions: { + hello: function(name) { + console.log("Hello", name); + } } }); ``` @@ -19717,7 +23252,8 @@ Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { view.appendChild(Ember.View, { isVirtual: true, tagName: '', - template: get(this, 'template'), + _contextView: parentView, + template: template, context: get(parentView, 'context'), controller: get(parentView, 'controller'), templateData: { keywords: parentView.cloneKeywords() } @@ -19725,6 +23261,14 @@ Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { } }, + /** + If the component is currently inserted into the DOM of a parent view, this + property will point to the controller of the parent view. + + @property targetObject + @type Ember.Controller + @default null + */ targetObject: Ember.computed(function(key) { var parentView = get(this, '_parentView'); return parentView ? get(parentView, 'controller') : null; @@ -19761,15 +23305,17 @@ Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { target's action method. Example: ```javascript - App.MyTree = Ember.Component.extend({ + App.MyTreeComponent = Ember.Component.extend({ click: function() { this.sendAction('didClickTreeNode', this.get('node')); } }); App.CategoriesController = Ember.Controller.extend({ - didClickCategory: function(category) { - //Do something with the node/category that was clicked + actions: { + didClickCategory: function(category) { + //Do something with the node/category that was clicked + } } }); ``` @@ -19783,8 +23329,9 @@ Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { @param [action] {String} the action to trigger @param [context] {*} a context to send with the action */ - sendAction: function(action, context) { - var actionName; + sendAction: function(action) { + var actionName, + contexts = a_slice.call(arguments, 1); // Send the default action if (action === undefined) { @@ -19800,7 +23347,7 @@ Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { this.triggerAction({ action: actionName, - actionContext: context + actionContext: contexts }); } }); @@ -20013,6 +23560,14 @@ define("metamorph", range.insertNode(fragment); }; + /** + * @public + * + * Remove this object (including starting and ending + * placeholders). + * + * @method remove + */ removeFunc = function() { // get a range for the current metamorph object including // the starting and ending placeholders. @@ -20053,7 +23608,7 @@ define("metamorph", }; } else { - /** + /* * This code is mostly taken from jQuery, with one exception. In jQuery's case, we * have some HTML and we need to figure out how to convert it into some nodes. * @@ -20107,12 +23662,12 @@ define("metamorph", } }; - /** + /* * Given a parent node and some HTML, generate a set of nodes. Return the first * node, which will allow us to traverse the rest using nextSibling. * * We need to do this because innerHTML in IE does not really parse the nodes. - **/ + */ var firstNodeFor = function(parentNode, html) { var arr = wrapMap[parentNode.tagName.toLowerCase()] || wrapMap._default; var depth = arr[0], start = arr[1], end = arr[2]; @@ -20145,7 +23700,7 @@ define("metamorph", return element; }; - /** + /* * In some cases, Internet Explorer can create an anonymous node in * the hierarchy with no tagName. You can create this scenario via: * @@ -20155,7 +23710,7 @@ define("metamorph", * * If our script markers are inside such a node, we need to find that * node and use *it* as the marker. - **/ + */ var realNode = function(start) { while (start.parentNode.tagName === "") { start = start.parentNode; @@ -20164,7 +23719,7 @@ define("metamorph", return start; }; - /** + /* * When automatically adding a tbody, Internet Explorer inserts the * tbody immediately before the first . Other browsers create it * before the first node, no matter what. @@ -20191,7 +23746,8 @@ define("metamorph", * * This code reparents the first script tag by making it the tbody's * first child. - **/ + * + */ var fixParentage = function(start, end) { if (start.parentNode !== end.parentNode) { end.parentNode.insertBefore(start, end.parentNode.firstChild); @@ -20390,20 +23946,6 @@ Ember.assert("Ember Handlebars requires Handlebars version 1.0.0, COMPILER_REVIS */ Ember.Handlebars = objectCreate(Handlebars); -function makeBindings(options) { - var hash = options.hash, - hashType = options.hashTypes; - - for (var prop in hash) { - if (hashType[prop] === 'ID') { - hash[prop + 'Binding'] = hash[prop]; - hashType[prop + 'Binding'] = 'STRING'; - delete hash[prop]; - delete hashType[prop]; - } - } -} - /** Register a bound helper or custom view helper. @@ -20458,21 +24000,11 @@ function makeBindings(options) { @param {String} dependentKeys* */ Ember.Handlebars.helper = function(name, value) { - if (Ember.Component.detect(value)) { - Ember.assert("You tried to register a component named '" + name + "', but component names must include a '-'", name.match(/-/)); - - var proto = value.proto(); - if (!proto.layoutName && !proto.templateName) { - value.reopen({ - layoutName: 'components/' + name - }); - } - } + Ember.assert("You tried to register a component named '" + name + "', but component names must include a '-'", !Ember.Component.detect(value) || name.match(/-/)); if (Ember.View.detect(value)) { Ember.Handlebars.registerHelper(name, function(options) { Ember.assert("You can only pass attributes (such as name=value) not bare values to a helper for a View", arguments.length < 2); - makeBindings(options); return Ember.Handlebars.helpers.view.call(this, value, options); }); } else { @@ -20520,7 +24052,6 @@ if (Handlebars.JavaScriptCompiler) { Ember.Handlebars.JavaScriptCompiler.prototype.namespace = "Ember.Handlebars"; - Ember.Handlebars.JavaScriptCompiler.prototype.initializeBuffer = function() { return "''"; }; @@ -20888,61 +24419,102 @@ Ember.Handlebars.registerBoundHelper = function(name, fn) { numProperties = properties.length, options = arguments[arguments.length - 1], normalizedProperties = [], + types = options.types, data = options.data, hash = options.hash, view = data.view, - currentContext = (options.contexts && options.contexts[0]) || this, - normalized, - pathRoot, path, prefixPathForDependentKeys = '', - loc, hashOption; + contexts = options.contexts, + currentContext = (contexts && contexts.length) ? contexts[0] : this, + prefixPathForDependentKeys = '', + loc, len, hashOption, + boundOption, property, + normalizedValue = Ember._SimpleHandlebarsView.prototype.normalizedValue; Ember.assert("registerBoundHelper-generated helpers do not support use with Handlebars blocks.", !options.fn); // Detect bound options (e.g. countBinding="otherCount") - hash.boundOptions = {}; + var boundOptions = hash.boundOptions = {}; for (hashOption in hash) { - if (!hash.hasOwnProperty(hashOption)) { continue; } - - if (Ember.IS_BINDING.test(hashOption) && typeof hash[hashOption] === 'string') { + if (Ember.IS_BINDING.test(hashOption)) { // Lop off 'Binding' suffix. - hash.boundOptions[hashOption.slice(0, -7)] = hash[hashOption]; + boundOptions[hashOption.slice(0, -7)] = hash[hashOption]; } } // Expose property names on data.properties object. + var watchedProperties = []; data.properties = []; for (loc = 0; loc < numProperties; ++loc) { data.properties.push(properties[loc]); - normalizedProperties.push(normalizePath(currentContext, properties[loc], data)); + if (types[loc] === 'ID') { + var normalizedProp = normalizePath(currentContext, properties[loc], data); + normalizedProperties.push(normalizedProp); + watchedProperties.push(normalizedProp); + } else { + normalizedProperties.push(null); + } } + // Handle case when helper invocation is preceded by `unbound`, e.g. + // {{unbound myHelper foo}} if (data.isUnbound) { return evaluateUnboundHelper(this, fn, normalizedProperties, options); } - if (dependentKeys.length === 0) { - return evaluateMultiPropertyBoundHelper(currentContext, fn, normalizedProperties, options); - } - - Ember.assert("Dependent keys can only be used with single-property helpers.", properties.length === 1); - - normalized = normalizedProperties[0]; - - pathRoot = normalized.root; - path = normalized.path; - - var bindView = new Ember._SimpleHandlebarsView( - path, pathRoot, !options.hash.unescaped, options.data - ); + var bindView = new Ember._SimpleHandlebarsView(null, null, !options.hash.unescaped, options.data); + // Override SimpleHandlebarsView's method for generating the view's content. bindView.normalizedValue = function() { - var value = Ember._SimpleHandlebarsView.prototype.normalizedValue.call(bindView); - return fn.call(view, value, options); + var args = [], boundOption; + + // Copy over bound hash options. + for (boundOption in boundOptions) { + if (!boundOptions.hasOwnProperty(boundOption)) { continue; } + property = normalizePath(currentContext, boundOptions[boundOption], data); + bindView.path = property.path; + bindView.pathRoot = property.root; + hash[boundOption] = normalizedValue.call(bindView); + } + + for (loc = 0; loc < numProperties; ++loc) { + property = normalizedProperties[loc]; + if (property) { + bindView.path = property.path; + bindView.pathRoot = property.root; + args.push(normalizedValue.call(bindView)); + } else { + args.push(properties[loc]); + } + } + args.push(options); + + // Run the supplied helper function. + return fn.apply(currentContext, args); }; view.appendChild(bindView); - view.registerObserver(pathRoot, path, bindView, bindView.rerender); + // Assemble list of watched properties that'll re-render this helper. + for (boundOption in boundOptions) { + if (boundOptions.hasOwnProperty(boundOption)) { + watchedProperties.push(normalizePath(currentContext, boundOptions[boundOption], data)); + } + } + + // Observe each property. + for (loc = 0, len = watchedProperties.length; loc < len; ++loc) { + property = watchedProperties[loc]; + view.registerObserver(property.root, property.path, bindView, bindView.rerender); + } + + if (types[0] !== 'ID' || normalizedProperties.length === 0) { + return; + } + + // Add dependent key observers to the first param + var normalized = normalizedProperties[0], + pathRoot = normalized.root, + path = normalized.path; if(!Ember.isEmpty(path)) { prefixPathForDependentKeys = path + '.'; @@ -20956,68 +24528,6 @@ Ember.Handlebars.registerBoundHelper = function(name, fn) { Ember.Handlebars.registerHelper(name, helper); }; -/** - @private - - Renders the unbound form of an otherwise bound helper function. - - @method evaluateMultiPropertyBoundHelper - @param {Function} fn - @param {Object} context - @param {Array} normalizedProperties - @param {String} options -*/ -function evaluateMultiPropertyBoundHelper(context, fn, normalizedProperties, options) { - var numProperties = normalizedProperties.length, - data = options.data, - view = data.view, - hash = options.hash, - boundOptions = hash.boundOptions, - watchedProperties, - boundOption, bindView, loc, property, len; - - bindView = new Ember._SimpleHandlebarsView(null, null, !hash.unescaped, data); - bindView.normalizedValue = function() { - var args = [], boundOption; - - // Copy over bound options. - for (boundOption in boundOptions) { - if (!boundOptions.hasOwnProperty(boundOption)) { continue; } - property = normalizePath(context, boundOptions[boundOption], data); - bindView.path = property.path; - bindView.pathRoot = property.root; - hash[boundOption] = Ember._SimpleHandlebarsView.prototype.normalizedValue.call(bindView); - } - - for (loc = 0; loc < numProperties; ++loc) { - property = normalizedProperties[loc]; - bindView.path = property.path; - bindView.pathRoot = property.root; - args.push(Ember._SimpleHandlebarsView.prototype.normalizedValue.call(bindView)); - } - args.push(options); - return fn.apply(context, args); - }; - - view.appendChild(bindView); - - // Assemble list of watched properties that'll re-render this helper. - watchedProperties = []; - for (boundOption in boundOptions) { - if (boundOptions.hasOwnProperty(boundOption)) { - watchedProperties.push(normalizePath(context, boundOptions[boundOption], data)); - } - } - watchedProperties = watchedProperties.concat(normalizedProperties); - - // Observe each property. - for (loc = 0, len = watchedProperties.length; loc < len; ++loc) { - property = watchedProperties[loc]; - view.registerObserver(property.root, property.path, bindView, bindView.rerender); - } - -} - /** @private @@ -21039,7 +24549,7 @@ function evaluateUnboundHelper(context, fn, normalizedProperties, options) { for(loc = 0, len = normalizedProperties.length; loc < len; ++loc) { property = normalizedProperties[loc]; - args.push(Ember.Handlebars.get(context, property.path, options)); + args.push(Ember.Handlebars.get(property.root, property.path, options)); } args.push(options); return fn.apply(context, args); @@ -21067,19 +24577,19 @@ Ember.Handlebars.template = function(spec) { (function() { /** - * Mark a string as safe for unescaped output with Handlebars. If you - * return HTML from a Handlebars helper, use this function to - * ensure Handlebars does not escape the HTML. - * - * ```javascript - * Ember.String.htmlSafe('
someString
') - * ``` - * - * @method htmlSafe - * @for Ember.String - * @static - * @return {Handlebars.SafeString} a string that will not be html escaped by Handlebars - */ + Mark a string as safe for unescaped output with Handlebars. If you + return HTML from a Handlebars helper, use this function to + ensure Handlebars does not escape the HTML. + + ```javascript + Ember.String.htmlSafe('
someString
') + ``` + + @method htmlSafe + @for Ember.String + @static + @return {Handlebars.SafeString} a string that will not be html escaped by Handlebars +*/ Ember.String.htmlSafe = function(str) { return new Handlebars.SafeString(str); }; @@ -21089,18 +24599,18 @@ var htmlSafe = Ember.String.htmlSafe; if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { /** - * Mark a string as being safe for unescaped output with Handlebars. - * - * ```javascript - * '
someString
'.htmlSafe() - * ``` - * - * See `Ember.String.htmlSafe`. - * - * @method htmlSafe - * @for String - * @return {Handlebars.SafeString} a string that will not be html escaped by Handlebars - */ + Mark a string as being safe for unescaped output with Handlebars. + + ```javascript + '
someString
'.htmlSafe() + ``` + + See [Ember.String.htmlSafe](/api/classes/Ember.String.html#method_htmlSafe). + + @method htmlSafe + @for String + @return {Handlebars.SafeString} a string that will not be html escaped by Handlebars + */ String.prototype.htmlSafe = function() { return htmlSafe(this); }; @@ -21280,6 +24790,8 @@ function SimpleHandlebarsView(path, pathRoot, isEscaped, templateData) { this.morph = Metamorph(); this.state = 'preRender'; this.updateId = null; + this._parentView = null; + this.buffer = null; } Ember._SimpleHandlebarsView = SimpleHandlebarsView; @@ -21293,7 +24805,11 @@ SimpleHandlebarsView.prototype = { Ember.run.cancel(this.updateId); this.updateId = null; } + if (this._parentView) { + this._parentView.removeChild(this); + } this.morph = null; + this.state = 'destroyed'; }, propertyWillChange: Ember.K, @@ -21348,7 +24864,7 @@ SimpleHandlebarsView.prototype = { rerender: function() { switch(this.state) { case 'preRender': - case 'destroying': + case 'destroyed': break; case 'inBuffer': throw new Ember.Error("Something you did tried to replace an {{expression}} before it was inserted into the DOM."); @@ -21677,16 +25193,16 @@ function bind(property, options, preserveContext, shouldDisplay, valueNormalizer } } -function simpleBind(property, options) { +function simpleBind(currentContext, property, options) { var data = options.data, view = data.view, - currentContext = this, - normalized, observer; + normalized, observer, pathRoot, output; normalized = normalizePath(currentContext, property, data); + pathRoot = normalized.root; // Set up observers for observable objects - if ('object' === typeof this) { + if (pathRoot && ('object' === typeof pathRoot)) { if (data.insideGroup) { observer = function() { Ember.run.once(view, 'rerender'); @@ -21718,7 +25234,8 @@ function simpleBind(property, options) { } else { // The object is not observable, so just render it out and // be done with it. - data.buffer.push(handlebarsGet(currentContext, property, options)); + output = handlebarsGet(currentContext, property, options); + data.buffer.push((output === null || typeof output === 'undefined') ? '' : output); } } @@ -21775,10 +25292,10 @@ EmberHandlebars.registerHelper('_triageMustache', function(property, fn) { EmberHandlebars.registerHelper('bind', function(property, options) { Ember.assert("You cannot pass more than one argument to the bind helper", arguments.length <= 2); - var context = (options.contexts && options.contexts[0]) || this; + var context = (options.contexts && options.contexts.length) ? options.contexts[0] : this; if (!options.fn) { - return simpleBind.call(context, property, options); + return simpleBind(context, property, options); } return bind.call(context, property, options, false, exists); @@ -21803,7 +25320,7 @@ EmberHandlebars.registerHelper('bind', function(property, options) { @return {String} HTML string */ EmberHandlebars.registerHelper('boundIf', function(property, fn) { - var context = (fn.contexts && fn.contexts[0]) || this; + var context = (fn.contexts && fn.contexts.length) ? fn.contexts[0] : this; var func = function(result) { var truthy = result && get(result, 'isTruthy'); if (typeof truthy === 'boolean') { return truthy; } @@ -21863,7 +25380,7 @@ EmberHandlebars.registerHelper('with', function(context, options) { /** - See `boundIf` + See [boundIf](/api/classes/Ember.Handlebars.helpers.html#method_boundIf) @method if @for Ember.Handlebars.helpers @@ -21898,11 +25415,11 @@ EmberHandlebars.registerHelper('unless', function(context, options) { }); /** - `bindAttr` allows you to create a binding between DOM element attributes and + `bind-attr` allows you to create a binding between DOM element attributes and Ember objects. For example: ```handlebars - imageTitle + imageTitle ``` The above handlebars template will fill the ``'s `src` attribute will @@ -21924,17 +25441,17 @@ EmberHandlebars.registerHelper('unless', function(context, options) { A humorous image of a cat ``` - `bindAttr` cannot redeclare existing DOM element attributes. The use of `src` - in the following `bindAttr` example will be ignored and the hard coded value + `bind-attr` cannot redeclare existing DOM element attributes. The use of `src` + in the following `bind-attr` example will be ignored and the hard coded value of `src="/failwhale.gif"` will take precedence: ```handlebars - imageTitle + imageTitle ``` - ### `bindAttr` and the `class` attribute + ### `bind-attr` and the `class` attribute - `bindAttr` supports a special syntax for handling a number of cases unique + `bind-attr` supports a special syntax for handling a number of cases unique to the `class` DOM element attribute. The `class` attribute combines multiple discreet values into a single attribute as a space-delimited list of strings. Each string can be: @@ -21943,7 +25460,7 @@ EmberHandlebars.registerHelper('unless', function(context, options) { * a boolean return value of an object's property * a hard-coded value - A string return value works identically to other uses of `bindAttr`. The + A string return value works identically to other uses of `bind-attr`. The return value of the property will become the value of the attribute. For example, the following view and template: @@ -21956,7 +25473,7 @@ EmberHandlebars.registerHelper('unless', function(context, options) { ``` ```handlebars - ``` Result in the following rendered output: @@ -21978,7 +25495,7 @@ EmberHandlebars.registerHelper('unless', function(context, options) { ``` ```handlebars - + ``` Result in the following rendered output: @@ -21992,14 +25509,14 @@ EmberHandlebars.registerHelper('unless', function(context, options) { value changes: ```handlebars - + ``` A hard-coded value can be used by prepending `:` to the desired class name: `:class-name-to-always-apply`. ```handlebars - + ``` Results in the following rendered output: @@ -22012,19 +25529,19 @@ EmberHandlebars.registerHelper('unless', function(context, options) { hard-coded value – can be combined in a single declaration: ```handlebars - + ``` - @method bindAttr + @method bind-attr @for Ember.Handlebars.helpers @param {Hash} options @return {String} HTML string */ -EmberHandlebars.registerHelper('bindAttr', function(options) { +EmberHandlebars.registerHelper('bind-attr', function(options) { var attrs = options.hash; - Ember.assert("You must specify at least one hash argument to bindAttr", !!Ember.keys(attrs).length); + Ember.assert("You must specify at least one hash argument to bind-attr", !!Ember.keys(attrs).length); var view = options.data.view; var ret = []; @@ -22086,7 +25603,7 @@ EmberHandlebars.registerHelper('bindAttr', function(options) { // When the observer fires, find the element using the // unique data id and update the attribute to the new value. // Note: don't add observer when path is 'this' or path - // is whole keyword e.g. {{#each x in list}} ... {{bindAttr attr="x"}} + // is whole keyword e.g. {{#each x in list}} ... {{bind-attr attr="x"}} if (path !== 'this' && !(normalized.isKeyword && normalized.path === '' )) { view.registerObserver(normalized.root, normalized.path, observer); } @@ -22106,6 +25623,18 @@ EmberHandlebars.registerHelper('bindAttr', function(options) { return new EmberHandlebars.SafeString(ret.join(' ')); }); +/** + See `bind-attr` + + @method bindAttr + @for Ember.Handlebars.helpers + @deprecated + @param {Function} context + @param {Hash} options + @return {String} HTML string +*/ +EmberHandlebars.registerHelper('bindAttr', EmberHandlebars.helpers['bind-attr']); + /** @private @@ -22241,6 +25770,35 @@ var EmberHandlebars = Ember.Handlebars; var LOWERCASE_A_Z = /^[a-z]/; var VIEW_PREFIX = /^view\./; +function makeBindings(thisContext, options) { + var hash = options.hash, + hashType = options.hashTypes; + + for (var prop in hash) { + if (hashType[prop] === 'ID') { + + var value = hash[prop]; + + if (Ember.IS_BINDING.test(prop)) { + Ember.warn("You're attempting to render a view by passing " + prop + "=" + value + " to a view helper, but this syntax is ambiguous. You should either surround " + value + " in quotes or remove `Binding` from " + prop + "."); + } else { + hash[prop + 'Binding'] = value; + hashType[prop + 'Binding'] = 'STRING'; + delete hash[prop]; + delete hashType[prop]; + } + } + } + + if (hash.hasOwnProperty('idBinding')) { + // id can't be bound, so just perform one-time lookup. + hash.id = EmberHandlebars.get(thisContext, hash.idBinding, options); + hashType.id = 'STRING'; + delete hash.idBinding; + delete hashType.idBinding; + } +} + EmberHandlebars.ViewHelper = Ember.Object.create({ propertiesFromHTMLOptions: function(options, thisContext) { @@ -22350,6 +25908,8 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ fn = options.fn, newView; + makeBindings(thisContext, options); + if ('string' === typeof path) { // TODO: this is a lame conditional, this should likely change @@ -22585,8 +26145,8 @@ var get = Ember.get, handlebarsGet = Ember.Handlebars.get, fmt = Ember.String.fm /** `{{collection}}` is a `Ember.Handlebars` helper for adding instances of - `Ember.CollectionView` to a template. See `Ember.CollectionView` for - additional information on how a `CollectionView` functions. + `Ember.CollectionView` to a template. See [Ember.CollectionView](/api/classes/Ember.CollectionView.html) + for additional information on how a `CollectionView` functions. `{{collection}}`'s primary use is as a block helper with a `contentBinding` option pointing towards an `Ember.Array`-compatible object. An `Ember.View` @@ -22840,7 +26400,7 @@ Ember.Handlebars.registerHelper('unbound', function(property, fn) { return out; } - context = (fn.contexts && fn.contexts[0]) || this; + context = (fn.contexts && fn.contexts.length) ? fn.contexts[0] : this; return handlebarsGet(context, property, fn); }); @@ -22870,7 +26430,7 @@ var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.norma @param {String} property */ Ember.Handlebars.registerHelper('log', function(property, options) { - var context = (options.contexts && options.contexts[0]) || this, + var context = (options.contexts && options.contexts.length) ? options.contexts[0] : this, normalized = normalizePath(context, property, options.data), pathRoot = normalized.root, path = normalized.path, @@ -23039,6 +26599,8 @@ GroupedEach.prototype = { }, addArrayObservers: function() { + if (!this.content) { return; } + this.content.addArrayObserver(this, { willChange: 'contentArrayWillChange', didChange: 'contentArrayDidChange' @@ -23046,6 +26608,8 @@ GroupedEach.prototype = { }, removeArrayObservers: function() { + if (!this.content) { return; } + this.content.removeArrayObserver(this, { willChange: 'contentArrayWillChange', didChange: 'contentArrayDidChange' @@ -23063,6 +26627,8 @@ GroupedEach.prototype = { }, render: function() { + if (!this.content) { return; } + var content = this.content, contentLength = get(content, 'length'), data = this.options.data, @@ -23075,12 +26641,21 @@ GroupedEach.prototype = { }, rerenderContainingView: function() { - Ember.run.scheduleOnce('render', this.containingView, 'rerender'); + var self = this; + Ember.run.scheduleOnce('render', this, function() { + // It's possible it's been destroyed after we enqueued a re-render call. + if (!self.destroyed) { + self.containingView.rerender(); + } + }); }, destroy: function() { this.removeContentObservers(); - this.removeArrayObservers(); + if (this.content) { + this.removeArrayObservers(); + } + this.destroyed = true; } }; @@ -23204,6 +26779,49 @@ GroupedEach.prototype = { Each itemController will receive a reference to the current controller as a `parentController` property. + ### (Experimental) Grouped Each + + When used in conjunction with the experimental [group helper](https://github.com/emberjs/group-helper), + you can inform Handlebars to re-render an entire group of items instead of + re-rendering them one at a time (in the event that they are changed en masse + or an item is added/removed). + + ```handlebars + {{#group}} + {{#each people}} + {{firstName}} {{lastName}} + {{/each}} + {{/group}} + ``` + + This can be faster than the normal way that Handlebars re-renders items + in some cases. + + If for some reason you have a group with more than one `#each`, you can make + one of the collections be updated in normal (non-grouped) fashion by setting + the option `groupedRows=true` (counter-intuitive, I know). + + For example, + + ```handlebars + {{dealershipName}} + + {{#group}} + {{#each dealers}} + {{firstName}} {{lastName}} + {{/each}} + + {{#each car in cars groupedRows=true}} + {{car.make}} {{car.model}} {{car.color}} + {{/each}} + {{/group}} + ``` + Any change to `dealershipName` or the `dealers` collection will cause the + entire group to be re-rendered. However, changes to the `cars` collection + will be re-rendered individually (as normal). + + Note that `group` behavior is also disabled by specifying an `itemViewClass`. + @method each @for Ember.Handlebars.helpers @param [name] {String} name for item (used with `in`) @@ -23211,6 +26829,7 @@ GroupedEach.prototype = { @param [options] {Object} Handlebars key/value pairs of options @param [options.itemViewClass] {String} a path to a view class used for each item @param [options.itemController] {String} name of a controller to be created for each item + @param [options.groupedRows] {boolean} enable normal item-by-item rendering when inside a `#group` helper */ Ember.Handlebars.registerHelper('each', function(path, options) { if (arguments.length === 4) { @@ -23367,6 +26986,10 @@ Ember.Handlebars.registerHelper('partial', function(name, options) { var get = Ember.get, set = Ember.set; /** + `{{yield}}` denotes an area of a template that will be rendered inside + of another template. It has two main uses: + + ### Use with `layout` When used in a Handlebars template that is assigned to an `Ember.View` instance's `layout` property Ember will render the layout template first, inserting the view's own rendered output at the `{{yield}}` location. @@ -23409,7 +27032,34 @@ var get = Ember.get, set = Ember.set; bView.appendTo('body'); // throws - // Uncaught Error: assertion failed: You called yield in a template that was not a layout + // Uncaught Error: assertion failed: + // You called yield in a template that was not a layout + ``` + + ### Use with Ember.Component + When designing components `{{yield}}` is used to denote where, inside the component's + template, an optional block passed to the component should render: + + ```handlebars + + {{#labeled-textfield value=someProperty}} + First name: + {{/my-component}} + ``` + + ```handlebars + + + ``` + + Result: + + ```html +