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