diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index f242a726c83..ff5a6fc288d 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -67,6 +67,7 @@ App.Person = DS.Model.extend({ lastName: DS.attr('string', { key: 'last_name' }), middleName: DS.attr('string', { key: 'middle_name' }) }); +``` This obviously got very annoying very fast. diff --git a/README.md b/README.md index 68fd33c58f8..33aff3cfc8a 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ No. Breaking changes, indexed by date, are listed in * Handle error states * Better built-in attributes * Editing "forked" records and rolling back transactions -* Out-of-the-box support for Rails apps that follow the `active_model_serializers` gem's conventions. +* Out-of-the-box support for Rails apps that follow the + [`active_model_serializers`](https://github.com/josevalim/active_model_serializers) gem's conventions. * Handle partially-loaded records ### Creating a Store @@ -35,23 +36,23 @@ not yet been loaded. ```javascript App.store = DS.Store.create({ - revision: 2 + revision: 3 }); ``` > NOTE: The revision property is used by `ember-data` to notify you of > breaking changes to the public API before 1.0. For new applications, > just set the revision to this number. See -> [BREAKING CHANGES](https://github.com/emberjs/data/blob/master/BREAKING_CHANGES.md) +> [BREAKING_CHANGES.md](https://github.com/emberjs/data/blob/master/BREAKING_CHANGES.md) > for more information. - + You can tell the store how to talk to your backend by specifying an *adapter*. Ember Data comes with a RESTful JSON API adapter. You can specify this adapter by setting the `adapter` property: ```javascript App.store = DS.Store.create({ - revision: 2, + revision: 3, adapter: DS.RESTAdapter.create({ bulkCommit: false }) }); ``` @@ -93,7 +94,7 @@ App.Person = DS.Model.extend({ }); ``` -Valid attribute types are `string`, `integer`, `boolean`, and `date`. You +Valid attribute types are `string`, `number`, `boolean`, and `date`. You can also register custom attribute types. For example, here's a `boolString` attribute type that converts booleans into the string `"Y"` or `"N"`: @@ -103,7 +104,7 @@ DS.attr.transforms.boolString: { if (serialized === 'Y') { return true; } - + return false; }, @@ -223,16 +224,21 @@ structure is as follows: ```javascript // Author { - "id": 1, - "name": "Tom Dale" + "author": { + "id": 1, + "name": "Tom Dale" + } + } } // Profile { - "id": 1, - "about": "Tom Dale is a software engineer that drinks too much beer.", - "postCount": 1984, - "author_id": 1 + "profile": { + "id": 1, + "about": "Tom Dale is a software engineer that drinks too much beer.", + "postCount": 1984, + "author_id": 1 + } } ``` @@ -255,9 +261,11 @@ As a performance optimization, the REST API can return the ID of the ```javascript // Author with included Profile id { - "id": 1, - "name": "Tom Dale", - "profile_id": 1 + "author": { + "id": 1, + "name": "Tom Dale", + "profile_id": 1 + } } ``` @@ -311,21 +319,23 @@ entirety of the association above like this: } ``` -However, imagine the JSON returned from the server for a Person looked like this: +However, imagine the JSON returned from the server for a `Person` looked like this: ```javascript { - "id": 1, - "name": "Tom Dale", - "tags": [{ + "person": { "id": 1, - "name": "good-looking" - }, + "name": "Tom Dale", + "tags": [{ + "id": 1, + "name": "good-looking" + }, - { - "id": 2, - "name": "not-too-bright" - }] + { + "id": 2, + "name": "not-too-bright" + }] + } } ``` @@ -340,13 +350,15 @@ App.Person = DS.Model.extend({ ``` It is also possible to change the data attribute that an association is mapped -to. Suppose the JSON for a person looked like this: +to. Suppose the JSON for a `Person` looked like this: ```javascript { + "person": { "id": 2, "name": "Carsten Nielsen", "tag_ids": [1, 2] + } } ``` @@ -544,7 +556,7 @@ To tell your store which adapter to use, set its `adapter` property: ```javascript App.store = DS.Store.create({ - revision: 2, + revision: 3, adapter: App.adapter }); ``` @@ -606,7 +618,7 @@ DS.Adapter.create({ findMany: function(store, type, ids) { var url = type.url; url = url.fmt(ids.join(',')); - + jQuery.getJSON(url, function(data) { // data is an Array of Hashes in the same order as the original // Array of IDs. If your server returns a root, simply do something @@ -653,7 +665,7 @@ App.Person.reopenClass({ DS.Adapter.create({ findQuery: function(store, type, query, modelArray) { var url = type.collectionUrl; - + jQuery.getJSON(url, query, function(data) { // data is expected to be an Array of Hashes, in an order // determined by the server. This order may be specified in @@ -681,7 +693,7 @@ Invoked when `findAll()` is called on the store. If you do nothing, only models that have already been loaded will be included in the results. Otherwise, this is your opportunity to load any unloaded records of this type. The implementation is similar to findMany(); see above for an example. - + ### createRecord() When `commit()` is called on the store and there are records that need to be @@ -708,7 +720,7 @@ DS.Adapter.create({ data: model.get('data'), dataType: 'json', type: 'POST', - + success: function(data) { // data is a hash of key/value pairs representing the record. // In general, this hash will contain a new id, which the @@ -739,7 +751,7 @@ DS.Adapter.create({ data: array.mapProperty('data'), dataType: 'json', type: 'POST', - + success: function(data) { // data is an array of hashes in the same order as // the original records that were sent. @@ -769,7 +781,7 @@ DS.Adapter.create({ url: url.fmt(model.get('id')), dataType: 'json', type: 'PUT', - + success: function(data) { // data is a hash of key/value pairs representing the record // in its current state on the server. @@ -797,7 +809,7 @@ DS.Adapter.create({ data: array.mapProperty('data'), dataType: 'json', type: 'PUT', - + success: function(data) { // data is an array of hashes in the same order as // the original records that were sent. @@ -827,7 +839,7 @@ DS.Adapter.create({ url: url.fmt(model.get('id')), dataType: 'json', type: 'DELETE', - + success: function() { store.didDeleteRecord(model); } @@ -853,7 +865,7 @@ DS.Adapter.create({ data: array.mapProperty('data'), dataType: 'json', type: 'DELETE', - + success: function(data) { store.didDeleteRecords(array); } @@ -889,8 +901,8 @@ commit: function(store, commitDetails) { ### Connecting to Views -Ember Data will always return records or arrays of records of a certain type -immediately, even though the underlying JSON objects have not yet been returned +Ember Data will always return records or arrays of records of a certain type +immediately, even though the underlying JSON objects have not yet been returned from the server. In general, this means that you can insert them into the DOM using Ember's diff --git a/packages/ember-data/lib/system/model/attributes.js b/packages/ember-data/lib/system/model/attributes.js index 1071afe45d4..57b0b38c220 100644 --- a/packages/ember-data/lib/system/model/attributes.js +++ b/packages/ember-data/lib/system/model/attributes.js @@ -16,7 +16,7 @@ DS.Model.reopenClass({ processAttributeKeys: function() { if (this.processedAttributeKeys) { return; } - var namingConvention = getPath(this, 'proto.namingConvention'); + var namingConvention = this.proto().namingConvention; this.eachComputedProperty(function(name, meta) { if (meta.isAttribute && !meta.options.key) { diff --git a/packages/ember-data/lib/system/model/states.js b/packages/ember-data/lib/system/model/states.js index 4690a97ac0e..11758f651e8 100644 --- a/packages/ember-data/lib/system/model/states.js +++ b/packages/ember-data/lib/system/model/states.js @@ -512,7 +512,7 @@ var states = { // A record is in this state if it has already been // saved to the server, but there are new local changes // that have not yet been saved. - updated: updatedState, + updated: updatedState }), // A record is in this state if it was deleted from the store. diff --git a/packages/ember-data/lib/system/store.js b/packages/ember-data/lib/system/store.js index caffec4e7fc..c322a43c29b 100644 --- a/packages/ember-data/lib/system/store.js +++ b/packages/ember-data/lib/system/store.js @@ -477,7 +477,7 @@ DS.Store = Ember.Object.extend({ didCreateRecords: function(type, array, hashes) { - var primaryKey = getPath(type, 'proto.primaryKey'), + var primaryKey = type.proto().primaryKey, typeMap = this.typeMapFor(type), id, clientId; @@ -497,7 +497,7 @@ DS.Store = Ember.Object.extend({ // The hash is optional, but if it is not provided, the client must have // provided a primary key. - primaryKey = getPath(type, 'proto.primaryKey'); + primaryKey = type.proto().primaryKey; // TODO: Make ember_assert more flexible and convert this into an ember_assert if (hash) { @@ -670,7 +670,7 @@ DS.Store = Ember.Object.extend({ load: function(type, id, hash) { if (hash === undefined) { hash = id; - var primaryKey = getPath(type, 'proto.primaryKey'); + var primaryKey = type.proto().primaryKey; ember_assert("A data hash was loaded for a model of type " + type.toString() + " but no primary key '" + primaryKey + "' was provided.", primaryKey in hash); id = hash[primaryKey]; } @@ -703,7 +703,7 @@ DS.Store = Ember.Object.extend({ if (hashes === undefined) { hashes = ids; ids = []; - var primaryKey = getPath(type, 'proto.primaryKey'); + var primaryKey = type.proto().primaryKey; ids = Ember.ArrayUtils.map(hashes, function(hash) { return hash[primaryKey]; diff --git a/packages/ember/lib/main.js b/packages/ember/lib/main.js index 668eca15c04..85fbb23d4fc 100644 --- a/packages/ember/lib/main.js +++ b/packages/ember/lib/main.js @@ -1,7 +1,5 @@ - (function(exports) { /*global __fail__*/ - /** Define an assertion that will throw an exception if the condition is not met. Ember build tools will remove any calls to ember_assert() when @@ -58,8 +56,6 @@ window.ember_warn = function(message, test) { if (!test) console.warn("WARNING: "+message); }; - - /** Display a deprecation warning with the provided message and a stack trace (Chrome and Firefox only). Ember build tools will remove any calls to @@ -75,10 +71,14 @@ window.ember_warn = function(message, test) { will be displayed. */ window.ember_deprecate = function(message, test) { + if (Ember.TESTING_DEPRECATION) { return; } + if (arguments.length === 1) { test = false; } if ('function' === typeof test) { test = test()!==false; } if (test) { return; } + if (Ember.ENV.RAISE_ON_DEPRECATION) { throw new Error(message); } + var error, stackStr = ''; // When using new Error, we can't do the arguments check for Chrome. Alternatives are welcome @@ -1863,7 +1863,6 @@ Ember.Logger = window.console || { log: Ember.K, warn: Ember.K, error: Ember.K } })({}); - (function(exports) { // ========================================================================== // Project: Ember Metal @@ -1888,6 +1887,7 @@ var platform = Ember.platform = {} ; platform.create = Object.create; if (!platform.create) { + /** @private */ var O_ctor = function() {}, O_proto = O_ctor.prototype; @@ -1909,13 +1909,16 @@ if (!platform.create) { platform.create.isSimulated = true; } -var defineProperty = Object.defineProperty, canRedefineProperties, canDefinePropertyOnDOM; +/** @private */ +var defineProperty = Object.defineProperty; +var canRedefineProperties, canDefinePropertyOnDOM; // Catch IE8 where Object.defineProperty exists but only works on DOM elements if (defineProperty) { try { defineProperty({}, 'a',{get:function(){}}); } catch (e) { + /** @private */ defineProperty = null; } } @@ -1923,6 +1926,7 @@ if (defineProperty) { if (defineProperty) { // Detects a bug in Android <3.2 where you cannot redefine a property using // Object.defineProperty once accessors have already been set. + /** @private */ canRedefineProperties = (function() { var obj = {}; @@ -1945,10 +1949,10 @@ if (defineProperty) { // This is for Safari 5.0, which supports Object.defineProperty, but not // on DOM nodes. - + /** @private */ canDefinePropertyOnDOM = (function(){ try { - defineProperty(document.body, 'definePropertyOnDOM', {}); + defineProperty(document.createElement('div'), 'definePropertyOnDOM', {}); return true; } catch(e) { } @@ -1956,8 +1960,10 @@ if (defineProperty) { })(); if (!canRedefineProperties) { + /** @private */ defineProperty = null; } else if (!canDefinePropertyOnDOM) { + /** @private */ defineProperty = function(obj, keyName, desc){ var isNode; @@ -2011,7 +2017,6 @@ if (!platform.defineProperty) { })({}); - (function(exports) { // ========================================================================== // Project: Ember Metal @@ -2030,7 +2035,7 @@ uuid = 0; numberCache = []; stringCache = {}; -var GUID_DESC = { +var GUID_DESC = Ember.GUID_DESC = { configurable: true, writable: true, enumerable: false @@ -2161,7 +2166,7 @@ var EMPTY_META = { if (Object.freeze) Object.freeze(EMPTY_META); -var createMeta = Ember.platform.defineProperty.isSimulated ? o_create : function(meta) { return meta; }; +var createMeta = Ember.platform.defineProperty.isSimulated ? o_create : (function(meta) { return meta; }); /** @private @@ -2211,6 +2216,8 @@ Ember.meta = function meta(obj, writable) { ret.lastSetValues = {}; ret.cache = {}; ret.source = obj; + + o_defineProperty(obj, META_KEY, META_DESC); ret = obj[META_KEY] = createMeta(ret); } return ret; @@ -2352,7 +2359,6 @@ Ember.makeArray = function(obj) { })({}); - (function(exports) { // ========================================================================== // Project: Ember Metal @@ -2407,6 +2413,7 @@ if (!USE_ACCESSORS) { var o_get = get, o_set = set; + /** @private */ get = function(obj, keyName) { if (keyName === undefined && 'string' === typeof obj) { keyName = obj; @@ -2421,6 +2428,7 @@ if (!USE_ACCESSORS) { else return o_get(obj, keyName); }; + /** @private */ set = function(obj, keyName, value) { ember_assert("You need to provide an object and key to `set`.", !!obj && keyName !== undefined); var desc = meta(obj, false).descs[keyName]; @@ -2496,6 +2504,7 @@ Ember.set = set; // PATHS // +/** @private */ function normalizePath(path) { ember_assert('must pass non-empty string to normalizePath()', path && path!==''); @@ -2507,6 +2516,7 @@ function normalizePath(path) { } // assumes normalized input; no *, normalized path, always a target... +/** @private */ function getPath(target, path) { var len = path.length, idx, next, key; @@ -2535,11 +2545,13 @@ var IS_GLOBAL_PATH = /^([A-Z$]|([0-9][A-Z$])).*[\.\*]/; var HAS_THIS = /^this[\.\*]/; var FIRST_KEY = /^([^\.\*]+)/; +/** @private */ function firstKey(path) { return path.match(FIRST_KEY)[0]; } // assumes path is already normalized +/** @private */ function normalizeTuple(target, path) { var hasThis = HAS_THIS.test(path), isGlobal = !hasThis && IS_GLOBAL_PATH.test(path), @@ -2736,631 +2748,327 @@ Ember.isGlobalPath = function(path) { })({}); - (function(exports) { -/*jshint newcap:true*/ +// ========================================================================== +// Project: Ember Metal +// Copyright: ©2011 Strobe Inc. and contributors. +// License: Licensed under MIT license (see license.js) +// ========================================================================== +/*globals ember_assert */ +var USE_ACCESSORS = Ember.USE_ACCESSORS; +var GUID_KEY = Ember.GUID_KEY; +var META_KEY = Ember.META_KEY; +var meta = Ember.meta; +var o_create = Ember.platform.create; +var o_defineProperty = Ember.platform.defineProperty; +var SIMPLE_PROPERTY, WATCHED_PROPERTY; -// Testing this is not ideal, but we want ArrayUtils to use native functions -// if available, but not to use versions created by libraries like Prototype -var isNativeFunc = function(func) { - // This should probably work in all browsers likely to have ES5 array methods - return func && Function.prototype.toString.call(func).indexOf('[native code]') > -1; -}; +// .......................................................... +// DESCRIPTOR +// -// From: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/map -var arrayMap = isNativeFunc(Array.prototype.map) ? Array.prototype.map : function(fun /*, thisp */) { - "use strict"; +var SIMPLE_DESC = { + writable: true, + configurable: true, + enumerable: true, + value: null +}; - if (this === void 0 || this === null) { - throw new TypeError(); - } +/** + @private + @constructor - var t = Object(this); - var len = t.length >>> 0; - if (typeof fun !== "function") { - throw new TypeError(); - } + Objects of this type can implement an interface to responds requests to + get and set. The default implementation handles simple properties. - var res = new Array(len); - var thisp = arguments[1]; - for (var i = 0; i < len; i++) { - if (i in t) { - res[i] = fun.call(thisp, t[i], i, t); - } - } + You generally won't need to create or subclass this directly. +*/ +var Dc = Ember.Descriptor = function() {}; - return res; +var setup = Dc.setup = function(obj, keyName, value) { + SIMPLE_DESC.value = value; + o_defineProperty(obj, keyName, SIMPLE_DESC); + SIMPLE_DESC.value = null; }; -// From: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/foreach -var arrayForEach = isNativeFunc(Array.prototype.forEach) ? Array.prototype.forEach : function(fun /*, thisp */) { - "use strict"; +var Dp = Ember.Descriptor.prototype; - if (this === void 0 || this === null) { - throw new TypeError(); - } +/** + Called whenever we want to set the property value. Should set the value + and return the actual set value (which is usually the same but may be + different in the case of computed properties.) - var t = Object(this); - var len = t.length >>> 0; - if (typeof fun !== "function") { - throw new TypeError(); - } + @param {Object} obj + The object to set the value on. - var thisp = arguments[1]; - for (var i = 0; i < len; i++) { - if (i in t) { - fun.call(thisp, t[i], i, t); - } - } -}; + @param {String} keyName + The key to set. -var arrayIndexOf = isNativeFunc(Array.prototype.indexOf) ? Array.prototype.indexOf : function (obj, fromIndex) { - if (fromIndex === null || fromIndex === undefined) { fromIndex = 0; } - else if (fromIndex < 0) { fromIndex = Math.max(0, this.length + fromIndex); } - for (var i = fromIndex, j = this.length; i < j; i++) { - if (this[i] === obj) { return i; } - } - return -1; -}; + @param {Object} value + The new value + @returns {Object} value actual set value +*/ +Dp.set = function(obj, keyName, value) { + obj[keyName] = value; + return value; +}; -Ember.ArrayUtils = { - map: function(obj) { - var args = Array.prototype.slice.call(arguments, 1); - return obj.map ? obj.map.apply(obj, args) : arrayMap.apply(obj, args); - }, +/** + Called whenever we want to get the property value. Should retrieve the + current value. - forEach: function(obj) { - var args = Array.prototype.slice.call(arguments, 1); - return obj.forEach ? obj.forEach.apply(obj, args) : arrayForEach.apply(obj, args); - }, + @param {Object} obj + The object to get the value on. - indexOf: function(obj) { - var args = Array.prototype.slice.call(arguments, 1); - return obj.indexOf ? obj.indexOf.apply(obj, args) : arrayIndexOf.apply(obj, args); - } -} + @param {String} keyName + The key to retrieve + @returns {Object} the current value +*/ +Dp.get = function(obj, keyName) { + return w_get(obj, keyName, obj); +}; -if (Ember.SHIM_ES5) { - if (!Array.prototype.map) { - Array.prototype.map = arrayMap; - } +/** + This is called on the descriptor to set it up on the object. The + descriptor is responsible for actually defining the property on the object + here. - if (!Array.prototype.forEach) { - Array.prototype.forEach = arrayForEach; - } + The passed `value` is the transferValue returned from any previous + descriptor. - if (!Array.prototype.indexOf) { - Array.prototype.indexOf = arrayIndexOf; - } -} + @param {Object} obj + The object to set the value on. -})({}); + @param {String} keyName + The key to set. + @param {Object} value + The transfer value from any previous descriptor. -(function(exports) { -// ========================================================================== -// Project: Ember Metal -// Copyright: ©2011 Strobe Inc. and contributors. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -var AFTER_OBSERVERS = ':change'; -var BEFORE_OBSERVERS = ':before'; -var guidFor = Ember.guidFor; -var normalizePath = Ember.normalizePath; + @returns {void} +*/ +Dp.setup = setup; -var suspended = 0; -var array_Slice = Array.prototype.slice; -var array_ForEach = Ember.ArrayUtils.forEach; +/** + This is called on the descriptor just before another descriptor takes its + place. This method should at least return the 'transfer value' of the + property - which is the value you want to passed as the input to the new + descriptor's setup() method. -var ObserverSet = function(iterateable) { - this.set = {}; - if (iterateable) { this.array = []; } -}; + It is not generally necessary to actually 'undefine' the property as a new + property descriptor will redefine it immediately after this method returns. -ObserverSet.prototype.add = function(target, name) { - var set = this.set, guid = Ember.guidFor(target), array; + @param {Object} obj + The object to set the value on. - if (!set[guid]) { set[guid] = {}; } - set[guid][name] = true; - if (array = this.array) { - array.push([target, name]); - } -}; + @param {String} keyName + The key to set. -ObserverSet.prototype.contains = function(target, name) { - var set = this.set, guid = Ember.guidFor(target), nameSet = set[guid]; - return nameSet && nameSet[name]; + @returns {Object} transfer value +*/ +Dp.teardown = function(obj, keyName) { + return obj[keyName]; }; -ObserverSet.prototype.empty = function() { - this.set = {}; - this.array = []; +Dp.val = function(obj, keyName) { + return obj[keyName]; }; -ObserverSet.prototype.forEach = function(fn) { - var q = this.array; - this.empty(); - array_ForEach(q, function(item) { - fn(item[0], item[1]); - }); -}; +// .......................................................... +// SIMPLE AND WATCHED PROPERTIES +// -var queue = new ObserverSet(true), beforeObserverSet = new ObserverSet(); +// if accessors are disabled for the app then this will act as a guard when +// testing on browsers that do support accessors. It will throw an exception +// if you do foo.bar instead of Ember.get(foo, 'bar') -function notifyObservers(obj, eventName, forceNotification) { - if (suspended && !forceNotification) { +// The exception to this is that any objects managed by Ember but not a descendant +// of Ember.Object will not throw an exception, instead failing silently. This +// prevent errors with other libraries that may attempt to access special +// properties on standard objects like Array. Usually this happens when copying +// an object by looping over all properties. - // if suspended add to the queue to send event later - but only send - // event once. - if (!queue.contains(obj, eventName)) { - queue.add(obj, eventName); +if (!USE_ACCESSORS) { + Ember.Descriptor.MUST_USE_GETTER = function() { + if (this instanceof Ember.Object) { + ember_assert('Must use Ember.get() to access this property', false); } + }; - } else { - Ember.sendEvent(obj, eventName); - } -} - -function flushObserverQueue() { - beforeObserverSet.empty(); - - if (!queue || queue.array.length===0) return ; - queue.forEach(function(target, event){ Ember.sendEvent(target, event); }); + Ember.Descriptor.MUST_USE_SETTER = function() { + if (this instanceof Ember.Object) { + if (this.isDestroyed) { + ember_assert('You cannot set observed properties on destroyed objects', false); + } else { + ember_assert('Must use Ember.set() to access this property', false); + } + } + }; } -Ember.beginPropertyChanges = function() { - suspended++; - return this; -}; - -Ember.endPropertyChanges = function() { - suspended--; - if (suspended<=0) flushObserverQueue(); +var WATCHED_DESC = { + configurable: true, + enumerable: true, + set: Ember.Descriptor.MUST_USE_SETTER }; -/** - Make a series of property changes together in an - exception-safe way. +/** @private */ +function w_get(obj, keyName, values) { + values = values || meta(obj, false).values; - Ember.changeProperties(function() { - obj1.set('foo', mayBlowUpWhenSet); - obj2.set('bar', baz); - }); -*/ -Ember.changeProperties = function(cb){ - Ember.beginPropertyChanges(); - try { - cb(); - } finally { - Ember.endPropertyChanges(); + if (values) { + var ret = values[keyName]; + if (ret !== undefined) { return ret; } + if (obj.unknownProperty) { return obj.unknownProperty(keyName); } } -}; -function changeEvent(keyName) { - return keyName+AFTER_OBSERVERS; } -function beforeEvent(keyName) { - return keyName+BEFORE_OBSERVERS; -} +/** @private */ +function w_set(obj, keyName, value) { + var m = meta(obj), watching; -function changeKey(eventName) { - return eventName.slice(0, -7); + watching = m.watching[keyName]>0 && value!==m.values[keyName]; + if (watching) Ember.propertyWillChange(obj, keyName); + m.values[keyName] = value; + if (watching) Ember.propertyDidChange(obj, keyName); + return value; } -function beforeKey(eventName) { - return eventName.slice(0, -7); +var WATCHED_GETTERS = {}; +/** @private */ +function mkWatchedGetter(keyName) { + var ret = WATCHED_GETTERS[keyName]; + if (!ret) { + ret = WATCHED_GETTERS[keyName] = function() { + return w_get(this, keyName); + }; + } + return ret; } -function xformForArgs(args) { - return function (target, method, params) { - var obj = params[0], keyName = changeKey(params[1]), val; - var copy_args = args.slice(); - if (method.length>2) { - val = Ember.getPath(Ember.isGlobalPath(keyName) ? window : obj, keyName); - } - copy_args.unshift(obj, keyName, val); - method.apply(target, copy_args); - }; +var WATCHED_SETTERS = {}; +/** @private */ +function mkWatchedSetter(keyName) { + var ret = WATCHED_SETTERS[keyName]; + if (!ret) { + ret = WATCHED_SETTERS[keyName] = function(value) { + return w_set(this, keyName, value); + }; + } + return ret; } -var xformChange = xformForArgs([]); +/** + @private -function xformBefore(target, method, params) { - var obj = params[0], keyName = beforeKey(params[1]), val; - if (method.length>2) val = Ember.getPath(obj, keyName); - method.call(target, obj, keyName, val); -} + Private version of simple property that invokes property change callbacks. +*/ +WATCHED_PROPERTY = new Ember.Descriptor(); -Ember.addObserver = function(obj, path, target, method) { - path = normalizePath(path); +if (Ember.platform.hasPropertyAccessors) { + WATCHED_PROPERTY.get = w_get ; + WATCHED_PROPERTY.set = w_set ; + + if (USE_ACCESSORS) { + WATCHED_PROPERTY.setup = function(obj, keyName, value) { + WATCHED_DESC.get = mkWatchedGetter(keyName); + WATCHED_DESC.set = mkWatchedSetter(keyName); + o_defineProperty(obj, keyName, WATCHED_DESC); + WATCHED_DESC.get = WATCHED_DESC.set = null; + if (value !== undefined) meta(obj).values[keyName] = value; + }; - var xform; - if (arguments.length > 4) { - var args = array_Slice.call(arguments, 4); - xform = xformForArgs(args); } else { - xform = xformChange; + WATCHED_PROPERTY.setup = function(obj, keyName, value) { + WATCHED_DESC.get = mkWatchedGetter(keyName); + o_defineProperty(obj, keyName, WATCHED_DESC); + WATCHED_DESC.get = null; + if (value !== undefined) meta(obj).values[keyName] = value; + }; } - Ember.addListener(obj, changeEvent(path), target, method, xform); - Ember.watch(obj, path); - return this; -}; - -/** @private */ -Ember.observersFor = function(obj, path) { - return Ember.listenersFor(obj, changeEvent(path)); -}; - -Ember.removeObserver = function(obj, path, target, method) { - path = normalizePath(path); - Ember.unwatch(obj, path); - Ember.removeListener(obj, changeEvent(path), target, method); - return this; -}; - -Ember.addBeforeObserver = function(obj, path, target, method) { - path = normalizePath(path); - Ember.addListener(obj, beforeEvent(path), target, method, xformBefore); - Ember.watch(obj, path); - return this; -}; - -/** @private */ -Ember.beforeObserversFor = function(obj, path) { - return Ember.listenersFor(obj, beforeEvent(path)); -}; -Ember.removeBeforeObserver = function(obj, path, target, method) { - path = normalizePath(path); - Ember.unwatch(obj, path); - Ember.removeListener(obj, beforeEvent(path), target, method); - return this; -}; + WATCHED_PROPERTY.teardown = function(obj, keyName) { + var ret = meta(obj).values[keyName]; + delete meta(obj).values[keyName]; + return ret; + }; -/** @private */ -Ember.notifyObservers = function(obj, keyName) { - notifyObservers(obj, changeEvent(keyName)); -}; +// NOTE: if platform does not have property accessors then we just have to +// set values and hope for the best. You just won't get any warnings... +} else { -/** @private */ -Ember.notifyBeforeObservers = function(obj, keyName) { - var guid, set, forceNotification = false; + WATCHED_PROPERTY.set = function(obj, keyName, value) { + var m = meta(obj), watching; - if (suspended) { - if (!beforeObserverSet.contains(obj, keyName)) { - beforeObserverSet.add(obj, keyName); - forceNotification = true; - } else { - return; - } - } + watching = m.watching[keyName]>0 && value!==obj[keyName]; + if (watching) Ember.propertyWillChange(obj, keyName); + obj[keyName] = value; + if (watching) Ember.propertyDidChange(obj, keyName); + return value; + }; - notifyObservers(obj, beforeEvent(keyName), forceNotification); -}; +} +/** + The default descriptor for simple properties. Pass as the third argument + to Ember.defineProperty() along with a value to set a simple value. -})({}); + @static + @default Ember.Descriptor +*/ +Ember.SIMPLE_PROPERTY = new Ember.Descriptor(); +SIMPLE_PROPERTY = Ember.SIMPLE_PROPERTY; +SIMPLE_PROPERTY.unwatched = WATCHED_PROPERTY.unwatched = SIMPLE_PROPERTY; +SIMPLE_PROPERTY.watched = WATCHED_PROPERTY.watched = WATCHED_PROPERTY; -(function(exports) { -// ========================================================================== -// Project: Ember Metal -// Copyright: ©2011 Strobe Inc. and contributors. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -/*globals ember_assert */ -var USE_ACCESSORS = Ember.USE_ACCESSORS; -var GUID_KEY = Ember.GUID_KEY; -var META_KEY = Ember.META_KEY; -var meta = Ember.meta; -var o_create = Ember.platform.create; -var o_defineProperty = Ember.platform.defineProperty; -var SIMPLE_PROPERTY, WATCHED_PROPERTY; // .......................................................... -// DESCRIPTOR +// DEFINING PROPERTIES API // -var SIMPLE_DESC = { - writable: true, - configurable: true, - enumerable: true, - value: null -}; +/** @private */ +function hasDesc(descs, keyName) { + if (keyName === 'toString') return 'function' !== typeof descs.toString; + else return !!descs[keyName]; +} /** @private - @constructor - Objects of this type can implement an interface to responds requests to - get and set. The default implementation handles simple properties. - - You generally won't need to create or subclass this directly. -*/ -var Dc = Ember.Descriptor = function() {}; - -var setup = Dc.setup = function(obj, keyName, value) { - SIMPLE_DESC.value = value; - o_defineProperty(obj, keyName, SIMPLE_DESC); - SIMPLE_DESC.value = null; -}; + NOTE: This is a low-level method used by other parts of the API. You almost + never want to call this method directly. Instead you should use Ember.mixin() + to define new properties. -var Dp = Ember.Descriptor.prototype; + Defines a property on an object. This method works much like the ES5 + Object.defineProperty() method except that it can also accept computed + properties and other special descriptors. -/** - Called whenever we want to set the property value. Should set the value - and return the actual set value (which is usually the same but may be - different in the case of computed properties.) + Normally this method takes only three parameters. However if you pass an + instance of Ember.Descriptor as the third param then you can pass an optional + value as the fourth parameter. This is often more efficient than creating + new descriptor hashes for each property. - @param {Object} obj - The object to set the value on. + ## Examples - @param {String} keyName - The key to set. + // ES5 compatible mode + Ember.defineProperty(contact, 'firstName', { + writable: true, + configurable: false, + enumerable: true, + value: 'Charles' + }); - @param {Object} value - The new value + // define a simple property + Ember.defineProperty(contact, 'lastName', Ember.SIMPLE_PROPERTY, 'Jolley'); - @returns {Object} value actual set value -*/ -Dp.set = function(obj, keyName, value) { - obj[keyName] = value; - return value; -}; - -/** - Called whenever we want to get the property value. Should retrieve the - current value. - - @param {Object} obj - The object to get the value on. - - @param {String} keyName - The key to retrieve - - @returns {Object} the current value -*/ -Dp.get = function(obj, keyName) { - return w_get(obj, keyName, obj); -}; - -/** - This is called on the descriptor to set it up on the object. The - descriptor is responsible for actually defining the property on the object - here. - - The passed `value` is the transferValue returned from any previous - descriptor. - - @param {Object} obj - The object to set the value on. - - @param {String} keyName - The key to set. - - @param {Object} value - The transfer value from any previous descriptor. - - @returns {void} -*/ -Dp.setup = setup; - -/** - This is called on the descriptor just before another descriptor takes its - place. This method should at least return the 'transfer value' of the - property - which is the value you want to passed as the input to the new - descriptor's setup() method. - - It is not generally necessary to actually 'undefine' the property as a new - property descriptor will redefine it immediately after this method returns. - - @param {Object} obj - The object to set the value on. - - @param {String} keyName - The key to set. - - @returns {Object} transfer value -*/ -Dp.teardown = function(obj, keyName) { - return obj[keyName]; -}; - -Dp.val = function(obj, keyName) { - return obj[keyName]; -}; - -// .......................................................... -// SIMPLE AND WATCHED PROPERTIES -// - -// if accessors are disabled for the app then this will act as a guard when -// testing on browsers that do support accessors. It will throw an exception -// if you do foo.bar instead of Ember.get(foo, 'bar') - -// The exception to this is that any objects managed by Ember but not a descendant -// of Ember.Object will not throw an exception, instead failing silently. This -// prevent errors with other libraries that may attempt to access special -// properties on standard objects like Array. Usually this happens when copying -// an object by looping over all properties. - -if (!USE_ACCESSORS) { - Ember.Descriptor.MUST_USE_GETTER = function() { - if (this instanceof Ember.Object) { - ember_assert('Must use Ember.get() to access this property', false); - } - }; - - Ember.Descriptor.MUST_USE_SETTER = function() { - if (this instanceof Ember.Object) { - if (this.isDestroyed) { - ember_assert('You cannot set observed properties on destroyed objects', false); - } else { - ember_assert('Must use Ember.set() to access this property', false); - } - } - }; -} - -var WATCHED_DESC = { - configurable: true, - enumerable: true, - set: Ember.Descriptor.MUST_USE_SETTER -}; - -function w_get(obj, keyName, values) { - values = values || meta(obj, false).values; - - if (values) { - var ret = values[keyName]; - if (ret !== undefined) { return ret; } - if (obj.unknownProperty) { return obj.unknownProperty(keyName); } - } - -} - -function w_set(obj, keyName, value) { - var m = meta(obj), watching; - - watching = m.watching[keyName]>0 && value!==m.values[keyName]; - if (watching) Ember.propertyWillChange(obj, keyName); - m.values[keyName] = value; - if (watching) Ember.propertyDidChange(obj, keyName); - return value; -} - -var WATCHED_GETTERS = {}; -function mkWatchedGetter(keyName) { - var ret = WATCHED_GETTERS[keyName]; - if (!ret) { - ret = WATCHED_GETTERS[keyName] = function() { - return w_get(this, keyName); - }; - } - return ret; -} - -var WATCHED_SETTERS = {}; -function mkWatchedSetter(keyName) { - var ret = WATCHED_SETTERS[keyName]; - if (!ret) { - ret = WATCHED_SETTERS[keyName] = function(value) { - return w_set(this, keyName, value); - }; - } - return ret; -} - -/** - @private - - Private version of simple property that invokes property change callbacks. -*/ -WATCHED_PROPERTY = new Ember.Descriptor(); - -if (Ember.platform.hasPropertyAccessors) { - WATCHED_PROPERTY.get = w_get ; - WATCHED_PROPERTY.set = w_set ; - - if (USE_ACCESSORS) { - WATCHED_PROPERTY.setup = function(obj, keyName, value) { - WATCHED_DESC.get = mkWatchedGetter(keyName); - WATCHED_DESC.set = mkWatchedSetter(keyName); - o_defineProperty(obj, keyName, WATCHED_DESC); - WATCHED_DESC.get = WATCHED_DESC.set = null; - if (value !== undefined) meta(obj).values[keyName] = value; - }; - - } else { - WATCHED_PROPERTY.setup = function(obj, keyName, value) { - WATCHED_DESC.get = mkWatchedGetter(keyName); - o_defineProperty(obj, keyName, WATCHED_DESC); - WATCHED_DESC.get = null; - if (value !== undefined) meta(obj).values[keyName] = value; - }; - } - - WATCHED_PROPERTY.teardown = function(obj, keyName) { - var ret = meta(obj).values[keyName]; - delete meta(obj).values[keyName]; - return ret; - }; - -// NOTE: if platform does not have property accessors then we just have to -// set values and hope for the best. You just won't get any warnings... -} else { - - WATCHED_PROPERTY.set = function(obj, keyName, value) { - var m = meta(obj), watching; - - watching = m.watching[keyName]>0 && value!==obj[keyName]; - if (watching) Ember.propertyWillChange(obj, keyName); - obj[keyName] = value; - if (watching) Ember.propertyDidChange(obj, keyName); - return value; - }; - -} - -/** - The default descriptor for simple properties. Pass as the third argument - to Ember.defineProperty() along with a value to set a simple value. - - @static - @default Ember.Descriptor -*/ -Ember.SIMPLE_PROPERTY = new Ember.Descriptor(); -SIMPLE_PROPERTY = Ember.SIMPLE_PROPERTY; - -SIMPLE_PROPERTY.unwatched = WATCHED_PROPERTY.unwatched = SIMPLE_PROPERTY; -SIMPLE_PROPERTY.watched = WATCHED_PROPERTY.watched = WATCHED_PROPERTY; - - -// .......................................................... -// DEFINING PROPERTIES API -// - -function hasDesc(descs, keyName) { - if (keyName === 'toString') return 'function' !== typeof descs.toString; - else return !!descs[keyName]; -} - -/** - @private - - NOTE: This is a low-level method used by other parts of the API. You almost - never want to call this method directly. Instead you should use Ember.mixin() - to define new properties. - - Defines a property on an object. This method works much like the ES5 - Object.defineProperty() method except that it can also accept computed - properties and other special descriptors. - - Normally this method takes only three parameters. However if you pass an - instance of Ember.Descriptor as the third param then you can pass an optional - value as the fourth parameter. This is often more efficient than creating - new descriptor hashes for each property. - - ## Examples - - // ES5 compatible mode - Ember.defineProperty(contact, 'firstName', { - writable: true, - configurable: false, - enumerable: true, - value: 'Charles' - }); - - // define a simple property - Ember.defineProperty(contact, 'lastName', Ember.SIMPLE_PROPERTY, 'Jolley'); - - // define a computed property - Ember.defineProperty(contact, 'fullName', Ember.computed(function() { - return this.firstName+' '+this.lastName; - }).property('firstName', 'lastName').cacheable()); + // define a computed property + Ember.defineProperty(contact, 'fullName', Ember.computed(function() { + return this.firstName+' '+this.lastName; + }).property('firstName', 'lastName').cacheable()); */ Ember.defineProperty = function(obj, keyName, desc, val) { var m = meta(obj, false), descs = m.descs, watching = m.watching[keyName]>0, override = true; @@ -3451,7 +3159,6 @@ Ember.createPrototype = function(obj, props) { })({}); - (function(exports) { // ========================================================================== // Project: Ember Metal @@ -3459,6767 +3166,7260 @@ Ember.createPrototype = function(obj, props) { // License: Licensed under MIT license (see license.js) // ========================================================================== /*globals ember_assert */ +var meta = Ember.meta; var guidFor = Ember.guidFor; -var meta = Ember.meta; -var get = Ember.get, set = Ember.set; -var normalizeTuple = Ember.normalizeTuple.primitive; -var normalizePath = Ember.normalizePath; -var SIMPLE_PROPERTY = Ember.SIMPLE_PROPERTY; -var GUID_KEY = Ember.GUID_KEY; -var META_KEY = Ember.META_KEY; -var notifyObservers = Ember.notifyObservers; -var forEach = Ember.ArrayUtils.forEach; - -var FIRST_KEY = /^([^\.\*]+)/; -var IS_PATH = /[\.\*]/; - -function firstKey(path) { - return path.match(FIRST_KEY)[0]; -} - -// returns true if the passed path is just a keyName -function isKeyName(path) { - return path==='*' || !IS_PATH.test(path); -} +var USE_ACCESSORS = Ember.USE_ACCESSORS; +var a_slice = Array.prototype.slice; +var o_create = Ember.platform.create; +var o_defineProperty = Ember.platform.defineProperty; // .......................................................... // DEPENDENT KEYS // -var DEP_SKIP = { __emberproto__: true }; // skip some keys and toString -function iterDeps(method, obj, depKey, seen, meta) { +// data structure: +// meta.deps = { +// 'depKey': { +// 'keyName': count, +// __emberproto__: SRC_OBJ [to detect clones] +// }, +// __emberproto__: SRC_OBJ +// } - var guid = guidFor(obj); - if (!seen[guid]) seen[guid] = {}; - if (seen[guid][depKey]) return ; - seen[guid][depKey] = true; +/** @private */ +function uniqDeps(obj, depKey) { + var m = meta(obj), deps, ret; + deps = m.deps; + if (!deps) { + deps = m.deps = { __emberproto__: obj }; + } else if (deps.__emberproto__ !== obj) { + deps = m.deps = o_create(deps); + deps.__emberproto__ = obj; + } - var deps = meta.deps; - deps = deps && deps[depKey]; - if (deps) { - for(var key in deps) { - if (DEP_SKIP[key]) continue; - method(obj, key); - } + ret = deps[depKey]; + if (!ret) { + ret = deps[depKey] = { __emberproto__: obj }; + } else if (ret.__emberproto__ !== obj) { + ret = deps[depKey] = o_create(ret); + ret.__emberproto__ = obj; } -} + return ret; +} -var WILL_SEEN, DID_SEEN; +/** @private */ +function addDependentKey(obj, keyName, depKey) { + var deps = uniqDeps(obj, depKey); + deps[keyName] = (deps[keyName] || 0) + 1; + Ember.watch(obj, depKey); +} -// called whenever a property is about to change to clear the cache of any dependent keys (and notify those properties of changes, etc...) -function dependentKeysWillChange(obj, depKey, meta) { - var seen = WILL_SEEN, top = !seen; - if (top) seen = WILL_SEEN = {}; - iterDeps(propertyWillChange, obj, depKey, seen, meta); - if (top) WILL_SEEN = null; +/** @private */ +function removeDependentKey(obj, keyName, depKey) { + var deps = uniqDeps(obj, depKey); + deps[keyName] = (deps[keyName] || 0) - 1; + Ember.unwatch(obj, depKey); } -// called whenever a property has just changed to update dependent keys -function dependentKeysDidChange(obj, depKey, meta) { - var seen = DID_SEEN, top = !seen; - if (top) seen = DID_SEEN = {}; - iterDeps(propertyDidChange, obj, depKey, seen, meta); - if (top) DID_SEEN = null; +/** @private */ +function addDependentKeys(desc, obj, keyName) { + var keys = desc._dependentKeys, + len = keys ? keys.length : 0; + for(var idx=0;idx0) { - setTimeout(flushPendingChains, 1); + if (cacheable) { + return function() { + var ret, cache = meta(this).cache; + if (keyName in cache) return cache[keyName]; + ret = cache[keyName] = func.call(this, keyName); + return ret ; + }; + } else { + return function() { + return func.call(this, keyName); + }; } } -function isProto(pvalue) { - return meta(pvalue, false).proto === pvalue; -} +/** @private */ +function mkCpSetter(keyName, desc) { + var cacheable = desc._cacheable, + func = desc.func; -// A ChainNode watches a single key on an object. If you provide a starting -// value for the key then the node won't actually watch it. For a root node -// pass null for parent and key and object for value. -var ChainNode = function(parent, key, value, separator) { - var obj; - this._parent = parent; - this._key = key; + return function(value) { + var m = meta(this, cacheable), + watched = (m.source===this) && m.watching[keyName]>0, + ret, oldSuspended, lastSetValues; - // _watching is true when calling get(this._parent, this._key) will - // return the value of this node. - // - // It is false for the root of a chain (because we have no parent) - // and for global paths (because the parent node is the object with - // the observer on it) - this._watching = value===undefined; + oldSuspended = desc._suspended; + desc._suspended = this; - this._value = value; - this._separator = separator || '.'; - this._paths = {}; - if (this._watching) { - this._object = parent.value(); - if (this._object) addChainWatcher(this._object, this._key, this); - } + watched = watched && m.lastSetValues[keyName]!==guidFor(value); + if (watched) { + m.lastSetValues[keyName] = guidFor(value); + Ember.propertyWillChange(this, keyName); + } - // Special-case: the EachProxy relies on immediate evaluation to - // establish its observers. - // - // TODO: Replace this with an efficient callback that the EachProxy - // can implement. - if (this._parent && this._parent._key === '@each') { - this.value(); - } -}; + if (cacheable) delete m.cache[keyName]; + ret = func.call(this, keyName, value); + if (cacheable) m.cache[keyName] = ret; + if (watched) Ember.propertyDidChange(this, keyName); + desc._suspended = oldSuspended; + return ret; + }; +} +/** + @extends Ember.ComputedProperty + @private +*/ +var Cp = ComputedProperty.prototype; -var Wp = ChainNode.prototype; +/** + Call on a computed property to set it into cacheable mode. When in this + mode the computed property will automatically cache the return value of + your function until one of the dependent keys changes. -Wp.value = function() { - if (this._value === undefined && this._watching){ - var obj = this._parent.value(); - this._value = (obj && !isProto(obj)) ? get(obj, this._key) : undefined; - } - return this._value; + @param {Boolean} aFlag optional set to false to disable cacheing + @returns {Ember.ComputedProperty} receiver +*/ +Cp.cacheable = function(aFlag) { + this._cacheable = aFlag!==false; + return this; }; -Wp.destroy = function() { - if (this._watching) { - var obj = this._object; - if (obj) removeChainWatcher(obj, this._key, this); - this._watching = false; // so future calls do nothing - } +/** + Sets the dependent keys on this computed property. Pass any number of + arguments containing key paths that this computed property depends on. + + @param {String} path... zero or more property paths + @returns {Ember.ComputedProperty} receiver +*/ +Cp.property = function() { + this._dependentKeys = a_slice.call(arguments); + return this; }; -// copies a top level object only -Wp.copy = function(obj) { - var ret = new ChainNode(null, null, obj, this._separator); - var paths = this._paths, path; - for(path in paths) { - if (paths[path] <= 0) continue; // this check will also catch non-number vals. - ret.add(path); - } - return ret; +/** + In some cases, you may want to annotate computed properties with additional + metadata about how they function or what values they operate on. For example, + computed property functions may close over variables that are then no longer + available for introspection. + + You can pass a hash of these values to a computed property like this: + + person: function() { + var personId = this.get('personId'); + return App.Person.create({ id: personId }); + }.property().meta({ type: App.Person }) + + The hash that you pass to the `meta()` function will be saved on the + computed property descriptor under the `_meta` key. Ember runtime + exposes a public API for retrieving these values from classes, + via the `metaForProperty()` function. +*/ + +Cp.meta = function(meta) { + this._meta = meta; + return this; }; -// called on the root node of a chain to setup watchers on the specified -// path. -Wp.add = function(path) { - var obj, tuple, key, src, separator, paths; +/** @private - impl descriptor API */ +Cp.setup = function(obj, keyName, value) { + CP_DESC.get = mkCpGetter(keyName, this); + CP_DESC.set = mkCpSetter(keyName, this); + o_defineProperty(obj, keyName, CP_DESC); + CP_DESC.get = CP_DESC.set = null; + addDependentKeys(this, obj, keyName); +}; - paths = this._paths; - paths[path] = (paths[path] || 0) + 1 ; +/** @private - impl descriptor API */ +Cp.teardown = function(obj, keyName) { + var keys = this._dependentKeys, + len = keys ? keys.length : 0; + for(var idx=0;idx 0) paths[path]--; + var m = meta(obj, cacheable), + watched = (m.source===obj) && m.watching[keyName]>0, + ret, oldSuspended, lastSetValues; - obj = this.value(); - tuple = normalizeTuple(obj, path); - if (tuple[0] === obj) { - path = tuple[1]; - key = firstKey(path); - path = path.slice(key.length+1); + oldSuspended = this._suspended; + this._suspended = obj; - } else { - src = tuple[0]; - key = path.slice(0, 0-(tuple[1].length+1)); - path = tuple[1]; + watched = watched && m.lastSetValues[keyName]!==guidFor(value); + if (watched) { + m.lastSetValues[keyName] = guidFor(value); + Ember.propertyWillChange(obj, keyName); } - tuple.length = 0; - this.unchain(key, path); + if (cacheable) delete m.cache[keyName]; + ret = this.func.call(obj, keyName, value); + if (cacheable) m.cache[keyName] = ret; + if (watched) Ember.propertyDidChange(obj, keyName); + this._suspended = oldSuspended; + return ret; }; -Wp.count = 0; +Cp.val = function(obj, keyName) { + return meta(obj, false).values[keyName]; +}; -Wp.chain = function(key, path, src, separator) { - var chains = this._chains, node; - if (!chains) chains = this._chains = {}; +if (!Ember.platform.hasPropertyAccessors) { + Cp.setup = function(obj, keyName, value) { + obj[keyName] = undefined; // so it shows up in key iteration + addDependentKeys(this, obj, keyName); + }; - node = chains[key]; - if (!node) node = chains[key] = new ChainNode(this, key, src, separator); - node.count++; // count chains... +} else if (!USE_ACCESSORS) { + Cp.setup = function(obj, keyName) { + // throw exception if not using Ember.get() and Ember.set() when supported + o_defineProperty(obj, keyName, CP_DESC); + addDependentKeys(this, obj, keyName); + }; +} - // chain rest of path if there is one - if (path && path.length>0) { - key = firstKey(path); - path = path.slice(key.length+1); - node.chain(key, path); // NOTE: no src means it will observe changes... - } -}; +/** + This helper returns a new property descriptor that wraps the passed + computed property function. You can use this helper to define properties + with mixins or via Ember.defineProperty(). -Wp.unchain = function(key, path) { - var chains = this._chains, node = chains[key]; + The function you pass will be used to both get and set property values. + The function should accept two parameters, key and value. If value is not + undefined you should set the value first. In either case return the + current value of the property. - // unchain rest of path first... - if (path && path.length>1) { - key = firstKey(path); - path = path.slice(key.length+1); - node.unchain(key, path); - } + @param {Function} func + The computed property function. - // delete node if needed. - node.count--; - if (node.count<=0) { - delete chains[node._key]; - node.destroy(); + @returns {Ember.ComputedProperty} property descriptor instance +*/ +Ember.computed = function(func) { + var args; + + if (arguments.length > 1) { + args = a_slice.call(arguments, 0, -1); + func = a_slice.call(arguments, -1)[0]; } -}; + var cp = new ComputedProperty(func); -Wp.willChange = function() { - var chains = this._chains; - if (chains) { - for(var key in chains) { - if (!chains.hasOwnProperty(key)) continue; - chains[key].willChange(); - } + if (args) { + cp.property.apply(cp, args); } - if (this._parent) this._parent.chainWillChange(this, this._key, 1); + return cp; }; -Wp.chainWillChange = function(chain, path, depth) { - if (this._key) path = this._key+this._separator+path; +})({}); - if (this._parent) { - this._parent.chainWillChange(this, path, depth+1); - } else { - if (depth>1) Ember.propertyWillChange(this.value(), path); - path = 'this.'+path; - if (this._paths[path]>0) Ember.propertyWillChange(this.value(), path); - } -}; +(function(exports) { +/*jshint newcap:false*/ -Wp.chainDidChange = function(chain, path, depth) { - if (this._key) path = this._key+this._separator+path; - if (this._parent) { - this._parent.chainDidChange(this, path, depth+1); - } else { - if (depth>1) Ember.propertyDidChange(this.value(), path); - path = 'this.'+path; - if (this._paths[path]>0) Ember.propertyDidChange(this.value(), path); - } +// NOTE: There is a bug in jshint that doesn't recognize `Object()` without `new` +// as being ok unless both `newcap:false` and not `use strict`. +// https://github.com/jshint/jshint/issues/392 + +// Testing this is not ideal, but we want ArrayUtils to use native functions +// if available, but not to use versions created by libraries like Prototype +/** @private */ +var isNativeFunc = function(func) { + // This should probably work in all browsers likely to have ES5 array methods + return func && Function.prototype.toString.call(func).indexOf('[native code]') > -1; }; -Wp.didChange = function(suppressEvent) { - // invalidate my own value first. - if (this._watching) { - var obj = this._parent.value(); - if (obj !== this._object) { - removeChainWatcher(this._object, this._key, this); - this._object = obj; - addChainWatcher(obj, this._key, this); - } - this._value = undefined; +// From: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/map +/** @private */ +var arrayMap = isNativeFunc(Array.prototype.map) ? Array.prototype.map : function(fun /*, thisp */) { + //"use strict"; - // Special-case: the EachProxy relies on immediate evaluation to - // establish its observers. - if (this._parent && this._parent._key === '@each') - this.value(); + if (this === void 0 || this === null) { + throw new TypeError(); } - // then notify chains... - var chains = this._chains; - if (chains) { - for(var key in chains) { - if (!chains.hasOwnProperty(key)) continue; - chains[key].didChange(suppressEvent); - } + var t = Object(this); + var len = t.length >>> 0; + if (typeof fun !== "function") { + throw new TypeError(); } - if (suppressEvent) return; - - // and finally tell parent about my path changing... - if (this._parent) this._parent.chainDidChange(this, this._key, 1); -}; - -// get the chains for the current object. If the current object has -// chains inherited from the proto they will be cloned and reconfigured for -// the current object. -function chainsFor(obj) { - var m = meta(obj), ret = m.chains; - if (!ret) { - ret = m.chains = new ChainNode(null, null, obj); - } else if (ret.value() !== obj) { - ret = m.chains = ret.copy(obj); + var res = new Array(len); + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (i in t) { + res[i] = fun.call(thisp, t[i], i, t); + } } - return ret ; -} - + return res; +}; -function notifyChains(obj, m, keyName, methodName, arg) { - var nodes = m.chainWatchers; +// From: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/foreach +/** @private */ +var arrayForEach = isNativeFunc(Array.prototype.forEach) ? Array.prototype.forEach : function(fun /*, thisp */) { + //"use strict"; - if (!nodes || nodes.__emberproto__ !== obj) return; // nothing to do + if (this === void 0 || this === null) { + throw new TypeError(); + } - nodes = nodes[keyName]; - if (!nodes) return; + var t = Object(this); + var len = t.length >>> 0; + if (typeof fun !== "function") { + throw new TypeError(); + } - for(var key in nodes) { - if (!nodes.hasOwnProperty(key)) continue; - nodes[key][methodName](arg); + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (i in t) { + fun.call(thisp, t[i], i, t); + } } -} +}; -Ember.overrideChains = function(obj, keyName, m) { - notifyChains(obj, m, keyName, 'didChange', true); +/** @private */ +var arrayIndexOf = isNativeFunc(Array.prototype.indexOf) ? Array.prototype.indexOf : function (obj, fromIndex) { + if (fromIndex === null || fromIndex === undefined) { fromIndex = 0; } + else if (fromIndex < 0) { fromIndex = Math.max(0, this.length + fromIndex); } + for (var i = fromIndex, j = this.length; i < j; i++) { + if (this[i] === obj) { return i; } + } + return -1; }; -function chainsWillChange(obj, keyName, m) { - notifyChains(obj, m, keyName, 'willChange'); -} -function chainsDidChange(obj, keyName, m) { - notifyChains(obj, m, keyName, 'didChange'); -} +Ember.ArrayUtils = { + map: function(obj) { + var args = Array.prototype.slice.call(arguments, 1); + return obj.map ? obj.map.apply(obj, args) : arrayMap.apply(obj, args); + }, -// .......................................................... -// WATCH -// + forEach: function(obj) { + var args = Array.prototype.slice.call(arguments, 1); + return obj.forEach ? obj.forEach.apply(obj, args) : arrayForEach.apply(obj, args); + }, -var WATCHED_PROPERTY = Ember.SIMPLE_PROPERTY.watched; + indexOf: function(obj) { + var args = Array.prototype.slice.call(arguments, 1); + return obj.indexOf ? obj.indexOf.apply(obj, args) : arrayIndexOf.apply(obj, args); + } +}; -/** - @private - Starts watching a property on an object. Whenever the property changes, - invokes Ember.propertyWillChange and Ember.propertyDidChange. This is the - primitive used by observers and dependent keys; usually you will never call - this method directly but instead use higher level methods like - Ember.addObserver(). -*/ -Ember.watch = function(obj, keyName) { +if (Ember.SHIM_ES5) { + if (!Array.prototype.map) { + /** @private */ + Array.prototype.map = arrayMap; + } - // can't watch length on Array - it is special... - if (keyName === 'length' && Ember.typeOf(obj)==='array') return this; + if (!Array.prototype.forEach) { + /** @private */ + Array.prototype.forEach = arrayForEach; + } - var m = meta(obj), watching = m.watching, desc; - keyName = normalizePath(keyName); + if (!Array.prototype.indexOf) { + /** @private */ + Array.prototype.indexOf = arrayIndexOf; + } +} - // activate watching first time - if (!watching[keyName]) { - watching[keyName] = 1; - if (isKeyName(keyName)) { - desc = m.descs[keyName]; - desc = desc ? desc.watched : WATCHED_PROPERTY; - if (desc) Ember.defineProperty(obj, keyName, desc); - } else { - chainsFor(obj).add(keyName); - } +})({}); - } else { - watching[keyName] = (watching[keyName]||0)+1; +(function(exports) { +// ========================================================================== +// Project: Ember Metal +// Copyright: ©2011 Strobe Inc. and contributors. +// License: Licensed under MIT license (see license.js) +// ========================================================================== +var AFTER_OBSERVERS = ':change'; +var BEFORE_OBSERVERS = ':before'; +var guidFor = Ember.guidFor; +var normalizePath = Ember.normalizePath; + +var deferred = 0; +var array_Slice = Array.prototype.slice; +var array_ForEach = Ember.ArrayUtils.forEach; + +/** @private */ +var ObserverSet = function () { + this.targetSet = {}; +}; +ObserverSet.prototype.add = function (target, path) { + var targetSet = this.targetSet, + targetGuid = Ember.guidFor(target), + pathSet = targetSet[targetGuid]; + if (!pathSet) { + targetSet[targetGuid] = pathSet = {}; + } + if (pathSet[path]) { + return false; + } else { + return pathSet[path] = true; } - return this; }; - -Ember.isWatching = function(obj, keyName) { - return !!meta(obj).watching[keyName]; +ObserverSet.prototype.clear = function () { + this.targetSet = {}; }; -Ember.watch.flushPending = flushPendingChains; - /** @private */ -Ember.unwatch = function(obj, keyName) { - // can't watch length on Array - it is special... - if (keyName === 'length' && Ember.typeOf(obj)==='array') return this; +var DeferredEventQueue = function() { + this.targetSet = {}; + this.queue = []; +}; - var watching = meta(obj).watching, desc, descs; - keyName = normalizePath(keyName); - if (watching[keyName] === 1) { - watching[keyName] = 0; - if (isKeyName(keyName)) { - desc = meta(obj).descs[keyName]; - desc = desc ? desc.unwatched : SIMPLE_PROPERTY; - if (desc) Ember.defineProperty(obj, keyName, desc); - } else { - chainsFor(obj).remove(keyName); - } +DeferredEventQueue.prototype.push = function(target, eventName) { + var targetSet = this.targetSet, + queue = this.queue, + targetGuid = Ember.guidFor(target), + eventNameSet = targetSet[targetGuid], + index; - } else if (watching[keyName]>1) { - watching[keyName]--; + if (!eventNameSet) { + targetSet[targetGuid] = eventNameSet = {}; + } + index = eventNameSet[eventName]; + if (index === undefined) { + eventNameSet[eventName] = queue.push(Ember.deferEvent(target, eventName)) - 1; + } else { + queue[index] = Ember.deferEvent(target, eventName); } - - return this; }; -/** - @private +DeferredEventQueue.prototype.flush = function() { + var queue = this.queue; + this.queue = []; + this.targetSet = {}; + for (var i=0, len=queue.length; i < len; ++i) { + queue[i](); + } +}; - Call on an object when you first beget it from another object. This will - setup any chained watchers on the object instance as needed. This method is - safe to call multiple times. -*/ -Ember.rewatch = function(obj) { - var m = meta(obj, false), chains = m.chains, bindings = m.bindings, key, b; +var queue = new DeferredEventQueue(), beforeObserverSet = new ObserverSet(); - // make sure the object has its own guid. - if (GUID_KEY in obj && !obj.hasOwnProperty(GUID_KEY)) { - Ember.generateGuid(obj, 'ember'); +/** @private */ +function notifyObservers(obj, eventName, forceNotification) { + if (deferred && !forceNotification) { + queue.push(obj, eventName); + } else { + Ember.sendEvent(obj, eventName); } +} - // make sure any chained watchers update. - if (chains && chains.value() !== obj) chainsFor(obj); +/** @private */ +function flushObserverQueue() { + beforeObserverSet.clear(); - // if the object has bindings then sync them.. - if (bindings && m.proto!==obj) { - for (key in bindings) { - b = !DEP_SKIP[key] && obj[key]; - if (b && b instanceof Ember.Binding) b.fromDidChange(obj); - } - } + queue.flush(); +} +Ember.beginPropertyChanges = function() { + deferred++; return this; }; -// .......................................................... -// PROPERTY CHANGES -// +Ember.endPropertyChanges = function() { + deferred--; + if (deferred<=0) flushObserverQueue(); +}; /** - This function is called just before an object property is about to change. - It will notify any before observers and prepare caches among other things. - - Normally you will not need to call this method directly but if for some - reason you can't directly watch a property you can invoke this method - manually along with `Ember.propertyDidChange()` which you should call just - after the property value changes. - - @param {Object} obj - The object with the property that will change - - @param {String} keyName - The property key (or path) that will change. + Make a series of property changes together in an + exception-safe way. - @returns {void} + Ember.changeProperties(function() { + obj1.set('foo', mayBlowUpWhenSet); + obj2.set('bar', baz); + }); */ -var propertyWillChange = Ember.propertyWillChange = function(obj, keyName) { - var m = meta(obj, false), proto = m.proto, desc = m.descs[keyName]; - if (proto === obj) return ; - if (desc && desc.willChange) desc.willChange(obj, keyName); - dependentKeysWillChange(obj, keyName, m); - chainsWillChange(obj, keyName, m); - Ember.notifyBeforeObservers(obj, keyName); +Ember.changeProperties = function(cb){ + Ember.beginPropertyChanges(); + try { + cb(); + } finally { + Ember.endPropertyChanges(); + } }; -/** - This function is called just after an object property has changed. - It will notify any observers and clear caches among other things. +/** @private */ +function changeEvent(keyName) { + return keyName+AFTER_OBSERVERS; +} - Normally you will not need to call this method directly but if for some - reason you can't directly watch a property you can invoke this method - manually along with `Ember.propertyWilLChange()` which you should call just - before the property value changes. +/** @private */ +function beforeEvent(keyName) { + return keyName+BEFORE_OBSERVERS; +} - @param {Object} obj - The object with the property that will change +/** @private */ +function changeKey(eventName) { + return eventName.slice(0, -7); +} - @param {String} keyName - The property key (or path) that will change. +/** @private */ +function beforeKey(eventName) { + return eventName.slice(0, -7); +} - @returns {void} -*/ -var propertyDidChange = Ember.propertyDidChange = function(obj, keyName) { - var m = meta(obj, false), proto = m.proto, desc = m.descs[keyName]; - if (proto === obj) return ; - if (desc && desc.didChange) desc.didChange(obj, keyName); - dependentKeysDidChange(obj, keyName, m); - chainsDidChange(obj, keyName, m); - Ember.notifyObservers(obj, keyName); +/** @private */ +function xformForArgs(args) { + return function (target, method, params) { + var obj = params[0], keyName = changeKey(params[1]), val; + var copy_args = args.slice(); + if (method.length>2) { + val = Ember.getPath(Ember.isGlobalPath(keyName) ? window : obj, keyName); + } + copy_args.unshift(obj, keyName, val); + method.apply(target, copy_args); + }; +} + +var xformChange = xformForArgs([]); + +/** @private */ +function xformBefore(target, method, params) { + var obj = params[0], keyName = beforeKey(params[1]), val; + if (method.length>2) val = Ember.getPath(obj, keyName); + method.call(target, obj, keyName, val); +} + +Ember.addObserver = function(obj, path, target, method) { + path = normalizePath(path); + + var xform; + if (arguments.length > 4) { + var args = array_Slice.call(arguments, 4); + xform = xformForArgs(args); + } else { + xform = xformChange; + } + Ember.addListener(obj, changeEvent(path), target, method, xform); + Ember.watch(obj, path); + return this; }; -var NODE_STACK = []; +/** @private */ +Ember.observersFor = function(obj, path) { + return Ember.listenersFor(obj, changeEvent(path)); +}; -/** - Tears down the meta on an object so that it can be garbage collected. - Multiple calls will have no effect. +Ember.removeObserver = function(obj, path, target, method) { + path = normalizePath(path); + Ember.unwatch(obj, path); + Ember.removeListener(obj, changeEvent(path), target, method); + return this; +}; - @param {Object} obj the object to destroy - @returns {void} -*/ -Ember.destroy = function (obj) { - var meta = obj[META_KEY], node, nodes, key, nodeObject; - if (meta) { - obj[META_KEY] = null; - // remove chainWatchers to remove circular references that would prevent GC - node = meta.chains; - if (node) { - NODE_STACK.push(node); - // process tree - while (NODE_STACK.length > 0) { - node = NODE_STACK.pop(); - // push children - nodes = node._chains; - if (nodes) { - for (key in nodes) { - if (nodes.hasOwnProperty(key)) { - NODE_STACK.push(nodes[key]); - } - } - } - // remove chainWatcher in node object - if (node._watching) { - nodeObject = node._object; - if (nodeObject) { - removeChainWatcher(nodeObject, node._key, node); - } - } - } +Ember.addBeforeObserver = function(obj, path, target, method) { + path = normalizePath(path); + Ember.addListener(obj, beforeEvent(path), target, method, xformBefore); + Ember.watch(obj, path); + return this; +}; + +// Suspend observer during callback. +// +// This should only be used by the target of the observer +// while it is setting the observed path. +/** @private */ +Ember._suspendObserver = function(obj, path, target, method, callback) { + return Ember._suspendListener(obj, changeEvent(path), target, method, callback); +}; + +/** @private */ +Ember.beforeObserversFor = function(obj, path) { + return Ember.listenersFor(obj, beforeEvent(path)); +}; + +Ember.removeBeforeObserver = function(obj, path, target, method) { + path = normalizePath(path); + Ember.unwatch(obj, path); + Ember.removeListener(obj, beforeEvent(path), target, method); + return this; +}; + +/** @private */ +Ember.notifyObservers = function(obj, keyName) { + notifyObservers(obj, changeEvent(keyName)); +}; + +/** @private */ +Ember.notifyBeforeObservers = function(obj, keyName) { + var guid, set, forceNotification = false; + + if (deferred) { + if (beforeObserverSet.add(obj, keyName)) { + forceNotification = true; + } else { + return; } } + + notifyObservers(obj, beforeEvent(keyName), forceNotification); }; -})({}); +})({}); (function(exports) { // ========================================================================== -// Project: Ember Runtime -// Copyright: ©2006-2011 Strobe Inc. and contributors. -// Portions ©2008-2010 Apple Inc. All rights reserved. +// Project: Ember Metal +// Copyright: ©2011 Strobe Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== /*globals ember_assert */ -// Ember.Logger -// Ember.watch.flushPending -// Ember.beginPropertyChanges, Ember.endPropertyChanges -// Ember.guidFor -// Ember.ArrayUtils - -// .......................................................... -// HELPERS -// - -var slice = Array.prototype.slice; +var guidFor = Ember.guidFor; +var meta = Ember.meta; +var get = Ember.get, set = Ember.set; +var normalizeTuple = Ember.normalizeTuple.primitive; +var normalizePath = Ember.normalizePath; +var SIMPLE_PROPERTY = Ember.SIMPLE_PROPERTY; +var GUID_KEY = Ember.GUID_KEY; +var META_KEY = Ember.META_KEY; +var notifyObservers = Ember.notifyObservers; var forEach = Ember.ArrayUtils.forEach; -// invokes passed params - normalizing so you can pass target/func, -// target/string or just func -function invoke(target, method, args, ignore) { - - if (method===undefined) { - method = target; - target = undefined; - } - - if ('string'===typeof method) method = target[method]; - if (args && ignore>0) { - args = args.length>ignore ? slice.call(args, ignore) : null; - } +var FIRST_KEY = /^([^\.\*]+)/; +var IS_PATH = /[\.\*]/; - // Unfortunately in some browsers we lose the backtrace if we rethrow the existing error, - // so in the event that we don't have an `onerror` handler we don't wrap in a try/catch - if ('function' === typeof Ember.onerror) { - try { - // IE8's Function.prototype.apply doesn't accept undefined/null arguments. - return method.apply(target || this, args || []); - } catch (error) { - Ember.onerror(error); - } - } else { - // IE8's Function.prototype.apply doesn't accept undefined/null arguments. - return method.apply(target || this, args || []); - } +/** @private */ +function firstKey(path) { + return path.match(FIRST_KEY)[0]; } +// returns true if the passed path is just a keyName +/** @private */ +function isKeyName(path) { + return path==='*' || !IS_PATH.test(path); +} // .......................................................... -// RUNLOOP +// DEPENDENT KEYS // -var timerMark; // used by timers... +var DEP_SKIP = { __emberproto__: true }; // skip some keys and toString -var K = function() {}; -var RunLoop = function(prev) { - var self; +/** @private */ +function iterDeps(method, obj, depKey, seen, meta) { - if (this instanceof RunLoop) { - self = this; - } else { - self = new K(); + var guid = guidFor(obj); + if (!seen[guid]) seen[guid] = {}; + if (seen[guid][depKey]) return ; + seen[guid][depKey] = true; + + var deps = meta.deps; + deps = deps && deps[depKey]; + if (deps) { + for(var key in deps) { + if (DEP_SKIP[key]) continue; + method(obj, key); + } } +} - self._prev = prev || null; - self.onceTimers = {}; - return self; -}; +var WILL_SEEN, DID_SEEN; -K.prototype = RunLoop.prototype; +// called whenever a property is about to change to clear the cache of any dependent keys (and notify those properties of changes, etc...) +/** @private */ +function dependentKeysWillChange(obj, depKey, meta) { + var seen = WILL_SEEN, top = !seen; + if (top) seen = WILL_SEEN = {}; + iterDeps(propertyWillChange, obj, depKey, seen, meta); + if (top) WILL_SEEN = null; +} -RunLoop.prototype = { - end: function() { - this.flush(); - }, +// called whenever a property has just changed to update dependent keys +/** @private */ +function dependentKeysDidChange(obj, depKey, meta) { + var seen = DID_SEEN, top = !seen; + if (top) seen = DID_SEEN = {}; + iterDeps(propertyDidChange, obj, depKey, seen, meta); + if (top) DID_SEEN = null; +} - prev: function() { - return this._prev; - }, +// .......................................................... +// CHAIN +// - // .......................................................... - // Delayed Actions - // - - schedule: function(queueName, target, method) { - var queues = this._queues, queue; - if (!queues) queues = this._queues = {}; - queue = queues[queueName]; - if (!queue) queue = queues[queueName] = []; - - var args = arguments.length>3 ? slice.call(arguments, 3) : null; - queue.push({ target: target, method: method, args: args }); - return this; - }, - - flush: function(queueName) { - var queues = this._queues, queueNames, idx, len, queue, log; - - if (!queues) return this; // nothing to do +/** @private */ +function addChainWatcher(obj, keyName, node) { + if (!obj || ('object' !== typeof obj)) return; // nothing to do + var m = meta(obj); + var nodes = m.chainWatchers; + if (!nodes || nodes.__emberproto__ !== obj) { + nodes = m.chainWatchers = { __emberproto__: obj }; + } - function iter(item) { - invoke(item.target, item.method, item.args); - } + if (!nodes[keyName]) nodes[keyName] = {}; + nodes[keyName][guidFor(node)] = node; + Ember.watch(obj, keyName); +} - Ember.watch.flushPending(); // make sure all chained watchers are setup +/** @private */ +function removeChainWatcher(obj, keyName, node) { + if (!obj || ('object' !== typeof obj)) return; // nothing to do + var m = meta(obj, false); + var nodes = m.chainWatchers; + if (!nodes || nodes.__emberproto__ !== obj) return; //nothing to do + if (nodes[keyName]) delete nodes[keyName][guidFor(node)]; + Ember.unwatch(obj, keyName); +} - if (queueName) { - while (this._queues && (queue = this._queues[queueName])) { - this._queues[queueName] = null; +var pendingQueue = []; - log = Ember.LOG_BINDINGS && queueName==='sync'; - if (log) Ember.Logger.log('Begin: Flush Sync Queue'); +// attempts to add the pendingQueue chains again. If some of them end up +// back in the queue and reschedule is true, schedules a timeout to try +// again. +/** @private */ +function flushPendingChains(reschedule) { + if (pendingQueue.length===0) return ; // nothing to do - // the sync phase is to allow property changes to propogate. don't - // invoke observers until that is finished. - if (queueName === 'sync') Ember.beginPropertyChanges(); - forEach(queue, iter); - if (queueName === 'sync') Ember.endPropertyChanges(); + var queue = pendingQueue; + pendingQueue = []; - if (log) Ember.Logger.log('End: Flush Sync Queue'); + forEach(queue, function(q) { q[0].add(q[1]); }); + if (reschedule!==false && pendingQueue.length>0) { + setTimeout(flushPendingChains, 1); + } +} - } +/** @private */ +function isProto(pvalue) { + return meta(pvalue, false).proto === pvalue; +} - } else { - queueNames = Ember.run.queues; - len = queueNames.length; - do { - this._queues = null; - for(idx=0;idx 0) paths[path]--; - var ret, loop; - run.begin(); - try { - if (target || method) ret = invoke(target, method, arguments, 2); - } finally { - run.end(); + obj = this.value(); + tuple = normalizeTuple(obj, path); + if (tuple[0] === obj) { + path = tuple[1]; + key = firstKey(path); + path = path.slice(key.length+1); + + } else { + src = tuple[0]; + key = path.slice(0, 0-(tuple[1].length+1)); + path = tuple[1]; } - return ret; + + tuple.length = 0; + this.unchain(key, path); }; -/** - 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(). +Wp.count = 0; - @returns {void} -*/ -Ember.run.begin = function() { - run.currentRunLoop = new RunLoop(run.currentRunLoop); -}; +Wp.chain = function(key, path, src, separator) { + var chains = this._chains, node; + if (!chains) chains = this._chains = {}; -/** - Ends a RunLoop. This must be called sometime after you call Ember.run.begin() - to flush any deferred actions. This is a lower-level way to use a RunLoop - instead of using Ember.run(). + node = chains[key]; + if (!node) node = chains[key] = new ChainNode(this, key, src, separator); + node.count++; // count chains... - @returns {void} -*/ -Ember.run.end = function() { - ember_assert('must have a current run loop', run.currentRunLoop); - try { - run.currentRunLoop.end(); - } - finally { - run.currentRunLoop = run.currentRunLoop.prev(); + // chain rest of path if there is one + if (path && path.length>0) { + key = firstKey(path); + path = path.slice(key.length+1); + node.chain(key, path); // NOTE: no src means it will observe changes... } }; -/** - Array of named queues. This array determines the order in which queues - are flushed at the end of the RunLoop. You can define your own queues by - simply adding the queue name to this array. Normally you should not need - to inspect or modify this property. +Wp.unchain = function(key, path) { + var chains = this._chains, node = chains[key]; - @property {String} -*/ -Ember.run.queues = ['sync', 'actions', 'destroy', 'timers']; - -/** - Adds the passed target/method and any optional arguments to the named - queue to be executed at the end of the RunLoop. If you have not already - started a RunLoop when calling this method one will be started for you - automatically. - - At the end of a RunLoop, any methods scheduled in this way will be invoked. - Methods will be invoked in an order matching the named queues defined in - the run.queues property. - - @param {String} queue - The name of the queue to schedule against. Default queues are 'sync' and - 'actions' - - @param {Object} target - (Optional) target object to use as the context when invoking a method. - - @param {String|Function} method - The method to invoke. If you pass a string it will be resolved on the - target object at the time the scheduled item is invoked allowing you to - change the target function. + // unchain rest of path first... + if (path && path.length>1) { + key = firstKey(path); + path = path.slice(key.length+1); + node.unchain(key, path); + } - @param {Object} arguments... - Optional arguments to be passed to the queued method. + // delete node if needed. + node.count--; + if (node.count<=0) { + delete chains[node._key]; + node.destroy(); + } - @returns {void} -*/ -Ember.run.schedule = function(queue, target, method) { - var loop = run.autorun(); - loop.schedule.apply(loop, arguments); }; -var autorunTimer; - -function autorun() { - autorunTimer = null; - if (run.currentRunLoop) run.end(); -} - -/** - Begins a new RunLoop if necessary and schedules a timer to flush the - RunLoop at a later time. This method is used by parts of Ember to - ensure the RunLoop always finishes. You normally do not need to call this - method directly. Instead use Ember.run(). - - @returns {Ember.RunLoop} the new current RunLoop -*/ -Ember.run.autorun = function() { - - if (!run.currentRunLoop) { - run.begin(); - - // TODO: throw during tests - if (Ember.testing) { - run.end(); - } else if (!autorunTimer) { - autorunTimer = setTimeout(autorun, 1); +Wp.willChange = function() { + var chains = this._chains; + if (chains) { + for(var key in chains) { + if (!chains.hasOwnProperty(key)) continue; + chains[key].willChange(); } } - return run.currentRunLoop; + if (this._parent) this._parent.chainWillChange(this, this._key, 1); }; -/** - Immediately flushes any events scheduled in the 'sync' queue. Bindings - use this queue so this method is a useful way to immediately force all - bindings in the application to sync. +Wp.chainWillChange = function(chain, path, depth) { + if (this._key) path = this._key+this._separator+path; - You should call this method anytime you need any changed state to propogate - throughout the app immediately without repainting the UI. + if (this._parent) { + this._parent.chainWillChange(this, path, depth+1); + } else { + if (depth>1) Ember.propertyWillChange(this.value(), path); + path = 'this.'+path; + if (this._paths[path]>0) Ember.propertyWillChange(this.value(), path); + } +}; - @returns {void} -*/ -Ember.run.sync = function() { - run.autorun(); - run.currentRunLoop.flush('sync'); +Wp.chainDidChange = function(chain, path, depth) { + if (this._key) path = this._key+this._separator+path; + if (this._parent) { + this._parent.chainDidChange(this, path, depth+1); + } else { + if (depth>1) Ember.propertyDidChange(this.value(), path); + path = 'this.'+path; + if (this._paths[path]>0) Ember.propertyDidChange(this.value(), path); + } }; -// .......................................................... -// TIMERS -// +Wp.didChange = function(suppressEvent) { + // invalidate my own value first. + if (this._watching) { + var obj = this._parent.value(); + if (obj !== this._object) { + removeChainWatcher(this._object, this._key, this); + this._object = obj; + addChainWatcher(obj, this._key, this); + } + this._value = undefined; -var timers = {}; // active timers... + // Special-case: the EachProxy relies on immediate evaluation to + // establish its observers. + if (this._parent && this._parent._key === '@each') + this.value(); + } -var laterScheduled = false; -function invokeLaterTimers() { - 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; - } + // then notify chains... + var chains = this._chains; + if (chains) { + for(var key in chains) { + if (!chains.hasOwnProperty(key)) continue; + chains[key].didChange(suppressEvent); } } - // schedule next timeout to fire... - if (earliest>0) setTimeout(invokeLaterTimers, earliest-(+ new Date())); -} - -/** - Invokes the passed target/method and optional arguments after a specified - period if time. The last parameter of this method must always be a number - of milliseconds. - - You should use this method whenever you need to run some action after a - period of time inside of using setTimeout(). This method will ensure that - items that expire during the same script execution cycle all execute - together, which is often more efficient than using a real setTimeout. + if (suppressEvent) return; - @param {Object} target - (optional) target of method to invoke + // and finally tell parent about my path changing... + if (this._parent) this._parent.chainDidChange(this, this._key, 1); +}; - @param {Function|String} method - The method to invoke. If you pass a string it will be resolved on the - target at the time the method is invoked. +// get the chains for the current object. If the current object has +// chains inherited from the proto they will be cloned and reconfigured for +// the current object. +/** @private */ +function chainsFor(obj) { + var m = meta(obj), ret = m.chains; + if (!ret) { + ret = m.chains = new ChainNode(null, null, obj); + } else if (ret.value() !== obj) { + ret = m.chains = ret.copy(obj); + } + return ret ; +} - @param {Object...} args - Optional arguments to pass to the timeout. - @param {Number} wait - Number of milliseconds to wait. +/** @private */ +function notifyChains(obj, m, keyName, methodName, arg) { + var nodes = m.chainWatchers; - @returns {Timer} an object you can use to cancel a timer at a later time. -*/ -Ember.run.later = function(target, method) { - var args, expires, timer, guid, wait; + if (!nodes || nodes.__emberproto__ !== obj) return; // nothing to do - // setTimeout compatibility... - if (arguments.length===2 && 'function' === typeof target) { - wait = method; - method = target; - target = undefined; - args = [target, method]; + nodes = nodes[keyName]; + if (!nodes) return; - } else { - args = slice.call(arguments); - wait = args.pop(); + for(var key in nodes) { + if (!nodes.hasOwnProperty(key)) continue; + nodes[key][methodName](arg); } +} - expires = (+ new Date())+wait; - timer = { target: target, method: method, expires: expires, args: args }; - guid = Ember.guidFor(timer); - timers[guid] = timer; - run.once(timers, invokeLaterTimers); - return guid; +Ember.overrideChains = function(obj, keyName, m) { + notifyChains(obj, m, keyName, 'didChange', true); }; -function invokeOnceTimer(guid, onceTimers) { - if (onceTimers[this.tguid]) delete onceTimers[this.tguid][this.mguid]; - if (timers[guid]) invoke(this.target, this.method, this.args, 2); - delete timers[guid]; +/** @private */ +function chainsWillChange(obj, keyName, m) { + notifyChains(obj, m, keyName, 'willChange'); } -/** - Schedules an item to run one time during the current RunLoop. Calling - this method with the same target/method combination will have no effect. - - Note that although you can pass optional arguments these will not be - considered when looking for duplicates. New arguments will replace previous - calls. - - @param {Object} target - (optional) target of method to invoke +/** @private */ +function chainsDidChange(obj, keyName, m) { + notifyChains(obj, m, keyName, 'didChange'); +} - @param {Function|String} method - The method to invoke. If you pass a string it will be resolved on the - target at the time the method is invoked. +// .......................................................... +// WATCH +// - @param {Object...} args - Optional arguments to pass to the timeout. +var WATCHED_PROPERTY = Ember.SIMPLE_PROPERTY.watched; +/** + @private - @returns {Object} timer + Starts watching a property on an object. Whenever the property changes, + invokes Ember.propertyWillChange and Ember.propertyDidChange. This is the + primitive used by observers and dependent keys; usually you will never call + this method directly but instead use higher level methods like + Ember.addObserver(). */ -Ember.run.once = function(target, method) { - var tguid = Ember.guidFor(target), mguid = Ember.guidFor(method), guid, timer; +Ember.watch = function(obj, keyName) { - var onceTimers = run.autorun().onceTimers; - guid = onceTimers[tguid] && onceTimers[tguid][mguid]; - if (guid && timers[guid]) { - timers[guid].args = slice.call(arguments); // replace args + // can't watch length on Array - it is special... + if (keyName === 'length' && Ember.typeOf(obj)==='array') return this; - } else { - timer = { - target: target, - method: method, - args: slice.call(arguments), - tguid: tguid, - mguid: mguid - }; + var m = meta(obj), watching = m.watching, desc; + keyName = normalizePath(keyName); - guid = Ember.guidFor(timer); - timers[guid] = timer; - if (!onceTimers[tguid]) onceTimers[tguid] = {}; - onceTimers[tguid][mguid] = guid; // so it isn't scheduled more than once + // activate watching first time + if (!watching[keyName]) { + watching[keyName] = 1; + if (isKeyName(keyName)) { + desc = m.descs[keyName]; + desc = desc ? desc.watched : WATCHED_PROPERTY; + if (desc) Ember.defineProperty(obj, keyName, desc); + } else { + chainsFor(obj).add(keyName); + } - run.schedule('actions', timer, invokeOnceTimer, guid, onceTimers); + } else { + watching[keyName] = (watching[keyName]||0)+1; } + return this; +}; - return guid; +Ember.isWatching = function(obj, keyName) { + return !!meta(obj).watching[keyName]; }; -var scheduledNext = false; -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); - } - } -} +Ember.watch.flushPending = flushPendingChains; -/** - Schedules an item to run after control has been returned to the system. - This is often equivalent to calling setTimeout(function...,1). +/** @private */ +Ember.unwatch = function(obj, keyName) { + // can't watch length on Array - it is special... + if (keyName === 'length' && Ember.typeOf(obj)==='array') return this; - @param {Object} target - (optional) target of method to invoke + var watching = meta(obj).watching, desc, descs; + keyName = normalizePath(keyName); + if (watching[keyName] === 1) { + watching[keyName] = 0; + if (isKeyName(keyName)) { + desc = meta(obj).descs[keyName]; + desc = desc ? desc.unwatched : SIMPLE_PROPERTY; + if (desc) Ember.defineProperty(obj, keyName, desc); + } else { + chainsFor(obj).remove(keyName); + } - @param {Function|String} method - The method to invoke. If you pass a string it will be resolved on the - target at the time the method is invoked. + } else if (watching[keyName]>1) { + watching[keyName]--; + } - @param {Object...} args - Optional arguments to pass to the timeout. + return this; +}; - @returns {Object} timer +/** + @private + + Call on an object when you first beget it from another object. This will + setup any chained watchers on the object instance as needed. This method is + safe to call multiple times. */ -Ember.run.next = function(target, method) { - var timer, guid; +Ember.rewatch = function(obj) { + var m = meta(obj, false), chains = m.chains, bindings = m.bindings, key, b; - timer = { - target: target, - method: method, - args: slice.call(arguments), - next: true - }; + // make sure the object has its own guid. + if (GUID_KEY in obj && !obj.hasOwnProperty(GUID_KEY)) { + Ember.generateGuid(obj, 'ember'); + } - guid = Ember.guidFor(timer); - timers[guid] = timer; + // make sure any chained watchers update. + if (chains && chains.value() !== obj) chainsFor(obj); - if (!scheduledNext) scheduledNext = setTimeout(invokeNextTimers, 1); - return guid; + // if the object has bindings then sync them.. + if (bindings && m.proto!==obj) { + for (key in bindings) { + b = !DEP_SKIP[key] && obj[key]; + if (b && b instanceof Ember.Binding) b.fromDidChange(obj); + } + } + + return this; }; +// .......................................................... +// PROPERTY CHANGES +// + /** - Cancels a scheduled item. Must be a value returned by `Ember.run.later()`, - `Ember.run.once()`, or `Ember.run.next()`. + This function is called just before an object property is about to change. + It will notify any before observers and prepare caches among other things. - @param {Object} timer - Timer object to cancel + Normally you will not need to call this method directly but if for some + reason you can't directly watch a property you can invoke this method + manually along with `Ember.propertyDidChange()` which you should call just + after the property value changes. + + @memberOf Ember + + @param {Object} obj + The object with the property that will change + + @param {String} keyName + The property key (or path) that will change. @returns {void} */ -Ember.run.cancel = function(timer) { - delete timers[timer]; -}; +function propertyWillChange(obj, keyName) { + var m = meta(obj, false), proto = m.proto, desc = m.descs[keyName]; + if (proto === obj) return ; + if (desc && desc.willChange) desc.willChange(obj, keyName); + dependentKeysWillChange(obj, keyName, m); + chainsWillChange(obj, keyName, m); + Ember.notifyBeforeObservers(obj, keyName); +} -// .......................................................... -// DEPRECATED API -// +Ember.propertyWillChange = propertyWillChange; /** - @namespace - @name Ember.RunLoop - @deprecated - @description Compatibility for Ember.run -*/ + This function is called just after an object property has changed. + It will notify any observers and clear caches among other things. -/** - @deprecated - @method + Normally you will not need to call this method directly but if for some + reason you can't directly watch a property you can invoke this method + manually along with `Ember.propertyWilLChange()` which you should call just + before the property value changes. - Use `#js:Ember.run.begin()` instead -*/ -Ember.RunLoop.begin = ember_deprecateFunc("Use Ember.run.begin instead of Ember.RunLoop.begin.", Ember.run.begin); + @memberOf Ember -/** - @deprecated - @method + @param {Object} obj + The object with the property that will change - Use `#js:Ember.run.end()` instead + @param {String} keyName + The property key (or path) that will change. + + @returns {void} */ -Ember.RunLoop.end = ember_deprecateFunc("Use Ember.run.end instead of Ember.RunLoop.end.", Ember.run.end); +function propertyDidChange(obj, keyName) { + var m = meta(obj, false), proto = m.proto, desc = m.descs[keyName]; + if (proto === obj) return ; + if (desc && desc.didChange) desc.didChange(obj, keyName); + dependentKeysDidChange(obj, keyName, m); + chainsDidChange(obj, keyName, m); + Ember.notifyObservers(obj, keyName); +} +Ember.propertyDidChange = propertyDidChange; +var NODE_STACK = []; -})({}); +/** + Tears down the meta on an object so that it can be garbage collected. + Multiple calls will have no effect. + + @param {Object} obj the object to destroy + @returns {void} +*/ +Ember.destroy = function (obj) { + var meta = obj[META_KEY], node, nodes, key, nodeObject; + if (meta) { + obj[META_KEY] = null; + // remove chainWatchers to remove circular references that would prevent GC + node = meta.chains; + if (node) { + NODE_STACK.push(node); + // process tree + while (NODE_STACK.length > 0) { + node = NODE_STACK.pop(); + // push children + nodes = node._chains; + if (nodes) { + for (key in nodes) { + if (nodes.hasOwnProperty(key)) { + NODE_STACK.push(nodes[key]); + } + } + } + // remove chainWatcher in node object + if (node._watching) { + nodeObject = node._object; + if (nodeObject) { + removeChainWatcher(nodeObject, node._key, node); + } + } + } + } + } +}; +})({}); (function(exports) { // ========================================================================== -// Project: Ember Runtime +// Project: Ember Metal // Copyright: ©2011 Strobe Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== /*globals ember_assert */ -// Ember.Logger -// get, getPath, setPath, trySetPath -// guidFor, isArray, meta -// addObserver, removeObserver -// Ember.run.schedule - -// .......................................................... -// CONSTANTS -// - +var o_create = Ember.platform.create; +var meta = Ember.meta; +var guidFor = Ember.guidFor; +var array_Slice = Array.prototype.slice; /** - @static - - Debug parameter you can turn on. This will log all bindings that fire to - the console. This should be disabled in production code. Note that you - can also enable this from the console or temporarily. + The event system uses a series of nested hashes to store listeners on an + object. When a listener is registered, or when an event arrives, these + hashes are consulted to determine which target and action pair to invoke. - @type Boolean - @default false -*/ -Ember.LOG_BINDINGS = false || !!Ember.ENV.LOG_BINDINGS; + The hashes are stored in the object's meta hash, and look like this: -/** - @static - - Performance paramter. This will benchmark the time spent firing each - binding. + // Object's meta hash + { + listeners: { // variable name: `listenerSet` + "foo:changed": { // variable name: `targetSet` + [targetGuid]: { // variable name: `actionSet` + [methodGuid]: { // variable name: `action` + target: [Object object], + method: [Function function], + xform: [Function function] + } + } + } + } + } - @type Boolean */ -Ember.BENCHMARK_BINDING_NOTIFICATIONS = !!Ember.ENV.BENCHMARK_BINDING_NOTIFICATIONS; -/** - @static +/** @private */ +var metaPath = Ember.metaPath; - Performance parameter. This will benchmark the time spend configuring each - binding. +// Gets the set of all actions, keyed on the guid of each action's +// method property. +/** @private */ +function actionSetFor(obj, eventName, target, writable) { + var targetGuid = guidFor(target); + return metaPath(obj, ['listeners', eventName, targetGuid], writable); +} - @type Boolean -*/ -Ember.BENCHMARK_BINDING_SETUP = !!Ember.ENV.BENCHMARK_BINDING_SETUP; +// Gets the set of all targets, keyed on the guid of each action's +// target property. +/** @private */ +function targetSetFor(obj, eventName) { + var listenerSet = meta(obj, false).listeners; + if (!listenerSet) { return false; } + return listenerSet[eventName] || false; +} -/** - @static +// TODO: This knowledge should really be a part of the +// meta system. +var SKIP_PROPERTIES = { __ember_source__: true }; - Default placeholder for multiple values in bindings. +/** @private */ +function iterateSet(targetSet, callback, params) { + if (!targetSet) { return false; } + // Iterate through all elements of the target set + for(var targetGuid in targetSet) { + if (SKIP_PROPERTIES[targetGuid]) { continue; } - @type String - @default '@@MULT@@' -*/ -Ember.MULTIPLE_PLACEHOLDER = '@@MULT@@'; + var actionSet = targetSet[targetGuid]; + if (actionSet) { + // Iterate through the elements of the action set + for(var methodGuid in actionSet) { + if (SKIP_PROPERTIES[methodGuid]) { continue; } + + var action = actionSet[methodGuid]; + if (action) { + if (callback(action, params) === true) { + return true; + } + } + } + } + } + return false; +} -/** - @static +/** @private */ +function invokeAction(action, params) { + var method = action.method, target = action.target, xform = action.xform; + // If there is no target, the target is the object + // on which the event was fired. + if (!target) { target = params[0]; } + if ('string' === typeof method) { method = target[method]; } + + // Listeners can provide an `xform` function, which can perform + // arbitrary transformations, such as changing the order of + // parameters. + // + // This is primarily used by ember-runtime's observer system, which + // provides a higher level abstraction on top of events, including + // dynamically looking up current values and passing them into the + // registered listener. + if (xform) { + xform(target, method, params); + } else { + method.apply(target, params); + } +} - Default placeholder for empty values in bindings. Used by notEmpty() - helper unless you specify an alternative. +/** + The parameters passed to an event listener are not exactly the + parameters passed to an observer. if you pass an xform function, it will + be invoked and is able to translate event listener parameters into the form + that observers are expecting. - @type String - @default '@@EMPTY@@' + @memberOf Ember */ -Ember.EMPTY_PLACEHOLDER = '@@EMPTY@@'; - -// .......................................................... -// TYPE COERCION HELPERS -// - -// Coerces a non-array value into an array. -function MULTIPLE(val) { - if (val instanceof Array) return val; - if (val === undefined || val === null) return []; - return [val]; -} +function addListener(obj, eventName, target, method, xform) { + ember_assert("You must pass at least an object and event name to Ember.addListener", !!obj && !!eventName); -// Treats a single-element array as the element. Otherwise -// returns a placeholder. -function SINGLE(val, placeholder) { - if (val instanceof Array) { - if (val.length>1) return placeholder; - else return val[0]; + if (!method && 'function' === typeof target) { + method = target; + target = null; } - return val; -} -// Coerces the binding value into a Boolean. + var actionSet = actionSetFor(obj, eventName, target, true), + methodGuid = guidFor(method), ret; -var BOOL = { - to: function (val) { - return !!val; + if (!actionSet[methodGuid]) { + actionSet[methodGuid] = { target: target, method: method, xform: xform }; + } else { + actionSet[methodGuid].xform = xform; // used by observers etc to map params } -}; -// Returns the Boolean inverse of the value. -var NOT = { - to: function NOT(val) { - return !val; + if ('function' === typeof obj.didAddListener) { + obj.didAddListener(eventName, target, method); } -}; - -var get = Ember.get, - getPath = Ember.getPath, - setPath = Ember.setPath, - guidFor = Ember.guidFor, - isGlobalPath = Ember.isGlobalPath; -// Applies a binding's transformations against a value. -function getTransformedValue(binding, val, obj, dir) { + return ret; // return true if this is the first listener. +} - // First run a type transform, if it exists, that changes the fundamental - // type of the value. For example, some transforms convert an array to a - // single object. +/** @memberOf Ember */ +function removeListener(obj, eventName, target, method) { + if (!method && 'function'===typeof target) { + method = target; + target = null; + } - var typeTransform = binding._typeTransform; - if (typeTransform) { val = typeTransform(val, binding._placeholder); } + var actionSet = actionSetFor(obj, eventName, target, true), + methodGuid = guidFor(method); - // handle transforms - var transforms = binding._transforms, - len = transforms ? transforms.length : 0, - idx; + // we can't simply delete this parameter, because if we do, we might + // re-expose the property from the prototype chain. + if (actionSet && actionSet[methodGuid]) { actionSet[methodGuid] = null; } - for(idx=0;idx null - - [a] => a - - [a,b,c] => Multiple Placeholder - - You can pass in an optional multiple placeholder or it will use the - default. - - Note that this transform will only happen on forwarded valued. Reverse - values are send unchanged. - - @param {String} fromPath from path or null - @param {Object} [placeholder] Placeholder value. - @returns {Ember.Binding} this - */ - single: function(placeholder) { - if (placeholder===undefined) placeholder = Ember.MULTIPLE_PLACEHOLDER; - this._typeTransform = SINGLE; - this._placeholder = placeholder; - return this; - }, - - /** - Adds a transform that will convert the passed value to an array. If - the value is null or undefined, it will be converted to an empty array. - - @param {String} [fromPath] - @returns {Ember.Binding} this - */ - multiple: function() { - this._typeTransform = MULTIPLE; - this._placeholder = null; - return this; - }, - - /** - Adds a transform to convert the value to a bool value. If the value is - an array it will return true if array is not empty. If the value is a - string it will return true if the string is not empty. - - @returns {Ember.Binding} this - */ - bool: function() { - this.transform(BOOL); - return this; - }, - - /** - Adds a transform that will return the placeholder value if the value is - null, undefined, an empty array or an empty string. See also notNull(). - - @param {Object} [placeholder] Placeholder value. - @returns {Ember.Binding} this - */ - notEmpty: function(placeholder) { - if (placeholder === null || placeholder === undefined) { - placeholder = Ember.EMPTY_PLACEHOLDER; - } - - this.transform({ - to: function(val) { return empty(val) ? placeholder : val; } - }); - - return this; - }, - - /** - Adds a transform that will return the placeholder value if the value is - null or undefined. Otherwise it will passthrough untouched. See also notEmpty(). - - @param {String} fromPath from path or null - @param {Object} [placeholder] Placeholder value. - @returns {Ember.Binding} this - */ - notNull: function(placeholder) { - if (placeholder === null || placeholder === undefined) { - placeholder = Ember.EMPTY_PLACEHOLDER; - } - - this.transform({ - to: function(val) { return (val === null || val === undefined) ? placeholder : val; } - }); - - return this; - }, - - /** - Adds a transform to convert the value to the inverse of a bool value. This - uses the same transform as bool() but inverts it. - - @returns {Ember.Binding} this - */ - not: function() { - this.transform(NOT); - return this; - }, - - /** - Adds a transform that will return true if the value is null or undefined, false otherwise. - - @returns {Ember.Binding} this - */ - isNull: function() { - this.transform(function(val) { return val === null || val === undefined; }); - return this; - }, - - /** @private */ - toString: function() { - var oneWay = this._oneWay ? '[oneWay]' : ''; - return "Ember.Binding<" + guidFor(this) + ">(" + this._from + " -> " + this._to + ")" + oneWay; - }, - - // .......................................................... - // CONNECT AND SYNC - // - - /** - Attempts to connect this binding instance so that it can receive and relay - changes. This method will raise an exception if you have not set the - from/to properties yet. - - @param {Object} obj - The root object for this binding. - - @param {Boolean} preferFromParam - private: Normally, `connect` cannot take an object if `from` already set - an object. Internally, we would like to be able to provide a default object - to be used if no object was provided via `from`, so this parameter turns - off the assertion. - - @returns {Ember.Binding} this - */ - connect: function(obj) { - ember_assert('Must pass a valid object to Ember.Binding.connect()', !!obj); - - var oneWay = this._oneWay, operand = this._operand; - - // add an observer on the object to be notified when the binding should be updated - Ember.addObserver(obj, this._from, this, this.fromDidChange); - - // if there is an operand, add an observer onto it as well - if (operand) { Ember.addObserver(obj, operand, this, this.fromDidChange); } - - // if the binding is a two-way binding, also set up an observer on the target - // object. - if (!oneWay) { Ember.addObserver(obj, this._to, this, this.toDidChange); } - - if (Ember.meta(obj,false).proto !== obj) { this._scheduleSync(obj, 'fwd'); } - - this._readyToSync = true; - return this; - }, - - /** - Disconnects the binding instance. Changes will no longer be relayed. You - will not usually need to call this method. - - @param {Object} obj - The root object you passed when connecting the binding. - - @returns {Ember.Binding} this - */ - disconnect: function(obj) { - ember_assert('Must pass a valid object to Ember.Binding.disconnect()', !!obj); - - var oneWay = this._oneWay, operand = this._operand; - - // remove an observer on the object so we're no longer notified of - // changes that should update bindings. - Ember.removeObserver(obj, this._from, this, this.fromDidChange); - - // if there is an operand, remove the observer from it as well - if (operand) Ember.removeObserver(obj, operand, this, this.fromDidChange); - - // if the binding is two-way, remove the observer from the target as well - if (!oneWay) Ember.removeObserver(obj, this._to, this, this.toDidChange); - - this._readyToSync = false; // disable scheduled syncs... - return this; - }, - - // .......................................................... - // PRIVATE - // - - /** @private - called when the from side changes */ - fromDidChange: function(target) { - this._scheduleSync(target, 'fwd'); - }, - - /** @private - called when the to side changes */ - toDidChange: function(target) { - this._scheduleSync(target, 'back'); - }, - - /** @private */ - _scheduleSync: function(obj, dir) { - var guid = guidFor(obj), existingDir = this[guid]; - - // if we haven't scheduled the binding yet, schedule it - if (!existingDir) { - Ember.run.schedule('sync', this, this._sync, obj); - this[guid] = dir; - } - - // If both a 'back' and 'fwd' sync have been scheduled on the same object, - // default to a 'fwd' sync so that it remains deterministic. - if (existingDir === 'back' && dir === 'fwd') { - this[guid] = 'fwd'; - } - }, - - /** @private */ - _sync: function(obj) { - var log = Ember.LOG_BINDINGS; - - // don't synchronize destroyed objects or disconnected bindings - if (obj.isDestroyed || !this._readyToSync) { return; } - - // get the direction of the binding for the object we are - // synchronizing from - var guid = guidFor(obj), direction = this[guid]; - - var fromPath = this._from, toPath = this._to, lastSet; - - delete this[guid]; - - if (direction === 'fwd') { - lastSet = this._cache.back; - } else if (direction === 'back') { - lastSet = this._cache.fwd; - } - - var fromValue, toValue; - - // There's a bit of duplicate logic here, but the order is important. - // - // We want to avoid ping-pong bindings. To do this, we store off the - // guid of the item we are setting. Later, we avoid synchronizing - // bindings in the other direction if the raw value we are copying - // is the same as the guid of the last thing we set. - // - // Use guids here to avoid unnecessarily holding hard references - // to objects. - if (direction === 'fwd') { - fromValue = getFromValue(obj, this); - if (this._cache.back === guidFor(fromValue)) { return; } - this._cache.fwd = guidFor(fromValue); - - toValue = getToValue(obj, this); - } else if (direction === 'back') { - toValue = getToValue(obj, this); - if (this._cache.fwd === guidFor(toValue)) { return; } - this._cache.back = guidFor(toValue); - - fromValue = getFromValue(obj, this); - } - - fromValue = getTransformedValue(this, fromValue, obj, 'to'); - toValue = getTransformedValue(this, toValue, obj, 'from'); - - if (toValue === fromValue) { return; } +(function(exports) { +// ========================================================================== +// Project: Ember Runtime +// Copyright: ©2011 Strobe Inc. and contributors. +// License: Licensed under MIT license (see license.js) +// ========================================================================== +var Mixin, MixinDelegate, REQUIRED, Alias; +var classToString, superClassString; - // if we're synchronizing from the remote object... - if (direction === 'fwd') { - if (log) { Ember.Logger.log(' ', this.toString(), toValue, '->', fromValue, obj); } - Ember.trySetPath(Ember.isGlobalPath(toPath) ? window : obj, toPath, fromValue); +var a_map = Ember.ArrayUtils.map; +var a_indexOf = Ember.ArrayUtils.indexOf; +var a_forEach = Ember.ArrayUtils.forEach; +var a_slice = Array.prototype.slice; +var EMPTY_META = {}; // dummy for non-writable meta +var META_SKIP = { __emberproto__: true, __ember_count__: true }; - // if we're synchronizing *to* the remote object - } else if (direction === 'back') {// && !this._oneWay) { - if (log) { Ember.Logger.log(' ', this.toString(), toValue, '<-', fromValue, obj); } - Ember.trySetPath(Ember.isGlobalPath(fromPath) ? window : obj, fromPath, toValue); - } - } +var o_create = Ember.platform.create; -}; +/** @private */ +function meta(obj, writable) { + var m = Ember.meta(obj, writable!==false), ret = m.mixins; + if (writable===false) return ret || EMPTY_META; -function mixinProperties(to, from) { - for (var key in from) { - if (from.hasOwnProperty(key)) { - to[key] = from[key]; - } + if (!ret) { + ret = m.mixins = { __emberproto__: obj }; + } else if (ret.__emberproto__ !== obj) { + ret = m.mixins = o_create(ret); + ret.__emberproto__ = obj; } + return ret; } -mixinProperties(Binding, -/** @scope Ember.Binding */ { - - /** - @see Ember.Binding.prototype.from - */ - from: function() { - var C = this, binding = new C(); - return binding.from.apply(binding, arguments); - }, - - /** - @see Ember.Binding.prototype.to - */ - to: function() { - var C = this, binding = new C(); - return binding.to.apply(binding, arguments); - }, - - /** - @see Ember.Binding.prototype.oneWay - */ - oneWay: function(from, flag) { - var C = this, binding = new C(null, from); - return binding.oneWay(flag); - }, - - /** - @see Ember.Binding.prototype.single - */ - single: function(from) { - var C = this, binding = new C(null, from); - return binding.single(); - }, - - /** - @see Ember.Binding.prototype.multiple - */ - multiple: function(from) { - var C = this, binding = new C(null, from); - return binding.multiple(); - }, - - /** - @see Ember.Binding.prototype.transform - */ - transform: function(func) { - var C = this, binding = new C(); - return binding.transform(func); - }, - - /** - @see Ember.Binding.prototype.notEmpty - */ - notEmpty: function(from, placeholder) { - var C = this, binding = new C(null, from); - return binding.notEmpty(placeholder); - }, - - /** - @see Ember.Binding.prototype.bool - */ - bool: function(from) { - var C = this, binding = new C(null, from); - return binding.bool(); - }, +/** @private */ +function initMixin(mixin, args) { + if (args && args.length > 0) { + mixin.mixins = a_map(args, function(x) { + if (x instanceof Mixin) return x; - /** - @see Ember.Binding.prototype.not - */ - not: function(from) { - var C = this, binding = new C(null, from); - return binding.not(); - }, + // Note: Manually setup a primitive mixin here. This is the only + // way to actually get a primitive mixin. This way normal creation + // of mixins will give you combined mixins... + var mixin = new Mixin(); + mixin.properties = x; + return mixin; + }); + } + return mixin; +} - /** - Adds a transform that forwards the logical 'AND' of values at 'pathA' and - 'pathB' whenever either source changes. Note that the transform acts - strictly as a one-way binding, working only in the direction +var NATIVES = [Boolean, Object, Number, Array, Date, String]; +/** @private */ +function isMethod(obj) { + if ('function' !== typeof obj || obj.isMethod===false) return false; + return a_indexOf(NATIVES, obj)<0; +} - 'pathA' AND 'pathB' --> value (value returned is the result of ('pathA' && 'pathB')) +/** @private */ +function mergeMixins(mixins, m, descs, values, base) { + var len = mixins.length, idx, mixin, guid, props, value, key, ovalue, concats; - Usage example where a delete button's `isEnabled` value is determined by - whether something is selected in a list and whether the current user is - allowed to delete: + /** @private */ + function removeKeys(keyName) { + delete descs[keyName]; + delete values[keyName]; + } - deleteButton: Ember.ButtonView.design({ - isEnabledBinding: Ember.Binding.and('MyApp.itemsController.hasSelection', 'MyApp.userController.canDelete') - }) + for(idx=0;idx value (value returned is the result of ('pathA' || 'pathB')) + if (props) { - @param {String} pathA The first part of the conditional - @param {String} pathB The second part of the conditional - */ - or: function(pathA, pathB) { - var C = this, binding = new C(null, pathA).oneWay(); - binding._operand = pathB; - binding._operation = OR_OPERATION; - return binding; - } + // reset before adding each new mixin to pickup concats from previous + concats = values.concatenatedProperties || base.concatenatedProperties; + if (props.concatenatedProperties) { + concats = concats ? concats.concat(props.concatenatedProperties) : props.concatenatedProperties; + } -}); + for (key in props) { + if (!props.hasOwnProperty(key)) continue; + value = props[key]; + if (value instanceof Ember.Descriptor) { + if (value === REQUIRED && descs[key]) { continue; } -/** - @class + descs[key] = value; + values[key] = undefined; + } else { - A binding simply connects the properties of two objects so that whenever the - value of one property changes, the other property will be changed also. You - do not usually work with Binding objects directly but instead describe - bindings in your class definition using something like: + // impl super if needed... + if (isMethod(value)) { + ovalue = (descs[key] === Ember.SIMPLE_PROPERTY) && values[key]; + if (!ovalue) ovalue = base[key]; + if ('function' !== typeof ovalue) ovalue = null; + if (ovalue) { + var o = value.__ember_observes__, ob = value.__ember_observesBefore__; + value = Ember.wrap(value, ovalue); + value.__ember_observes__ = o; + value.__ember_observesBefore__ = ob; + } + } else if ((concats && a_indexOf(concats, key)>=0) || key === 'concatenatedProperties') { + var baseValue = values[key] || base[key]; + value = baseValue ? baseValue.concat(value) : Ember.makeArray(value); + } - valueBinding: "MyApp.someController.title" + descs[key] = Ember.SIMPLE_PROPERTY; + values[key] = value; + } + } - This will create a binding from `MyApp.someController.title` to the `value` - property of your object instance automatically. Now the two values will be - kept in sync. + // manually copy toString() because some JS engines do not enumerate it + if (props.hasOwnProperty('toString')) { + base.toString = props.toString; + } - ## Customizing Your Bindings + } else if (mixin.mixins) { + mergeMixins(mixin.mixins, m, descs, values, base); + if (mixin._without) a_forEach(mixin._without, removeKeys); + } + } +} - In addition to synchronizing values, bindings can also perform some basic - transforms on values. These transforms can help to make sure the data fed - into one object always meets the expectations of that object regardless of - what the other object outputs. +/** @private */ +var defineProperty = Ember.defineProperty; - To customize a binding, you can use one of the many helper methods defined - on Ember.Binding like so: +/** @private */ +function writableReq(obj) { + var m = Ember.meta(obj), req = m.required; + if (!req || (req.__emberproto__ !== obj)) { + req = m.required = req ? o_create(req) : { __ember_count__: 0 }; + req.__emberproto__ = obj; + } + return req; +} - valueBinding: Ember.Binding.single("MyApp.someController.title") +/** @private */ +function getObserverPaths(value) { + return ('function' === typeof value) && value.__ember_observes__; +} - This will create a binding just like the example above, except that now the - binding will convert the value of `MyApp.someController.title` to a single - object (removing any arrays) before applying it to the `value` property of - your object. +/** @private */ +function getBeforeObserverPaths(value) { + return ('function' === typeof value) && value.__ember_observesBefore__; +} - You can also chain helper methods to build custom bindings like so: +Ember._mixinBindings = function(obj, key, value, m) { + return value; +}; - valueBinding: Ember.Binding.single("MyApp.someController.title").notEmpty("(EMPTY)") +/** @private */ +function applyMixin(obj, mixins, partial) { + var descs = {}, values = {}, m = Ember.meta(obj), req = m.required; + var key, willApply, didApply, value, desc; - This will force the value of MyApp.someController.title to be a single value - and then check to see if the value is "empty" (null, undefined, empty array, - or an empty string). If it is empty, the value will be set to the string - "(EMPTY)". + var mixinBindings = Ember._mixinBindings; - ## One Way Bindings + // Go through all mixins and hashes passed in, and: + // + // * Handle concatenated properties + // * Set up _super wrapping if necessary + // * Set up descriptors (simple, watched or computed properties) + // * Copying `toString` in broken browsers + mergeMixins(mixins, meta(obj), descs, values, obj); - One especially useful binding customization you can use is the `oneWay()` - helper. This helper tells Ember that you are only interested in - receiving changes on the object you are binding from. For example, if you - are binding to a preference and you want to be notified if the preference - has changed, but your object will not be changing the preference itself, you - could do: + if (MixinDelegate.detect(obj)) { + willApply = values.willApplyProperty || obj.willApplyProperty; + didApply = values.didApplyProperty || obj.didApplyProperty; + } - bigTitlesBinding: Ember.Binding.oneWay("MyApp.preferencesController.bigTitles") + for(key in descs) { + if (!descs.hasOwnProperty(key)) continue; - This way if the value of MyApp.preferencesController.bigTitles changes the - "bigTitles" property of your object will change also. However, if you - change the value of your "bigTitles" property, it will not update the - preferencesController. + desc = descs[key]; + value = values[key]; - One way bindings are almost twice as fast to setup and twice as fast to - execute because the binding only has to worry about changes to one side. + if (desc === REQUIRED) { + if (!(key in obj)) { + if (!partial) throw new Error('Required property not defined: '+key); - You should consider using one way bindings anytime you have an object that - may be created frequently and you do not intend to change a property; only - to monitor it for changes. (such as in the example above). + // for partial applies add to hash of required keys + req = writableReq(obj); + req.__ember_count__++; + req[key] = true; + } - ## Adding Custom Transforms + } else { - In addition to using the standard helpers provided by Ember, you can - also defined your own custom transform functions which will be used to - convert the value. To do this, just define your transform function and add - it to the binding with the transform() helper. The following example will - not allow Integers less than ten. Note that it checks the value of the - bindings and allows all other values to pass: + while (desc instanceof Alias) { - valueBinding: Ember.Binding.transform(function(value, binding) { - return ((Ember.typeOf(value) === 'number') && (value < 10)) ? 10 : value; - }).from("MyApp.someController.value") + var altKey = desc.methodName; + if (descs[altKey]) { + value = values[altKey]; + desc = descs[altKey]; + } else if (m.descs[altKey]) { + desc = m.descs[altKey]; + value = desc.val(obj, altKey); + } else { + value = obj[altKey]; + desc = Ember.SIMPLE_PROPERTY; + } + } - If you would like to instead use this transform on a number of bindings, - you can also optionally add your own helper method to Ember.Binding. This - method should simply return the value of `this.transform()`. The example - below adds a new helper called `notLessThan()` which will limit the value to - be not less than the passed minimum: + if (willApply) willApply.call(obj, key); - Ember.Binding.reopen({ - notLessThan: function(minValue) { - return this.transform(function(value, binding) { - return ((Ember.typeOf(value) === 'number') && (value < minValue)) ? minValue : value; - }); + var observerPaths = getObserverPaths(value), + curObserverPaths = observerPaths && getObserverPaths(obj[key]), + beforeObserverPaths = getBeforeObserverPaths(value), + curBeforeObserverPaths = beforeObserverPaths && getBeforeObserverPaths(obj[key]), + len, idx; + + if (curObserverPaths) { + len = curObserverPaths.length; + for(idx=0;idx0) { + var keys = []; + for(key in req) { + if (META_SKIP[key]) continue; + keys.push(key); + } + throw new Error('Required properties not defined: '+keys.join(',')); + } + return obj; +} - When you define a custom binding, you are usually describing the property - you want to bind from (such as "MyApp.someController.value" in the examples - above). When your object is created, it will automatically assign the value - you want to bind "to" based on the name of your binding key. In the - examples above, during init, Ember objects will effectively call - something like this on your binding: +Ember.mixin = function(obj) { + var args = a_slice.call(arguments, 1); + return applyMixin(obj, args, false); +}; - binding = Ember.Binding.from(this.valueBinding).to("value"); - This creates a new binding instance based on the template you provide, and - sets the to path to the "value" property of the new object. Now that the - binding is fully configured with a "from" and a "to", it simply needs to be - connected to become active. This is done through the connect() method: +/** + @constructor +*/ +Ember.Mixin = function() { return initMixin(this, arguments); }; - binding.connect(this); +/** @private */ +Mixin = Ember.Mixin; - Note that when you connect a binding you pass the object you want it to be - connected to. This object will be used as the root for both the from and - to side of the binding when inspecting relative paths. This allows the - binding to be automatically inherited by subclassed objects as well. +/** @private */ +Mixin._apply = applyMixin; - Now that the binding is connected, it will observe both the from and to side - and relay changes. +Mixin.applyPartial = function(obj) { + var args = a_slice.call(arguments, 1); + return applyMixin(obj, args, true); +}; - If you ever needed to do so (you almost never will, but it is useful to - understand this anyway), you could manually create an active binding by - using the Ember.bind() helper method. (This is the same method used by - to setup your bindings on objects): +Mixin.create = function() { + classToString.processed = false; + var M = this; + return initMixin(new M(), arguments); +}; - Ember.bind(MyApp.anotherObject, "value", "MyApp.someController.value"); +Mixin.prototype.reopen = function() { - Both of these code fragments have the same effect as doing the most friendly - form of binding creation like so: + var mixin, tmp; - MyApp.anotherObject = Ember.Object.create({ - valueBinding: "MyApp.someController.value", + if (this.properties) { + mixin = Mixin.create(); + mixin.properties = this.properties; + delete this.properties; + this.mixins = [mixin]; + } - // OTHER CODE FOR THIS OBJECT... + var len = arguments.length, mixins = this.mixins, idx; - }); + for(idx=0;idx= 0) { + if (_detect(mixins[loc], targetMixin, seen)) return true; + } + return false; +} - @returns {Ember.Binding} binding instance -*/ -Ember.bind = function(obj, to, from) { - return new Ember.Binding(to, from).connect(obj); +Mixin.prototype.detect = function(obj) { + if (!obj) return false; + if (obj instanceof Mixin) return _detect(obj, this, {}); + return !!meta(obj, false)[Ember.guidFor(this)]; }; -Ember.oneWay = function(obj, to, from) { - return new Ember.Binding(to, from).oneWay().connect(obj); +Mixin.prototype.without = function() { + var ret = new Mixin(this); + ret._without = a_slice.call(arguments); + return ret; }; -})({}); +/** @private */ +function _keys(ret, mixin, seen) { + if (seen[Ember.guidFor(mixin)]) return; + seen[Ember.guidFor(mixin)] = true; + if (mixin.properties) { + var props = mixin.properties; + for(var key in props) { + if (props.hasOwnProperty(key)) ret[key] = true; + } + } else if (mixin.mixins) { + a_forEach(mixin.mixins, function(x) { _keys(ret, x, seen); }); + } +} -(function(exports) { -// ========================================================================== -// Project: Ember Metal -// Copyright: ©2011 Strobe Inc. and contributors. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -/*globals ember_assert */ -var meta = Ember.meta; -var guidFor = Ember.guidFor; -var USE_ACCESSORS = Ember.USE_ACCESSORS; -var a_slice = Array.prototype.slice; -var o_create = Ember.platform.create; -var o_defineProperty = Ember.platform.defineProperty; +Mixin.prototype.keys = function() { + var keys = {}, seen = {}, ret = []; + _keys(keys, this, seen); + for(var key in keys) { + if (keys.hasOwnProperty(key)) ret.push(key); + } + return ret; +}; -// .......................................................... -// DEPENDENT KEYS -// +/** @private - make Mixin's have nice displayNames */ -// data structure: -// meta.deps = { -// 'depKey': { -// 'keyName': count, -// __emberproto__: SRC_OBJ [to detect clones] -// }, -// __emberproto__: SRC_OBJ -// } +var NAME_KEY = Ember.GUID_KEY+'_name'; +var get = Ember.get; -function uniqDeps(obj, depKey) { - var m = meta(obj), deps, ret; - deps = m.deps; - if (!deps) { - deps = m.deps = { __emberproto__: obj }; - } else if (deps.__emberproto__ !== obj) { - deps = m.deps = o_create(deps); - deps.__emberproto__ = obj; - } +/** @private */ +function processNames(paths, root, seen) { + var idx = paths.length; + for(var key in root) { + if (!root.hasOwnProperty || !root.hasOwnProperty(key)) continue; + var obj = root[key]; + paths[idx] = key; + + if (obj && obj.toString === classToString) { + obj[NAME_KEY] = paths.join('.'); + } else if (obj && get(obj, 'isNamespace')) { + if (seen[Ember.guidFor(obj)]) continue; + seen[Ember.guidFor(obj)] = true; + processNames(paths, obj, seen); + } - ret = deps[depKey]; - if (!ret) { - ret = deps[depKey] = { __emberproto__: obj }; - } else if (ret.__emberproto__ !== obj) { - ret = deps[depKey] = o_create(ret); - ret.__emberproto__ = obj; } - - return ret; + paths.length = idx; // cut out last item } -function addDependentKey(obj, keyName, depKey) { - var deps = uniqDeps(obj, depKey); - deps[keyName] = (deps[keyName] || 0) + 1; - Ember.watch(obj, depKey); -} +/** @private */ +function findNamespaces() { + var Namespace = Ember.Namespace, obj; -function removeDependentKey(obj, keyName, depKey) { - var deps = uniqDeps(obj, depKey); - deps[keyName] = (deps[keyName] || 0) - 1; - Ember.unwatch(obj, depKey); -} + if (Namespace.PROCESSED) { return; } -function addDependentKeys(desc, obj, keyName) { - var keys = desc._dependentKeys, - len = keys ? keys.length : 0; - for(var idx=0;idx0, - ret, oldSuspended, lastSetValues; + // TODO: Namespace should really be in Metal + if (Namespace) { + if (!this[NAME_KEY] && !classToString.processed) { + if (!Namespace.PROCESSED) { + findNamespaces(); + Namespace.PROCESSED = true; + } - oldSuspended = desc._suspended; - desc._suspended = this; + classToString.processed = true; - watched = watched && m.lastSetValues[keyName]!==guidFor(value); - if (watched) { - m.lastSetValues[keyName] = guidFor(value); - Ember.propertyWillChange(this, keyName); + var namespaces = Namespace.NAMESPACES; + for (var i=0, l=namespaces.length; i0) { + args = args.length>ignore ? slice.call(args, ignore) : null; + } - if (this._cacheable) { - cache = meta(obj).cache; - if (keyName in cache) return cache[keyName]; - ret = cache[keyName] = this.func.call(obj, keyName); + // Unfortunately in some browsers we lose the backtrace if we rethrow the existing error, + // so in the event that we don't have an `onerror` handler we don't wrap in a try/catch + if ('function' === typeof Ember.onerror) { + try { + // IE8's Function.prototype.apply doesn't accept undefined/null arguments. + return method.apply(target || this, args || []); + } catch (error) { + Ember.onerror(error); + } } else { - ret = this.func.call(obj, keyName); + // IE8's Function.prototype.apply doesn't accept undefined/null arguments. + return method.apply(target || this, args || []); } - return ret ; -}; +} -/** @private - impl descriptor API */ -Cp.set = function(obj, keyName, value) { - var cacheable = this._cacheable; - var m = meta(obj, cacheable), - watched = (m.source===obj) && m.watching[keyName]>0, - ret, oldSuspended, lastSetValues; +// .......................................................... +// RUNLOOP +// - oldSuspended = this._suspended; - this._suspended = obj; +var timerMark; // used by timers... - watched = watched && m.lastSetValues[keyName]!==guidFor(value); - if (watched) { - m.lastSetValues[keyName] = guidFor(value); - Ember.propertyWillChange(obj, keyName); +/** @private */ +var K = function() {}; + +/** @private */ +var RunLoop = function(prev) { + var self; + + if (this instanceof RunLoop) { + self = this; + } else { + self = new K(); } - if (cacheable) delete m.cache[keyName]; - ret = this.func.call(obj, keyName, value); - if (cacheable) m.cache[keyName] = ret; - if (watched) Ember.propertyDidChange(obj, keyName); - this._suspended = oldSuspended; - return ret; -}; + self._prev = prev || null; + self.onceTimers = {}; -Cp.val = function(obj, keyName) { - return meta(obj, false).values[keyName]; + return self; }; -if (!Ember.platform.hasPropertyAccessors) { - Cp.setup = function(obj, keyName, value) { - obj[keyName] = undefined; // so it shows up in key iteration - addDependentKeys(this, obj, keyName); - }; +K.prototype = RunLoop.prototype; -} else if (!USE_ACCESSORS) { - Cp.setup = function(obj, keyName) { - // throw exception if not using Ember.get() and Ember.set() when supported - o_defineProperty(obj, keyName, CP_DESC); - addDependentKeys(this, obj, keyName); - }; -} +RunLoop.prototype = { + end: function() { + this.flush(); + }, -/** - This helper returns a new property descriptor that wraps the passed - computed property function. You can use this helper to define properties - with mixins or via Ember.defineProperty(). + prev: function() { + return this._prev; + }, - The function you pass will be used to both get and set property values. - The function should accept two parameters, key and value. If value is not - undefined you should set the value first. In either case return the - current value of the property. + // .......................................................... + // Delayed Actions + // - @param {Function} func - The computed property function. + schedule: function(queueName, target, method) { + var queues = this._queues, queue; + if (!queues) queues = this._queues = {}; + queue = queues[queueName]; + if (!queue) queue = queues[queueName] = []; - @returns {Ember.ComputedProperty} property descriptor instance -*/ -Ember.computed = function(func) { - var args; + var args = arguments.length>3 ? slice.call(arguments, 3) : null; + queue.push({ target: target, method: method, args: args }); + return this; + }, - if (arguments.length > 1) { - args = [].slice.call(arguments, 0, -1); - func = [].slice.call(arguments, -1)[0]; - } + flush: function(queueName) { + var queues = this._queues, queueNames, idx, len, queue, log; - var cp = new ComputedProperty(func); + if (!queues) return this; // nothing to do - if (args) { - cp.property.apply(cp, args); - } + function iter(item) { + invoke(item.target, item.method, item.args); + } - return cp; -}; + Ember.watch.flushPending(); // make sure all chained watchers are setup -})({}); + if (queueName) { + while (this._queues && (queue = this._queues[queueName])) { + this._queues[queueName] = null; + // the sync phase is to allow property changes to propogate. don't + // invoke observers until that is finished. + if (queueName === 'sync') { + log = Ember.LOG_BINDINGS; + if (log) Ember.Logger.log('Begin: Flush Sync Queue'); -(function(exports) { -// ========================================================================== -// Project: Ember Metal -// Copyright: ©2011 Strobe Inc. and contributors. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -/*globals ember_assert */ -var o_create = Ember.platform.create; -var meta = Ember.meta; -var guidFor = Ember.guidFor; -var array_Slice = Array.prototype.slice; + Ember.beginPropertyChanges(); + try { + forEach(queue, iter); + } finally { + Ember.endPropertyChanges(); + } -/** - The event system uses a series of nested hashes to store listeners on an - object. When a listener is registered, or when an event arrives, these - hashes are consulted to determine which target and action pair to invoke. + if (log) Ember.Logger.log('End: Flush Sync Queue'); - The hashes are stored in the object's meta hash, and look like this: + } else { + forEach(queue, iter); + } + } - // Object's meta hash - { - listeners: { // variable name: `listenerSet` - "foo:changed": { // variable name: `targetSet` - [targetGuid]: { // variable name: `actionSet` - [methodGuid]: { // variable name: `action` - target: [Object object], - method: [Function function], - xform: [Function function] + } else { + queueNames = Ember.run.queues; + len = queueNames.length; + do { + this._queues = null; + for(idx=0;idx= timer.expires) { + delete timers[key]; + invoke(timer.target, timer.method, timer.args, 2); + } else { + if (earliest<0 || (timer.expires < earliest)) earliest=timer.expires; + } } } - return ret; + // schedule next timeout to fire... + if (earliest>0) setTimeout(invokeLaterTimers, earliest-(+ new Date())); } -Ember.addListener = addListener; -Ember.removeListener = removeListener; -Ember.sendEvent = sendEvent; -Ember.hasListeners = hasListeners; -Ember.watchedEvents = watchedEvents; -Ember.listenersFor = listenersFor; - -})({}); - +/** + Invokes the passed target/method and optional arguments after a specified + period if time. The last parameter of this method must always be a number + of milliseconds. -(function(exports) { -// ========================================================================== -// Project: Ember Runtime -// Copyright: ©2011 Strobe Inc. and contributors. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -var Mixin, MixinDelegate, REQUIRED, Alias; -var classToString, superClassString; + You should use this method whenever you need to run some action after a + period of time inside of using setTimeout(). This method will ensure that + items that expire during the same script execution cycle all execute + together, which is often more efficient than using a real setTimeout. -var a_map = Ember.ArrayUtils.map; -var a_indexOf = Ember.ArrayUtils.indexOf; -var a_forEach = Ember.ArrayUtils.forEach; -var a_slice = Array.prototype.slice; -var EMPTY_META = {}; // dummy for non-writable meta -var META_SKIP = { __emberproto__: true, __ember_count__: true }; + @param {Object} target + (optional) target of method to invoke -var o_create = Ember.platform.create; + @param {Function|String} method + The method to invoke. If you pass a string it will be resolved on the + target at the time the method is invoked. -function meta(obj, writable) { - var m = Ember.meta(obj, writable!==false), ret = m.mixins; - if (writable===false) return ret || EMPTY_META; + @param {Object...} args + Optional arguments to pass to the timeout. - if (!ret) { - ret = m.mixins = { __emberproto__: obj }; - } else if (ret.__emberproto__ !== obj) { - ret = m.mixins = o_create(ret); - ret.__emberproto__ = obj; - } - return ret; -} + @param {Number} wait + Number of milliseconds to wait. -function initMixin(mixin, args) { - if (args && args.length > 0) { - mixin.mixins = a_map(args, function(x) { - if (x instanceof Mixin) return x; + @returns {Timer} an object you can use to cancel a timer at a later time. +*/ +Ember.run.later = function(target, method) { + var args, expires, timer, guid, wait; - // Note: Manually setup a primitive mixin here. This is the only - // way to actually get a primitive mixin. This way normal creation - // of mixins will give you combined mixins... - var mixin = new Mixin(); - mixin.properties = x; - return mixin; - }); + // setTimeout compatibility... + if (arguments.length===2 && 'function' === typeof target) { + wait = method; + method = target; + target = undefined; + args = [target, method]; + + } else { + args = slice.call(arguments); + wait = args.pop(); } - return mixin; -} -var NATIVES = [Boolean, Object, Number, Array, Date, String]; -function isMethod(obj) { - if ('function' !== typeof obj || obj.isMethod===false) return false; - return a_indexOf(NATIVES, obj)<0; + expires = (+ new Date())+wait; + timer = { target: target, method: method, expires: expires, args: args }; + guid = Ember.guidFor(timer); + timers[guid] = timer; + run.once(timers, invokeLaterTimers); + return guid; +}; + +/** @private */ +function invokeOnceTimer(guid, onceTimers) { + if (onceTimers[this.tguid]) delete onceTimers[this.tguid][this.mguid]; + if (timers[guid]) invoke(this.target, this.method, this.args, 2); + delete timers[guid]; } -function mergeMixins(mixins, m, descs, values, base) { - var len = mixins.length, idx, mixin, guid, props, value, key, ovalue, concats; +/** + Schedules an item to run one time during the current RunLoop. Calling + this method with the same target/method combination will have no effect. - function removeKeys(keyName) { - delete descs[keyName]; - delete values[keyName]; - } + Note that although you can pass optional arguments these will not be + considered when looking for duplicates. New arguments will replace previous + calls. - for(idx=0;idx=0) || key === 'concatenatedProperties') { - var baseValue = values[key] || base[key]; - value = baseValue ? baseValue.concat(value) : Ember.makeArray(value); - } + guid = Ember.guidFor(timer); + timers[guid] = timer; + if (!onceTimers[tguid]) onceTimers[tguid] = {}; + onceTimers[tguid][mguid] = guid; // so it isn't scheduled more than once - descs[key] = Ember.SIMPLE_PROPERTY; - values[key] = value; - } - } + run.schedule('actions', timer, invokeOnceTimer, guid, onceTimers); + } - // manually copy toString() because some JS engines do not enumerate it - if (props.hasOwnProperty('toString')) { - base.toString = props.toString; - } + return guid; +}; - } else if (mixin.mixins) { - mergeMixins(mixin.mixins, m, descs, values, base); - if (mixin._without) a_forEach(mixin._without, removeKeys); +var scheduledNext = false; +/** @private */ +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); } } } -var defineProperty = Ember.defineProperty; +/** + Schedules an item to run after control has been returned to the system. + This is often equivalent to calling setTimeout(function...,1). -function writableReq(obj) { - var m = Ember.meta(obj), req = m.required; - if (!req || (req.__emberproto__ !== obj)) { - req = m.required = req ? o_create(req) : { __ember_count__: 0 }; - req.__emberproto__ = obj; - } - return req; -} + @param {Object} target + (optional) target of method to invoke -function getObserverPaths(value) { - return ('function' === typeof value) && value.__ember_observes__; -} + @param {Function|String} method + The method to invoke. If you pass a string it will be resolved on the + target at the time the method is invoked. -function getBeforeObserverPaths(value) { - return ('function' === typeof value) && value.__ember_observesBefore__; -} + @param {Object...} args + Optional arguments to pass to the timeout. -Ember._mixinBindings = function(obj, key, value, m) { - return value; + @returns {Object} timer +*/ +Ember.run.next = function(target, method) { + var timer, 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; }; -function applyMixin(obj, mixins, partial) { - var descs = {}, values = {}, m = Ember.meta(obj), req = m.required; - var key, willApply, didApply, value, desc; +/** + Cancels a scheduled item. Must be a value returned by `Ember.run.later()`, + `Ember.run.once()`, or `Ember.run.next()`. - var mixinBindings = Ember._mixinBindings; + @param {Object} timer + Timer object to cancel - mergeMixins(mixins, meta(obj), descs, values, obj); + @returns {void} +*/ +Ember.run.cancel = function(timer) { + delete timers[timer]; +}; - if (MixinDelegate.detect(obj)) { - willApply = values.willApplyProperty || obj.willApplyProperty; - didApply = values.didApplyProperty || obj.didApplyProperty; - } +// .......................................................... +// DEPRECATED API +// - for(key in descs) { - if (!descs.hasOwnProperty(key)) continue; +/** + @namespace + @name Ember.RunLoop + @deprecated + @description Compatibility for Ember.run +*/ - desc = descs[key]; - value = values[key]; +/** + @deprecated + @method - if (desc === REQUIRED) { - if (!(key in obj)) { - if (!partial) throw new Error('Required property not defined: '+key); + Use `#js:Ember.run.begin()` instead +*/ +Ember.RunLoop.begin = ember_deprecateFunc("Use Ember.run.begin instead of Ember.RunLoop.begin.", Ember.run.begin); - // for partial applies add to hash of required keys - req = writableReq(obj); - req.__ember_count__++; - req[key] = true; - } +/** + @deprecated + @method - } else { + Use `#js:Ember.run.end()` instead +*/ +Ember.RunLoop.end = ember_deprecateFunc("Use Ember.run.end instead of Ember.RunLoop.end.", Ember.run.end); - while (desc instanceof Alias) { - var altKey = desc.methodName; - if (descs[altKey]) { - value = values[altKey]; - desc = descs[altKey]; - } else if (m.descs[altKey]) { - desc = m.descs[altKey]; - value = desc.val(obj, altKey); - } else { - value = obj[altKey]; - desc = Ember.SIMPLE_PROPERTY; - } - } - if (willApply) willApply.call(obj, key); +})({}); + +(function(exports) { +// ========================================================================== +// Project: Ember Runtime +// Copyright: ©2011 Strobe Inc. and contributors. +// License: Licensed under MIT license (see license.js) +// ========================================================================== +/*globals ember_assert */ +// Ember.Logger +// get, getPath, setPath, trySetPath +// guidFor, isArray, meta +// addObserver, removeObserver +// Ember.run.schedule + +// .......................................................... +// CONSTANTS +// - var observerPaths = getObserverPaths(value), - curObserverPaths = observerPaths && getObserverPaths(obj[key]), - beforeObserverPaths = getBeforeObserverPaths(value), - curBeforeObserverPaths = beforeObserverPaths && getBeforeObserverPaths(obj[key]), - len, idx; - if (curObserverPaths) { - len = curObserverPaths.length; - for(idx=0;idx0) { - var keys = []; - for(key in req) { - if (META_SKIP[key]) continue; - keys.push(key); - } - throw new Error('Required properties not defined: '+keys.join(',')); - } - return obj; -} -Ember.mixin = function(obj) { - var args = a_slice.call(arguments, 1); - return applyMixin(obj, args, false); -}; +/** + @static + Default placeholder for multiple values in bindings. -/** - @constructor - @name Ember.Mixin + @type String + @default '@@MULT@@' */ -Mixin = function() { return initMixin(this, arguments); }; +Ember.MULTIPLE_PLACEHOLDER = '@@MULT@@'; -Mixin._apply = applyMixin; +/** + @static -Mixin.applyPartial = function(obj) { - var args = a_slice.call(arguments, 1); - return applyMixin(obj, args, true); -}; + Default placeholder for empty values in bindings. Used by notEmpty() + helper unless you specify an alternative. -Mixin.create = function() { - classToString.processed = false; - var M = this; - return initMixin(new M(), arguments); -}; + @type String + @default '@@EMPTY@@' +*/ +Ember.EMPTY_PLACEHOLDER = '@@EMPTY@@'; -Mixin.prototype.reopen = function() { +// .......................................................... +// TYPE COERCION HELPERS +// - var mixin, tmp; +// Coerces a non-array value into an array. +/** @private */ +function MULTIPLE(val) { + if (val instanceof Array) return val; + if (val === undefined || val === null) return []; + return [val]; +} - if (this.properties) { - mixin = Mixin.create(); - mixin.properties = this.properties; - delete this.properties; - this.mixins = [mixin]; +// Treats a single-element array as the element. Otherwise +// returns a placeholder. +/** @private */ +function SINGLE(val, placeholder) { + if (val instanceof Array) { + if (val.length>1) return placeholder; + else return val[0]; } + return val; +} - var len = arguments.length, mixins = this.mixins, idx; +// Coerces the binding value into a Boolean. - for(idx=0;idx= 0) { - if (_detect(mixins[loc], targetMixin, seen)) return true; + var typeTransform = binding._typeTransform; + if (typeTransform) { val = typeTransform(val, binding._placeholder); } + + // handle transforms + var transforms = binding._transforms, + len = transforms ? transforms.length : 0, + idx; + + for(idx=0;idx null + - [a] => a + - [a,b,c] => Multiple Placeholder -Ember.alias = function(methodName) { - return new Alias(methodName); -}; + You can pass in an optional multiple placeholder or it will use the + default. -Ember.Mixin = Mixin; + Note that this transform will only happen on forwarded valued. Reverse + values are send unchanged. -MixinDelegate = Mixin.create({ + @param {String} fromPath from path or null + @param {Object} [placeholder] Placeholder value. + @returns {Ember.Binding} this + */ + single: function(placeholder) { + if (placeholder===undefined) placeholder = Ember.MULTIPLE_PLACEHOLDER; + this._typeTransform = SINGLE; + this._placeholder = placeholder; + return this; + }, - willApplyProperty: Ember.required(), - didApplyProperty: Ember.required() + /** + Adds a transform that will convert the passed value to an array. If + the value is null or undefined, it will be converted to an empty array. -}); + @param {String} [fromPath] + @returns {Ember.Binding} this + */ + multiple: function() { + this._typeTransform = MULTIPLE; + this._placeholder = null; + return this; + }, -Ember.MixinDelegate = MixinDelegate; + /** + Adds a transform to convert the value to a bool value. If the value is + an array it will return true if array is not empty. If the value is a + string it will return true if the string is not empty. + @returns {Ember.Binding} this + */ + bool: function() { + this.transform(BOOL); + return this; + }, -// .......................................................... -// OBSERVER HELPER -// + /** + Adds a transform that will return the placeholder value if the value is + null, undefined, an empty array or an empty string. See also notNull(). -Ember.observer = function(func) { - var paths = a_slice.call(arguments, 1); - func.__ember_observes__ = paths; - return func; -}; + @param {Object} [placeholder] Placeholder value. + @returns {Ember.Binding} this + */ + notEmpty: function(placeholder) { + if (placeholder === null || placeholder === undefined) { + placeholder = Ember.EMPTY_PLACEHOLDER; + } -Ember.beforeObserver = function(func) { - var paths = a_slice.call(arguments, 1); - func.__ember_observesBefore__ = paths; - return func; -}; + this.transform({ + to: function(val) { return empty(val) ? placeholder : val; } + }); + return this; + }, + /** + Adds a transform that will return the placeholder value if the value is + null or undefined. Otherwise it will passthrough untouched. See also notEmpty(). + @param {String} fromPath from path or null + @param {Object} [placeholder] Placeholder value. + @returns {Ember.Binding} this + */ + notNull: function(placeholder) { + if (placeholder === null || placeholder === undefined) { + placeholder = Ember.EMPTY_PLACEHOLDER; + } + this.transform({ + to: function(val) { return (val === null || val === undefined) ? placeholder : val; } + }); + return this; + }, + /** + Adds a transform to convert the value to the inverse of a bool value. This + uses the same transform as bool() but inverts it. -})({}); + @returns {Ember.Binding} this + */ + not: function() { + this.transform(NOT); + return this; + }, + /** + Adds a transform that will return true if the value is null or undefined, false otherwise. -(function(exports) { -// ========================================================================== -// Project: Ember Metal -// Copyright: ©2011 Strobe Inc. and contributors. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -})({}); + @returns {Ember.Binding} this + */ + isNull: function() { + this.transform(function(val) { return val === null || val === undefined; }); + return this; + }, -(function(exports) { -// ========================================================================== -// Project: Ember Runtime -// Copyright: ©2011 Strobe Inc. and contributors. -// License: Licensed under MIT license (see license.js) -// ========================================================================== + /** @private */ + toString: function() { + var oneWay = this._oneWay ? '[oneWay]' : ''; + return "Ember.Binding<" + guidFor(this) + ">(" + this._from + " -> " + this._to + ")" + oneWay; + }, + // .......................................................... + // CONNECT AND SYNC + // + /** + Attempts to connect this binding instance so that it can receive and relay + changes. This method will raise an exception if you have not set the + from/to properties yet. + @param {Object} obj + The root object for this binding. + @param {Boolean} preferFromParam + private: Normally, `connect` cannot take an object if `from` already set + an object. Internally, we would like to be able to provide a default object + to be used if no object was provided via `from`, so this parameter turns + off the assertion. -// .......................................................... -// HELPERS -// + @returns {Ember.Binding} this + */ + connect: function(obj) { + ember_assert('Must pass a valid object to Ember.Binding.connect()', !!obj); -var get = Ember.get, set = Ember.set; -var a_slice = Array.prototype.slice; -var a_indexOf = Ember.ArrayUtils.indexOf; + var oneWay = this._oneWay, operand = this._operand; -var contexts = []; -function popCtx() { - return contexts.length===0 ? {} : contexts.pop(); -} + // add an observer on the object to be notified when the binding should be updated + Ember.addObserver(obj, this._from, this, this.fromDidChange); -function pushCtx(ctx) { - contexts.push(ctx); - return null; -} + // if there is an operand, add an observer onto it as well + if (operand) { Ember.addObserver(obj, operand, this, this.fromDidChange); } -function iter(key, value) { - var valueProvided = arguments.length === 2; + // if the binding is a two-way binding, also set up an observer on the target + // object. + if (!oneWay) { Ember.addObserver(obj, this._to, this, this.toDidChange); } - function i(item) { - var cur = get(item, key); - return valueProvided ? value===cur : !!cur; - } - return i ; -} + if (Ember.meta(obj,false).proto !== obj) { this._scheduleSync(obj, 'fwd'); } -function xform(target, method, params) { - method.call(target, params[0], params[2], params[3]); -} + this._readyToSync = true; + return this; + }, -/** - @class + /** + Disconnects the binding instance. Changes will no longer be relayed. You + will not usually need to call this method. - This mixin defines the common interface implemented by enumerable objects - in Ember. Most of these methods follow the standard Array iteration - API defined up to JavaScript 1.8 (excluding language-specific features that - cannot be emulated in older versions of JavaScript). + @param {Object} obj + The root object you passed when connecting the binding. - This mixin is applied automatically to the Array class on page load, so you - can use any of these methods on simple arrays. If Array already implements - one of these methods, the mixin will not override them. + @returns {Ember.Binding} this + */ + disconnect: function(obj) { + ember_assert('Must pass a valid object to Ember.Binding.disconnect()', !!obj); - h3. Writing Your Own Enumerable + var oneWay = this._oneWay, operand = this._operand; - To make your own custom class enumerable, you need two items: + // remove an observer on the object so we're no longer notified of + // changes that should update bindings. + Ember.removeObserver(obj, this._from, this, this.fromDidChange); - 1. You must have a length property. This property should change whenever - the number of items in your enumerable object changes. If you using this - with an Ember.Object subclass, you should be sure to change the length - property using set(). + // if there is an operand, remove the observer from it as well + if (operand) Ember.removeObserver(obj, operand, this, this.fromDidChange); - 2. You must implement nextObject(). See documentation. + // if the binding is two-way, remove the observer from the target as well + if (!oneWay) Ember.removeObserver(obj, this._to, this, this.toDidChange); - Once you have these two methods implemented, apply the Ember.Enumerable mixin - to your class and you will be able to enumerate the contents of your object - like any other collection. + this._readyToSync = false; // disable scheduled syncs... + return this; + }, - h3. Using Ember Enumeration with Other Libraries + // .......................................................... + // PRIVATE + // - Many other libraries provide some kind of iterator or enumeration like - facility. This is often where the most common API conflicts occur. - Ember's API is designed to be as friendly as possible with other - libraries by implementing only methods that mostly correspond to the - JavaScript 1.8 API. + /** @private - called when the from side changes */ + fromDidChange: function(target) { + this._scheduleSync(target, 'fwd'); + }, - @since Ember 0.9 -*/ -Ember.Enumerable = Ember.Mixin.create( /** @lends Ember.Enumerable */ { + /** @private - called when the to side changes */ + toDidChange: function(target) { + this._scheduleSync(target, 'back'); + }, - /** @private - compatibility */ - isEnumerable: true, + /** @private */ + _scheduleSync: function(obj, dir) { + var guid = guidFor(obj), existingDir = this[guid]; - /** - Implement this method to make your class enumerable. + // if we haven't scheduled the binding yet, schedule it + if (!existingDir) { + Ember.run.schedule('sync', this, this._sync, obj); + this[guid] = dir; + } - This method will be call repeatedly during enumeration. The index value - will always begin with 0 and increment monotonically. You don't have to - rely on the index value to determine what object to return, but you should - always check the value and start from the beginning when you see the - requested index is 0. + // If both a 'back' and 'fwd' sync have been scheduled on the same object, + // default to a 'fwd' sync so that it remains deterministic. + if (existingDir === 'back' && dir === 'fwd') { + this[guid] = 'fwd'; + } + }, - The previousObject is the object that was returned from the last call - to nextObject for the current iteration. This is a useful way to - manage iteration if you are tracing a linked list, for example. + /** @private */ + _sync: function(obj) { + var log = Ember.LOG_BINDINGS; - Finally the context parameter will always contain a hash you can use as - a "scratchpad" to maintain any other state you need in order to iterate - properly. The context object is reused and is not reset between - iterations so make sure you setup the context with a fresh state whenever - the index parameter is 0. + // don't synchronize destroyed objects or disconnected bindings + if (obj.isDestroyed || !this._readyToSync) { return; } - Generally iterators will continue to call nextObject until the index - reaches the your current length-1. If you run out of data before this - time for some reason, you should simply return undefined. + // get the direction of the binding for the object we are + // synchronizing from + var guid = guidFor(obj), direction = this[guid]; - The default impementation of this method simply looks up the index. - This works great on any Array-like objects. + var fromPath = this._from, toPath = this._to; - @param index {Number} the current index of the iteration - @param previousObject {Object} the value returned by the last call to nextObject. - @param context {Object} a context object you can use to maintain state. - @returns {Object} the next object in the iteration or undefined - */ - nextObject: Ember.required(Function), + delete this[guid]; - /** - Helper method returns the first object from a collection. This is usually - used by bindings and other parts of the framework to extract a single - object if the enumerable contains only one item. + // if we're synchronizing from the remote object... + if (direction === 'fwd') { + var fromValue = getTransformedFromValue(obj, this); + if (log) { + Ember.Logger.log(' ', this.toString(), '->', fromValue, obj); + } + if (this._oneWay) { + Ember.trySetPath(Ember.isGlobalPath(toPath) ? window : obj, toPath, fromValue); + } else { + Ember._suspendObserver(obj, toPath, this, this.toDidChange, function () { + Ember.trySetPath(Ember.isGlobalPath(toPath) ? window : obj, toPath, fromValue); + }); + } + // if we're synchronizing *to* the remote object + } else if (direction === 'back') {// && !this._oneWay) { + var toValue = getTransformedToValue(obj, this); + if (log) { + Ember.Logger.log(' ', this.toString(), '<-', toValue, obj); + } + Ember._suspendObserver(obj, fromPath, this, this.fromDidChange, function () { + Ember.trySetPath(Ember.isGlobalPath(fromPath) ? window : obj, fromPath, toValue); + }); + } + } - If you override this method, you should implement it so that it will - always return the same value each time it is called. If your enumerable - contains only one object, this method should always return that object. - If your enumerable is empty, this method should return undefined. +}; - @returns {Object} the object or undefined +/** @private */ +function mixinProperties(to, from) { + for (var key in from) { + if (from.hasOwnProperty(key)) { + to[key] = from[key]; + } + } +} - @example - var arr = ["a", "b", "c"]; - arr.firstObject(); => "a" +mixinProperties(Binding, +/** @scope Ember.Binding */ { - var arr = []; - arr.firstObject(); => undefined + /** + @see Ember.Binding.prototype.from */ - firstObject: Ember.computed(function() { - if (get(this, 'length')===0) return undefined ; - if (Ember.Array && Ember.Array.detect(this)) return this.objectAt(0); - - // handle generic enumerables - var context = popCtx(), ret; - ret = this.nextObject(0, null, context); - pushCtx(context); - return ret ; - }).property(), + from: function() { + var C = this, binding = new C(); + return binding.from.apply(binding, arguments); + }, /** - Helper method returns the last object from a collection. If your enumerable - contains only one object, this method should always return that object. - If your enumerable is empty, this method should return undefined. + @see Ember.Binding.prototype.to + */ + to: function() { + var C = this, binding = new C(); + return binding.to.apply(binding, arguments); + }, - @returns {Object} the last object or undefined + /** + @see Ember.Binding.prototype.oneWay + */ + oneWay: function(from, flag) { + var C = this, binding = new C(null, from); + return binding.oneWay(flag); + }, - @example - var arr = ["a", "b", "c"]; - arr.lastObject(); => "c" + /** + @see Ember.Binding.prototype.single + */ + single: function(from) { + var C = this, binding = new C(null, from); + return binding.single(); + }, - var arr = []; - arr.lastObject(); => undefined + /** + @see Ember.Binding.prototype.multiple */ - lastObject: Ember.computed(function() { - var len = get(this, 'length'); - if (len===0) return undefined ; - if (Ember.Array && Ember.Array.detect(this)) { - return this.objectAt(len-1); - } else { - var context = popCtx(), idx=0, cur, last = null; - do { - last = cur; - cur = this.nextObject(idx++, last, context); - } while (cur !== undefined); - pushCtx(context); - return last; - } - }).property(), + multiple: function(from) { + var C = this, binding = new C(null, from); + return binding.multiple(); + }, /** - Returns true if the passed object can be found in the receiver. The - default version will iterate through the enumerable until the object - is found. You may want to override this with a more efficient version. + @see Ember.Binding.prototype.transform + */ + transform: function(func) { + var C = this, binding = new C(); + return binding.transform(func); + }, - @param {Object} obj - The object to search for. + /** + @see Ember.Binding.prototype.notEmpty + */ + notEmpty: function(from, placeholder) { + var C = this, binding = new C(null, from); + return binding.notEmpty(placeholder); + }, - @returns {Boolean} true if object is found in enumerable. + /** + @see Ember.Binding.prototype.bool */ - contains: function(obj) { - return this.find(function(item) { return item===obj; }) !== undefined; + bool: function(from) { + var C = this, binding = new C(null, from); + return binding.bool(); }, /** - Iterates through the enumerable, calling the passed function on each - item. This method corresponds to the forEach() method defined in - JavaScript 1.6. + @see Ember.Binding.prototype.not + */ + not: function(from) { + var C = this, binding = new C(null, from); + return binding.not(); + }, - The callback method you provide should have the following signature (all - parameters are optional): + /** + Adds a transform that forwards the logical 'AND' of values at 'pathA' and + 'pathB' whenever either source changes. Note that the transform acts + strictly as a one-way binding, working only in the direction - function(item, index, enumerable); + 'pathA' AND 'pathB' --> value (value returned is the result of ('pathA' && 'pathB')) - - *item* is the current item in the iteration. - - *index* is the current index in the iteration - - *enumerable* is the enumerable object itself. + Usage example where a delete button's `isEnabled` value is determined by + whether something is selected in a list and whether the current user is + allowed to delete: - Note that in addition to a callback, you can also pass an optional target - object that will be set as "this" on the context. This is a good way - to give your iterator function access to the current object. + deleteButton: Ember.ButtonView.design({ + isEnabledBinding: Ember.Binding.and('MyApp.itemsController.hasSelection', 'MyApp.userController.canDelete') + }) - @param {Function} callback The callback to execute - @param {Object} target The target object to use - @returns {Object} receiver + @param {String} pathA The first part of the conditional + @param {String} pathB The second part of the conditional */ - forEach: function(callback, target) { - if (typeof callback !== "function") throw new TypeError() ; - var len = get(this, 'length'), last = null, context = popCtx(); - - if (target === undefined) target = null; - - for(var idx=0;idx value (value returned is the result of ('pathA' || 'pathB')) + + @param {String} pathA The first part of the conditional + @param {String} pathB The second part of the conditional */ - getEach: function(key) { - return this.mapProperty(key); - }, + or: function(pathA, pathB) { + var C = this, binding = new C(null, pathA).oneWay(); + binding._operand = pathB; + binding._operation = OR_OPERATION; + return binding; + } - /** - Sets the value on the named property for each member. This is more - efficient than using other methods defined on this helper. If the object - implements Ember.Observable, the value will be changed to set(), otherwise - it will be set directly. null objects are skipped. +}); - @param {String} key The key to set - @param {Object} value The object to set - @returns {Object} receiver - */ - setEach: function(key, value) { - return this.forEach(function(item) { - set(item, key, value); - }); - }, +/** + @class - /** - Maps all of the items in the enumeration to another value, returning - a new array. This method corresponds to map() defined in JavaScript 1.6. + A binding simply connects the properties of two objects so that whenever the + value of one property changes, the other property will be changed also. You + do not usually work with Binding objects directly but instead describe + bindings in your class definition using something like: - The callback method you provide should have the following signature (all - parameters are optional): + valueBinding: "MyApp.someController.title" - function(item, index, enumerable); + This will create a binding from `MyApp.someController.title` to the `value` + property of your object instance automatically. Now the two values will be + kept in sync. - - *item* is the current item in the iteration. - - *index* is the current index in the iteration - - *enumerable* is the enumerable object itself. + ## Customizing Your Bindings - It should return the mapped value. + In addition to synchronizing values, bindings can also perform some basic + transforms on values. These transforms can help to make sure the data fed + into one object always meets the expectations of that object regardless of + what the other object outputs. - Note that in addition to a callback, you can also pass an optional target - object that will be set as "this" on the context. This is a good way - to give your iterator function access to the current object. + To customize a binding, you can use one of the many helper methods defined + on Ember.Binding like so: - @param {Function} callback The callback to execute - @param {Object} target The target object to use - @returns {Array} The mapped array. - */ - map: function(callback, target) { - var ret = []; - this.forEach(function(x, idx, i) { - ret[idx] = callback.call(target, x, idx,i); - }); - return ret ; - }, + valueBinding: Ember.Binding.single("MyApp.someController.title") - /** - Similar to map, this specialized function returns the value of the named - property on all items in the enumeration. + This will create a binding just like the example above, except that now the + binding will convert the value of `MyApp.someController.title` to a single + object (removing any arrays) before applying it to the `value` property of + your object. - @params key {String} name of the property - @returns {Array} The mapped array. - */ - mapProperty: function(key) { - return this.map(function(next) { - return get(next, key); - }); - }, + You can also chain helper methods to build custom bindings like so: - /** - Returns an array with all of the items in the enumeration that the passed - function returns true for. This method corresponds to filter() defined in - JavaScript 1.6. + valueBinding: Ember.Binding.single("MyApp.someController.title").notEmpty("(EMPTY)") - The callback method you provide should have the following signature (all - parameters are optional): + This will force the value of MyApp.someController.title to be a single value + and then check to see if the value is "empty" (null, undefined, empty array, + or an empty string). If it is empty, the value will be set to the string + "(EMPTY)". - function(item, index, enumerable); + ## One Way Bindings + + One especially useful binding customization you can use is the `oneWay()` + helper. This helper tells Ember that you are only interested in + receiving changes on the object you are binding from. For example, if you + are binding to a preference and you want to be notified if the preference + has changed, but your object will not be changing the preference itself, you + could do: - - *item* is the current item in the iteration. - - *index* is the current index in the iteration - - *enumerable* is the enumerable object itself. + bigTitlesBinding: Ember.Binding.oneWay("MyApp.preferencesController.bigTitles") - It should return the true to include the item in the results, false otherwise. + This way if the value of MyApp.preferencesController.bigTitles changes the + "bigTitles" property of your object will change also. However, if you + change the value of your "bigTitles" property, it will not update the + preferencesController. - Note that in addition to a callback, you can also pass an optional target - object that will be set as "this" on the context. This is a good way - to give your iterator function access to the current object. + One way bindings are almost twice as fast to setup and twice as fast to + execute because the binding only has to worry about changes to one side. - @param {Function} callback The callback to execute - @param {Object} target The target object to use - @returns {Array} A filtered array. - */ - filter: function(callback, target) { - var ret = []; - this.forEach(function(x, idx, i) { - if (callback.call(target, x, idx, i)) ret.push(x); - }); - return ret ; - }, + You should consider using one way bindings anytime you have an object that + may be created frequently and you do not intend to change a property; only + to monitor it for changes. (such as in the example above). - /** - Returns an array with just the items with the matched property. You - can pass an optional second argument with the target value. Otherwise - this will match any property that evaluates to true. + ## Adding Custom Transforms - @params key {String} the property to test - @param value {String} optional value to test against. - @returns {Array} filtered array - */ - filterProperty: function(key, value) { - return this.filter(iter.apply(this, arguments)); - }, + In addition to using the standard helpers provided by Ember, you can + also defined your own custom transform functions which will be used to + convert the value. To do this, just define your transform function and add + it to the binding with the transform() helper. The following example will + not allow Integers less than ten. Note that it checks the value of the + bindings and allows all other values to pass: - /** - Returns the first item in the array for which the callback returns true. - This method works similar to the filter() method defined in JavaScript 1.6 - except that it will stop working on the array once a match is found. + valueBinding: Ember.Binding.transform(function(value, binding) { + return ((Ember.typeOf(value) === 'number') && (value < 10)) ? 10 : value; + }).from("MyApp.someController.value") - The callback method you provide should have the following signature (all - parameters are optional): + If you would like to instead use this transform on a number of bindings, + you can also optionally add your own helper method to Ember.Binding. This + method should simply return the value of `this.transform()`. The example + below adds a new helper called `notLessThan()` which will limit the value to + be not less than the passed minimum: - function(item, index, enumerable); + Ember.Binding.reopen({ + notLessThan: function(minValue) { + return this.transform(function(value, binding) { + return ((Ember.typeOf(value) === 'number') && (value < minValue)) ? minValue : value; + }); + } + }); - - *item* is the current item in the iteration. - - *index* is the current index in the iteration - - *enumerable* is the enumerable object itself. + You could specify this in your core.js file, for example. Then anywhere in + your application you can use it to define bindings like so: - It should return the true to include the item in the results, false otherwise. + valueBinding: Ember.Binding.from("MyApp.someController.value").notLessThan(10) - Note that in addition to a callback, you can also pass an optional target - object that will be set as "this" on the context. This is a good way - to give your iterator function access to the current object. + Also, remember that helpers are chained so you can use your helper along + with any other helpers. The example below will create a one way binding that + does not allow empty values or values less than 10: - @param {Function} callback The callback to execute - @param {Object} target The target object to use - @returns {Object} Found item or null. - */ - find: function(callback, target) { - var len = get(this, 'length') ; - if (target === undefined) target = null; + valueBinding: Ember.Binding.oneWay("MyApp.someController.value").notEmpty().notLessThan(10) - var last = null, next, found = false, ret ; - var context = popCtx(); - for(var idx=0;idx1) args = a_slice.call(arguments, 1); + | Return Value Constant | Meaning | + | 'string' | String primitive | + | 'number' | Number primitive | + | 'boolean' | Boolean primitive | + | 'null' | Null value | + | 'undefined' | Undefined value | + | 'function' | A function | + | 'array' | An instance of Array | + | 'class' | A Ember class (created using Ember.Object.extend()) | + | 'instance' | A Ember object instance | + | 'error' | An instance of the Error object | + | 'object' | A JavaScript object not inheriting from Ember.Object | - this.forEach(function(x, idx) { - var method = x && x[methodName]; - if ('function' === typeof method) { - ret[idx] = args ? method.apply(x, args) : method.call(x); - } - }, this); + @param item {Object} the item to check + @returns {String} the type +*/ +Ember.typeOf = function(item) { + var ret; - return ret; - }, + ret = (item === null || item === undefined) ? String(item) : TYPE_MAP[toString.call(item)] || 'object'; - /** - Simply converts the enumerable into a genuine array. The order is not - guaranteed. Corresponds to the method implemented by Prototype. + if (ret === 'function') { + if (Ember.Object && Ember.Object.detect(item)) ret = 'class'; + } else if (ret === 'object') { + if (item instanceof Error) ret = 'error'; + else if (Ember.Object && item instanceof Ember.Object) ret = 'instance'; + else ret = 'object'; + } - @returns {Array} the enumerable as an array. - */ - toArray: function() { - var ret = []; - this.forEach(function(o, idx) { ret[idx] = o; }); - return ret ; - }, + return ret; +}; - /** - Generates a new array with the contents of the old array, sans any null - values. +/** + Returns true if the passed value is null or undefined. This avoids errors + from JSLint complaining about use of ==, which can be technically + confusing. - @returns {Array} - */ - compact: function() { return this.without(null); }, + @param {Object} obj Value to test + @returns {Boolean} +*/ +Ember.none = function(obj) { + return obj === null || obj === undefined; +}; - /** - Returns a new enumerable that excludes the passed value. The default - implementation returns an array regardless of the receiver type unless - the receiver does not contain the value. +/** + Verifies that a value is null or an empty string | array | function. - @param {Object} value - @returns {Ember.Enumerable} - */ - without: function(value) { - if (!this.contains(value)) return this; // nothing to do - var ret = [] ; - this.forEach(function(k) { - if (k !== value) ret[ret.length] = k; - }) ; - return ret ; - }, + @param {Object} obj Value to test + @returns {Boolean} +*/ +Ember.empty = function(obj) { + return obj === null || obj === undefined || (obj.length === 0 && typeof obj !== 'function'); +}; - /** - Returns a new enumerable that contains only unique values. The default - implementation returns an array regardless of the receiver type. +/** + Ember.isArray defined in ember-metal/lib/utils +**/ - @returns {Ember.Enumerable} - */ - uniq: function() { - var ret = []; - this.forEach(function(k){ - if (a_indexOf(ret, k)<0) ret.push(k); - }); - return ret; - }, +/** + This will compare two javascript values of possibly different types. + It will tell you which one is greater than the other by returning: - /** - This property will trigger anytime the enumerable's content changes. - You can observe this property to be notified of changes to the enumerables - content. + - -1 if the first is smaller than the second, + - 0 if both are equal, + - 1 if the first is greater than the second. - For plain enumerables, this property is read only. Ember.Array overrides - this method. + The order is calculated based on Ember.ORDER_DEFINITION, if types are different. + In case they have the same type an appropriate comparison for this type is made. - @property {Ember.Array} - */ - '[]': Ember.computed(function(key, value) { - return this; - }).property().cacheable(), + @param {Object} v First value to compare + @param {Object} w Second value to compare + @returns {Number} -1 if v < w, 0 if v = w and 1 if v > w. +*/ +Ember.compare = function compare(v, w) { + if (v === w) { return 0; } - // .......................................................... - // ENUMERABLE OBSERVERS - // + var type1 = Ember.typeOf(v); + var type2 = Ember.typeOf(w); - /** - Registers an enumerable observer. Must implement Ember.EnumerableObserver - mixin. - */ - addEnumerableObserver: function(target, opts) { - var willChange = (opts && opts.willChange) || 'enumerableWillChange', - didChange = (opts && opts.didChange) || 'enumerableDidChange'; + var Comparable = Ember.Comparable; + if (Comparable) { + if (type1==='instance' && Comparable.detect(v.constructor)) { + return v.constructor.compare(v, w); + } + + if (type2 === 'instance' && Comparable.detect(w.constructor)) { + return 1-w.constructor.compare(w, v); + } + } + + // If we haven't yet generated a reverse-mapping of Ember.ORDER_DEFINITION, + // do so now. + var mapping = Ember.ORDER_DEFINITION_MAPPING; + if (!mapping) { + var order = Ember.ORDER_DEFINITION; + mapping = Ember.ORDER_DEFINITION_MAPPING = {}; + var idx, len; + for (idx = 0, len = order.length; idx < len; ++idx) { + mapping[order[idx]] = idx; + } - var hasObservers = get(this, 'hasEnumerableObservers'); - if (!hasObservers) Ember.propertyWillChange(this, 'hasEnumerableObservers'); - Ember.addListener(this, '@enumerable:before', target, willChange, xform); - Ember.addListener(this, '@enumerable:change', target, didChange, xform); - if (!hasObservers) Ember.propertyDidChange(this, 'hasEnumerableObservers'); - return this; - }, + // We no longer need Ember.ORDER_DEFINITION. + delete Ember.ORDER_DEFINITION; + } - /** - Removes a registered enumerable observer. - */ - removeEnumerableObserver: function(target, opts) { - var willChange = (opts && opts.willChange) || 'enumerableWillChange', - didChange = (opts && opts.didChange) || 'enumerableDidChange'; + var type1Index = mapping[type1]; + var type2Index = mapping[type2]; - var hasObservers = get(this, 'hasEnumerableObservers'); - if (hasObservers) Ember.propertyWillChange(this, 'hasEnumerableObservers'); - Ember.removeListener(this, '@enumerable:before', target, willChange); - Ember.removeListener(this, '@enumerable:change', target, didChange); - if (hasObservers) Ember.propertyDidChange(this, 'hasEnumerableObservers'); - return this; - }, + if (type1Index < type2Index) { return -1; } + if (type1Index > type2Index) { return 1; } - /** - Becomes true whenever the array currently has observers watching changes - on the array. + // types are equal - so we have to check values now + switch (type1) { + case 'boolean': + case 'number': + if (v < w) { return -1; } + if (v > w) { return 1; } + return 0; - @property {Boolean} - */ - hasEnumerableObservers: Ember.computed(function() { - return Ember.hasListeners(this, '@enumerable:change') || Ember.hasListeners(this, '@enumerable:before'); - }).property().cacheable(), + case 'string': + var comp = v.localeCompare(w); + if (comp < 0) { return -1; } + if (comp > 0) { return 1; } + return 0; + case 'array': + var vLen = v.length; + var wLen = w.length; + var l = Math.min(vLen, wLen); + var r = 0; + var i = 0; + while (r === 0 && i < l) { + r = compare(v[i],w[i]); + i++; + } + if (r !== 0) { return r; } - /** - Invoke this method just before the contents of your enumerable will - change. You can either omit the parameters completely or pass the objects - to be removed or added if available or just a count. + // all elements are equal now + // shorter array should be ordered first + if (vLen < wLen) { return -1; } + if (vLen > wLen) { return 1; } + // arrays are equal now + return 0; - @param {Ember.Enumerable|Number} removing - An enumerable of the objects to be removed or the number of items to - be removed. + case 'instance': + if (Ember.Comparable && Ember.Comparable.detect(v)) { + return v.compare(v, w); + } + return 0; - @param {Ember.Enumerable|Number} adding - An enumerable of the objects to be added or the number of items to be - added. + default: + return 0; + } +}; - @returns {Ember.Enumerable} receiver - */ - enumerableContentWillChange: function(removing, adding) { +/** @private */ +function _copy(obj, deep, seen, copies) { + var ret, loc, key; - var removeCnt, addCnt, hasDelta; + // primitive data types are immutable, just return them. + if ('object' !== typeof obj || obj===null) return obj; - if ('number' === typeof removing) removeCnt = removing; - else if (removing) removeCnt = get(removing, 'length'); - else removeCnt = removing = -1; + // avoid cyclical loops + if (deep && (loc=indexOf(seen, obj))>=0) return copies[loc]; - if ('number' === typeof adding) addCnt = adding; - else if (adding) addCnt = get(adding,'length'); - else addCnt = adding = -1; + ember_assert('Cannot clone an Ember.Object that does not implement Ember.Copyable', !(obj instanceof Ember.Object) || (Ember.Copyable && Ember.Copyable.detect(obj))); - hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; + // IMPORTANT: this specific test will detect a native array only. Any other + // object will need to implement Copyable. + if (Ember.typeOf(obj) === 'array') { + ret = obj.slice(); + if (deep) { + loc = ret.length; + while(--loc>=0) ret[loc] = _copy(ret[loc], deep, seen, copies); + } + } else if (Ember.Copyable && Ember.Copyable.detect(obj)) { + ret = obj.copy(deep, seen, copies); + } else { + ret = {}; + for(key in obj) { + if (!obj.hasOwnProperty(key)) continue; + ret[key] = deep ? _copy(obj[key], deep, seen, copies) : obj[key]; + } + } - if (removing === -1) removing = null; - if (adding === -1) adding = null; + if (deep) { + seen.push(obj); + copies.push(ret); + } - if (hasDelta) Ember.propertyWillChange(this, 'length'); - Ember.sendEvent(this, '@enumerable:before', removing, adding); + return ret; +} - return this; - }, +/** + Creates a clone of the passed object. This function can take just about + any type of object and create a clone of it, including primitive values + (which are not actually cloned because they are immutable). - /** - Invoke this method when the contents of your enumerable has changed. - This will notify any observers watching for content changes. If your are - implementing an ordered enumerable (such as an array), also pass the - start and end values where the content changed so that it can be used to - notify range observers. + If the passed object implements the clone() method, then this function + will simply call that method and return the result. - @param {Number} start - optional start offset for the content change. For unordered - enumerables, you should always pass -1. + @param {Object} object The object to clone + @param {Boolean} deep If true, a deep copy of the object is made + @returns {Object} The cloned object +*/ +Ember.copy = function(obj, deep) { + // fast paths + if ('object' !== typeof obj || obj===null) return obj; // can't copy primitives + if (Ember.Copyable && Ember.Copyable.detect(obj)) return obj.copy(deep); + return _copy(obj, deep, deep ? [] : null, deep ? [] : null); +}; - @param {Enumerable} added - optional enumerable containing items that were added to the set. For - ordered enumerables, this should be an ordered array of items. If no - items were added you can pass null. +/** + Convenience method to inspect an object. This method will attempt to + convert the object into a useful string description. - @param {Enumerable} removes - optional enumerable containing items that were removed from the set. - For ordered enumerables, this hsould be an ordered array of items. If - no items were removed you can pass null. + @param {Object} obj The object you want to inspect. + @returns {String} A description of the object +*/ +Ember.inspect = function(obj) { + var v, ret = []; + for(var key in obj) { + if (obj.hasOwnProperty(key)) { + v = obj[key]; + if (v === 'toString') { continue; } // ignore useless items + if (Ember.typeOf(v) === 'function') { v = "function() { ... }"; } + ret.push(key + ": " + v); + } + } + return "{" + ret.join(" , ") + "}"; +}; - @returns {Object} receiver - */ - enumerableContentDidChange: function(removing, adding) { - var notify = this.propertyDidChange, removeCnt, addCnt, hasDelta; +/** + Compares two objects, returning true if they are logically equal. This is + a deeper comparison than a simple triple equal. For arrays and enumerables + it will compare the internal objects. For any other object that implements + `isEqual()` it will respect that method. - if ('number' === typeof removing) removeCnt = removing; - else if (removing) removeCnt = get(removing, 'length'); - else removeCnt = removing = -1; + @param {Object} a first object to compare + @param {Object} b second object to compare + @returns {Boolean} +*/ +Ember.isEqual = function(a, b) { + if (a && 'function'===typeof a.isEqual) return a.isEqual(b); + return a === b; +}; - if ('number' === typeof adding) addCnt = adding; - else if (adding) addCnt = get(adding, 'length'); - else addCnt = adding = -1; +/** + @private + Used by Ember.compare +*/ +Ember.ORDER_DEFINITION = Ember.ENV.ORDER_DEFINITION || [ + 'undefined', + 'null', + 'boolean', + 'number', + 'string', + 'array', + 'object', + 'instance', + 'function', + 'class' +]; - hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; +/** + Returns all of the keys defined on an object or hash. This is useful + when inspecting objects for debugging. On browsers that support it, this + uses the native Object.keys implementation. - if (removing === -1) removing = null; - if (adding === -1) adding = null; + @function + @param {Object} obj + @returns {Array} Array containing keys of obj +*/ +Ember.keys = Object.keys; - Ember.sendEvent(this, '@enumerable:change', removing, adding); - if (hasDelta) Ember.propertyDidChange(this, 'length'); +if (!Ember.keys) { + Ember.keys = function(obj) { + var ret = []; + for(var key in obj) { + if (obj.hasOwnProperty(key)) { ret.push(key); } + } + return ret; + }; +} - return this ; - } +// .......................................................... +// ERROR +// -}) ; +/** + @class + A subclass of the JavaScript Error object for use in Ember. +*/ +Ember.Error = function() { + var tmp = Error.prototype.constructor.apply(this, arguments); + for (var p in tmp) { + if (tmp.hasOwnProperty(p)) { this[p] = tmp[p]; } + } + this.message = tmp.message; +}; +Ember.Error.prototype = Ember.create(Error.prototype); })({}); - (function(exports) { // ========================================================================== // Project: Ember Runtime -// Copyright: ©2011 Strobe Inc. and contributors. +// Copyright: ©2011 Strobe Inc. // License: Licensed under MIT license (see license.js) // ========================================================================== -// .......................................................... -// HELPERS -// -var get = Ember.get, set = Ember.set, meta = Ember.meta; +/** @private **/ +var STRING_DASHERIZE_REGEXP = (/[ _]/g); +var STRING_DASHERIZE_CACHE = {}; +var STRING_DECAMELIZE_REGEXP = (/([a-z])([A-Z])/g); +var STRING_CAMELIZE_REGEXP = (/(\-|_|\s)+(.)?/g); +var STRING_UNDERSCORE_REGEXP_1 = (/([a-z\d])([A-Z]+)/g); +var STRING_UNDERSCORE_REGEXP_2 = (/\-|\s+/g); -function none(obj) { return obj===null || obj===undefined; } +/** + Defines the hash of localized strings for the current language. Used by + the `Ember.String.loc()` helper. To localize, add string values to this + hash. -function xform(target, method, params) { - method.call(target, params[0], params[2], params[3], params[4]); -} + @property {String} +*/ +Ember.STRINGS = {}; -// .......................................................... -// ARRAY -// /** + Defines string helper methods including string formatting and localization. + Unless Ember.EXTEND_PROTOTYPES = false these methods will also be added to the + String.prototype as well. + @namespace +*/ +Ember.String = { - This module implements Observer-friendly Array-like behavior. This mixin is - picked up by the Array class as well as other controllers, etc. that want to - appear to be arrays. + /** + Apply formatting options to the string. This will look for occurrences + of %@ in your string and substitute them with the arguments you pass into + this method. If you want to control the specific order of replacement, + you can add a number after the key as well to indicate which argument + you want to insert. - Unlike Ember.Enumerable, this mixin defines methods specifically for - collections that provide index-ordered access to their contents. When you - are designing code that needs to accept any kind of Array-like object, you - should use these methods instead of Array primitives because these will - properly notify observers of changes to the array. + Ordered insertions are most useful when building loc strings where values + you need to insert may appear in different orders. - Although these methods are efficient, they do add a layer of indirection to - your application so it is a good idea to use them only when you need the - flexibility of using both true JavaScript arrays and "virtual" arrays such - as controllers and collections. + "Hello %@ %@".fmt('John', 'Doe') => "Hello John Doe" + "Hello %@2, %@1".fmt('John', 'Doe') => "Hello Doe, John" - You can use the methods defined in this module to access and modify array - contents in a KVO-friendly way. You can also be notified whenever the - membership if an array changes by changing the syntax of the property to - .observes('*myProperty.[]') . + @param {Object...} [args] + @returns {String} formatted string + */ + fmt: function(str, formats) { + // first, replace any ORDERED replacements. + var idx = 0; // the current index for non-numerical replacements + return str.replace(/%@([0-9]+)?/g, function(s, argIndex) { + argIndex = (argIndex) ? parseInt(argIndex,0) - 1 : idx++ ; + s = formats[argIndex]; + return ((s === null) ? '(null)' : (s === undefined) ? '' : s).toString(); + }) ; + }, - To support Ember.Array in your own class, you must override two - primitives to use it: replace() and objectAt(). + /** + Formats the passed string, but first looks up the string in the localized + strings hash. This is a convenient way to localize text. See + `Ember.String.fmt()` for more information on formatting. - Note that the Ember.Array mixin also incorporates the Ember.Enumerable mixin. All - Ember.Array-like objects are also enumerable. + Note that it is traditional but not required to prefix localized string + keys with an underscore or other character so you can easily identify + localized strings. - @extends Ember.Enumerable - @since Ember 0.9.0 -*/ -Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.prototype */ { + Ember.STRINGS = { + '_Hello World': 'Bonjour le monde', + '_Hello %@ %@': 'Bonjour %@ %@' + }; - /** @private - compatibility */ - isSCArray: true, + Ember.String.loc("_Hello World"); + => 'Bonjour le monde'; - /** - @field {Number} length + Ember.String.loc("_Hello %@ %@", ["John", "Smith"]); + => "Bonjour John Smith"; - Your array must support the length property. Your replace methods should - set this property whenever it changes. - */ - length: Ember.required(), + @param {String} str + The string to format - /** - This is one of the primitives you must implement to support Ember.Array. - Returns the object at the named index. If your object supports retrieving - the value of an array item using get() (i.e. myArray.get(0)), then you do - not need to implement this method yourself. + @param {Array} formats + Optional array of parameters to interpolate into string. - @param {Number} idx - The index of the item to return. If idx exceeds the current length, - return null. + @returns {String} formatted string */ - objectAt: function(idx) { - if ((idx < 0) || (idx>=get(this, 'length'))) return undefined ; - return get(this, idx); - }, - - /** @private (nodoc) - overrides Ember.Enumerable version */ - nextObject: function(idx) { - return this.objectAt(idx); + loc: function(str, formats) { + str = Ember.STRINGS[str] || str; + return Ember.String.fmt(str, formats) ; }, /** - @field [] + Splits a string into separate units separated by spaces, eliminating any + empty strings in the process. This is a convenience method for split that + is mostly useful when applied to the String.prototype. - This is the handler for the special array content property. If you get - this property, it will return this. If you set this property it a new - array, it will replace the current content. + Ember.String.w("alpha beta gamma").forEach(function(key) { + console.log(key); + }); + > alpha + > beta + > gamma - This property overrides the default property defined in Ember.Enumerable. - */ - '[]': Ember.computed(function(key, value) { - if (value !== undefined) this.replace(0, get(this, 'length'), value) ; - return this ; - }).property().cacheable(), + @param {String} str + The string to split - /** @private (nodoc) - optimized version from Enumerable */ - contains: function(obj){ - return this.indexOf(obj) >= 0; - }, + @returns {String} split string + */ + w: function(str) { return str.split(/\s+/); }, - // Add any extra methods to Ember.Array that are native to the built-in Array. /** - Returns a new array that is a slice of the receiver. This implementation - uses the observable array methods to retrieve the objects for the new - slice. + Converts a camelized string into all lower case separated by underscores. + + 'innerHTML'.decamelize() => 'inner_html' + 'action_name'.decamelize() => 'action_name' + 'css-class-name'.decamelize() => 'css-class-name' + 'my favorite items'.decamelize() => 'my favorite items' - @param beginIndex {Integer} (Optional) index to begin slicing from. - @param endIndex {Integer} (Optional) index to end the slice at. - @returns {Array} New array with specified slice + @param {String} str + The string to decamelize. + + @returns {String} the decamelized string. */ - slice: function(beginIndex, endIndex) { - var ret = []; - var length = get(this, 'length') ; - if (none(beginIndex)) beginIndex = 0 ; - if (none(endIndex) || (endIndex > length)) endIndex = length ; - while(beginIndex < endIndex) { - ret[ret.length] = this.objectAt(beginIndex++) ; - } - return ret ; + decamelize: function(str) { + return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); }, /** - Returns the index of the given object's first occurrence. - If no startAt argument is given, the starting location to - search is 0. If it's negative, will count backward from - the end of the array. Returns -1 if no match is found. + Replaces underscores or spaces with dashes. + + 'innerHTML'.dasherize() => 'inner-html' + 'action_name'.dasherize() => 'action-name' + 'css-class-name'.dasherize() => 'css-class-name' + 'my favorite items'.dasherize() => 'my-favorite-items' - @param {Object} object the item to search for - @param {Number} startAt optional starting location to search, default 0 - @returns {Number} index or -1 if not found + @param {String} str + The string to dasherize. - @example - var arr = ["a", "b", "c", "d", "a"]; - arr.indexOf("a"); => 0 - arr.indexOf("z"); => -1 - arr.indexOf("a", 2); => 4 - arr.indexOf("a", -1); => 4 - arr.indexOf("b", 3); => -1 - arr.indexOf("a", 100); => -1 + @returns {String} the dasherized string. */ - indexOf: function(object, startAt) { - var idx, len = get(this, 'length'); - - if (startAt === undefined) startAt = 0; - if (startAt < 0) startAt += len; + dasherize: function(str) { + var cache = STRING_DASHERIZE_CACHE, + ret = cache[str]; - for(idx=startAt;idx 'innerHTML' + 'action_name'.camelize() => 'actionName' + 'css-class-name'.camelize() => 'cssClassName' + 'my favorite items'.camelize() => 'myFavoriteItems' - @param {Object} object the item to search for - @param {Number} startAt optional starting location to search, default 0 - @returns {Number} index or -1 if not found + @param {String} str + The string to camelize. - @example - var arr = ["a", "b", "c", "d", "a"]; - arr.lastIndexOf("a"); => 4 - arr.lastIndexOf("z"); => -1 - arr.lastIndexOf("a", 2); => 0 - arr.lastIndexOf("a", -1); => 4 - arr.lastIndexOf("b", 3); => 1 - arr.lastIndexOf("a", 100); => 4 + @returns {String} the camelized string. */ - lastIndexOf: function(object, startAt) { - var idx, len = get(this, 'length'); - - if (startAt === undefined || startAt >= len) startAt = len-1; - if (startAt < 0) startAt += len; - - for(idx=startAt;idx>=0;idx--) { - if (this.objectAt(idx) === object) return idx ; - } - return -1; - }, - - // .......................................................... - // ARRAY OBSERVERS - // + camelize: function(str) { + return str.replace(STRING_CAMELIZE_REGEXP, function(match, separator, chr) { + return chr ? chr.toUpperCase() : ''; + }); + }, /** - Adds an array observer to the receiving array. The array observer object - normally must implement two methods: + More general than decamelize. Returns the lower_case_and_underscored + form of a string. - * `arrayWillChange(start, removeCount, addCount)` - This method will be - called just before the array is modified. - * `arrayDidChange(start, removeCount, addCount)` - This method will be - called just after the array is modified. + 'innerHTML'.underscore() => 'inner_html' + 'action_name'.underscore() => 'action_name' + 'css-class-name'.underscore() => 'css_class_name' + 'my favorite items'.underscore() => 'my_favorite_items' - Both callbacks will be passed the starting index of the change as well a - a count of the items to be removed and added. You can use these callbacks - to optionally inspect the array during the change, clear caches, or do - any other bookkeeping necessary. + @param {String} str + The string to underscore. - In addition to passing a target, you can also include an options hash - which you can use to override the method names that will be invoked on the - target. + @returns {String} the underscored string. + */ + underscore: function(str) { + return str.replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2'). + replace(STRING_UNDERSCORE_REGEXP_2, '_').toLowerCase(); + } +}; +})({}); - @param {Object} target - The observer object. +(function(exports) { +// ========================================================================== +// Project: Ember Runtime +// Copyright: ©2006-2011 Strobe Inc. and contributors. +// Portions ©2008-2011 Apple Inc. All rights reserved. +// License: Licensed under MIT license (see license.js) +// ========================================================================== +var fmt = Ember.String.fmt, + w = Ember.String.w, + loc = Ember.String.loc, + camelize = Ember.String.camelize, + decamelize = Ember.String.decamelize, + dasherize = Ember.String.dasherize, + underscore = Ember.String.underscore; - @param {Hash} opts - Optional hash of configuration options including willChange, didChange, - and a context option. +if (Ember.EXTEND_PROTOTYPES) { - @returns {Ember.Array} receiver + /** + @see Ember.String.fmt */ - addArrayObserver: function(target, opts) { - var willChange = (opts && opts.willChange) || 'arrayWillChange', - didChange = (opts && opts.didChange) || 'arrayDidChange'; - - var hasObservers = get(this, 'hasArrayObservers'); - if (!hasObservers) Ember.propertyWillChange(this, 'hasArrayObservers'); - Ember.addListener(this, '@array:before', target, willChange, xform); - Ember.addListener(this, '@array:change', target, didChange, xform); - if (!hasObservers) Ember.propertyDidChange(this, 'hasArrayObservers'); - return this; - }, + String.prototype.fmt = function() { + return fmt(this, arguments); + }; /** - Removes an array observer from the object if the observer is current - registered. Calling this method multiple times with the same object will - have no effect. - - @param {Object} target - The object observing the array. - - @returns {Ember.Array} receiver + @see Ember.String.w */ - removeArrayObserver: function(target, opts) { - var willChange = (opts && opts.willChange) || 'arrayWillChange', - didChange = (opts && opts.didChange) || 'arrayDidChange'; + String.prototype.w = function() { + return w(this); + }; - var hasObservers = get(this, 'hasArrayObservers'); - if (hasObservers) Ember.propertyWillChange(this, 'hasArrayObservers'); - Ember.removeListener(this, '@array:before', target, willChange, xform); - Ember.removeListener(this, '@array:change', target, didChange, xform); - if (hasObservers) Ember.propertyDidChange(this, 'hasArrayObservers'); - return this; - }, + /** + @see Ember.String.loc + */ + String.prototype.loc = function() { + return loc(this, arguments); + }; /** - Becomes true whenever the array currently has observers watching changes - on the array. + @see Ember.String.camelize + */ + String.prototype.camelize = function() { + return camelize(this); + }; - @property {Boolean} + /** + @see Ember.String.decamelize */ - hasArrayObservers: Ember.computed(function() { - return Ember.hasListeners(this, '@array:change') || Ember.hasListeners(this, '@array:before'); - }).property().cacheable(), + String.prototype.decamelize = function() { + return decamelize(this); + }; /** - If you are implementing an object that supports Ember.Array, call this - method just before the array content changes to notify any observers and - invalidate any related properties. Pass the starting index of the change - as well as a delta of the amounts to change. + @see Ember.String.dasherize + */ + String.prototype.dasherize = function() { + return dasherize(this); + }; - @param {Number} startIdx - The starting index in the array that will change. + /** + @see Ember.String.underscore + */ + String.prototype.underscore = function() { + return underscore(this); + }; - @param {Number} removeAmt - The number of items that will be removed. If you pass null assumes 0 +} - @param {Number} addAmt - The number of items that will be added. If you pass null assumes 0. - @returns {Ember.Array} receiver - */ - arrayContentWillChange: function(startIdx, removeAmt, addAmt) { +})({}); - // if no args are passed assume everything changes - if (startIdx===undefined) { - startIdx = 0; - removeAmt = addAmt = -1; - } else { - if (!removeAmt) removeAmt=0; - if (!addAmt) addAmt=0; - } +(function(exports) { +// ========================================================================== +// Project: Ember Runtime +// Copyright: ©2006-2011 Strobe Inc. and contributors. +// Portions ©2008-2011 Apple Inc. All rights reserved. +// License: Licensed under MIT license (see license.js) +// ========================================================================== +var a_slice = Array.prototype.slice; - Ember.sendEvent(this, '@array:before', startIdx, removeAmt, addAmt); +if (Ember.EXTEND_PROTOTYPES) { - var removing, lim; - if (startIdx>=0 && removeAmt>=0 && get(this, 'hasEnumerableObservers')) { - removing = []; - lim = startIdx+removeAmt; - for(var idx=startIdx;idx=0 && addAmt>=0 && get(this, 'hasEnumerableObservers')) { - adding = []; - lim = startIdx+addAmt; - for(var idx=startIdx;idx "a" - @param {Object} object - The object to remove from the enumerable. + var arr = []; + arr.firstObject(); => undefined - @returns {Object} the passed object + @returns {Object} the object or undefined */ - removeObject: Ember.required(Function), + firstObject: Ember.computed(function() { + if (get(this, 'length')===0) return undefined ; + if (Ember.Array && Ember.Array.detect(this)) return this.objectAt(0); + // handle generic enumerables + var context = popCtx(), ret; + ret = this.nextObject(0, null, context); + pushCtx(context); + return ret ; + }).property(), /** - Removes each objects in the passed enumerable from the receiver. - - @param {Ember.Enumerable} objects the objects to remove - @returns {Object} receiver - */ - removeObjects: function(objects) { - Ember.beginPropertyChanges(this); - forEach(objects, function(obj) { this.removeObject(obj); }, this); - Ember.endPropertyChanges(this); - return this; - } - -}); - -})({}); - - -(function(exports) { -// ========================================================================== -// Project: Ember Runtime -// Copyright: ©2011 Strobe Inc. and contributors. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -// .......................................................... -// CONSTANTS -// + Helper method returns the last object from a collection. If your enumerable + contains only one object, this method should always return that object. + If your enumerable is empty, this method should return undefined. -var OUT_OF_RANGE_EXCEPTION = "Index out of range" ; -var EMPTY = []; + var arr = ["a", "b", "c"]; + arr.lastObject(); => "c" -// .......................................................... -// HELPERS -// + var arr = []; + arr.lastObject(); => undefined -var get = Ember.get, set = Ember.set, forEach = Ember.ArrayUtils.forEach; + @returns {Object} the last object or undefined + */ + lastObject: Ember.computed(function() { + var len = get(this, 'length'); + if (len===0) return undefined ; + if (Ember.Array && Ember.Array.detect(this)) { + return this.objectAt(len-1); + } else { + var context = popCtx(), idx=0, cur, last = null; + do { + last = cur; + cur = this.nextObject(idx++, last, context); + } while (cur !== undefined); + pushCtx(context); + return last; + } + }).property(), -/** - @class + /** + Returns true if the passed object can be found in the receiver. The + default version will iterate through the enumerable until the object + is found. You may want to override this with a more efficient version. - This mixin defines the API for modifying array-like objects. These methods - can be applied only to a collection that keeps its items in an ordered set. + var arr = ["a", "b", "c"]; + arr.contains("a"); => true + arr.contains("z"); => false - Note that an Array can change even if it does not implement this mixin. - For example, a SparyArray may not be directly modified but if its - underlying enumerable changes, it will change also. + @param {Object} obj + The object to search for. - @extends Ember.Mixin - @extends Ember.Array - @extends Ember.MutableEnumerable -*/ -Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, - /** @scope Ember.MutableArray.prototype */ { + @returns {Boolean} true if object is found in enumerable. + */ + contains: function(obj) { + return this.find(function(item) { return item===obj; }) !== undefined; + }, /** - __Required.__ You must implement this method to apply this mixin. + Iterates through the enumerable, calling the passed function on each + item. This method corresponds to the forEach() method defined in + JavaScript 1.6. - This is one of the primitves you must implement to support Ember.Array. You - should replace amt objects started at idx with the objects in the passed - array. You should also call this.enumerableContentDidChange() ; + The callback method you provide should have the following signature (all + parameters are optional): - @param {Number} idx - Starting index in the array to replace. If idx >= length, then append - to the end of the array. + function(item, index, enumerable); - @param {Number} amt - Number of elements that should be removed from the array, starting at - *idx*. + - *item* is the current item in the iteration. + - *index* is the current index in the iteration + - *enumerable* is the enumerable object itself. - @param {Array} objects - An array of zero or more objects that should be inserted into the array - at *idx* + Note that in addition to a callback, you can also pass an optional target + object that will be set as "this" on the context. This is a good way + to give your iterator function access to the current object. + + @param {Function} callback The callback to execute + @param {Object} target The target object to use + @returns {Object} receiver */ - replace: Ember.required(), + forEach: function(callback, target) { + if (typeof callback !== "function") throw new TypeError() ; + var len = get(this, 'length'), last = null, context = popCtx(); - /** - This will use the primitive replace() method to insert an object at the - specified index. + if (target === undefined) target = null; - @param {Number} idx index of insert the object at. - @param {Object} object object to insert - */ - insertAt: function(idx, object) { - if (idx > get(this, 'length')) throw new Error(OUT_OF_RANGE_EXCEPTION) ; - this.replace(idx, 0, [object]) ; + for(var idx=0;idx= get(this, 'length'))) { - throw new Error(OUT_OF_RANGE_EXCEPTION); - } + function(item, index, enumerable); - // fast case - if (len === undefined) len = 1; - this.replace(start, len, EMPTY); - } + - *item* is the current item in the iteration. + - *index* is the current index in the iteration + - *enumerable* is the enumerable object itself. - return this ; - }, + It should return the mapped value. - /** - Push the object onto the end of the array. Works just like push() but it - is KVO-compliant. + Note that in addition to a callback, you can also pass an optional target + object that will be set as "this" on the context. This is a good way + to give your iterator function access to the current object. + + @param {Function} callback The callback to execute + @param {Object} target The target object to use + @returns {Array} The mapped array. */ - pushObject: function(obj) { - this.insertAt(get(this, 'length'), obj) ; - return obj ; + map: function(callback, target) { + var ret = []; + this.forEach(function(x, idx, i) { + ret[idx] = callback.call(target, x, idx,i); + }); + return ret ; }, - /** - Add the objects in the passed numerable to the end of the array. Defers - notifying observers of the change until all objects are added. + Similar to map, this specialized function returns the value of the named + property on all items in the enumeration. - @param {Ember.Enumerable} objects the objects to add - @returns {Ember.Array} receiver + @params key {String} name of the property + @returns {Array} The mapped array. */ - pushObjects: function(objects) { - this.replace(get(this, 'length'), 0, objects); - return this; + mapProperty: function(key) { + return this.map(function(next) { + return get(next, key); + }); }, /** - Pop object from array or nil if none are left. Works just like pop() but - it is KVO-compliant. - */ - popObject: function() { - var len = get(this, 'length') ; - if (len === 0) return null ; + Returns an array with all of the items in the enumeration that the passed + function returns true for. This method corresponds to filter() defined in + JavaScript 1.6. - var ret = this.objectAt(len-1) ; - this.removeAt(len-1, 1) ; - return ret ; - }, + The callback method you provide should have the following signature (all + parameters are optional): - /** - Shift an object from start of array or nil if none are left. Works just - like shift() but it is KVO-compliant. + function(item, index, enumerable); + + - *item* is the current item in the iteration. + - *index* is the current index in the iteration + - *enumerable* is the enumerable object itself. + + It should return the true to include the item in the results, false otherwise. + + Note that in addition to a callback, you can also pass an optional target + object that will be set as "this" on the context. This is a good way + to give your iterator function access to the current object. + + @param {Function} callback The callback to execute + @param {Object} target The target object to use + @returns {Array} A filtered array. */ - shiftObject: function() { - if (get(this, 'length') === 0) return null ; - var ret = this.objectAt(0) ; - this.removeAt(0) ; + filter: function(callback, target) { + var ret = []; + this.forEach(function(x, idx, i) { + if (callback.call(target, x, idx, i)) ret.push(x); + }); return ret ; }, /** - Unshift an object to start of array. Works just like unshift() but it is - KVO-compliant. + Returns an array with just the items with the matched property. You + can pass an optional second argument with the target value. Otherwise + this will match any property that evaluates to true. + + @params key {String} the property to test + @param value {String} optional value to test against. + @returns {Array} filtered array */ - unshiftObject: function(obj) { - this.insertAt(0, obj) ; - return obj ; + filterProperty: function(key, value) { + return this.filter(iter.apply(this, arguments)); }, - /** - Adds the named objects to the beginning of the array. Defers notifying - observers until all objects have been added. + Returns the first item in the array for which the callback returns true. + This method works similar to the filter() method defined in JavaScript 1.6 + except that it will stop working on the array once a match is found. - @param {Ember.Enumerable} objects the objects to add - @returns {Ember.Array} receiver - */ - unshiftObjects: function(objects) { - this.beginPropertyChanges(); - forEach(objects, function(obj) { this.unshiftObject(obj); }, this); - this.endPropertyChanges(); - return this; - }, + The callback method you provide should have the following signature (all + parameters are optional): - // .......................................................... - // IMPLEMENT Ember.MutableEnumerable - // + function(item, index, enumerable); - /** @private (nodoc) */ - removeObject: function(obj) { - var loc = get(this, 'length') || 0; - while(--loc >= 0) { - var curObject = this.objectAt(loc) ; - if (curObject === obj) this.removeAt(loc) ; + - *item* is the current item in the iteration. + - *index* is the current index in the iteration + - *enumerable* is the enumerable object itself. + + It should return the true to include the item in the results, false otherwise. + + Note that in addition to a callback, you can also pass an optional target + object that will be set as "this" on the context. This is a good way + to give your iterator function access to the current object. + + @param {Function} callback The callback to execute + @param {Object} target The target object to use + @returns {Object} Found item or null. + */ + find: function(callback, target) { + var len = get(this, 'length') ; + if (target === undefined) target = null; + + var last = null, next, found = false, ret ; + var context = popCtx(); + for(var idx=0;idx1) args = a_slice.call(arguments, 1); + + this.forEach(function(x, idx) { + var method = x && x[methodName]; + if ('function' === typeof method) { + ret[idx] = args ? method.apply(x, args) : method.call(x); + } + }, this); + + return ret; }, /** - To set multiple properties at once, call setProperties - with a Hash: - - record.setProperties({ firstName: 'Charles', lastName: 'Jolley' }); + Simply converts the enumerable into a genuine array. The order is not + guaranteed. Corresponds to the method implemented by Prototype. - @param {Hash} hash the hash of keys and values to set - @returns {Ember.Observable} + @returns {Array} the enumerable as an array. */ - setProperties: function(hash) { - var self = this; - Ember.changeProperties(function(){ - for(var prop in hash) { - if (hash.hasOwnProperty(prop)) set(self, prop, hash[prop]); - } - }); - return this; + toArray: function() { + var ret = []; + this.forEach(function(o, idx) { ret[idx] = o; }); + return ret ; }, /** - Begins a grouping of property changes. + Returns a copy of the array with all null elements removed. + + var arr = ["a", null, "c", null]; + arr.compact(); => ["a", "c"] - You can use this method to group property changes so that notifications - will not be sent until the changes are finished. If you plan to make a - large number of changes to an object at one time, you should call this - method at the beginning of the changes to suspend change notifications. - When you are done making changes, call endPropertyChanges() to allow - notification to resume. + @returns {Array} the array without null elements. + */ + compact: function() { return this.without(null); }, - @returns {Ember.Observable} + /** + Returns a new enumerable that excludes the passed value. The default + implementation returns an array regardless of the receiver type unless + the receiver does not contain the value. + + var arr = ["a", "b", "a", "c"]; + arr.without("a"); => ["b", "c"] + + @param {Object} value + @returns {Ember.Enumerable} */ - beginPropertyChanges: function() { - Ember.beginPropertyChanges(); - return this; + without: function(value) { + if (!this.contains(value)) return this; // nothing to do + var ret = [] ; + this.forEach(function(k) { + if (k !== value) ret[ret.length] = k; + }) ; + return ret ; }, /** - Ends a grouping of property changes. + Returns a new enumerable that contains only unique values. The default + implementation returns an array regardless of the receiver type. - You can use this method to group property changes so that notifications - will not be sent until the changes are finished. If you plan to make a - large number of changes to an object at one time, you should call - beginPropertyChanges() at the beginning of the changes to suspend change - notifications. When you are done making changes, call this method to allow - notification to resume. + var arr = ["a", "a", "b", "b"]; + arr.uniq(); => ["a", "b"] - @returns {Ember.Observable} + @returns {Ember.Enumerable} */ - endPropertyChanges: function() { - Ember.endPropertyChanges(); - return this; + uniq: function() { + var ret = []; + this.forEach(function(k){ + if (a_indexOf(ret, k)<0) ret.push(k); + }); + return ret; }, /** - Notify the observer system that a property is about to change. + This property will trigger anytime the enumerable's content changes. + You can observe this property to be notified of changes to the enumerables + content. - Sometimes you need to change a value directly or indirectly without - actually calling get() or set() on it. In this case, you can use this - method and propertyDidChange() instead. Calling these two methods - together will notify all observers that the property has potentially - changed value. + For plain enumerables, this property is read only. Ember.Array overrides + this method. - Note that you must always call propertyWillChange and propertyDidChange as - a pair. If you do not, it may get the property change groups out of order - and cause notifications to be delivered more often than you would like. + @property {Ember.Array} + */ + '[]': Ember.computed(function(key, value) { + return this; + }).property().cacheable(), + + // .......................................................... + // ENUMERABLE OBSERVERS + // + + /** + Registers an enumerable observer. Must implement Ember.EnumerableObserver + mixin. + */ + addEnumerableObserver: function(target, opts) { + var willChange = (opts && opts.willChange) || 'enumerableWillChange', + didChange = (opts && opts.didChange) || 'enumerableDidChange'; - @param {String} key The property key that is about to change. - @returns {Ember.Observable} - */ - propertyWillChange: function(keyName){ - Ember.propertyWillChange(this, keyName); + var hasObservers = get(this, 'hasEnumerableObservers'); + if (!hasObservers) Ember.propertyWillChange(this, 'hasEnumerableObservers'); + Ember.addListener(this, '@enumerable:before', target, willChange, xform); + Ember.addListener(this, '@enumerable:change', target, didChange, xform); + if (!hasObservers) Ember.propertyDidChange(this, 'hasEnumerableObservers'); return this; }, /** - Notify the observer system that a property has just changed. - - Sometimes you need to change a value directly or indirectly without - actually calling get() or set() on it. In this case, you can use this - method and propertyWillChange() instead. Calling these two methods - together will notify all observers that the property has potentially - changed value. - - Note that you must always call propertyWillChange and propertyDidChange as - a pair. If you do not, it may get the property change groups out of order - and cause notifications to be delivered more often than you would like. - - @param {String} key The property key that has just changed. - @param {Object} value The new value of the key. May be null. - @param {Boolean} _keepCache Private property - @returns {Ember.Observable} + Removes a registered enumerable observer. */ - propertyDidChange: function(keyName) { - Ember.propertyDidChange(this, keyName); - return this; - }, + removeEnumerableObserver: function(target, opts) { + var willChange = (opts && opts.willChange) || 'enumerableWillChange', + didChange = (opts && opts.didChange) || 'enumerableDidChange'; - notifyPropertyChange: function(keyName) { - this.propertyWillChange(keyName); - this.propertyDidChange(keyName); + var hasObservers = get(this, 'hasEnumerableObservers'); + if (hasObservers) Ember.propertyWillChange(this, 'hasEnumerableObservers'); + Ember.removeListener(this, '@enumerable:before', target, willChange); + Ember.removeListener(this, '@enumerable:change', target, didChange); + if (hasObservers) Ember.propertyDidChange(this, 'hasEnumerableObservers'); return this; }, /** - Adds an observer on a property. + Becomes true whenever the array currently has observers watching changes + on the array. - This is the core method used to register an observer for a property. + @property {Boolean} + */ + hasEnumerableObservers: Ember.computed(function() { + return Ember.hasListeners(this, '@enumerable:change') || Ember.hasListeners(this, '@enumerable:before'); + }).property().cacheable(), - Once you call this method, anytime the key's value is set, your observer - will be notified. Note that the observers are triggered anytime the - value is set, regardless of whether it has actually changed. Your - observer should be prepared to handle that. - You can also pass an optional context parameter to this method. The - context will be passed to your observer method whenever it is triggered. - Note that if you add the same target/method pair on a key multiple times - with different context parameters, your observer will only be called once - with the last context you passed. + /** + Invoke this method just before the contents of your enumerable will + change. You can either omit the parameters completely or pass the objects + to be removed or added if available or just a count. - ## Observer Methods + @param {Ember.Enumerable|Number} removing + An enumerable of the objects to be removed or the number of items to + be removed. - Observer methods you pass should generally have the following signature if - you do not pass a "context" parameter: + @param {Ember.Enumerable|Number} adding + An enumerable of the objects to be added or the number of items to be + added. - fooDidChange: function(sender, key, value, rev); + @returns {Ember.Enumerable} receiver + */ + enumerableContentWillChange: function(removing, adding) { - The sender is the object that changed. The key is the property that - changes. The value property is currently reserved and unused. The rev - is the last property revision of the object when it changed, which you can - use to detect if the key value has really changed or not. + var removeCnt, addCnt, hasDelta; - If you pass a "context" parameter, the context will be passed before the - revision like so: + if ('number' === typeof removing) removeCnt = removing; + else if (removing) removeCnt = get(removing, 'length'); + else removeCnt = removing = -1; - fooDidChange: function(sender, key, value, context, rev); + if ('number' === typeof adding) addCnt = adding; + else if (adding) addCnt = get(adding,'length'); + else addCnt = adding = -1; - Usually you will not need the value, context or revision parameters at - the end. In this case, it is common to write observer methods that take - only a sender and key value as parameters or, if you aren't interested in - any of these values, to write an observer that has no parameters at all. + hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; - @param {String} key The key to observer - @param {Object} target The target object to invoke - @param {String|Function} method The method to invoke. - @returns {Ember.Object} self - */ - addObserver: function(key, target, method) { - Ember.addObserver(this, key, target, method); - }, + if (removing === -1) removing = null; + if (adding === -1) adding = null; - /** - Remove an observer you have previously registered on this object. Pass - the same key, target, and method you passed to addObserver() and your - target will no longer receive notifications. + if (hasDelta) Ember.propertyWillChange(this, 'length'); + Ember.sendEvent(this, '@enumerable:before', removing, adding); - @param {String} key The key to observer - @param {Object} target The target object to invoke - @param {String|Function} method The method to invoke. - @returns {Ember.Observable} reciever - */ - removeObserver: function(key, target, method) { - Ember.removeObserver(this, key, target, method); + return this; }, /** - Returns true if the object currently has observers registered for a - particular key. You can use this method to potentially defer performing - an expensive action until someone begins observing a particular property - on the object. + Invoke this method when the contents of your enumerable has changed. + This will notify any observers watching for content changes. If your are + implementing an ordered enumerable (such as an array), also pass the + start and end values where the content changed so that it can be used to + notify range observers. - @param {String} key Key to check - @returns {Boolean} - */ - hasObserverFor: function(key) { - return Ember.hasListeners(this, key+':change'); - }, + @param {Number} start + optional start offset for the content change. For unordered + enumerables, you should always pass -1. - unknownProperty: function(key) { - return undefined; - }, + @param {Enumerable} added + optional enumerable containing items that were added to the set. For + ordered enumerables, this should be an ordered array of items. If no + items were added you can pass null. - setUnknownProperty: function(key, value) { - this[key] = value; - }, + @param {Enumerable} removes + optional enumerable containing items that were removed from the set. + For ordered enumerables, this hsould be an ordered array of items. If + no items were removed you can pass null. - getPath: function(path) { - return Ember.getPath(this, path); - }, + @returns {Object} receiver + */ + enumerableContentDidChange: function(removing, adding) { + var notify = this.propertyDidChange, removeCnt, addCnt, hasDelta; - setPath: function(path, value) { - Ember.setPath(this, path, value); - return this; - }, + if ('number' === typeof removing) removeCnt = removing; + else if (removing) removeCnt = get(removing, 'length'); + else removeCnt = removing = -1; - getWithDefault: function(key, defaultValue) { - return Ember.getWithDefault(this, key, defaultValue); - }, + if ('number' === typeof adding) addCnt = adding; + else if (adding) addCnt = get(adding, 'length'); + else addCnt = adding = -1; - incrementProperty: function(keyName, increment) { - if (!increment) { increment = 1; } - set(this, keyName, (get(this, keyName) || 0)+increment); - return get(this, keyName); - }, + hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; - decrementProperty: function(keyName, increment) { - if (!increment) { increment = 1; } - set(this, keyName, (get(this, keyName) || 0)-increment); - return get(this, keyName); - }, + if (removing === -1) removing = null; + if (adding === -1) adding = null; - toggleProperty: function(keyName) { - set(this, keyName, !get(this, keyName)); - return get(this, keyName); - }, + Ember.sendEvent(this, '@enumerable:change', removing, adding); + if (hasDelta) Ember.propertyDidChange(this, 'length'); - observersForKey: function(keyName) { - return Ember.observersFor(this, keyName); + return this ; } -}); +}) ; })({}); - (function(exports) { // ========================================================================== // Project: Ember Runtime // Copyright: ©2011 Strobe Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== +// .......................................................... +// HELPERS +// +var get = Ember.get, set = Ember.set, meta = Ember.meta; +/** @private */ +function none(obj) { return obj===null || obj===undefined; } -// NOTE: this object should never be included directly. Instead use Ember. -// Ember.Object. We only define this separately so that Ember.Set can depend on it - - - -var rewatch = Ember.rewatch; -var classToString = Ember.Mixin.prototype.toString; -var set = Ember.set, get = Ember.get; -var o_create = Ember.platform.create, - meta = Ember.meta; - -function makeCtor() { - - // Note: avoid accessing any properties on the object since it makes the - // method a lot faster. This is glue code so we want it to be as fast as - // possible. - - var isPrepared = false, initMixins, init = false, hasChains = false; - - var Class = function() { - if (!isPrepared) { get(Class, 'proto'); } // prepare prototype... - if (initMixins) { - this.reopen.apply(this, initMixins); - initMixins = null; - rewatch(this); // ålways rewatch just in case - this.init.apply(this, arguments); - } else { - if (hasChains) { - rewatch(this); - } else { - this[Ember.GUID_KEY] = undefined; - } - if (init===false) { init = this.init; } // cache for later instantiations - init.apply(this, arguments); - } - }; - - Class.toString = classToString; - Class._prototypeMixinDidChange = function() { - ember_assert("Reopening already instantiated classes is not supported. We plan to support this in the future.", isPrepared === false); - isPrepared = false; - }; - Class._initMixins = function(args) { initMixins = args; }; - - Ember.defineProperty(Class, 'proto', Ember.computed(function() { - if (!isPrepared) { - isPrepared = true; - Class.PrototypeMixin.applyPartial(Class.prototype); - hasChains = !!meta(Class.prototype, false).chains; // avoid rewatch - } - return this.prototype; - })); +/** @private */ +function xform(target, method, params) { + method.call(target, params[0], params[2], params[3], params[4]); +} - return Class; +// .......................................................... +// ARRAY +// +/** + @namespace -} + This module implements Observer-friendly Array-like behavior. This mixin is + picked up by the Array class as well as other controllers, etc. that want to + appear to be arrays. -var CoreObject = makeCtor(); + Unlike Ember.Enumerable, this mixin defines methods specifically for + collections that provide index-ordered access to their contents. When you + are designing code that needs to accept any kind of Array-like object, you + should use these methods instead of Array primitives because these will + properly notify observers of changes to the array. -CoreObject.PrototypeMixin = Ember.Mixin.create( -/** @scope Ember.CoreObject */ { + Although these methods are efficient, they do add a layer of indirection to + your application so it is a good idea to use them only when you need the + flexibility of using both true JavaScript arrays and "virtual" arrays such + as controllers and collections. - reopen: function() { - Ember.Mixin._apply(this, arguments, true); - return this; - }, + You can use the methods defined in this module to access and modify array + contents in a KVO-friendly way. You can also be notified whenever the + membership if an array changes by changing the syntax of the property to + .observes('*myProperty.[]') . - isInstance: true, + To support Ember.Array in your own class, you must override two + primitives to use it: replace() and objectAt(). - init: function() {}, + Note that the Ember.Array mixin also incorporates the Ember.Enumerable mixin. All + Ember.Array-like objects are also enumerable. - isDestroyed: false, + @extends Ember.Enumerable + @since Ember 0.9.0 +*/ +Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.prototype */ { + /** @private - compatibility */ + isSCArray: true, + /** - Destroys an object by setting the isDestroyed flag and removing its - metadata, which effectively destroys observers and bindings. + @field {Number} length - If you try to set a property on a destroyed object, an exception will be - raised. + Your array must support the length property. Your replace methods should + set this property whenever it changes. + */ + length: Ember.required(), - Note that destruction is scheduled for the end of the run loop and does not - happen immediately. + /** + This is one of the primitives you must implement to support Ember.Array. + Returns the object at the named index. If your object supports retrieving + the value of an array item using get() (i.e. myArray.get(0)), then you do + not need to implement this method yourself. - @returns {Ember.Object} receiver + @param {Number} idx + The index of the item to return. If idx exceeds the current length, + return null. */ - destroy: function() { - set(this, 'isDestroyed', true); - Ember.run.schedule('destroy', this, this._scheduledDestroy); - return this; + objectAt: function(idx) { + if ((idx < 0) || (idx>=get(this, 'length'))) return undefined ; + return get(this, idx); }, + /** @private (nodoc) - overrides Ember.Enumerable version */ + nextObject: function(idx) { + return this.objectAt(idx); + }, + /** - Invoked by the run loop to actually destroy the object. This is - scheduled for execution by the `destroy` method. + @field [] - @private + This is the handler for the special array content property. If you get + this property, it will return this. If you set this property it a new + array, it will replace the current content. + + This property overrides the default property defined in Ember.Enumerable. */ - _scheduledDestroy: function() { - Ember.destroy(this); - }, + '[]': Ember.computed(function(key, value) { + if (value !== undefined) this.replace(0, get(this, 'length'), value) ; + return this ; + }).property().cacheable(), - bind: function(to, from) { - if (!(from instanceof Ember.Binding)) { from = Ember.Binding.from(from); } - from.to(to).connect(this); - return from; + /** @private (nodoc) - optimized version from Enumerable */ + contains: function(obj){ + return this.indexOf(obj) >= 0; }, - toString: function() { - return '<'+this.constructor.toString()+':'+Ember.guidFor(this)+'>'; - } -}); + // Add any extra methods to Ember.Array that are native to the built-in Array. + /** + Returns a new array that is a slice of the receiver. This implementation + uses the observable array methods to retrieve the objects for the new + slice. -CoreObject.__super__ = null; + var arr = ['red', 'green', 'blue']; + arr.slice(0); => ['red', 'green', 'blue'] + arr.slice(0, 2); => ['red', 'green'] + arr.slice(1, 100); => ['green', 'blue'] -var ClassMixin = Ember.Mixin.create({ + @param beginIndex {Integer} (Optional) index to begin slicing from. + @param endIndex {Integer} (Optional) index to end the slice at. + @returns {Array} New array with specified slice + */ + slice: function(beginIndex, endIndex) { + var ret = []; + var length = get(this, 'length') ; + if (none(beginIndex)) beginIndex = 0 ; + if (none(endIndex) || (endIndex > length)) endIndex = length ; + while(beginIndex < endIndex) { + ret[ret.length] = this.objectAt(beginIndex++) ; + } + return ret ; + }, - ClassMixin: Ember.required(), + /** + Returns the index of the given object's first occurrence. + If no startAt argument is given, the starting location to + search is 0. If it's negative, will count backward from + the end of the array. Returns -1 if no match is found. - PrototypeMixin: Ember.required(), + @param {Object} object the item to search for + @param {Number} startAt optional starting location to search, default 0 + @returns {Number} index or -1 if not found - isClass: true, + @example + var arr = ["a", "b", "c", "d", "a"]; + arr.indexOf("a"); => 0 + arr.indexOf("z"); => -1 + arr.indexOf("a", 2); => 4 + arr.indexOf("a", -1); => 4 + arr.indexOf("b", 3); => -1 + arr.indexOf("a", 100); => -1 + */ + indexOf: function(object, startAt) { + var idx, len = get(this, 'length'); - isMethod: false, + if (startAt === undefined) startAt = 0; + if (startAt < 0) startAt += len; - extend: function() { - var Class = makeCtor(), proto; - Class.ClassMixin = Ember.Mixin.create(this.ClassMixin); - Class.PrototypeMixin = Ember.Mixin.create(this.PrototypeMixin); + for(idx=startAt;idx 4 + arr.lastIndexOf("z"); => -1 + arr.lastIndexOf("a", 2); => 0 + arr.lastIndexOf("a", -1); => 4 + arr.lastIndexOf("b", 3); => 1 + arr.lastIndexOf("a", 100); => 4 + */ + lastIndexOf: function(object, startAt) { + var idx, len = get(this, 'length'); - proto = Class.prototype = o_create(this.prototype); - proto.constructor = Class; - Ember.generateGuid(proto, 'ember'); - meta(proto).proto = proto; // this will disable observers on prototype - Ember.rewatch(proto); // setup watch chains if needed. + if (startAt === undefined || startAt >= len) startAt = len-1; + if (startAt < 0) startAt += len; + for(idx=startAt;idx>=0;idx--) { + if (this.objectAt(idx) === object) return idx ; + } + return -1; + }, + + // .......................................................... + // ARRAY OBSERVERS + // + + /** + Adds an array observer to the receiving array. The array observer object + normally must implement two methods: + + * `arrayWillChange(start, removeCount, addCount)` - This method will be + called just before the array is modified. + * `arrayDidChange(start, removeCount, addCount)` - This method will be + called just after the array is modified. + + Both callbacks will be passed the starting index of the change as well a + a count of the items to be removed and added. You can use these callbacks + to optionally inspect the array during the change, clear caches, or do + any other bookkeeping necessary. + + In addition to passing a target, you can also include an options hash + which you can use to override the method names that will be invoked on the + target. + + @param {Object} target + The observer object. + + @param {Hash} opts + Optional hash of configuration options including willChange, didChange, + and a context option. + + @returns {Ember.Array} receiver + */ + addArrayObserver: function(target, opts) { + var willChange = (opts && opts.willChange) || 'arrayWillChange', + didChange = (opts && opts.didChange) || 'arrayDidChange'; - Class.subclasses = Ember.Set ? new Ember.Set() : null; - if (this.subclasses) { this.subclasses.add(Class); } + var hasObservers = get(this, 'hasArrayObservers'); + if (!hasObservers) Ember.propertyWillChange(this, 'hasArrayObservers'); + Ember.addListener(this, '@array:before', target, willChange, xform); + Ember.addListener(this, '@array:change', target, didChange, xform); + if (!hasObservers) Ember.propertyDidChange(this, 'hasArrayObservers'); + return this; + }, + + /** + Removes an array observer from the object if the observer is current + registered. Calling this method multiple times with the same object will + have no effect. + + @param {Object} target + The object observing the array. + + @returns {Ember.Array} receiver + */ + removeArrayObserver: function(target, opts) { + var willChange = (opts && opts.willChange) || 'arrayWillChange', + didChange = (opts && opts.didChange) || 'arrayDidChange'; - Class.ClassMixin.apply(Class); - return Class; + var hasObservers = get(this, 'hasArrayObservers'); + if (hasObservers) Ember.propertyWillChange(this, 'hasArrayObservers'); + Ember.removeListener(this, '@array:before', target, willChange, xform); + Ember.removeListener(this, '@array:change', target, didChange, xform); + if (hasObservers) Ember.propertyDidChange(this, 'hasArrayObservers'); + return this; }, + + /** + Becomes true whenever the array currently has observers watching changes + on the array. + + @property {Boolean} + */ + hasArrayObservers: Ember.computed(function() { + return Ember.hasListeners(this, '@array:change') || Ember.hasListeners(this, '@array:before'); + }).property().cacheable(), + + /** + If you are implementing an object that supports Ember.Array, call this + method just before the array content changes to notify any observers and + invalidate any related properties. Pass the starting index of the change + as well as a delta of the amounts to change. + + @param {Number} startIdx + The starting index in the array that will change. + + @param {Number} removeAmt + The number of items that will be removed. If you pass null assumes 0 + + @param {Number} addAmt + The number of items that will be added. If you pass null assumes 0. + + @returns {Ember.Array} receiver + */ + arrayContentWillChange: function(startIdx, removeAmt, addAmt) { + + // if no args are passed assume everything changes + if (startIdx===undefined) { + startIdx = 0; + removeAmt = addAmt = -1; + } else { + if (!removeAmt) removeAmt=0; + if (!addAmt) addAmt=0; + } - create: function() { - var C = this; - if (arguments.length>0) { this._initMixins(arguments); } - return new C(); - }, + Ember.sendEvent(this, '@array:before', startIdx, removeAmt, addAmt); - reopen: function() { - var PrototypeMixin = this.PrototypeMixin; - PrototypeMixin.reopen.apply(PrototypeMixin, arguments); - this._prototypeMixinDidChange(); - return this; - }, + var removing, lim; + if (startIdx>=0 && removeAmt>=0 && get(this, 'hasEnumerableObservers')) { + removing = []; + lim = startIdx+removeAmt; + for(var idx=startIdx;idx=0 && addAmt>=0 && get(this, 'hasEnumerableObservers')) { + adding = []; + lim = startIdx+addAmt; + for(var idx=startIdx;idx b` - Use this instead of the built-in Ember.typeOf() to get the type of an item. - It will return the same result across all browsers and includes a bit - more detail. Here is what will be returned: + Default implementation raises an exception. - | Return Value Constant | Meaning | - | 'string' | String primitive | - | 'number' | Number primitive | - | 'boolean' | Boolean primitive | - | 'null' | Null value | - | 'undefined' | Undefined value | - | 'function' | A function | - | 'array' | An instance of Array | - | 'class' | A Ember class (created using Ember.Object.extend()) | - | 'instance' | A Ember object instance | - | 'error' | An instance of the Error object | - | 'object' | A JavaScript object not inheriting from Ember.Object | + @param a {Object} the first object to compare + @param b {Object} the second object to compare + @returns {Integer} the result of the comparison + */ + compare: Ember.required(Function) - @param item {Object} the item to check - @returns {String} the type -*/ -Ember.typeOf = function(item) { - var ret; +}); - ret = (item === null || item === undefined) ? String(item) : TYPE_MAP[toString.call(item)] || 'object'; - if (ret === 'function') { - if (Ember.Object && Ember.Object.detect(item)) ret = 'class'; - } else if (ret === 'object') { - if (item instanceof Error) ret = 'error'; - else if (Ember.Object && item instanceof Ember.Object) ret = 'instance'; - else ret = 'object'; - } +})({}); - return ret; -}; +(function(exports) { +// ========================================================================== +// Project: Ember Runtime +// Copyright: ©2006-2011 Strobe Inc. and contributors. +// Portions ©2008-2010 Apple Inc. All rights reserved. +// License: Licensed under MIT license (see license.js) +// ========================================================================== +var get = Ember.get, set = Ember.set; /** - Returns true if the passed value is null or undefined. This avoids errors - from JSLint complaining about use of ==, which can be technically - confusing. - - @param {Object} obj Value to test - @returns {Boolean} -*/ -Ember.none = function(obj) { - return obj === null || obj === undefined; -}; + @namespace -/** - Verifies that a value is null or an empty string | array | function. + Implements some standard methods for copying an object. Add this mixin to + any object you create that can create a copy of itself. This mixin is + added automatically to the built-in array. - @param {Object} obj Value to test - @returns {Boolean} -*/ -Ember.empty = function(obj) { - return obj === null || obj === undefined || (obj.length === 0 && typeof obj !== 'function'); -}; + You should generally implement the copy() method to return a copy of the + receiver. -/** - Ember.isArray defined in ember-metal/lib/utils -**/ + Note that frozenCopy() will only work if you also implement Ember.Freezable. -/** - This will compare two javascript values of possibly different types. - It will tell you which one is greater than the other by returning: + @since Ember 0.9 +*/ +Ember.Copyable = Ember.Mixin.create( +/** @scope Ember.Copyable.prototype */ { - - -1 if the first is smaller than the second, - - 0 if both are equal, - - 1 if the first is greater than the second. + /** + Override to return a copy of the receiver. Default implementation raises + an exception. - The order is calculated based on Ember.ORDER_DEFINITION, if types are different. - In case they have the same type an appropriate comparison for this type is made. + @param deep {Boolean} if true, a deep copy of the object should be made + @returns {Object} copy of receiver + */ + copy: Ember.required(Function), - @param {Object} v First value to compare - @param {Object} w Second value to compare - @returns {Number} -1 if v < w, 0 if v = w and 1 if v > w. -*/ -Ember.compare = function compare(v, w) { - if (v === w) { return 0; } + /** + If the object implements Ember.Freezable, then this will return a new copy + if the object is not frozen and the receiver if the object is frozen. - var type1 = Ember.typeOf(v); - var type2 = Ember.typeOf(w); + Raises an exception if you try to call this method on a object that does + not support freezing. - var Comparable = Ember.Comparable; - if (Comparable) { - if (type1==='instance' && Comparable.detect(v.constructor)) { - return v.constructor.compare(v, w); - } + You should use this method whenever you want a copy of a freezable object + since a freezable object can simply return itself without actually + consuming more memory. - if (type2 === 'instance' && Comparable.detect(w.constructor)) { - return 1-w.constructor.compare(w, v); + @returns {Object} copy of receiver or receiver + */ + frozenCopy: function() { + if (Ember.Freezable && Ember.Freezable.detect(this)) { + return get(this, 'isFrozen') ? this : this.copy().freeze(); + } else { + throw new Error(Ember.String.fmt("%@ does not support freezing", [this])); } } +}); - // If we haven't yet generated a reverse-mapping of Ember.ORDER_DEFINITION, - // do so now. - var mapping = Ember.ORDER_DEFINITION_MAPPING; - if (!mapping) { - var order = Ember.ORDER_DEFINITION; - mapping = Ember.ORDER_DEFINITION_MAPPING = {}; - var idx, len; - for (idx = 0, len = order.length; idx < len; ++idx) { - mapping[order[idx]] = idx; - } - // We no longer need Ember.ORDER_DEFINITION. - delete Ember.ORDER_DEFINITION; - } - var type1Index = mapping[type1]; - var type2Index = mapping[type2]; - if (type1Index < type2Index) { return -1; } - if (type1Index > type2Index) { return 1; } +})({}); - // types are equal - so we have to check values now - switch (type1) { - case 'boolean': - case 'number': - if (v < w) { return -1; } - if (v > w) { return 1; } - return 0; +(function(exports) { +// ========================================================================== +// Project: Ember Runtime +// Copyright: ©2006-2011 Strobe Inc. and contributors. +// Portions ©2008-2010 Apple Inc. All rights reserved. +// License: Licensed under MIT license (see license.js) +// ========================================================================== - case 'string': - var comp = v.localeCompare(w); - if (comp < 0) { return -1; } - if (comp > 0) { return 1; } - return 0; - case 'array': - var vLen = v.length; - var wLen = w.length; - var l = Math.min(vLen, wLen); - var r = 0; - var i = 0; - while (r === 0 && i < l) { - r = compare(v[i],w[i]); - i++; - } - if (r !== 0) { return r; } - // all elements are equal now - // shorter array should be ordered first - if (vLen < wLen) { return -1; } - if (vLen > wLen) { return 1; } - // arrays are equal now - return 0; - case 'instance': - if (Ember.Comparable && Ember.Comparable.detect(v)) { - return v.compare(v, w); - } - return 0; - default: - return 0; - } -}; +var get = Ember.get, set = Ember.set; -function _copy(obj, deep, seen, copies) { - var ret, loc, key; +/** + @namespace - // primitive data types are immutable, just return them. - if ('object' !== typeof obj || obj===null) return obj; + The Ember.Freezable mixin implements some basic methods for marking an object + as frozen. Once an object is frozen it should be read only. No changes + may be made the internal state of the object. - // avoid cyclical loops - if (deep && (loc=indexOf(seen, obj))>=0) return copies[loc]; + ## Enforcement - ember_assert('Cannot clone an Ember.Object that does not implement Ember.Copyable', !(obj instanceof Ember.Object) || (Ember.Copyable && Ember.Copyable.detect(obj))); + To fully support freezing in your subclass, you must include this mixin and + override any method that might alter any property on the object to instead + raise an exception. You can check the state of an object by checking the + isFrozen property. - // IMPORTANT: this specific test will detect a native array only. Any other - // object will need to implement Copyable. - if (Ember.typeOf(obj) === 'array') { - ret = obj.slice(); - if (deep) { - loc = ret.length; - while(--loc>=0) ret[loc] = _copy(ret[loc], deep, seen, copies); - } - } else if (Ember.Copyable && Ember.Copyable.detect(obj)) { - ret = obj.copy(deep, seen, copies); - } else { - ret = {}; - for(key in obj) { - if (!obj.hasOwnProperty(key)) continue; - ret[key] = deep ? _copy(obj[key], deep, seen, copies) : obj[key]; - } - } + Although future versions of JavaScript may support language-level freezing + object objects, that is not the case today. Even if an object is freezable, + it is still technically possible to modify the object, even though it could + break other parts of your application that do not expect a frozen object to + change. It is, therefore, very important that you always respect the + isFrozen property on all freezable objects. - if (deep) { - seen.push(obj); - copies.push(ret); - } + ## Example Usage - return ret; -} + The example below shows a simple object that implement the Ember.Freezable + protocol. -/** - Creates a clone of the passed object. This function can take just about - any type of object and create a clone of it, including primitive values - (which are not actually cloned because they are immutable). + Contact = Ember.Object.extend(Ember.Freezable, { - If the passed object implements the clone() method, then this function - will simply call that method and return the result. + firstName: null, - @param {Object} object The object to clone - @param {Boolean} deep If true, a deep copy of the object is made - @returns {Object} The cloned object -*/ -Ember.copy = function(obj, deep) { - // fast paths - if ('object' !== typeof obj || obj===null) return obj; // can't copy primitives - if (Ember.Copyable && Ember.Copyable.detect(obj)) return obj.copy(deep); - return _copy(obj, deep, deep ? [] : null, deep ? [] : null); -}; + lastName: null, -/** - Convenience method to inspect an object. This method will attempt to - convert the object into a useful string description. + // swaps the names + swapNames: function() { + if (this.get('isFrozen')) throw Ember.FROZEN_ERROR; + var tmp = this.get('firstName'); + this.set('firstName', this.get('lastName')); + this.set('lastName', tmp); + return this; + } - @param {Object} obj The object you want to inspect. - @returns {String} A description of the object -*/ -Ember.inspect = function(obj) { - var v, ret = []; - for(var key in obj) { - if (obj.hasOwnProperty(key)) { - v = obj[key]; - if (v === 'toString') { continue; } // ignore useless items - if (Ember.typeOf(v) === 'function') { v = "function() { ... }"; } - ret.push(key + ": " + v); - } - } - return "{" + ret.join(" , ") + "}"; -}; + }); -/** - Compares two objects, returning true if they are logically equal. This is - a deeper comparison than a simple triple equal. For arrays and enumerables - it will compare the internal objects. For any other object that implements - `isEqual()` it will respect that method. + c = Context.create({ firstName: "John", lastName: "Doe" }); + c.swapNames(); => returns c + c.freeze(); + c.swapNames(); => EXCEPTION - @param {Object} a first object to compare - @param {Object} b second object to compare - @returns {Boolean} -*/ -Ember.isEqual = function(a, b) { - if (a && 'function'===typeof a.isEqual) return a.isEqual(b); - return a === b; -}; + ## Copying -/** - @private - Used by Ember.compare + Usually the Ember.Freezable protocol is implemented in cooperation with the + Ember.Copyable protocol, which defines a frozenCopy() method that will return + a frozen object, if the object implements this method as well. + + @since Ember 0.9 */ -Ember.ORDER_DEFINITION = Ember.ENV.ORDER_DEFINITION || [ - 'undefined', - 'null', - 'boolean', - 'number', - 'string', - 'array', - 'object', - 'instance', - 'function', - 'class' -]; +Ember.Freezable = Ember.Mixin.create( +/** @scope Ember.Freezable.prototype */ { -/** - Returns all of the keys defined on an object or hash. This is useful - when inspecting objects for debugging. On browsers that support it, this - uses the native Object.keys implementation. + /** + Set to true when the object is frozen. Use this property to detect whether + your object is frozen or not. - @function - @param {Object} obj - @returns {Array} Array containing keys of obj -*/ -Ember.keys = Object.keys; + @property {Boolean} + */ + isFrozen: false, -if (!Ember.keys) { - Ember.keys = function(obj) { - var ret = []; - for(var key in obj) { - if (obj.hasOwnProperty(key)) { ret.push(key); } - } - return ret; - }; -} + /** + Freezes the object. Once this method has been called the object should + no longer allow any properties to be edited. + + @returns {Object} reciever + */ + freeze: function() { + if (get(this, 'isFrozen')) return this; + set(this, 'isFrozen', true); + return this; + } -// .......................................................... -// ERROR -// +}); -/** - @class +Ember.FROZEN_ERROR = "Frozen object cannot be modified."; - A subclass of the JavaScript Error object for use in Ember. -*/ -Ember.Error = function() { - var tmp = Error.prototype.constructor.apply(this, arguments); - for (var p in tmp) { - if (tmp.hasOwnProperty(p)) { this[p] = tmp[p]; } - } - this.message = tmp.message; -}; -Ember.Error.prototype = Ember.create(Error.prototype); })({}); - (function(exports) { // ========================================================================== // Project: Ember Runtime -// Copyright: ©2011 Strobe Inc. +// Copyright: ©2011 Strobe Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== +var forEach = Ember.ArrayUtils.forEach; +/** + @class + This mixin defines the API for modifying generic enumerables. These methods + can be applied to an object regardless of whether it is ordered or + unordered. + Note that an Enumerable can change even if it does not implement this mixin. + For example, a MappedEnumerable cannot be directly modified but if its + underlying enumerable changes, it will change also. + ## Adding Objects -/** @private **/ -var STRING_DASHERIZE_REGEXP = (/[ _]/g); -var STRING_DASHERIZE_CACHE = {}; -var STRING_DECAMELIZE_REGEXP = (/([a-z])([A-Z])/g); -var STRING_CAMELIZE_REGEXP = (/(\-|_|\s)+(.)?/g); -var STRING_UNDERSCORE_REGEXP_1 = (/([a-z\d])([A-Z]+)/g); -var STRING_UNDERSCORE_REGEXP_2 = (/\-|\s+/g); + To add an object to an enumerable, use the addObject() method. This + method will only add the object to the enumerable if the object is not + already present and the object if of a type supported by the enumerable. -/** - Defines the hash of localized strings for the current language. Used by - the `Ember.String.loc()` helper. To localize, add string values to this - hash. + javascript: + set.addObject(contact); - @property {String} -*/ -Ember.STRINGS = {}; + ## Removing Objects -/** - Defines string helper methods including string formatting and localization. - Unless Ember.EXTEND_PROTOTYPES = false these methods will also be added to the - String.prototype as well. + To remove an object form an enumerable, use the removeObject() method. This + will only remove the object if it is already in the enumerable, otherwise + this method has no effect. - @namespace + javascript: + set.removeObject(contact); + + ## Implementing In Your Own Code + + If you are implementing an object and want to support this API, just include + this mixin in your class and implement the required methods. In your unit + tests, be sure to apply the Ember.MutableEnumerableTests to your object. + + @extends Ember.Mixin + @extends Ember.Enumerable */ -Ember.String = { +Ember.MutableEnumerable = Ember.Mixin.create(Ember.Enumerable, + /** @scope Ember.MutableEnumerable.prototype */ { /** - Apply formatting options to the string. This will look for occurrences - of %@ in your string and substitute them with the arguments you pass into - this method. If you want to control the specific order of replacement, - you can add a number after the key as well to indicate which argument - you want to insert. + __Required.__ You must implement this method to apply this mixin. - Ordered insertions are most useful when building loc strings where values - you need to insert may appear in different orders. + Attempts to add the passed object to the receiver if the object is not + already present in the collection. If the object is present, this method + has no effect. - ## Examples + If the passed object is of a type not supported by the receiver + then this method should raise an exception. - "Hello %@ %@".fmt('John', 'Doe') => "Hello John Doe" - "Hello %@2, %@1".fmt('John', 'Doe') => "Hello Doe, John" + @param {Object} object + The object to add to the enumerable. - @param {Object...} [args] - @returns {String} formatted string + @returns {Object} the passed object */ - fmt: function(str, formats) { - // first, replace any ORDERED replacements. - var idx = 0; // the current index for non-numerical replacements - return str.replace(/%@([0-9]+)?/g, function(s, argIndex) { - argIndex = (argIndex) ? parseInt(argIndex,0) - 1 : idx++ ; - s = formats[argIndex]; - return ((s === null) ? '(null)' : (s === undefined) ? '' : s).toString(); - }) ; + addObject: Ember.required(Function), + + /** + Adds each object in the passed enumerable to the receiver. + + @param {Ember.Enumerable} objects the objects to add. + @returns {Object} receiver + */ + addObjects: function(objects) { + Ember.beginPropertyChanges(this); + forEach(objects, function(obj) { this.addObject(obj); }, this); + Ember.endPropertyChanges(this); + return this; }, /** - Formats the passed string, but first looks up the string in the localized - strings hash. This is a convenient way to localize text. See - `Ember.String.fmt()` for more information on formatting. + __Required.__ You must implement this method to apply this mixin. - Note that it is traditional but not required to prefix localized string - keys with an underscore or other character so you can easily identify - localized strings. + Attempts to remove the passed object from the receiver collection if the + object is in present in the collection. If the object is not present, + this method has no effect. - # Example Usage + If the passed object is of a type not supported by the receiver + then this method should raise an exception. - @javascript@ - Ember.STRINGS = { - '_Hello World': 'Bonjour le monde', - '_Hello %@ %@': 'Bonjour %@ %@' - }; + @param {Object} object + The object to remove from the enumerable. - Ember.String.loc("_Hello World"); - => 'Bonjour le monde'; + @returns {Object} the passed object + */ + removeObject: Ember.required(Function), - Ember.String.loc("_Hello %@ %@", ["John", "Smith"]); - => "Bonjour John Smith"; + /** + Removes each objects in the passed enumerable from the receiver. + @param {Ember.Enumerable} objects the objects to remove + @returns {Object} receiver + */ + removeObjects: function(objects) { + Ember.beginPropertyChanges(this); + forEach(objects, function(obj) { this.removeObject(obj); }, this); + Ember.endPropertyChanges(this); + return this; + } - @param {String} str - The string to format +}); - @param {Array} formats - Optional array of parameters to interpolate into string. +})({}); - @returns {String} formatted string - */ - loc: function(str, formats) { - str = Ember.STRINGS[str] || str; - return Ember.String.fmt(str, formats) ; - }, +(function(exports) { +// ========================================================================== +// Project: Ember Runtime +// Copyright: ©2011 Strobe Inc. and contributors. +// License: Licensed under MIT license (see license.js) +// ========================================================================== +// .......................................................... +// CONSTANTS +// + +var OUT_OF_RANGE_EXCEPTION = "Index out of range" ; +var EMPTY = []; + +// .......................................................... +// HELPERS +// + +var get = Ember.get, set = Ember.set, forEach = Ember.ArrayUtils.forEach; + +/** + @class + + This mixin defines the API for modifying array-like objects. These methods + can be applied only to a collection that keeps its items in an ordered set. + + Note that an Array can change even if it does not implement this mixin. + For example, a SparyArray may not be directly modified but if its + underlying enumerable changes, it will change also. + + @extends Ember.Mixin + @extends Ember.Array + @extends Ember.MutableEnumerable +*/ +Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, + /** @scope Ember.MutableArray.prototype */ { /** - Splits a string into separate units separated by spaces, eliminating any - empty strings in the process. This is a convenience method for split that - is mostly useful when applied to the String.prototype. + __Required.__ You must implement this method to apply this mixin. - # Example Usage + This is one of the primitves you must implement to support Ember.Array. You + should replace amt objects started at idx with the objects in the passed + array. You should also call this.enumerableContentDidChange() ; - @javascript@ - Ember.String.w("alpha beta gamma").forEach(function(key) { - console.log(key); - }); - > alpha - > beta - > gamma + @param {Number} idx + Starting index in the array to replace. If idx >= length, then append + to the end of the array. - @param {String} str - The string to split + @param {Number} amt + Number of elements that should be removed from the array, starting at + *idx*. - @returns {String} split string + @param {Array} objects + An array of zero or more objects that should be inserted into the array + at *idx* */ - w: function(str) { return str.split(/\s+/); }, + replace: Ember.required(), /** - Converts a camelized string into all lower case separated by underscores. - - h2. Examples + This will use the primitive replace() method to insert an object at the + specified index. - | *Input String* | *Output String* | - | my favorite items | my favorite items | - | css-class-name | css-class-name | - | action_name | action_name | - | innerHTML | inner_html | + var colors = ["red", "green", "blue"]; + colors.insertAt(2, "yellow"); => ["red", "green", "yellow", "blue"] + colors.insertAt(5, "orange"); => Error: Index out of range - @returns {String} the decamelized string. + @param {Number} idx index of insert the object at. + @param {Object} object object to insert */ - decamelize: function(str) { - return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); + insertAt: function(idx, object) { + if (idx > get(this, 'length')) throw new Error(OUT_OF_RANGE_EXCEPTION) ; + this.replace(idx, 0, [object]) ; + return this ; }, /** - Converts a camelized string or a string with spaces or underscores into - a string with components separated by dashes. + Remove an object at the specified index using the replace() primitive + method. You can pass either a single index, or a start and a length. - h2. Examples + If you pass a start and length that is beyond the + length this method will throw an Ember.OUT_OF_RANGE_EXCEPTION - | *Input String* | *Output String* | - | my favorite items | my-favorite-items | - | css-class-name | css-class-name | - | action_name | action-name | - | innerHTML | inner-html | + var colors = ["red", "green", "blue", "yellow", "orange"]; + colors.removeAt(0); => ["green", "blue", "yellow", "orange"] + colors.removeAt(2, 2); => ["green", "blue"] + colors.removeAt(4, 2); => Error: Index out of range - @returns {String} the dasherized string. + @param {Number|Ember.IndexSet} start index, start of range, or index set + @param {Number} len length of passing range + @returns {Object} receiver */ - dasherize: function(str) { - var cache = STRING_DASHERIZE_CACHE, - ret = cache[str]; + removeAt: function(start, len) { - if (ret) { - return ret; - } else { - ret = Ember.String.decamelize(str).replace(STRING_DASHERIZE_REGEXP,'-'); - cache[str] = ret; + var delta = 0; + + if ('number' === typeof start) { + + if ((start < 0) || (start >= get(this, 'length'))) { + throw new Error(OUT_OF_RANGE_EXCEPTION); + } + + // fast case + if (len === undefined) len = 1; + this.replace(start, len, EMPTY); } - return ret; + return this ; }, /** - Converts a dasherized string or a string with spaces or underscores into - camelized string. - - h2. Examples + Push the object onto the end of the array. Works just like push() but it + is KVO-compliant. - | *Input String* | *Output String* | - | my favorite items | myFavoriteItems | - | css-class-name | cssClassName | - | action_name | actionName | - | innerHTML | innerHTML | + var colors = ["red", "green", "blue"]; + colors.pushObject("black"); => ["red", "green", "blue", "black"] + colors.pushObject(["yellow", "orange"]); => ["red", "green", "blue", "black", ["yellow", "orange"]] - @returns {String} the camelized string. */ - camelize: function(str) { - return str.replace(STRING_CAMELIZE_REGEXP, function(match, separator, chr) { - return chr ? chr.toUpperCase() : ''; - }); + pushObject: function(obj) { + this.insertAt(get(this, 'length'), obj) ; + return obj ; }, /** - More general than decamelize, converts a dasherized or camelcased string or a string with spaces into - all lower case separated by undescores. - - h2. Examples + Add the objects in the passed numerable to the end of the array. Defers + notifying observers of the change until all objects are added. - | *Input String* | *Output String* | - | my favorite items | my_favorite_items | - | css-class-name | css_class_name | - | action_name | action_name | - | innerHTML | inner_html | + var colors = ["red", "green", "blue"]; + colors.pushObjects("black"); => ["red", "green", "blue", "black"] + colors.pushObjects(["yellow", "orange"]); => ["red", "green", "blue", "black", "yellow", "orange"] - @returns {String} the camelized string. + @param {Ember.Enumerable} objects the objects to add + @returns {Ember.Array} receiver */ - underscore: function(str) { - return str.replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2'). - replace(STRING_UNDERSCORE_REGEXP_2, '_').toLowerCase(); - } -}; - - - - -})({}); + pushObjects: function(objects) { + this.replace(get(this, 'length'), 0, objects); + return this; + }, + /** + Pop object from array or nil if none are left. Works just like pop() but + it is KVO-compliant. -(function(exports) { -// ========================================================================== -// Project: Ember Runtime -// Copyright: ©2006-2011 Strobe Inc. and contributors. -// Portions ©2008-2010 Apple Inc. All rights reserved. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -var get = Ember.get, set = Ember.set; + var colors = ["red", "green", "blue"]; + colors.popObject(); => "blue" + console.log(colors); => ["red", "green"] -/** - @namespace + */ + popObject: function() { + var len = get(this, 'length') ; + if (len === 0) return null ; - Implements some standard methods for copying an object. Add this mixin to - any object you create that can create a copy of itself. This mixin is - added automatically to the built-in array. + var ret = this.objectAt(len-1) ; + this.removeAt(len-1, 1) ; + return ret ; + }, - You should generally implement the copy() method to return a copy of the - receiver. + /** + Shift an object from start of array or nil if none are left. Works just + like shift() but it is KVO-compliant. - Note that frozenCopy() will only work if you also implement Ember.Freezable. + var colors = ["red", "green", "blue"]; + colors.shiftObject(); => "red" + console.log(colors); => ["green", "blue"] - @since Ember 0.9 -*/ -Ember.Copyable = Ember.Mixin.create( -/** @scope Ember.Copyable.prototype */ { + */ + shiftObject: function() { + if (get(this, 'length') === 0) return null ; + var ret = this.objectAt(0) ; + this.removeAt(0) ; + return ret ; + }, /** - Override to return a copy of the receiver. Default implementation raises - an exception. + Unshift an object to start of array. Works just like unshift() but it is + KVO-compliant. + + var colors = ["red", "green", "blue"]; + colors.unshiftObject("yellow"); => ["yellow", "red", "green", "blue"] + colors.unshiftObject(["black", "white"]); => [["black", "white"], "yellow", "red", "green", "blue"] - @param deep {Boolean} if true, a deep copy of the object should be made - @returns {Object} copy of receiver */ - copy: Ember.required(Function), + unshiftObject: function(obj) { + this.insertAt(0, obj) ; + return obj ; + }, /** - If the object implements Ember.Freezable, then this will return a new copy - if the object is not frozen and the receiver if the object is frozen. - - Raises an exception if you try to call this method on a object that does - not support freezing. + Adds the named objects to the beginning of the array. Defers notifying + observers until all objects have been added. - You should use this method whenever you want a copy of a freezable object - since a freezable object can simply return itself without actually - consuming more memory. + var colors = ["red", "green", "blue"]; + colors.unshiftObjects(["black", "white"]); => ["black", "white", "red", "green", "blue"] + colors.unshiftObjects("yellow"); => Type Error: 'undefined' is not a function - @returns {Object} copy of receiver or receiver + @param {Ember.Enumerable} objects the objects to add + @returns {Ember.Array} receiver */ - frozenCopy: function() { - if (Ember.Freezable && Ember.Freezable.detect(this)) { - return get(this, 'isFrozen') ? this : this.copy().freeze(); - } else { - throw new Error(Ember.String.fmt("%@ does not support freezing", [this])); + unshiftObjects: function(objects) { + this.beginPropertyChanges(); + forEach(objects, function(obj) { this.unshiftObject(obj); }, this); + this.endPropertyChanges(); + return this; + }, + + // .......................................................... + // IMPLEMENT Ember.MutableEnumerable + // + + /** @private (nodoc) */ + removeObject: function(obj) { + var loc = get(this, 'length') || 0; + while(--loc >= 0) { + var curObject = this.objectAt(loc) ; + if (curObject === obj) this.removeAt(loc) ; } - } -}); + return this ; + }, + /** @private (nodoc) */ + addObject: function(obj) { + if (!this.contains(obj)) this.pushObject(obj); + return this ; + } +}); })({}); - (function(exports) { // ========================================================================== // Project: Ember Runtime -// Copyright: ©2006-2011 Strobe Inc. and contributors. -// Portions ©2008-2010 Apple Inc. All rights reserved. +// Copyright: ©2011 Strobe Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== - - - - var get = Ember.get, set = Ember.set; /** - @namespace - - The Ember.Freezable mixin implements some basic methods for marking an object - as frozen. Once an object is frozen it should be read only. No changes - may be made the internal state of the object. + @class - ## Enforcement + ## Overview + + This mixin provides properties and property observing functionality, core + features of the Ember object model. + + Properties and observers allow one object to observe changes to a + property on another object. This is one of the fundamental ways that + models, controllers and views communicate with each other in an Ember + application. + + Any object that has this mixin applied can be used in observer + operations. That includes Ember.Object and most objects you will + interact with as you write your Ember application. + + Note that you will not generally apply this mixin to classes yourself, + but you will use the features provided by this module frequently, so it + is important to understand how to use it. + + ## Using get() and set() + + Because of Ember's support for bindings and observers, you will always + access properties using the get method, and set properties using the + set method. This allows the observing objects to be notified and + computed properties to be handled properly. + + More documentation about `get` and `set` are below. + + ## Observing Property Changes + + You typically observe property changes simply by adding the `observes` + call to the end of your method declarations in classes that you write. + For example: + + Ember.Object.create({ + valueObserver: function() { + // Executes whenever the "value" property changes + }.observes('value') + }); + + Although this is the most common way to add an observer, this capability + is actually built into the Ember.Object class on top of two methods + defined in this mixin: `addObserver` and `removeObserver`. You can use + these two methods to add and remove observers yourself if you need to + do so at runtime. - To fully support freezing in your subclass, you must include this mixin and - override any method that might alter any property on the object to instead - raise an exception. You can check the state of an object by checking the - isFrozen property. + To add an observer for a property, call: - Although future versions of JavaScript may support language-level freezing - object objects, that is not the case today. Even if an object is freezable, - it is still technically possible to modify the object, even though it could - break other parts of your application that do not expect a frozen object to - change. It is, therefore, very important that you always respect the - isFrozen property on all freezable objects. + object.addObserver('propertyKey', targetObject, targetAction) - ## Example Usage + This will call the `targetAction` method on the `targetObject` to be called + whenever the value of the `propertyKey` changes. + + @extends Ember.Mixin +*/ +Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { - The example below shows a simple object that implement the Ember.Freezable - protocol. + /** @private - compatibility */ + isObserverable: true, - Contact = Ember.Object.extend(Ember.Freezable, { + /** + Retrieves the value of a property from the object. - firstName: null, + This method is usually similar to using object[keyName] or object.keyName, + however it supports both computed properties and the unknownProperty + handler. + + Because `get` unifies the syntax for accessing all these kinds + of properties, it can make many refactorings easier, such as replacing a + simple property with a computed property, or vice versa. - lastName: null, + ### Computed Properties - // swaps the names - swapNames: function() { - if (this.get('isFrozen')) throw Ember.FROZEN_ERROR; - var tmp = this.get('firstName'); - this.set('firstName', this.get('lastName')); - this.set('lastName', tmp); - return this; - } + Computed properties are methods defined with the `property` modifier + declared at the end, such as: - }); + fullName: function() { + return this.getEach('firstName', 'lastName').compact().join(' '); + }.property('firstName', 'lastName') - c = Context.create({ firstName: "John", lastName: "Doe" }); - c.swapNames(); => returns c - c.freeze(); - c.swapNames(); => EXCEPTION + When you call `get` on a computed property, the function will be + called and the return value will be returned instead of the function + itself. - ## Copying + ### Unknown Properties - Usually the Ember.Freezable protocol is implemented in cooperation with the - Ember.Copyable protocol, which defines a frozenCopy() method that will return - a frozen object, if the object implements this method as well. + Likewise, if you try to call `get` on a property whose value is + undefined, the unknownProperty() method will be called on the object. + If this method returns any value other than undefined, it will be returned + instead. This allows you to implement "virtual" properties that are + not defined upfront. - @since Ember 0.9 -*/ -Ember.Freezable = Ember.Mixin.create( -/** @scope Ember.Freezable.prototype */ { + @param {String} key The property to retrieve + @returns {Object} The property value or undefined. + */ + get: function(keyName) { + return get(this, keyName); + }, /** - Set to true when the object is frozen. Use this property to detect whether - your object is frozen or not. + To get multiple properties at once, call getProperties + with a list of strings: - @property {Boolean} + record.getProperties('firstName', 'lastName', 'zipCode'); // => { firstName: 'John', lastName: 'Doe', zipCode: '10011' } + + @param {String...} list of keys to get + @returns {Hash} */ - isFrozen: false, + getProperties: function() { + var ret = {}; + for(var i = 0; i < arguments.length; i++) { + ret[arguments[i]] = get(this, arguments[i]); + } + return ret; + }, /** - Freezes the object. Once this method has been called the object should - no longer allow any properties to be edited. + Sets the key equal to value. - @returns {Object} reciever - */ - freeze: function() { - if (get(this, 'isFrozen')) return this; - set(this, 'isFrozen', true); - return this; - } + This method is generally very similar to calling object[key] = value or + object.key = value, except that it provides support for computed + properties, the unknownProperty() method and property observers. -}); + ### Computed Properties -Ember.FROZEN_ERROR = "Frozen object cannot be modified."; + If you try to set a value on a key that has a computed property handler + defined (see the get() method for an example), then set() will call + that method, passing both the value and key instead of simply changing + the value itself. This is useful for those times when you need to + implement a property that is composed of one or more member + properties. + ### Unknown Properties + If you try to set a value on a key that is undefined in the target + object, then the unknownProperty() handler will be called instead. This + gives you an opportunity to implement complex "virtual" properties that + are not predefined on the obejct. If unknownProperty() returns + undefined, then set() will simply set the value on the object. + ### Property Observers -})({}); + In addition to changing the property, set() will also register a + property change with the object. Unless you have placed this call + inside of a beginPropertyChanges() and endPropertyChanges(), any "local" + observers (i.e. observer methods declared on the same object), will be + called immediately. Any "remote" observers (i.e. observer methods + declared on another object) will be placed in a queue and called at a + later time in a coelesced manner. + ### Chaining -(function(exports) { -// ========================================================================== -// Project: Ember Runtime -// Copyright: ©2011 Strobe Inc. and contributors. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, none = Ember.none; + In addition to property changes, set() returns the value of the object + itself so you can do chaining like this: -/** - @class + record.set('firstName', 'Charles').set('lastName', 'Jolley'); - An unordered collection of objects. + @param {String} key The property to set + @param {Object} value The value to set or null. + @returns {Ember.Observable} + */ + set: function(keyName, value) { + set(this, keyName, value); + return this; + }, - 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. + /** + To set multiple properties at once, call setProperties + with a Hash: - Starting with Ember 2.0 all Sets are now observable since there is no - added cost to providing this support. Sets also do away with the more - specialized Set Observer API in favor of the more generic Enumerable - Observer API - which works on any enumerable object including both Sets and - Arrays. + record.setProperties({ firstName: 'Charles', lastName: 'Jolley' }); - ## Creating a Set + @param {Hash} hash the hash of keys and values to set + @returns {Ember.Observable} + */ + setProperties: function(hash) { + var self = this; + Ember.changeProperties(function(){ + for(var prop in hash) { + if (hash.hasOwnProperty(prop)) set(self, prop, hash[prop]); + } + }); + return this; + }, - 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. + /** + Begins a grouping of property changes. - 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()`. + You can use this method to group property changes so that notifications + will not be sent until the changes are finished. If you plan to make a + large number of changes to an object at one time, you should call this + method at the beginning of the changes to begin deferring change + notifications. When you are done making changes, call endPropertyChanges() + to deliver the deferred change notifications and end deferring. - #js - // creates a new empty set - var foundNames = new Ember.Set(); + @returns {Ember.Observable} + */ + beginPropertyChanges: function() { + Ember.beginPropertyChanges(); + return this; + }, - // creates a set with four names in it. - var names = new Ember.Set(["Charles", "Tom", "Juan", "Alex"]); // :P + /** + Ends a grouping of property changes. - // creates a copy of the names set. - var namesCopy = new Ember.Set(names); + You can use this method to group property changes so that notifications + will not be sent until the changes are finished. If you plan to make a + large number of changes to an object at one time, you should call + beginPropertyChanges() at the beginning of the changes to defer change + notifications. When you are done making changes, call this method to + deliver the deferred change notifications and end deferring. - // same as above. - var anotherNamesCopy = names.copy(); + @returns {Ember.Observable} + */ + endPropertyChanges: function() { + Ember.endPropertyChanges(); + return this; + }, - ## Adding/Removing Objects + /** + Notify the observer system that a property is about to change. - 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. + Sometimes you need to change a value directly or indirectly without + actually calling get() or set() on it. In this case, you can use this + method and propertyDidChange() instead. Calling these two methods + together will notify all observers that the property has potentially + changed value. - Unlike arrays, objects can only exist one time in a set. If you call `add()` - on a set with the same object multiple times, the object will only be added - once. Likewise, calling `remove()` with the same object multiple times will - remove the object the first time and have no effect on future calls until - you add the object to the set again. + Note that you must always call propertyWillChange and propertyDidChange as + a pair. If you do not, it may get the property change groups out of order + and cause notifications to be delivered more often than you would like. - NOTE: You cannot add/remove null or undefined to a set. Any attempt to do so - will be ignored. + @param {String} key The property key that is about to change. + @returns {Ember.Observable} + */ + propertyWillChange: function(keyName){ + Ember.propertyWillChange(this, keyName); + return this; + }, - 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. + /** + Notify the observer system that a property has just changed. - ## Testing for an Object + Sometimes you need to change a value directly or indirectly without + actually calling get() or set() on it. In this case, you can use this + method and propertyWillChange() instead. Calling these two methods + together will notify all observers that the property has potentially + changed value. - To test for an object's presence in a set you simply call - `Ember.Set#contains()`. + Note that you must always call propertyWillChange and propertyDidChange as + a pair. If you do not, it may get the property change groups out of order + and cause notifications to be delivered more often than you would like. - ## Observing changes + @param {String} keyName The property key that has just changed. + @returns {Ember.Observable} + */ + propertyDidChange: function(keyName) { + Ember.propertyDidChange(this, keyName); + return this; + }, + + /** + Convenience method to call `propertyWillChange` and `propertyDidChange` in + succession. + + @param {String} keyName The property key to be notified about. + @returns {Ember.Observable} + */ + notifyPropertyChange: function(keyName) { + this.propertyWillChange(keyName); + this.propertyDidChange(keyName); + return this; + }, - 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. + /** + Adds an observer on a property. - 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. + This is the core method used to register an observer for a property. - ## Other Methods + Once you call this method, anytime the key's value is set, your observer + will be notified. Note that the observers are triggered anytime the + value is set, regardless of whether it has actually changed. Your + observer should be prepared to handle that. - `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. + You can also pass an optional context parameter to this method. The + context will be passed to your observer method whenever it is triggered. + Note that if you add the same target/method pair on a key multiple times + with different context parameters, your observer will only be called once + with the last context you passed. - 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. + ### Observer Methods - @extends Ember.Enumerable - @extends Ember.MutableEnumerable - @extends Ember.Copyable - @extends Ember.Freezable + Observer methods you pass should generally have the following signature if + you do not pass a "context" parameter: - @since Ember 0.9 -*/ -Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Ember.Freezable, - /** @scope Ember.Set.prototype */ { + fooDidChange: function(sender, key, value, rev); - // .......................................................... - // IMPLEMENT ENUMERABLE APIS - // + The sender is the object that changed. The key is the property that + changes. The value property is currently reserved and unused. The rev + is the last property revision of the object when it changed, which you can + use to detect if the key value has really changed or not. - /** - This property will change as the number of objects in the set changes. + If you pass a "context" parameter, the context will be passed before the + revision like so: - @property Number - @default 0 - */ - length: 0, + fooDidChange: function(sender, key, value, context, rev); - /** - Clears the set. This is useful if you want to reuse an existing set - without having to recreate it. + Usually you will not need the value, context or revision parameters at + the end. In this case, it is common to write observer methods that take + only a sender and key value as parameters or, if you aren't interested in + any of these values, to write an observer that has no parameters at all. - @returns {Ember.Set} + @param {String} key The key to observer + @param {Object} target The target object to invoke + @param {String|Function} method The method to invoke. + @returns {Ember.Object} self */ - clear: function() { - if (this.isFrozen) { throw new Error(Ember.FROZEN_ERROR); } - var len = get(this, 'length'); - this.enumerableContentWillChange(len, 0); - set(this, 'length', 0); - this.enumerableContentDidChange(len, 0); - return this; + addObserver: function(key, target, method) { + Ember.addObserver(this, key, target, method); }, /** - Returns true if the passed object is also an enumerable that contains the - same objects as the receiver. - - @param {Ember.Set} obj the other object - @returns {Boolean} - */ - isEqual: function(obj) { - // fail fast - if (!Ember.Enumerable.detect(obj)) return false; - - var loc = get(this, 'length'); - if (get(obj, 'length') !== loc) return false; - - while(--loc >= 0) { - if (!obj.contains(this[loc])) return false; - } + Remove an observer you have previously registered on this object. Pass + the same key, target, and method you passed to addObserver() and your + target will no longer receive notifications. - return true; + @param {String} key The key to observer + @param {Object} target The target object to invoke + @param {String|Function} method The method to invoke. + @returns {Ember.Observable} reciever + */ + removeObserver: function(key, target, method) { + Ember.removeObserver(this, key, target, method); }, /** - Adds an object to the set. Only non-null objects can be added to a set - and those can only be added once. If the object is already in the set or - the passed value is null this method will have no effect. - - This is an alias for `Ember.MutableEnumerable.addObject()`. + Returns true if the object currently has observers registered for a + particular key. You can use this method to potentially defer performing + an expensive action until someone begins observing a particular property + on the object. - @function - @param {Object} obj The object to add - @returns {Ember.Set} receiver + @param {String} key Key to check + @returns {Boolean} */ - add: Ember.alias('addObject'), + hasObserverFor: function(key) { + return Ember.hasListeners(this, key+':change'); + }, /** - 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()`. + This method will be called when a client attempts to get the value of a + property that has not been defined in one of the typical ways. Override + this method to create "virtual" properties. + + @param {String} key The name of the unknown property that was requested. + @returns {Object} The property value or undefined. Default is undefined. + */ + unknownProperty: function(key) { + return undefined; + }, - @function - @param {Object} obj The object to remove - @returns {Ember.Set} receiver + /** + This method will be called when a client attempts to set the value of a + property that has not been defined in one of the typical ways. Override + this method to create "virtual" properties. + + @param {String} key The name of the unknown property to be set. + @param {Object} value The value the unknown property is to be set to. */ - remove: Ember.alias('removeObject'), + setUnknownProperty: function(key, value) { + this[key] = value; + }, /** - Removes an arbitrary object from the set and returns it. + This is like `get`, but allows you to pass in a dot-separated property + path. + + person.getPath('address.zip'); // return the zip + person.getPath('children.firstObject.age'); // return the first kid's age - @returns {Object} An object from the set or null + This reads much better than chained `get` calls. + + @param {String} path The property path to retrieve + @returns {Object} The property value or undefined. */ - 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; + getPath: function(path) { + return Ember.getPath(this, path); }, /** - This is an alias for `Ember.MutableEnumerable.addObject()`. + This is like `set`, but allows you to specify the property you want to + set as a dot-separated property path. + + person.setPath('address.zip', 10011); // set the zip to 10011 + person.setPath('children.firstObject.age', 6); // set the first kid's age to 6 - @function + This is not as commonly used as `getPath`, but it can be useful. + + @param {String} path The path to the property that will be set + @param {Object} value The value to set or null. + @returns {Ember.Observable} */ - push: Ember.alias('addObject'), + setPath: function(path, value) { + Ember.setPath(this, path, value); + return this; + }, /** - This is an alias for `Ember.Set.pop()`. - @function + Retrieves the value of a property, or a default value in the case that the property + returns undefined. + + person.getWithDefault('lastName', 'Doe'); + + @param {String} keyName The name of the property to retrieve + @param {Object} defaultValue The value to return if the property value is undefined + @returns {Object} The property value or the defaultValue. */ - shift: Ember.alias('pop'), + getWithDefault: function(keyName, defaultValue) { + return Ember.getWithDefault(this, keyName, defaultValue); + }, /** - This is an alias of `Ember.Set.push()` - @function + Set the value of a property to the current value plus some amount. + + person.incrementProperty('age'); + team.incrementProperty('score', 2); + + @param {String} keyName The name of the property to increment + @param {Object} increment The amount to increment by. Defaults to 1 + @returns {Object} The new property value */ - unshift: Ember.alias('push'), - + incrementProperty: function(keyName, increment) { + if (!increment) { increment = 1; } + set(this, keyName, (get(this, keyName) || 0)+increment); + return get(this, keyName); + }, + /** - This is an alias of `Ember.MutableEnumerable.addObjects()` - @function + Set the value of a property to the current value minus some amount. + + player.decrementProperty('lives'); + orc.decrementProperty('health', 5); + + @param {String} keyName The name of the property to decrement + @param {Object} increment The amount to decrement by. Defaults to 1 + @returns {Object} The new property value */ - addEach: Ember.alias('addObjects'), + decrementProperty: function(keyName, increment) { + if (!increment) { increment = 1; } + set(this, keyName, (get(this, keyName) || 0)-increment); + return get(this, keyName); + }, /** - This is an alias of `Ember.MutableEnumerable.removeObjects()` - @function + Set the value of a boolean property to the opposite of it's + current value. + + starship.toggleProperty('warpDriveEnaged'); + + @param {String} keyName The name of the property to toggle + @returns {Object} The new property value */ - removeEach: Ember.alias('removeObjects'), + toggleProperty: function(keyName) { + set(this, keyName, !get(this, keyName)); + return get(this, keyName); + }, - // .......................................................... - // PRIVATE ENUMERABLE SUPPORT - // + /** @private - intended for debugging purposes */ + observersForKey: function(keyName) { + return Ember.observersFor(this, keyName); + } - /** @private */ - init: function(items) { - this._super(); - if (items) this.addObjects(items); - }, +}); - /** @private (nodoc) - implement Ember.Enumerable */ - nextObject: function(idx) { - return this[idx]; - }, - /** @private - more optimized version */ - firstObject: Ember.computed(function() { - return this.length > 0 ? this[0] : undefined; - }).property('[]').cacheable(), - /** @private - more optimized version */ - lastObject: Ember.computed(function() { - return this.length > 0 ? this[this.length-1] : undefined; - }).property('[]').cacheable(), - /** @private (nodoc) - implements Ember.MutableEnumerable */ - addObject: function(obj) { - if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); - if (none(obj)) return this; // nothing to do +})({}); - var guid = guidFor(obj), - idx = this[guid], - len = get(this, 'length'), - added ; +(function(exports) { +var get = Ember.get, set = Ember.set, getPath = Ember.getPath; - if (idx>=0 && idx=0 && idx=0; + fire: function(name) { + Ember.sendEvent.apply(null, [this, name].concat([].slice.call(arguments, 1))); }, - /** @private (nodoc) */ - 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; + off: function(name, target, method) { + Ember.removeListener(this, name, target, method); + } +}); + +})({}); + +(function(exports) { +// ========================================================================== +// Project: Ember Runtime +// Copyright: ©2011 Strobe Inc. and contributors. +// License: Licensed under MIT license (see license.js) +// ========================================================================== + +})({}); + +(function(exports) { +// ========================================================================== +// Project: Ember Runtime +// Copyright: ©2011 Strobe Inc. and contributors. +// License: Licensed under MIT license (see license.js) +// ========================================================================== + + + +// NOTE: this object should never be included directly. Instead use Ember. +// Ember.Object. We only define this separately so that Ember.Set can depend on it + + + +var rewatch = Ember.rewatch; +var classToString = Ember.Mixin.prototype.toString; +var set = Ember.set, get = Ember.get; +var o_create = Ember.platform.create, + o_defineProperty = Ember.platform.defineProperty, + meta = Ember.meta; + +/** @private */ +function makeCtor() { + + // Note: avoid accessing any properties on the object since it makes the + // method a lot faster. This is glue code so we want it to be as fast as + // possible. + + var wasApplied = false, initMixins, init = false, hasChains = false; + + var Class = function() { + if (!wasApplied) { Class.proto(); } // prepare prototype... + if (initMixins) { + this.reopen.apply(this, initMixins); + initMixins = null; + rewatch(this); // ålways rewatch just in case + this.init.apply(this, arguments); + } else { + if (hasChains) { + rewatch(this); + } else { + Ember.GUID_DESC.value = undefined; + o_defineProperty(this, Ember.GUID_KEY, Ember.GUID_DESC); + } + if (init===false) { init = this.init; } // cache for later instantiations + Ember.GUID_DESC.value = undefined; + o_defineProperty(this, '_super', Ember.GUID_DESC); + init.apply(this, arguments); + } + }; + + Class.toString = classToString; + Class.willReopen = function() { + if (wasApplied) { + Class.PrototypeMixin = Ember.Mixin.create(Class.PrototypeMixin); } - return ret; - }, - /** @private */ - toString: function() { - var len = this.length, idx, array = []; - for(idx = 0; idx < len; idx++) { - array[idx] = this[idx]; + wasApplied = false; + }; + Class._initMixins = function(args) { initMixins = args; }; + + Class.proto = function() { + var superclass = Class.superclass; + if (superclass) { superclass.proto(); } + + if (!wasApplied) { + wasApplied = true; + Class.PrototypeMixin.applyPartial(Class.prototype); + hasChains = !!meta(Class.prototype, false).chains; // avoid rewatch } - return "Ember.Set<%@>".fmt(array.join(',')); - }, - // .......................................................... - // DEPRECATED - // + return this.prototype; + }; - /** @deprecated + return Class; - This property is often used to determine that a given object is a set. - Instead you should use instanceof: +} - #js: - // SproutCore 1.x: - isSet = myobject && myobject.isSet; +var CoreObject = makeCtor(); - // Ember: - isSet = myobject instanceof Ember.Set +CoreObject.PrototypeMixin = Ember.Mixin.create( +/** @scope Ember.CoreObject */ { - @type Boolean - @default true - */ - isSet: true + reopen: function() { + Ember.Mixin._apply(this, arguments, true); + return this; + }, -}); + isInstance: true, -// Support the older API -var o_create = Ember.Set.create; -Ember.Set.create = function(items) { - if (items && Ember.Enumerable.detect(items)) { - ember_deprecate('Passing an enumerable to Ember.Set.create() is deprecated and will be removed in a future version of Ember. Use new Ember.Set(items) instead.'); - return new Ember.Set(items); - } else { - return o_create.apply(this, arguments); - } -}; + init: function() {}, + isDestroyed: false, + /** + Destroys an object by setting the isDestroyed flag and removing its + metadata, which effectively destroys observers and bindings. -})({}); + If you try to set a property on a destroyed object, an exception will be + raised. + Note that destruction is scheduled for the end of the run loop and does not + happen immediately. -(function(exports) { -// ========================================================================== -// Project: Ember Runtime -// Copyright: ©2011 Strobe Inc. and contributors. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -Ember.CoreObject.subclasses = new Ember.Set(); + @returns {Ember.Object} receiver + */ + destroy: function() { + set(this, 'isDestroyed', true); + Ember.run.schedule('destroy', this, this._scheduledDestroy); + return this; + }, -/** - @class - @extends Ember.CoreObject - @extends Ember.Observable -*/ -Ember.Object = Ember.CoreObject.extend(Ember.Observable); + /** + Invoked by the run loop to actually destroy the object. This is + scheduled for execution by the `destroy` method. + @private + */ + _scheduledDestroy: function() { + Ember.destroy(this); + }, + bind: function(to, from) { + if (!(from instanceof Ember.Binding)) { from = Ember.Binding.from(from); } + from.to(to).connect(this); + return from; + }, + toString: function() { + return '<'+this.constructor.toString()+':'+Ember.guidFor(this)+'>'; + } +}); -})({}); +CoreObject.__super__ = null; +var ClassMixin = Ember.Mixin.create({ -(function(exports) { -// ========================================================================== -// Project: Ember Runtime -// Copyright: ©2011 Strobe Inc. and contributors. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -var get = Ember.get, set = Ember.set; + ClassMixin: Ember.required(), -/** - @class + PrototypeMixin: Ember.required(), - An ArrayProxy wraps any other object that implements Ember.Array and/or - Ember.MutableArray, forwarding all requests. ArrayProxy isn't useful by itself - but you can extend it to do specialized things like transforming values, - etc. + isClass: true, - @extends Ember.Object - @extends Ember.Array - @extends Ember.MutableArray -*/ -Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray, -/** @scope Ember.ArrayProxy.prototype */ { + isMethod: false, - /** - The content array. Must be an object that implements Ember.Array and or - Ember.MutableArray. + extend: function() { + var Class = makeCtor(), proto; + Class.ClassMixin = Ember.Mixin.create(this.ClassMixin); + Class.PrototypeMixin = Ember.Mixin.create(this.PrototypeMixin); - @property {Ember.Array} - */ - content: null, + Class.ClassMixin.ownerConstructor = Class; + Class.PrototypeMixin.ownerConstructor = Class; - /** - 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. + var PrototypeMixin = Class.PrototypeMixin; + PrototypeMixin.reopen.apply(PrototypeMixin, arguments); - This method will only be called if content is non-null. + Class.superclass = this; + Class.__super__ = this.prototype; - @param {Number} idx - The index to retreive. + proto = Class.prototype = o_create(this.prototype); + proto.constructor = Class; + Ember.generateGuid(proto, 'ember'); + meta(proto).proto = proto; // this will disable observers on prototype + Ember.rewatch(proto); // setup watch chains if needed. - @returns {Object} the value or undefined if none found - */ - objectAtContent: function(idx) { - return get(this, 'content').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. + Class.subclasses = Ember.Set ? new Ember.Set() : null; + if (this.subclasses) { this.subclasses.add(Class); } - This method will only be called if content is non-null. + Class.ClassMixin.apply(Class); + return Class; + }, - @param {Number} idx - The starting index + create: function() { + var C = this; + if (arguments.length>0) { this._initMixins(arguments); } + return new C(); + }, - @param {Number} amt - The number of items to remove from the content. + reopen: function() { + this.willReopen(); + var PrototypeMixin = this.PrototypeMixin; + PrototypeMixin.reopen.apply(PrototypeMixin, arguments); + return this; + }, - @param {Array} objects - Optional array of objects to insert or null if no objects. + reopenClass: function() { + var ClassMixin = this.ClassMixin; + ClassMixin.reopen.apply(ClassMixin, arguments); + Ember.Mixin._apply(this, arguments, false); + return this; + }, - @returns {void} - */ - replaceContent: function(idx, amt, objects) { - get(this, 'content').replace(idx, amt, objects); + detect: function(obj) { + if ('function' !== typeof obj) { return false; } + while(obj) { + if (obj===this) { return true; } + obj = obj.superclass; + } + return false; }, - contentWillChange: Ember.beforeObserver(function() { - var content = get(this, 'content'), - len = content ? get(content, 'length') : 0; - this.arrayWillChange(content, 0, len, undefined); - if (content) content.removeArrayObserver(this); - }, 'content'), + detectInstance: function(obj) { + return obj instanceof this; + }, /** - Invoked when the content property changes. Notifies observers that the - entire array content has changed. - */ - contentDidChange: Ember.observer(function() { - var content = get(this, 'content'), - len = content ? get(content, 'length') : 0; - if (content) content.addArrayObserver(this); - this.arrayDidChange(content, 0, undefined, len); - }, 'content'), + In some cases, you may want to annotate computed properties with additional + metadata about how they function or what values they operate on. For example, + computed property functions may close over variables that are then no longer + available for introspection. - /** @private (nodoc) */ - objectAt: function(idx) { - return get(this, 'content') && this.objectAtContent(idx); - }, + You can pass a hash of these values to a computed property like this: - /** @private (nodoc) */ - length: Ember.computed(function() { - var content = get(this, 'content'); - return content ? get(content, 'length') : 0; - }).property('content.length').cacheable(), + person: function() { + var personId = this.get('personId'); + return App.Person.create({ id: personId }); + }.property().meta({ type: App.Person }) - /** @private (nodoc) */ - replace: function(idx, amt, objects) { - if (get(this, 'content')) this.replaceContent(idx, amt, objects); - return this; - }, + Once you've done this, you can retrieve the values saved to the computed + property from your class like this: + + MyClass.metaForProperty('person'); + + This will return the original hash that was passed to `meta()`. + */ + metaForProperty: function(key) { + var desc = meta(this.proto(), false).descs[key]; - /** @private (nodoc) */ - arrayWillChange: function(item, idx, removedCnt, addedCnt) { - this.arrayContentWillChange(idx, removedCnt, addedCnt); + ember_assert("metaForProperty() could not find a computed property with key '"+key+"'.", !!desc && desc instanceof Ember.ComputedProperty); + return desc._meta || {}; }, - /** @private (nodoc) */ - arrayDidChange: function(item, idx, removedCnt, addedCnt) { - this.arrayContentDidChange(idx, removedCnt, addedCnt); - }, + /** + Iterate over each computed property for the class, passing its name + and any associated metadata (see `metaForProperty`) to the callback. + */ + eachComputedProperty: function(callback, binding) { + var proto = this.proto(), + descs = meta(proto).descs, + empty = {}, + property; - init: function() { - this._super(); - this.contentDidChange(); + for (var name in descs) { + property = descs[name]; + + if (property instanceof Ember.ComputedProperty) { + callback.call(binding || this, name, property._meta || empty); + } + } } }); +CoreObject.ClassMixin = ClassMixin; +ClassMixin.apply(CoreObject); + +/** + @class +*/ +Ember.CoreObject = CoreObject; + })({}); - (function(exports) { // ========================================================================== -// Project: Ember Metal +// Project: Ember Runtime // Copyright: ©2011 Strobe Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== +var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, none = Ember.none; + /** @class - Ember.ArrayController provides a way for you to publish an array of objects for - Ember.CollectionView or other controllers to work with. To work with an - ArrayController, set the content property to the array you want the controller - to manage. Then work directly with the controller object as if it were the - array itself. + An unordered collection of objects. - For example, imagine you wanted to display a list of items fetched via an XHR - request. Create an Ember.ArrayController and set its `content` property: + 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. - MyApp.listController = Ember.ArrayController.create(); + Starting with Ember 2.0 all Sets are now observable since there is no + added cost to providing this support. Sets also do away with the more + specialized Set Observer API in favor of the more generic Enumerable + Observer API - which works on any enumerable object including both Sets and + Arrays. - $.get('people.json', function(data) { - MyApp.listController.set('content', data); - }); + ## Creating a Set - Then, create a view that binds to your new controller: + 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. - {{#each MyApp.listController}} - {{firstName}} {{lastName}} - {{/each}} + 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()`. - The advantage of using an array controller is that you only have to set up - your view bindings once; to change what's displayed, simply swap out the - `content` property on the controller. + #js + // creates a new empty set + var foundNames = new Ember.Set(); - @extends Ember.ArrayProxy -*/ + // creates a set with four names in it. + var names = new Ember.Set(["Charles", "Tom", "Juan", "Alex"]); // :P -Ember.ArrayController = Ember.ArrayProxy.extend(); + // creates a copy of the names set. + var namesCopy = new Ember.Set(names); -})({}); + // same as above. + var anotherNamesCopy = names.copy(); + ## Adding/Removing Objects -(function(exports) { -})({}); + You generally add or remove objects from a set using `add()` or + `remove()`. You can add any type of object including primitives such as + numbers, strings, and booleans. + Unlike arrays, objects can only exist one time in a set. If you call `add()` + on a set with the same object multiple times, the object will only be added + once. Likewise, calling `remove()` with the same object multiple times will + remove the object the first time and have no effect on future calls until + you add the object to the set again. -(function(exports) { -// ========================================================================== -// Project: Ember Runtime -// Copyright: ©2006-2011 Strobe Inc. and contributors. -// Portions ©2008-2011 Apple Inc. All rights reserved. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -var fmt = Ember.String.fmt, - w = Ember.String.w, - loc = Ember.String.loc, - camelize = Ember.String.camelize, - decamelize = Ember.String.decamelize, - dasherize = Ember.String.dasherize, - underscore = Ember.String.underscore; + NOTE: You cannot add/remove null or undefined to a set. Any attempt to do so + will be ignored. -if (Ember.EXTEND_PROTOTYPES) { + 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. - /** - @see Ember.String.fmt - */ - String.prototype.fmt = function() { - return fmt(this, arguments); - }; + ## Testing for an Object - /** - @see Ember.String.w - */ - String.prototype.w = function() { - return w(this); - }; + To test for an object's presence in a set you simply call + `Ember.Set#contains()`. - /** - @see Ember.String.loc - */ - String.prototype.loc = function() { - return loc(this, arguments); - }; + ## 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. + + @extends Ember.Enumerable + @extends Ember.MutableEnumerable + @extends Ember.Copyable + @extends Ember.Freezable + + @since Ember 0.9 +*/ +Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Ember.Freezable, + /** @scope Ember.Set.prototype */ { + + // .......................................................... + // IMPLEMENT ENUMERABLE APIS + // /** - @see Ember.String.camelize + This property will change as the number of objects in the set changes. + + @property Number + @default 0 */ - String.prototype.camelize = function() { - return camelize(this); - }; + length: 0, /** - @see Ember.String.decamelize + Clears the set. This is useful if you want to reuse an existing set + without having to recreate it. + + var colors = new Ember.Set(["red", "green", "blue"]); + colors.length; => 3 + colors.clear(); + colors.length; => 0 + + @returns {Ember.Set} An empty Set */ - String.prototype.decamelize = function() { - return decamelize(this); - }; + clear: function() { + if (this.isFrozen) { throw new Error(Ember.FROZEN_ERROR); } + var len = get(this, 'length'); + this.enumerableContentWillChange(len, 0); + set(this, 'length', 0); + this.enumerableContentDidChange(len, 0); + return this; + }, /** - @see Ember.String.dasherize + Returns true if the passed object is also an enumerable that contains the + same objects as the receiver. + + var colors = ["red", "green", "blue"], + same_colors = new Ember.Set(colors); + same_colors.isEqual(colors); => true + same_colors.isEqual(["purple", "brown"]); => false + + @param {Ember.Set} obj the other object. + @returns {Boolean} */ - String.prototype.dasherize = function() { - return dasherize(this); - }; + isEqual: function(obj) { + // fail fast + if (!Ember.Enumerable.detect(obj)) return false; + + var loc = get(this, 'length'); + if (get(obj, 'length') !== loc) return false; + + while(--loc >= 0) { + if (!obj.contains(this[loc])) return false; + } + + return true; + }, /** - @see Ember.String.underscore - */ - String.prototype.underscore = function() { - return underscore(this); - }; + Adds an object to the set. Only non-null objects can be added to a set + and those can only be added once. If the object is already in the set or + the passed value is null this method will have no effect. -} + This is an alias for `Ember.MutableEnumerable.addObject()`. + 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"] -})({}); + @function + @param {Object} obj The object to add. + @returns {Ember.Set} The set itself. + */ + add: Ember.alias('addObject'), + /** + Removes the object from the set if it is found. If you pass a null value + or an object that is already not in the set, this method will have no + effect. This is an alias for `Ember.MutableEnumerable.removeObject()`. -(function(exports) { -// ========================================================================== -// Project: Ember Runtime -// Copyright: ©2006-2011 Strobe Inc. and contributors. -// Portions ©2008-2011 Apple Inc. All rights reserved. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -var a_slice = Array.prototype.slice; + var colors = new Ember.Set(["red", "green", "blue"]); + colors.remove("red"); => ["blue", "green"] + colors.remove("purple"); => ["blue", "green"] + colors.remove(null); => ["blue", "green"] + + @function + @param {Object} obj The object to remove + @returns {Ember.Set} The set itself. + */ + remove: Ember.alias('removeObject'), + + /** + Removes the last element from the set and returns it, or null if it's empty. + + var colors = new Ember.Set(["green", "blue"]); + colors.pop(); => "blue" + colors.pop(); => "green" + colors.pop(); => null + + @returns {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; + }, -if (Ember.EXTEND_PROTOTYPES) { + /** + Inserts the given object on to the end of the set. It returns + the set itself. - Function.prototype.property = function() { - var ret = Ember.computed(this); - return ret.property.apply(ret, arguments); - }; + This is an alias for `Ember.MutableEnumerable.addObject()`. - Function.prototype.observes = function() { - this.__ember_observes__ = a_slice.call(arguments); - return this; - }; + var colors = new Ember.Set(); + colors.push("red"); => ["red"] + colors.push("green"); => ["red", "green"] + colors.push("blue"); => ["red", "green", "blue"] - Function.prototype.observesBefore = function() { - this.__ember_observesBefore__ = a_slice.call(arguments); - return this; - }; + @function + @returns {Ember.Set} The set itself. + */ + push: Ember.alias('addObject'), -} + /** + Removes the last element from the set and returns it, or null if it's empty. + This is an alias for `Ember.Set.pop()`. -})({}); + var colors = new Ember.Set(["green", "blue"]); + colors.shift(); => "blue" + colors.shift(); => "green" + colors.shift(); => null + @function + @returns {Object} The removed object from the set or null. + */ + shift: Ember.alias('pop'), -(function(exports) { -// ========================================================================== -// Project: Ember Runtime -// Copyright: ©2006-2011 Strobe Inc. and contributors. -// Portions ©2008-2011 Apple Inc. All rights reserved. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -var IS_BINDING = Ember.IS_BINDING = /^.+Binding$/; + /** + Inserts the given object on to the end of the set. It returns + the set itself. -Ember._mixinBindings = function(obj, key, value, m) { - if (IS_BINDING.test(key)) { - if (!(value instanceof Ember.Binding)) { - value = new Ember.Binding(key.slice(0,-7), value); // make binding - } else { - value.to(key.slice(0, -7)); - } - value.connect(obj); + This is an alias of `Ember.Set.push()` - // keep a set of bindings in the meta so that when we rewatch we can - // resync them... - var bindings = m.bindings; - if (!bindings) { - bindings = m.bindings = { __emberproto__: obj }; - } else if (bindings.__emberproto__ !== obj) { - bindings = m.bindings = Ember.create(m.bindings); - bindings.__emberproto__ = obj; - } + var colors = new Ember.Set(); + colors.unshift("red"); => ["red"] + colors.unshift("green"); => ["red", "green"] + colors.unshift("blue"); => ["red", "green", "blue"] - bindings[key] = true; - } + @function + @returns {Ember.Set} The set itself. + */ + unshift: Ember.alias('push'), - return value; -}; + /** + Adds each object in the passed enumerable to the set. -})({}); + This is an alias of `Ember.MutableEnumerable.addObjects()` + var colors = new Ember.Set(); + colors.addEach(["red", "green", "blue"]); => ["red", "green", "blue"] -(function(exports) { -// ========================================================================== -// Project: Ember Runtime -// Copyright: ©2011 Strobe Inc. and contributors. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -})({}); + @function + @param {Ember.Enumerable} objects the objects to add. + @returns {Ember.Set} The set itself. + */ + addEach: Ember.alias('addObjects'), + /** + Removes each object in the passed enumerable to the set. -(function(exports) { -/** - * @license - * ========================================================================== - * Ember - * Copyright ©2006-2011, Strobe Inc. and contributors. - * Portions copyright ©2008-2011 Apple Inc. All rights reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - * - * For more information about Ember, visit http://www.emberjs.com - * - * ========================================================================== - */ + This is an alias of `Ember.MutableEnumerable.removeObjects()` -})({}); + var colors = new Ember.Set(["red", "green", "blue"]); + colors.removeEach(["red", "blue"]); => ["green"] + @function + @param {Ember.Enumerable} objects the objects to remove. + @returns {Ember.Set} The set itself. + */ + removeEach: Ember.alias('removeObjects'), -(function(exports) { -// ========================================================================== -// Project: Ember Runtime -// Copyright: ©2006-2011 Strobe Inc. and contributors. -// Portions ©2008-2011 Apple Inc. All rights reserved. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -/** - @namespace + // .......................................................... + // PRIVATE ENUMERABLE SUPPORT + // - Implements some standard methods for comparing objects. Add this mixin to - any class you create that can compare its instances. + /** @private */ + init: function(items) { + this._super(); + if (items) this.addObjects(items); + }, - You should implement the compare() method. + /** @private (nodoc) - implement Ember.Enumerable */ + nextObject: function(idx) { + return this[idx]; + }, - @since Ember 0.9 -*/ -Ember.Comparable = Ember.Mixin.create( /** @scope Ember.Comparable.prototype */{ + /** @private - more optimized version */ + firstObject: Ember.computed(function() { + return this.length > 0 ? this[0] : undefined; + }).property('[]').cacheable(), - /** - walk like a duck. Indicates that the object can be compared. + /** @private - more optimized version */ + lastObject: Ember.computed(function() { + return this.length > 0 ? this[this.length-1] : undefined; + }).property('[]').cacheable(), - @type Boolean - @default true - @constant - */ - isComparable: true, + /** @private (nodoc) - implements Ember.MutableEnumerable */ + addObject: function(obj) { + if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); + if (none(obj)) return this; // nothing to do - /** - Override to return the result of the comparison of the two parameters. The - compare method should return: + var guid = guidFor(obj), + idx = this[guid], + len = get(this, 'length'), + added ; - - -1 if a < b - - 0 if a == b - - 1 if a > b + if (idx>=0 && idx=0 && idx=0; + }, - if (typeof target.send === 'function') { - ret = target.send(action, this); - } else { - if (typeof action === 'string') { - action = target[action]; - } - ret = action.call(target, this); - } - if (ret !== false) ret = true; + /** @private (nodoc) */ + 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; + }, - return ret; - } else { - return false; + /** @private */ + toString: function() { + var len = this.length, idx, array = []; + for(idx = 0; idx < len; idx++) { + array[idx] = this[idx]; } - } -}); + return "Ember.Set<%@>".fmt(array.join(',')); + }, -})({}); + // .......................................................... + // DEPRECATED + // + /** @deprecated -(function(exports) { -var get = Ember.get, set = Ember.set; + This property is often used to determine that a given object is a set. + Instead you should use instanceof: -function xform(target, method, params) { - var args = [].slice.call(params, 2); - method.apply(target, args); -} + #js: + // SproutCore 1.x: + isSet = myobject && myobject.isSet; -Ember.Evented = Ember.Mixin.create({ - on: function(name, target, method) { - if (!method) { - method = target; - target = null; - } + // Ember: + isSet = myobject instanceof Ember.Set - Ember.addListener(this, name, target, method, xform); - }, + @type Boolean + @default true + */ + isSet: true - fire: function(name) { - Ember.sendEvent.apply(null, [this, name].concat([].slice.call(arguments, 1))); - }, +}); - off: function(name, target, method) { - Ember.removeListener(this, name, target, method); +// Support the older API +var o_create = Ember.Set.create; +Ember.Set.create = function(items) { + if (items && Ember.Enumerable.detect(items)) { + ember_deprecate('Passing an enumerable to Ember.Set.create() is deprecated and will be removed in a future version of Ember. Use new Ember.Set(items) instead.'); + return new Ember.Set(items); + } else { + return o_create.apply(this, arguments); } -}); +}; })({}); - (function(exports) { // ========================================================================== // Project: Ember Runtime // Copyright: ©2011 Strobe Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== -})({}); +Ember.CoreObject.subclasses = new Ember.Set(); + +/** + @class + @extends Ember.CoreObject + @extends Ember.Observable +*/ +Ember.Object = Ember.CoreObject.extend(Ember.Observable); + + +})({}); + (function(exports) { // ========================================================================== // Project: Ember Runtime @@ -10267,7 +10467,6 @@ Ember.Namespace.PROCESSED = false; })({}); - (function(exports) { // ========================================================================== // Project: Ember Runtime @@ -10302,6 +10501,158 @@ Ember.Application = Ember.Namespace.extend(); })({}); +(function(exports) { +// ========================================================================== +// Project: Ember Runtime +// Copyright: ©2011 Strobe Inc. and contributors. +// License: Licensed under MIT license (see license.js) +// ========================================================================== +var get = Ember.get, set = Ember.set; + +/** + @class + + An ArrayProxy wraps any other object that implements Ember.Array and/or + Ember.MutableArray, forwarding all requests. This makes it very useful for + a number of binding use cases or other cases where being able to swap + out the underlying array is useful. + + A simple example of usage: + + var pets = ['dog', 'cat', 'fish']; + var arrayProxy = Ember.ArrayProxy.create({ content: Ember.A(pets) }); + ap.get('firstObject'); // => 'dog' + ap.set('content', ['amoeba', 'paramecium']); + ap.get('firstObject'); // => 'amoeba' + + This class can also be useful as a layer to transform the contents of + an array, as they are accessed. This can be done by overriding + `objectAtContent`: + + var pets = ['dog', 'cat', 'fish']; + var ap = Ember.ArrayProxy.create({ + content: Ember.A(pets), + objectAtContent: function(idx) { + return this.get('content').objectAt(idx).toUpperCase(); + } + }); + ap.get('firstObject'); // => 'DOG' + + + @extends Ember.Object + @extends Ember.Array + @extends Ember.MutableArray +*/ +Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray, +/** @scope Ember.ArrayProxy.prototype */ { + + /** + The content array. Must be an object that implements Ember.Array and/or + Ember.MutableArray. + + @property {Ember.Array} + */ + content: null, + + /** + 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. + + @param {Number} idx + The index to retreive. + + @returns {Object} the value or undefined if none found + */ + objectAtContent: function(idx) { + return get(this, 'content').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. + + @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. + + @returns {void} + */ + replaceContent: function(idx, amt, objects) { + get(this, 'content').replace(idx, amt, objects); + }, + + /** + Invoked when the content property is about to change. Notifies observers that the + entire array content will change. + */ + contentWillChange: Ember.beforeObserver(function() { + var content = get(this, 'content'), + len = content ? get(content, 'length') : 0; + this.arrayWillChange(content, 0, len, undefined); + if (content) content.removeArrayObserver(this); + }, 'content'), + + /** + Invoked when the content property changes. Notifies observers that the + entire array content has changed. + */ + contentDidChange: Ember.observer(function() { + var content = get(this, 'content'), + len = content ? get(content, 'length') : 0; + if (content) content.addArrayObserver(this); + this.arrayDidChange(content, 0, undefined, len); + }, 'content'), + + /** @private (nodoc) */ + objectAt: function(idx) { + return get(this, 'content') && this.objectAtContent(idx); + }, + + /** @private (nodoc) */ + length: Ember.computed(function() { + var content = get(this, 'content'); + return content ? get(content, 'length') : 0; + }).property('content.length').cacheable(), + + /** @private (nodoc) */ + replace: function(idx, amt, objects) { + if (get(this, 'content')) this.replaceContent(idx, amt, objects); + return this; + }, + + /** @private (nodoc) */ + arrayWillChange: function(item, idx, removedCnt, addedCnt) { + this.arrayContentWillChange(idx, removedCnt, addedCnt); + }, + + /** @private (nodoc) */ + arrayDidChange: function(item, idx, removedCnt, addedCnt) { + this.arrayContentDidChange(idx, removedCnt, addedCnt); + }, + + /** @private (nodoc) */ + init: function() { + this._super(); + this.contentDidChange(); + } + +}); + + + + +})({}); (function(exports) { // ========================================================================== @@ -10335,6 +10686,7 @@ var EachArray = Ember.Object.extend(Ember.Array, { var IS_OBSERVER = /^.+:(before|change)$/; +/** @private */ function addObserverForContentKey(content, keyName, proxy, idx, loc) { var objects = proxy._objects, guid; if (!objects) objects = proxy._objects = {}; @@ -10354,6 +10706,7 @@ function addObserverForContentKey(content, keyName, proxy, idx, loc) { } } +/** @private */ function removeObserverForContentKey(content, keyName, proxy, idx, loc) { var objects = proxy._objects; if (!objects) objects = proxy._objects = {}; @@ -10504,7 +10857,6 @@ Ember.EachProxy = Ember.Object.extend({ })({}); - (function(exports) { // ========================================================================== // Project: Ember Runtime @@ -10651,7 +11003,6 @@ if (Ember.EXTEND_PROTOTYPES) Ember.NativeArray.activate(); })({}); - (function(exports) { /** JavaScript (before ES6) does not have a Map implementation. Objects, @@ -10673,6 +11024,7 @@ if (Ember.EXTEND_PROTOTYPES) Ember.NativeArray.activate(); `Ember.Map.create()` for symmetry with other Ember classes. */ +/** @private */ var guidFor = Ember.guidFor; var indexOf = Ember.ArrayUtils.indexOf; @@ -10752,6 +11104,7 @@ OrderedSet.prototype = { we delete its entry in `keys` and `values`. */ +/** @private */ var Map = Ember.Map = function() { this.keys = Ember.OrderedSet.create(); this.values = {}; @@ -10825,41 +11178,93 @@ Map.prototype = { var values = this.values, guid = guidFor(key); - return values.hasOwnProperty(guid); - }, + return values.hasOwnProperty(guid); + }, + + /** + Iterate over all the keys and values. Calls the function once + for each key, passing in the key and value, in that order. + + The keys are guaranteed to be iterated over in insertion order. + + @param {Function} callback + @param {anything} self if passed, the `this` value inside the + callback. By default, `this` is the map. + */ + forEach: function(callback, self) { + var keys = this.keys, + values = this.values; + + keys.forEach(function(key) { + var guid = guidFor(key); + callback.call(self, key, values[guid]); + }); + } +}; + +})({}); + +(function(exports) { +// ========================================================================== +// Project: Ember Runtime +// Copyright: ©2011 Strobe Inc. and contributors. +// License: Licensed under MIT license (see license.js) +// ========================================================================== + +})({}); + +(function(exports) { +// ========================================================================== +// Project: Ember Metal +// Copyright: ©2011 Strobe Inc. and contributors. +// License: Licensed under MIT license (see license.js) +// ========================================================================== +/** + @class + + Ember.ArrayController provides a way for you to publish a collection of objects + so that you can easily bind to the collection from a Handlebars #each helper, + an Ember.CollectionView, or other controllers. + + The advantage of using an ArrayController is that you only have to set up + your view bindings once; to change what's displayed, simply swap out the + `content` property on the controller. + + For example, imagine you wanted to display a list of items fetched via an XHR + request. Create an Ember.ArrayController and set its `content` property: + + MyApp.listController = Ember.ArrayController.create(); + + $.get('people.json', function(data) { + MyApp.listController.set('content', data); + }); + + Then, create a view that binds to your new controller: + + {{#each MyApp.listController}} + {{firstName}} {{lastName}} + {{/each}} - /** - Iterate over all the keys and values. Calls the function once - for each key, passing in the key and value, in that order. + Although you are binding to the controller, the behavior of this controller + is to pass through any methods or properties to the underlying array. This + capability comes from `Ember.ArrayProxy`, which this class inherits from. - The keys are guaranteed to be iterated over in insertion order. + Note: As of this writing, `ArrayController` does not add any functionality + to its superclass, `ArrayProxy`. The Ember team plans to add additional + controller-specific functionality in the future, e.g. single or multiple + selection support. If you are creating something that is conceptually a + controller, use this class. - @param {Function} callback - @param {anything} self if passed, the `this` value inside the - callback. By default, `this` is the map. - */ - forEach: function(callback, self) { - var keys = this.keys, - values = this.values; + @extends Ember.ArrayProxy +*/ - keys.forEach(function(key) { - var guid = guidFor(key); - callback.call(self, key, values[guid]); - }); - } -}; +Ember.ArrayController = Ember.ArrayProxy.extend(); })({}); - (function(exports) { -// ========================================================================== -// Project: Ember Runtime -// Copyright: ©2011 Strobe Inc. and contributors. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -})({}); +})({}); (function(exports) { // ========================================================================== @@ -10867,6 +11272,7 @@ Map.prototype = { // Copyright: ©2011 Strobe Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== + })({}); (function(exports) { @@ -10877,12 +11283,11 @@ Map.prototype = { // License: Licensed under MIT license (see license.js) // ========================================================================== -ember_assert("Ember requires jQuery 1.6 or 1.7", window.jQuery && jQuery().jquery.match(/^1\.[67](.\d+)?$/)); +ember_assert("Ember requires jQuery 1.6 or 1.7", window.jQuery && window.jQuery().jquery.match(/^1\.[67](.\d+)?$/)); Ember.$ = window.jQuery; })({}); - (function(exports) { // ========================================================================== // Project: Ember - JavaScript Application Framework @@ -10895,6 +11300,24 @@ var get = Ember.get, set = Ember.set; var forEach = Ember.ArrayUtils.forEach; var indexOf = Ember.ArrayUtils.indexOf; +var ClassSet = function() { + this.seen = {}; + this.list = []; +}; + +ClassSet.prototype = { + add: function(string) { + if (string in this.seen) { return; } + this.seen[string] = true; + + this.list.push(string); + }, + + toDOM: function() { + return this.list.join(" "); + } +}; + /** @class @@ -10905,10 +11328,15 @@ var indexOf = Ember.ArrayUtils.indexOf; @extends Ember.Object */ Ember.RenderBuffer = function(tagName) { - return Ember._RenderBuffer.create({ elementTag: tagName }); + return new Ember._RenderBuffer(tagName); }; -Ember._RenderBuffer = Ember.Object.extend( +Ember._RenderBuffer = function(tagName) { + this.elementTag = tagName; + this.childBuffers = []; +}; + +Ember._RenderBuffer.prototype = /** @scope Ember.RenderBuffer.prototype */ { /** @@ -10982,17 +11410,6 @@ Ember._RenderBuffer = Ember.Object.extend( */ parentBuffer: null, - /** @private */ - init: function() { - this._super(); - - set(this ,'elementClasses', Ember.A()); - set(this, 'elementAttributes', {}); - set(this, 'elementStyle', {}); - set(this, 'childBuffers', []); - set(this, 'elements', {}); - }, - /** Adds a string of HTML to the RenderBuffer. @@ -11000,7 +11417,7 @@ Ember._RenderBuffer = Ember.Object.extend( @returns {Ember.RenderBuffer} this */ push: function(string) { - get(this, 'childBuffers').push(String(string)); + this.childBuffers.push(String(string)); return this; }, @@ -11011,7 +11428,10 @@ Ember._RenderBuffer = Ember.Object.extend( @returns {Ember.RenderBuffer} this */ addClass: function(className) { - get(this, 'elementClasses').addObject(className); + // lazily create elementClasses + var elementClasses = this.elementClasses = (this.elementClasses || new ClassSet()); + this.elementClasses.add(className); + return this; }, @@ -11022,7 +11442,7 @@ Ember._RenderBuffer = Ember.Object.extend( @returns {Ember.RenderBuffer} this */ id: function(id) { - set(this, 'elementId', id); + this.elementId = id; return this; }, @@ -11037,7 +11457,7 @@ Ember._RenderBuffer = Ember.Object.extend( @returns {Ember.RenderBuffer|String} this or the current attribute value */ attr: function(name, value) { - var attributes = get(this, 'elementAttributes'); + var attributes = this.elementAttributes = (this.elementAttributes || {}); if (arguments.length === 1) { return attributes[name]; @@ -11055,8 +11475,8 @@ Ember._RenderBuffer = Ember.Object.extend( @returns {Ember.RenderBuffer} this */ removeAttr: function(name) { - var attributes = get(this, 'elementAttributes'); - delete attributes[name]; + var attributes = this.elementAttributes; + if (attributes) { delete attributes[name]; } return this; }, @@ -11069,7 +11489,9 @@ Ember._RenderBuffer = Ember.Object.extend( @returns {Ember.RenderBuffer} this */ style: function(name, value) { - get(this, 'elementStyle')[name] = value; + var style = this.elementStyle = (this.elementStyle || {}); + + this.elementStyle[name] = value; return this; }, @@ -11090,10 +11512,8 @@ Ember._RenderBuffer = Ember.Object.extend( buffer. */ newBuffer: function(tagName, parent, fn, other) { - var buffer = Ember._RenderBuffer.create({ - parentBuffer: parent, - elementTag: tagName - }); + var buffer = new Ember._RenderBuffer(tagName); + buffer.parentBuffer = parent; if (other) { buffer.setProperties(other); } if (fn) { fn.call(this, buffer); } @@ -11111,10 +11531,10 @@ Ember._RenderBuffer = Ember.Object.extend( the existing buffer. */ replaceWithBuffer: function(newBuffer) { - var parent = get(this, 'parentBuffer'); + var parent = this.parentBuffer; if (!parent) { return; } - var childBuffers = get(parent, 'childBuffers'); + var childBuffers = parent.childBuffers; var index = indexOf(childBuffers, this); @@ -11135,7 +11555,7 @@ Ember._RenderBuffer = Ember.Object.extend( */ begin: function(tagName) { return this.newBuffer(tagName, this, function(buffer) { - get(this, 'childBuffers').push(buffer); + this.childBuffers.push(buffer); }); }, @@ -11146,7 +11566,7 @@ Ember._RenderBuffer = Ember.Object.extend( */ prepend: function(tagName) { return this.newBuffer(tagName, this, function(buffer) { - get(this, 'childBuffers').splice(0, 0, buffer); + this.childBuffers.splice(0, 0, buffer); }); }, @@ -11156,7 +11576,7 @@ Ember._RenderBuffer = Ember.Object.extend( @param {String} tagName Tag name to use for the new buffer's element */ replaceWith: function(tagName) { - var parentBuffer = get(this, 'parentBuffer'); + var parentBuffer = this.parentBuffer; return this.newBuffer(tagName, parentBuffer, function(buffer) { this.replaceWithBuffer(buffer); @@ -11172,7 +11592,7 @@ Ember._RenderBuffer = Ember.Object.extend( var parentBuffer = get(this, 'parentBuffer'); return this.newBuffer(tagName, parentBuffer, function(buffer) { - var siblings = get(parentBuffer, 'childBuffers'); + var siblings = parentBuffer.childBuffers; var index = indexOf(siblings, this); siblings.splice(index + 1, 0, buffer); }); @@ -11184,7 +11604,7 @@ Ember._RenderBuffer = Ember.Object.extend( @returns {Ember.RenderBuffer} The parentBuffer, if one exists. Otherwise, this */ end: function() { - var parent = get(this, 'parentBuffer'); + var parent = this.parentBuffer; return parent || this; }, @@ -11206,40 +11626,42 @@ Ember._RenderBuffer = Ember.Object.extend( @returns {String} The generated HTMl */ string: function() { - var id = get(this, 'elementId'), - classes = get(this, 'elementClasses'), - attrs = get(this, 'elementAttributes'), - style = get(this, 'elementStyle'), - tag = get(this, 'elementTag'), - content = '', - styleBuffer = [], prop, openTag; + var content = '', tag = this.elementTag, openTag; if (tag) { + var id = this.elementId, + classes = this.elementClasses, + attrs = this.elementAttributes, + style = this.elementStyle, + styleBuffer = '', prop; + openTag = ["<" + tag]; if (id) { openTag.push('id="' + id + '"'); } - if (classes.length) { openTag.push('class="' + classes.join(" ") + '"'); } + if (classes) { openTag.push('class="' + classes.toDOM() + '"'); } - if (!Ember.$.isEmptyObject(style)) { + if (style) { for (prop in style) { if (style.hasOwnProperty(prop)) { - styleBuffer.push(prop + ':' + style[prop] + ';'); + styleBuffer += (prop + ':' + style[prop] + ';'); } } - openTag.push('style="' + styleBuffer.join("") + '"'); + openTag.push('style="' + styleBuffer + '"'); } - for (prop in attrs) { - if (attrs.hasOwnProperty(prop)) { - openTag.push(prop + '="' + attrs[prop] + '"'); + if (attrs) { + for (prop in attrs) { + if (attrs.hasOwnProperty(prop)) { + openTag.push(prop + '="' + attrs[prop] + '"'); + } } } openTag = openTag.join(" ") + '>'; } - var childBuffers = get(this, 'childBuffers'); + var childBuffers = this.childBuffers; forEach(childBuffers, function(buffer) { var stringy = typeof buffer === 'string'; @@ -11253,11 +11675,10 @@ Ember._RenderBuffer = Ember.Object.extend( } } -}); +}; })({}); - (function(exports) { // ========================================================================== // Project: Ember - JavaScript Application Framework @@ -11315,6 +11736,7 @@ Ember.EventDispatcher = Ember.Object.extend( keypress : 'keyPress', mousedown : 'mouseDown', mouseup : 'mouseUp', + contextmenu : 'contextMenu', click : 'click', dblclick : 'doubleClick', mousemove : 'mouseMove', @@ -11393,7 +11815,7 @@ Ember.EventDispatcher = Ember.Object.extend( }); rootElement.delegate('[data-ember-action]', event + '.ember', function(evt) { - var actionId = $(evt.currentTarget).attr('data-ember-action'), + var actionId = Ember.$(evt.currentTarget).attr('data-ember-action'), action = Ember.Handlebars.ActionHelper.registeredActions[actionId], handler = action.handler; @@ -11450,7 +11872,6 @@ Ember.EventDispatcher = Ember.Object.extend( })({}); - (function(exports) { // ========================================================================== // Project: Ember - JavaScript Application Framework @@ -11564,7 +11985,6 @@ Ember.Application = Ember.Namespace.extend( })({}); - (function(exports) { // ========================================================================== // Project: Ember - JavaScript Application Framework @@ -11576,11 +11996,10 @@ Ember.Application = Ember.Namespace.extend( // Add a new named queue for rendering views that happens // after bindings have synced. var queues = Ember.run.queues; -queues.splice(jQuery.inArray('actions', queues)+1, 0, 'render'); +queues.splice(Ember.$.inArray('actions', queues)+1, 0, 'render'); })({}); - (function(exports) { // ========================================================================== // Project: Ember - JavaScript Application Framework @@ -11588,8 +12007,8 @@ queues.splice(jQuery.inArray('actions', queues)+1, 0, 'render'); // Portions ©2008-2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ========================================================================== -})({}); +})({}); (function(exports) { // ========================================================================== @@ -11631,6 +12050,14 @@ var childViewsProperty = Ember.computed(function() { */ Ember.TEMPLATES = {}; +var invokeForState = { + preRender: {}, + inBuffer: {}, + hasElement: {}, + inDOM: {}, + destroyed: {} +}; + /** @class @since Ember 0.9 @@ -11786,7 +12213,7 @@ Ember.View = Ember.Object.extend(Ember.Evented, @type Boolean @default null */ - isVisible: null, + isVisible: true, /** Array of child views. You should never edit this array directly. @@ -11926,8 +12353,18 @@ Ember.View = Ember.Object.extend(Ember.Evented, }, invokeForState: function(name) { - var parent = this, states = parent.states; - var stateName = get(this, 'state'), state; + var stateName = this.state, args; + + // try to find the function for the state in the cache + if (fn = invokeForState[stateName][name]) { + args = a_slice.call(arguments); + args[0] = this; + + return fn.apply(this, args); + } + + // otherwise, find and cache the function for this state + var parent = this, states = parent.states, state; while (states) { state = states[stateName]; @@ -11936,7 +12373,9 @@ Ember.View = Ember.Object.extend(Ember.Evented, var fn = state[name]; if (fn) { - var args = a_slice.call(arguments, 1); + invokeForState[stateName][name] = fn; + + args = a_slice.call(arguments, 1); args.unshift(this); return fn.apply(this, args); @@ -11968,9 +12407,8 @@ Ember.View = Ember.Object.extend(Ember.Evented, }, clearRenderedChildren: function() { - var viewMeta = meta(this)['Ember.View'], - lengthBefore = viewMeta.lengthBeforeRender, - lengthAfter = viewMeta.lengthAfterRender; + var lengthBefore = this.lengthBeforeRender, + lengthAfter = this.lengthAfterRender; // If there were child views created during the last call to render(), // remove them under the assumption that they will be re-created when @@ -12213,9 +12651,6 @@ Ember.View = Ember.Object.extend(Ember.Evented, // Schedule the DOM element to be created and appended to the given // element after bindings have synchronized. this._insertElementLater(function() { - if (get(this, 'isVisible') === null) { - set(this, 'isVisible', true); - } this.$().appendTo(target); }); @@ -12265,7 +12700,7 @@ Ember.View = Ember.Object.extend(Ember.Evented, @param {Function} fn the function that inserts the element into the DOM */ _insertElementLater: function(fn) { - Ember.run.schedule('render', this, 'invokeForState', 'insertElement', fn); + Ember.run.schedule('render', this, this.invokeForState, 'insertElement', fn); }, /** @@ -12537,7 +12972,6 @@ Ember.View = Ember.Object.extend(Ember.Evented, be used. */ renderToBuffer: function(parentBuffer, bufferOperation) { - var viewMeta = meta(this)['Ember.View']; var buffer; Ember.run.sync(); @@ -12563,16 +12997,16 @@ Ember.View = Ember.Object.extend(Ember.Evented, buffer = this.renderBuffer(); } - viewMeta.buffer = buffer; - this.transitionTo('inBuffer'); + this.buffer = buffer; + this.transitionTo('inBuffer', false); - viewMeta.lengthBeforeRender = getPath(this, '_childViews.length'); + this.lengthBeforeRender = get(get(this, '_childViews'), 'length'); this.beforeRender(buffer); this.render(buffer); this.afterRender(buffer); - viewMeta.lengthAfterRender = getPath(this, '_childViews.length'); + this.lengthAfterRender = get(get(this, '_childViews'), 'length'); return buffer; }, @@ -12711,6 +13145,8 @@ Ember.View = Ember.Object.extend(Ember.Evented, */ attributeBindings: [], + state: 'preRender', + // ....................................................... // CORE DISPLAY METHODS // @@ -12724,8 +13160,6 @@ Ember.View = Ember.Object.extend(Ember.Evented, dispatch */ init: function() { - set(this, 'state', 'preRender'); - var parentView = get(this, '_parentView'); this._super(); @@ -12735,20 +13169,16 @@ Ember.View = Ember.Object.extend(Ember.Evented, Ember.View.views[get(this, 'elementId')] = this; var childViews = Ember.A(get(this, '_childViews').slice()); + // setup child views. be sure to clone the child views array first set(this, '_childViews', childViews); - ember_assert("Only arrays are allowed for 'classNameBindings'", Ember.typeOf(this.classNameBindings) === 'array'); this.classNameBindings = Ember.A(this.classNameBindings.slice()); ember_assert("Only arrays are allowed for 'classNames'", Ember.typeOf(this.classNames) === 'array'); this.classNames = Ember.A(this.classNames.slice()); - set(this, 'domManager', this.domManagerClass.create({ view: this })); - - meta(this)["Ember.View"] = {}; - var viewController = get(this, 'viewController'); if (viewController) { viewController = Ember.getPath(viewController); @@ -12823,8 +13253,7 @@ Ember.View = Ember.Object.extend(Ember.Evented, // calling this._super() will nuke computed properties and observers, // so collect any information we need before calling super. - var viewMeta = meta(this)['Ember.View'], - childViews = get(this, '_childViews'), + var childViews = get(this, '_childViews'), parent = get(this, '_parentView'), elementId = get(this, 'elementId'), childLen; @@ -12833,6 +13262,14 @@ Ember.View = Ember.Object.extend(Ember.Evented, // the element over and over again... if (!this.removedFromDOM) { this.destroyElement(); } + // remove from non-virtual parent view if viewName was specified + if (this.viewName) { + var nonVirtualParentView = get(this, 'parentView'); + if (nonVirtualParentView) { + set(nonVirtualParentView, this.viewName, null); + } + } + // remove from parent if found. Don't call removeFromParent, // as removeFromParent will try to remove the element from // the DOM again. @@ -12870,7 +13307,7 @@ Ember.View = Ember.Object.extend(Ember.Evented, if (Ember.View.detect(view)) { view = view.create(attrs || {}, { _parentView: this }); - var viewName = attrs && attrs.viewName || view.viewName; + var viewName = view.viewName; // don't set the property on a virtual view, as they are invisible to // consumers of the view API @@ -12942,12 +13379,12 @@ Ember.View = Ember.Object.extend(Ember.Evented, clearBuffer: function() { this.invokeRecursively(function(view) { - meta(view)['Ember.View'].buffer = null; + this.buffer = null; }); }, transitionTo: function(state, children) { - set(this, 'state', state); + this.state = state; if (children !== false) { this.forEachChildView(function(view) { @@ -13007,49 +13444,44 @@ Ember.View = Ember.Object.extend(Ember.Evented, // once the view has been inserted into the DOM, legal manipulations // are done on the DOM element. -Ember.View.reopen({ - states: Ember.View.states, - domManagerClass: Ember.Object.extend({ - view: this, - - prepend: function(childView) { - var view = get(this, 'view'); - - childView._insertElementLater(function() { - var element = view.$(); - element.prepend(childView.$()); - }); - }, +/** @private */ +var DOMManager = { + prepend: function(view, childView) { + childView._insertElementLater(function() { + var element = view.$(); + element.prepend(childView.$()); + }); + }, - after: function(nextView) { - var view = get(this, 'view'); + after: function(view, nextView) { + nextView._insertElementLater(function() { + var element = view.$(); + element.after(nextView.$()); + }); + }, - nextView._insertElementLater(function() { - var element = view.$(); - element.after(nextView.$()); - }); - }, + replace: function(view) { + var element = get(view, 'element'); - replace: function() { - var view = get(this, 'view'); - var element = get(view, 'element'); + set(view, 'element', null); - set(view, 'element', null); + view._insertElementLater(function() { + Ember.$(element).replaceWith(get(view, 'element')); + }); + }, - view._insertElementLater(function() { - Ember.$(element).replaceWith(get(view, 'element')); - }); - }, + remove: function(view) { + var elem = get(view, 'element'); - remove: function() { - var view = get(this, 'view'); - var elem = get(view, 'element'); + set(view, 'element', null); - set(view, 'element', null); + Ember.$(elem).remove(); + } +}; - Ember.$(elem).remove(); - } - }) +Ember.View.reopen({ + states: Ember.View.states, + domManager: DOMManager }); // Create a global view hash. @@ -13078,7 +13510,6 @@ Ember.View.applyAttributeBindings = function(elem, name, value) { })({}); - (function(exports) { // ========================================================================== // Project: Ember - JavaScript Application Framework @@ -13116,7 +13547,6 @@ Ember.View.reopen({ })({}); - (function(exports) { // ========================================================================== // Project: Ember - JavaScript Application Framework @@ -13170,7 +13600,6 @@ Ember.View.states.preRender = { })({}); - (function(exports) { // ========================================================================== // Project: Ember - JavaScript Application Framework @@ -13195,19 +13624,19 @@ Ember.View.states.inBuffer = { // when a view is rendered in a buffer, rerendering it simply // replaces the existing buffer with a new one rerender: function(view) { - view._notifyWillRerender(); + ember_deprecate("Something you did caused a view to re-render after it rendered but before it was inserted into the DOM. Because this is avoidable and the cause of significant performance issues in applications, this behavior is deprecated. If you want to use the debugger to find out what caused this, you can set ENV.RAISE_ON_DEPRECATION to true."); - var buffer = meta(view)['Ember.View'].buffer; + view._notifyWillRerender(); view.clearRenderedChildren(); - view.renderToBuffer(buffer, 'replaceWith'); + view.renderToBuffer(view.buffer, 'replaceWith'); }, // when a view is rendered in a buffer, appending a child // view will render that view and append the resulting // buffer into its buffer. appendChild: function(view, childView, options) { - var buffer = meta(view)['Ember.View'].buffer; + var buffer = view.buffer; childView = this.createChildView(childView, options); get(view, '_childViews').pushObject(childView); @@ -13249,7 +13678,6 @@ Ember.View.states.inBuffer = { })({}); - (function(exports) { // ========================================================================== // Project: Ember - JavaScript Application Framework @@ -13292,7 +13720,7 @@ Ember.View.states.hasElement = { view.clearRenderedChildren(); - get(view, 'domManager').replace(); + view.domManager.replace(view); return view; }, @@ -13302,7 +13730,7 @@ Ember.View.states.hasElement = { destroyElement: function(view) { view._notifyWillDestroyElement(); - get(view, 'domManager').remove(); + view.domManager.remove(view); return view; }, @@ -13327,7 +13755,6 @@ Ember.View.states.inDOM = { })({}); - (function(exports) { // ========================================================================== // Project: Ember - JavaScript Application Framework @@ -13362,7 +13789,6 @@ Ember.View.states.destroyed = { })({}); - (function(exports) { // ========================================================================== // Project: Ember - JavaScript Application Framework @@ -13370,8 +13796,8 @@ Ember.View.states.destroyed = { // Portions ©2008-2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ========================================================================== -})({}); +})({}); (function(exports) { // ========================================================================== @@ -13513,9 +13939,9 @@ Ember.ContainerView = Ember.View.extend({ */ _scheduleInsertion: function(view, prev) { if (prev) { - prev.get('domManager').after(view); + prev.domManager.after(prev, view); } else { - this.get('domManager').prepend(view); + this.domManager.prepend(this, view); } } }); @@ -13527,7 +13953,7 @@ Ember.ContainerView.states = { inBuffer: { childViewsDidChange: function(parentView, views, start, added) { - var buffer = meta(parentView)['Ember.View'].buffer, + var buffer = parentView.buffer, startWith, prev, prevBuffer, view; // Determine where to begin inserting the child view(s) in the @@ -13549,7 +13975,7 @@ Ember.ContainerView.states = { for (var i=startWith; i{{title}}'), - - change: function() { - Ember.run.once(this, this._updateElementValue); - // returning false will cause IE to not change checkbox state - }, - - _updateElementValue: function() { - var input = this.$('input:checkbox'); - set(this, 'value', input.prop('checked')); - } -}); - - -})({}); - - -(function(exports) { -// ========================================================================== -// Project: Ember Handlebar Views -// Copyright: ©2011 Strobe Inc. and contributors. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -var get = Ember.get, set = Ember.set; - -/** @class */ -Ember.TextSupport = Ember.Mixin.create( -/** @scope Ember.TextSupport.prototype */ { - - value: "", - - attributeBindings: ['placeholder', 'disabled'], - placeholder: null, - disabled: false, - - insertNewline: Ember.K, - cancel: Ember.K, - - focusOut: function(event) { - this._elementValueDidChange(); - }, - - change: function(event) { - this._elementValueDidChange(); - }, - - keyUp: function(event) { - this.interpretKeyEvents(event); - }, - - /** - @private - */ - interpretKeyEvents: function(event) { - var map = Ember.TextSupport.KEY_EVENTS; - var method = map[event.keyCode]; - - this._elementValueDidChange(); - if (method) { return this[method](event); } - }, - - _elementValueDidChange: function() { - set(this, 'value', this.$().val()); - } - -}); - -Ember.TextSupport.KEY_EVENTS = { - 13: 'insertNewline', - 27: 'cancel' -}; - -})({}); - - -(function(exports) { -// ========================================================================== -// Project: Ember Handlebar Views -// Copyright: ©2011 Strobe Inc. and contributors. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -var get = Ember.get, set = Ember.set; - -/** - @class - @extends Ember.TextSupport -*/ -Ember.TextField = Ember.View.extend(Ember.TextSupport, - /** @scope Ember.TextField.prototype */ { + Registers a helper in Handlebars that will be called if no property with the + given name can be found on the current context object, and no helper with + that name is registered. - classNames: ['ember-text-field'], + This throws an exception with a more helpful error message so the user can + track down where the problem is happening. - tagName: "input", - attributeBindings: ['type', 'value'], - type: "text" + @name Handlebars.helpers.helperMissing + @param {String} path + @param {Hash} options +*/ +Ember.Handlebars.registerHelper('helperMissing', function(path, options) { + var error, view = ""; + error = "%@ Handlebars error: Could not find property '%@' on object %@."; + if (options.data){ + view = options.data.view; + } + throw new Ember.Error(Ember.String.fmt(error, [view, path, this])); }); -})({}); +})({}); (function(exports) { -// ========================================================================== -// Project: Ember Handlebar Views -// Copyright: ©2011 Strobe Inc. and contributors. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -var get = Ember.get, set = Ember.set; - -Ember.Button = Ember.View.extend(Ember.TargetActionSupport, { - classNames: ['ember-button'], - classNameBindings: ['isActive'], - - tagName: 'button', - - propagateEvents: false, +/*jshint newcap:false*/ +var set = Ember.set, get = Ember.get, getPath = Ember.getPath; - attributeBindings: ['type', 'disabled', 'href'], +var DOMManager = { + remove: function(view) { + var morph = view.morph; + if (morph.isRemoved()) { return; } + morph.remove(); + }, - // Defaults to 'button' if tagName is 'input' or 'button' - type: Ember.computed(function(key, value) { - var tagName = this.get('tagName'); - if (value !== undefined) { this._type = value; } - if (this._type !== undefined) { return this._type; } - if (tagName === 'input' || tagName === 'button') { return 'button'; } - }).property('tagName').cacheable(), + prepend: function(view, childView) { + childView._insertElementLater(function() { + var morph = view.morph; + morph.prepend(childView.outerHTML); + childView.outerHTML = null; + }); + }, - disabled: false, + after: function(view, nextView) { + nextView._insertElementLater(function() { + var morph = view.morph; + morph.after(nextView.outerHTML); + nextView.outerHTML = null; + }); + }, - // Allow 'a' tags to act like buttons - href: Ember.computed(function() { - return this.get('tagName') === 'a' ? '#' : null; - }).property('tagName').cacheable(), + replace: function(view) { + var morph = view.morph; - mouseDown: function() { - if (!get(this, 'disabled')) { - set(this, 'isActive', true); - this._mouseDown = true; - this._mouseEntered = true; - } - return get(this, 'propagateEvents'); - }, + view.transitionTo('preRender'); + view.clearRenderedChildren(); + var buffer = view.renderToBuffer(); - mouseLeave: function() { - if (this._mouseDown) { - set(this, 'isActive', false); - this._mouseEntered = false; - } - }, + Ember.run.schedule('render', this, function() { + if (get(view, 'isDestroyed')) { return; } + view.invalidateRecursively('element'); + view._notifyWillInsertElement(); + morph.replaceWith(buffer.string()); + view.transitionTo('inDOM'); + view._notifyDidInsertElement(); + }); + } +}; - mouseEnter: function() { - if (this._mouseDown) { - set(this, 'isActive', true); - this._mouseEntered = true; - } - }, +// The `morph` and `outerHTML` properties are internal only +// and not observable. - mouseUp: function(event) { - if (get(this, 'isActive')) { - // Actually invoke the button's target and action. - // This method comes from the Ember.TargetActionSupport mixin. - this.triggerAction(); - set(this, 'isActive', false); - } +Ember.Metamorph = Ember.Mixin.create({ + isVirtual: true, + tagName: '', - this._mouseDown = false; - this._mouseEntered = false; - return get(this, 'propagateEvents'); + init: function() { + this._super(); + this.morph = Metamorph(); }, - keyDown: function(event) { - // Handle space or enter - if (event.keyCode === 13 || event.keyCode === 32) { - this.mouseDown(); - } + beforeRender: function(buffer) { + buffer.push(this.morph.startTag()); }, - keyUp: function(event) { - // Handle space or enter - if (event.keyCode === 13 || event.keyCode === 32) { - this.mouseUp(); - } + afterRender: function(buffer) { + buffer.push(this.morph.endTag()); }, - // TODO: Handle proper touch behavior. Including should make inactive when - // finger moves more than 20x outside of the edge of the button (vs mouse - // which goes inactive as soon as mouse goes out of edges.) - - touchStart: function(touch) { - return this.mouseDown(touch); + createElement: function() { + var buffer = this.renderToBuffer(); + this.outerHTML = buffer.string(); + this.clearBuffer(); }, - touchEnd: function(touch) { - return this.mouseUp(touch); - } + domManager: DOMManager }); -})({}); +})({}); (function(exports) { // ========================================================================== @@ -14956,1273 +15235,1417 @@ Ember.Button = Ember.View.extend(Ember.TargetActionSupport, { // Copyright: ©2011 Strobe Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== -var get = Ember.get, set = Ember.set; +/*globals Handlebars */ +var get = Ember.get, set = Ember.set, getPath = Ember.Handlebars.getPath; /** + @ignore + @private @class - @extends Ember.TextSupport + + Ember._BindableSpanView is a private view created by the Handlebars `{{bind}}` + helpers that is used to keep track of bound properties. + + Every time a property is bound using a `{{mustache}}`, an anonymous subclass + of Ember._BindableSpanView is created with the appropriate sub-template and + context set up. When the associated property changes, just the template for + this view will re-render. */ -Ember.TextArea = Ember.View.extend(Ember.TextSupport, -/** @scope Ember.TextArea.prototype */ { +Ember._BindableSpanView = Ember.View.extend(Ember.Metamorph, +/** @scope Ember._BindableSpanView.prototype */{ - classNames: ['ember-text-area'], + /** + The function used to determine if the `displayTemplate` or + `inverseTemplate` should be rendered. This should be a function that takes + a value and returns a Boolean. - tagName: "textarea", + @type Function + @default null + */ + shouldDisplayFunc: null, /** - @private + Whether the template rendered by this view gets passed the context object + of its parent template, or gets passed the value of retrieving `property` + from the previous context. + + For example, this is true when using the `{{#if}}` helper, because the + template inside the helper should look up properties relative to the same + object as outside the block. This would be false when used with `{{#with + foo}}` because the template should receive the object found by evaluating + `foo`. + + @type Boolean + @default false */ - didInsertElement: function() { - this._updateElementValue(); - }, + preserveContext: false, - _updateElementValue: Ember.observer(function() { - this.$().val(get(this, 'value')); - }, 'value') + /** + The template to render when `shouldDisplayFunc` evaluates to true. -}); + @type Function + @default null + */ + displayTemplate: null, -})({}); + /** + The template to render when `shouldDisplayFunc` evaluates to false. + @type Function + @default null + */ + inverseTemplate: null, -(function(exports) { -Ember.TabContainerView = Ember.View.extend(); + /** + The key to look up on `previousContext` that is passed to + `shouldDisplayFunc` to determine which template to render. -})({}); + In addition, if `preserveContext` is false, this object will be passed to + the template when rendering. + + @type String + @default null + */ + property: null, + normalizedValue: Ember.computed(function() { + var property = get(this, 'property'), + context = get(this, 'previousContext'), + valueNormalizer = get(this, 'valueNormalizerFunc'), + result; -(function(exports) { -var get = Ember.get, getPath = Ember.getPath; + // Use the current context as the result if no + // property is provided. + if (property === '') { + result = context; + } else { + result = getPath(context, property); + } -Ember.TabPaneView = Ember.View.extend({ - tabsContainer: Ember.computed(function() { - return this.nearestInstanceOf(Ember.TabContainerView); - }).property(), + return valueNormalizer ? valueNormalizer(result) : result; + }).property('property', 'previousContext', 'valueNormalizerFunc'), - isVisible: Ember.computed(function() { - return get(this, 'viewName') === getPath(this, 'tabsContainer.currentView'); - }).property('tabsContainer.currentView') -}); + rerenderIfNeeded: function() { + if (!get(this, 'isDestroyed') && get(this, 'normalizedValue') !== this._lastNormalizedValue) { + this.rerender(); + } + }, + + /** + Determines which template to invoke, sets up the correct state based on + that logic, then invokes the default Ember.View `render` implementation. + + This method will first look up the `property` key on `previousContext`, + then pass that value to the `shouldDisplayFunc` function. If that returns + true, the `displayTemplate` function will be rendered to DOM. Otherwise, + `inverseTemplate`, if specified, will be rendered. + + For example, if this Ember._BindableSpan represented the {{#with foo}} + helper, it would look up the `foo` property of its context, and + `shouldDisplayFunc` would always return true. The object found by looking + up `foo` would be passed to `displayTemplate`. + + @param {Ember.RenderBuffer} buffer + */ + render: function(buffer) { + // If not invoked via a triple-mustache ({{{foo}}}), escape + // the content of the template. + var escape = get(this, 'isEscaped'); + + var shouldDisplay = get(this, 'shouldDisplayFunc'), + preserveContext = get(this, 'preserveContext'), + context = get(this, 'previousContext'); + + var inverseTemplate = get(this, 'inverseTemplate'), + displayTemplate = get(this, 'displayTemplate'); + + var result = get(this, 'normalizedValue'); + this._lastNormalizedValue = result; -})({}); + // First, test the conditional to see if we should + // render the template or not. + if (shouldDisplay(result)) { + set(this, 'template', displayTemplate); + // If we are preserving the context (for example, if this + // is an #if block, call the template with the same object. + if (preserveContext) { + set(this, 'templateContext', context); + } else { + // Otherwise, determine if this is a block bind or not. + // If so, pass the specified object to the template + if (displayTemplate) { + set(this, 'templateContext', result); + } else { + // This is not a bind block, just push the result of the + // expression to the render context and return. + if (result === null || result === undefined) { + result = ""; + } else { + result = String(result); + } -(function(exports) { -var get = Ember.get, setPath = Ember.setPath; + if (escape) { result = Handlebars.Utils.escapeExpression(result); } + buffer.push(result); + return; + } + } + } else if (inverseTemplate) { + set(this, 'template', inverseTemplate); -Ember.TabView = Ember.View.extend({ - tabsContainer: Ember.computed(function() { - return this.nearestInstanceOf(Ember.TabContainerView); - }).property(), + if (preserveContext) { + set(this, 'templateContext', context); + } else { + set(this, 'templateContext', result); + } + } else { + set(this, 'template', function() { return ''; }); + } - mouseUp: function() { - setPath(this, 'tabsContainer.currentView', get(this, 'value')); + return this._super(buffer); } }); })({}); - -(function(exports) { -})({}); - - (function(exports) { -var set = Ember.set, get = Ember.get, getPath = Ember.getPath; -var indexOf = Ember.ArrayUtils.indexOf; +// ========================================================================== +// Project: Ember Handlebar Views +// Copyright: ©2011 Strobe Inc. and contributors. +// License: Licensed under MIT license (see license.js) +// ========================================================================== +var get = Ember.get, getPath = Ember.Handlebars.getPath, set = Ember.set, fmt = Ember.String.fmt; +var forEach = Ember.ArrayUtils.forEach; -Ember.Select = Ember.View.extend({ - tagName: 'select', - template: Ember.Handlebars.compile( - '{{#if prompt}}{{/if}}' + - '{{#each content}}{{view Ember.SelectOption contentBinding="this"}}{{/each}}' - ), +var EmberHandlebars = Ember.Handlebars, helpers = EmberHandlebars.helpers; - content: null, - selection: null, - prompt: null, +(function() { + // Binds a property into the DOM. This will create a hook in DOM that the + // KVO system will look for and update if the property changes. + var bind = function(property, options, preserveContext, shouldDisplay, valueNormalizer) { + var data = options.data, + fn = options.fn, + inverse = options.inverse, + view = data.view, + ctx = this; - optionLabelPath: 'content', - optionValuePath: 'content', + // Set up observers for observable objects + if ('object' === typeof this) { + // Create the view that will wrap the output of this template/property + // and add it to the nearest view's childViews array. + // See the documentation of Ember._BindableSpanView for more. + var bindView = view.createChildView(Ember._BindableSpanView, { + preserveContext: preserveContext, + shouldDisplayFunc: shouldDisplay, + valueNormalizerFunc: valueNormalizer, + displayTemplate: fn, + inverseTemplate: inverse, + property: property, + previousContext: ctx, + isEscaped: options.hash.escaped + }); + view.appendChild(bindView); - didInsertElement: function() { - var selection = get(this, 'selection'); + /** @private */ + var observer = function() { + Ember.run.once(bindView, 'rerenderIfNeeded'); + }; - if (selection) { this.selectionDidChange(); } + // Observes the given property on the context and + // tells the Ember._BindableSpan to re-render. If property + // is an empty string, we are printing the current context + // object ({{this}}) so updating it is not our responsibility. + if (property !== '') { + Ember.addObserver(ctx, property, observer); + } + } else { + // The object is not observable, so just render it out and + // be done with it. + data.buffer.push(getPath(this, property)); + } + }; - this.change(); - }, + /** + '_triageMustache' is used internally select between a binding and helper for + the given context. Until this point, it would be hard to determine if the + mustache is a property reference or a regular helper reference. This triage + helper resolves that. - change: function() { - var selectedIndex = this.$()[0].selectedIndex, - content = get(this, 'content'), - prompt = get(this, 'prompt'); + This would not be typically invoked by directly. - if (!content) { return; } - if (prompt && selectedIndex === 0) { set(this, 'selection', null); return; } + @private + @name Handlebars.helpers._triageMustache + @param {String} property Property/helperID to triage + @param {Function} fn Context to provide for rendering + @returns {String} HTML string + */ + EmberHandlebars.registerHelper('_triageMustache', function(property, fn) { + ember_assert("You cannot pass more than one argument to the _triageMustache helper", arguments.length <= 2); + if (helpers[property]) { + return helpers[property].call(this, fn); + } + else { + return helpers.bind.apply(this, arguments); + } + }); - if (prompt) { selectedIndex -= 1; } - set(this, 'selection', content.objectAt(selectedIndex)); - }, + /** + `bind` can be used to display a value, then update that value if it + changes. For example, if you wanted to print the `title` property of + `content`: - selectionDidChange: Ember.observer(function() { - var el = this.$()[0], - content = get(this, 'content'), - selection = get(this, 'selection'), - selectionIndex = indexOf(content, selection), - prompt = get(this, 'prompt'); + {{bind "content.title"}} - if (prompt) { selectionIndex += 1; } - if (el) { el.selectedIndex = selectionIndex; } - }, 'selection') -}); + This will return the `title` property as a string, then create a new + observer at the specified path. If it changes, it will update the value in + DOM. Note that if you need to support IE7 and IE8 you must modify the + model objects properties using Ember.get() and Ember.set() for this to work as + it relies on Ember's KVO system. For all other browsers this will be handled + for you automatically. -Ember.SelectOption = Ember.View.extend({ - tagName: 'option', - template: Ember.Handlebars.compile("{{label}}"), - attributeBindings: ['value'], + @private + @name Handlebars.helpers.bind + @param {String} property Property to bind + @param {Function} fn Context to provide for rendering + @returns {String} HTML string + */ + EmberHandlebars.registerHelper('bind', function(property, fn) { + ember_assert("You cannot pass more than one argument to the bind helper", arguments.length <= 2); - init: function() { - this.labelPathDidChange(); - this.valuePathDidChange(); + var context = (fn.contexts && fn.contexts[0]) || this; - this._super(); - }, + return bind.call(context, property, fn, false, function(result) { + return !Ember.none(result); + }); + }); - labelPathDidChange: Ember.observer(function() { - var labelPath = getPath(this, 'parentView.optionLabelPath'); + /** + Use the `boundIf` helper to create a conditional that re-evaluates + whenever the bound value changes. - if (!labelPath) { return; } + {{#boundIf "content.shouldDisplayTitle"}} + {{content.title}} + {{/boundIf}} - Ember.defineProperty(this, 'label', Ember.computed(function() { - return getPath(this, labelPath); - }).property(labelPath).cacheable()); - }, 'parentView.optionLabelPath'), + @private + @name Handlebars.helpers.boundIf + @param {String} property Property to bind + @param {Function} fn Context to provide for rendering + @returns {String} HTML string + */ + EmberHandlebars.registerHelper('boundIf', function(property, fn) { + var context = (fn.contexts && fn.contexts[0]) || this; + var func = function(result) { + if (Ember.typeOf(result) === 'array') { + return get(result, 'length') !== 0; + } else { + return !!result; + } + }; - valuePathDidChange: Ember.observer(function() { - var valuePath = getPath(this, 'parentView.optionValuePath'); + return bind.call(context, property, fn, true, func, func); + }); +})(); - if (!valuePath) { return; } +/** + @name Handlebars.helpers.with + @param {Function} context + @param {Hash} options + @returns {String} HTML string +*/ +EmberHandlebars.registerHelper('with', function(context, options) { + ember_assert("You must pass exactly one argument to the with helper", arguments.length === 2); + ember_assert("You must pass a block to the with helper", options.fn && options.fn !== Handlebars.VM.noop); - Ember.defineProperty(this, 'value', Ember.computed(function() { - return getPath(this, valuePath); - }).property(valuePath).cacheable()); - }, 'parentView.optionValuePath') + return helpers.bind.call(options.contexts[0], context, options); }); -})({}); - - -(function(exports) { -// ========================================================================== -// Project: Ember Handlebar Views -// Copyright: ©2011 Strobe Inc. and contributors. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -})({}); +/** + @name Handlebars.helpers.if + @param {Function} context + @param {Hash} options + @returns {String} HTML string +*/ +EmberHandlebars.registerHelper('if', function(context, options) { + ember_assert("You must pass exactly one argument to the if helper", arguments.length === 2); + ember_assert("You must pass a block to the if helper", options.fn && options.fn !== Handlebars.VM.noop); + return helpers.boundIf.call(options.contexts[0], context, options); +}); -(function(exports) { -/*jshint newcap:false*/ -var set = Ember.set, get = Ember.get, getPath = Ember.getPath; +/** + @name Handlebars.helpers.unless + @param {Function} context + @param {Hash} options + @returns {String} HTML string +*/ +EmberHandlebars.registerHelper('unless', function(context, options) { + ember_assert("You must pass exactly one argument to the unless helper", arguments.length === 2); + ember_assert("You must pass a block to the unless helper", options.fn && options.fn !== Handlebars.VM.noop); -Ember.Metamorph = Ember.Mixin.create({ - isVirtual: true, - tagName: '', + var fn = options.fn, inverse = options.inverse; - init: function() { - this._super(); - set(this, 'morph', Metamorph()); - }, + options.fn = inverse; + options.inverse = fn; - beforeRender: function(buffer) { - var morph = get(this, 'morph'); - buffer.push(morph.startTag()); - }, + return helpers.boundIf.call(options.contexts[0], context, options); +}); - afterRender: function(buffer) { - var morph = get(this, 'morph'); - buffer.push(morph.endTag()); - }, +/** + `bindAttr` allows you to create a binding between DOM element attributes and + Ember objects. For example: - createElement: function() { - var buffer = this.renderToBuffer(); - set(this, 'outerHTML', buffer.string()); - this.clearBuffer(); - }, + imageTitle - domManagerClass: Ember.Object.extend({ - remove: function(view) { - var morph = getPath(this, 'view.morph'); - if (morph.isRemoved()) { return; } - getPath(this, 'view.morph').remove(); - }, + @name Handlebars.helpers.bindAttr + @param {Hash} options + @returns {String} HTML string +*/ +EmberHandlebars.registerHelper('bindAttr', function(options) { - prepend: function(childView) { - var view = get(this, 'view'); + var attrs = options.hash; - childView._insertElementLater(function() { - var morph = get(view, 'morph'); - morph.prepend(get(childView, 'outerHTML')); - childView.set('outerHTML', null); - }); - }, + ember_assert("You must specify at least one hash argument to bindAttr", !!Ember.keys(attrs).length); - after: function(nextView) { - var view = get(this, 'view'); + var view = options.data.view; + var ret = []; + var ctx = this; - nextView._insertElementLater(function() { - var morph = get(view, 'morph'); - morph.after(get(nextView, 'outerHTML')); - nextView.set('outerHTML', null); - }); - }, + // Generate a unique id for this element. This will be added as a + // data attribute to the element so it can be looked up when + // the bound property changes. + var dataId = ++Ember.$.uuid; - replace: function() { - var view = get(this, 'view'); - var morph = getPath(this, 'view.morph'); + // Handle classes differently, as we can bind multiple classes + var classBindings = attrs['class']; + if (classBindings !== null && classBindings !== undefined) { + var classResults = EmberHandlebars.bindClasses(this, classBindings, view, dataId); + ret.push('class="' + classResults.join(' ') + '"'); + delete attrs['class']; + } - view.transitionTo('preRender'); - view.clearRenderedChildren(); - var buffer = view.renderToBuffer(); - - Ember.run.schedule('render', this, function() { - if (get(view, 'isDestroyed')) { return; } - view.invalidateRecursively('element'); - view._notifyWillInsertElement(); - morph.replaceWith(buffer.string()); - view.transitionTo('inDOM'); - view._notifyDidInsertElement(); - }); - } - }) -}); + var attrKeys = Ember.keys(attrs); + // For each attribute passed, create an observer and emit the + // current value of the property as an attribute. + forEach(attrKeys, function(attr) { + var property = attrs[attr]; -})({}); + ember_assert(fmt("You must provide a String for a bound attribute, not %@", [property]), typeof property === 'string'); + var value = (property === 'this') ? ctx : getPath(ctx, property), + type = Ember.typeOf(value); -(function(exports) { -// ========================================================================== -// Project: Ember Handlebar Views -// Copyright: ©2011 Strobe Inc. and contributors. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -/*globals Handlebars */ + ember_assert(fmt("Attributes must be numbers, strings or booleans, not %@", [value]), value === null || value === undefined || type === 'number' || type === 'string' || type === 'boolean'); -var get = Ember.get, set = Ember.set, getPath = Ember.Handlebars.getPath; -/** - @ignore - @private - @class + var observer, invoker; - Ember._BindableSpanView is a private view created by the Handlebars `{{bind}}` - helpers that is used to keep track of bound properties. + /** @private */ + observer = function observer() { + var result = getPath(ctx, property); - Every time a property is bound using a `{{mustache}}`, an anonymous subclass - of Ember._BindableSpanView is created with the appropriate sub-template and - context set up. When the associated property changes, just the template for - this view will re-render. -*/ -Ember._BindableSpanView = Ember.View.extend(Ember.Metamorph, -/** @scope Ember._BindableSpanView.prototype */{ + ember_assert(fmt("Attributes must be numbers, strings or booleans, not %@", [result]), result === null || result === undefined || typeof result === 'number' || typeof result === 'string' || typeof result === 'boolean'); - /** - The function used to determine if the `displayTemplate` or - `inverseTemplate` should be rendered. This should be a function that takes - a value and returns a Boolean. + var elem = view.$("[data-bindattr-" + dataId + "='" + dataId + "']"); - @type Function - @default null - */ - shouldDisplayFunc: null, + // If we aren't able to find the element, it means the element + // to which we were bound has been removed from the view. + // In that case, we can assume the template has been re-rendered + // and we need to clean up the observer. + if (elem.length === 0) { + Ember.removeObserver(ctx, property, invoker); + return; + } - /** - Whether the template rendered by this view gets passed the context object - of its parent template, or gets passed the value of retrieving `property` - from the previous context. + Ember.View.applyAttributeBindings(elem, attr, result); + }; - For example, this is true when using the `{{#if}}` helper, because the - template inside the helper should look up properties relative to the same - object as outside the block. This would be false when used with `{{#with - foo}}` because the template should receive the object found by evaluating - `foo`. + /** @private */ + invoker = function() { + Ember.run.once(observer); + }; - @type Boolean - @default false - */ - preserveContext: false, + // 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 (property !== 'this') { + Ember.addObserver(ctx, property, invoker); + } - /** - The template to render when `shouldDisplayFunc` evaluates to true. + // if this changes, also change the logic in ember-views/lib/views/view.js + if ((type === 'string' || (type === 'number' && !isNaN(value)))) { + ret.push(attr + '="' + value + '"'); + } else if (value && type === 'boolean') { + ret.push(attr + '="' + attr + '"'); + } + }, this); - @type Function - @default null - */ - displayTemplate: null, + // Add the unique identifier + // NOTE: We use all lower-case since Firefox has problems with mixed case in SVG + ret.push('data-bindattr-' + dataId + '="' + dataId + '"'); + return new EmberHandlebars.SafeString(ret.join(' ')); +}); - /** - The template to render when `shouldDisplayFunc` evaluates to false. +/** + Helper that, given a space-separated string of property paths and a context, + returns an array of class names. Calling this method also has the side + effect of setting up observers at those property paths, such that if they + change, the correct class name will be reapplied to the DOM element. - @type Function - @default null - */ - inverseTemplate: null, + For example, if you pass the string "fooBar", it will first look up the + "fooBar" value of the context. If that value is true, it will add the + "foo-bar" class to the current element (i.e., the dasherized form of + "fooBar"). If the value is a string, it will add that string as the class. + Otherwise, it will not add any new class name. - /** - The key to look up on `previousContext` that is passed to - `shouldDisplayFunc` to determine which template to render. + @param {Ember.Object} context + The context from which to lookup properties - In addition, if `preserveContext` is false, this object will be passed to - the template when rendering. + @param {String} classBindings + A string, space-separated, of class bindings to use - @type String - @default null - */ - property: null, + @param {Ember.View} view + The view in which observers should look for the element to update - normalizedValue: Ember.computed(function() { - var property = get(this, 'property'), - context = get(this, 'previousContext'), - valueNormalizer = get(this, 'valueNormalizerFunc'), - result; + @param {Srting} bindAttrId + Optional bindAttr id used to lookup elements - // Use the current context as the result if no - // property is provided. - if (property === '') { - result = context; - } else { - result = getPath(context, property); - } + @returns {Array} An array of class names to add +*/ +EmberHandlebars.bindClasses = function(context, classBindings, view, bindAttrId) { + var ret = [], newClass, value, elem; - return valueNormalizer ? valueNormalizer(result) : result; - }).property('property', 'previousContext', 'valueNormalizerFunc'), + // Helper method to retrieve the property from the context and + // determine which class string to return, based on whether it is + // a Boolean or not. + var classStringForProperty = function(property) { + var split = property.split(':'), + className = split[1]; - rerenderIfNeeded: function() { - if (!get(this, 'isDestroyed') && get(this, 'normalizedValue') !== this._lastNormalizedValue) { - this.rerender(); - } - }, + property = split[0]; - /** - Determines which template to invoke, sets up the correct state based on - that logic, then invokes the default Ember.View `render` implementation. + var val = property !== '' ? getPath(context, property) : true; - This method will first look up the `property` key on `previousContext`, - then pass that value to the `shouldDisplayFunc` function. If that returns - true, the `displayTemplate` function will be rendered to DOM. Otherwise, - `inverseTemplate`, if specified, will be rendered. + // If value is a Boolean and true, return the dasherized property + // name. + if (val === true) { + if (className) { return className; } - For example, if this Ember._BindableSpan represented the {{#with foo}} - helper, it would look up the `foo` property of its context, and - `shouldDisplayFunc` would always return true. The object found by looking - up `foo` would be passed to `displayTemplate`. + // Normalize property path to be suitable for use + // as a class name. For exaple, content.foo.barBaz + // becomes bar-baz. + var parts = property.split('.'); + return Ember.String.dasherize(parts[parts.length-1]); - @param {Ember.RenderBuffer} buffer - */ - render: function(buffer) { - // If not invoked via a triple-mustache ({{{foo}}}), escape - // the content of the template. - var escape = get(this, 'isEscaped'); + // If the value is not false, undefined, or null, return the current + // value of the property. + } else if (val !== false && val !== undefined && val !== null) { + return val; - var shouldDisplay = get(this, 'shouldDisplayFunc'), - preserveContext = get(this, 'preserveContext'), - context = get(this, 'previousContext'); + // Nothing to display. Return null so that the old class is removed + // but no new class is added. + } else { + return null; + } + }; - var inverseTemplate = get(this, 'inverseTemplate'), - displayTemplate = get(this, 'displayTemplate'); + // For each property passed, loop through and setup + // an observer. + forEach(classBindings.split(' '), function(binding) { - var result = get(this, 'normalizedValue'); - this._lastNormalizedValue = result; + // Variable in which the old class value is saved. The observer function + // closes over this variable, so it knows which string to remove when + // the property changes. + var oldClass; - // First, test the conditional to see if we should - // render the template or not. - if (shouldDisplay(result)) { - set(this, 'template', displayTemplate); + var observer, invoker; - // If we are preserving the context (for example, if this - // is an #if block, call the template with the same object. - if (preserveContext) { - set(this, 'templateContext', context); + // Set up an observer on the context. If the property changes, toggle the + // class name. + /** @private */ + observer = function() { + // Get the current value of the property + newClass = classStringForProperty(binding); + elem = bindAttrId ? view.$("[data-bindattr-" + bindAttrId + "='" + bindAttrId + "']") : view.$(); + + // If we can't find the element anymore, a parent template has been + // re-rendered and we've been nuked. Remove the observer. + if (elem.length === 0) { + Ember.removeObserver(context, binding, invoker); } else { - // Otherwise, determine if this is a block bind or not. - // If so, pass the specified object to the template - if (displayTemplate) { - set(this, 'templateContext', result); - } else { - // This is not a bind block, just push the result of the - // expression to the render context and return. - if (result === null || result === undefined) { - result = ""; - } else { - result = String(result); - } + // If we had previously added a class to the element, remove it. + if (oldClass) { + elem.removeClass(oldClass); + } - if (escape) { result = Handlebars.Utils.escapeExpression(result); } - buffer.push(result); - return; + // If necessary, add a new class. Make sure we keep track of it so + // it can be removed in the future. + if (newClass) { + elem.addClass(newClass); + oldClass = newClass; + } else { + oldClass = null; } } - } else if (inverseTemplate) { - set(this, 'template', inverseTemplate); + }; - if (preserveContext) { - set(this, 'templateContext', context); - } else { - set(this, 'templateContext', result); - } - } else { - set(this, 'template', function() { return ''; }); + /** @private */ + invoker = function() { + Ember.run.once(observer); + }; + + var property = binding.split(':')[0]; + if (property !== '') { + Ember.addObserver(context, property, invoker); } - return this._super(buffer); - } -}); + // We've already setup the observer; now we just need to figure out the + // correct behavior right now on the first pass through. + value = classStringForProperty(binding); -})({}); + if (value) { + ret.push(value); + + // Make sure we save the current value so that it can be removed if the + // observer fires. + oldClass = value; + } + }); + + return ret; +}; +})({}); + (function(exports) { // ========================================================================== // Project: Ember Handlebar Views // Copyright: ©2011 Strobe Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== -var get = Ember.get, getPath = Ember.Handlebars.getPath, set = Ember.set, fmt = Ember.String.fmt; -var forEach = Ember.ArrayUtils.forEach; - -var EmberHandlebars = Ember.Handlebars, helpers = EmberHandlebars.helpers; -var helpers = EmberHandlebars.helpers; +/*globals Handlebars ember_assert */ -(function() { - // Binds a property into the DOM. This will create a hook in DOM that the - // KVO system will look for and update if the property changes. - var bind = function(property, options, preserveContext, shouldDisplay, valueNormalizer) { - var data = options.data, - fn = options.fn, - inverse = options.inverse, - view = data.view, - ctx = this; +// TODO: Don't require the entire module +var get = Ember.get, set = Ember.set; +var indexOf = Ember.ArrayUtils.indexOf; +var PARENT_VIEW_PATH = /^parentView\./; - // Set up observers for observable objects - if ('object' === typeof this) { - // Create the view that will wrap the output of this template/property - // and add it to the nearest view's childViews array. - // See the documentation of Ember._BindableSpanView for more. - var bindView = view.createChildView(Ember._BindableSpanView, { - preserveContext: preserveContext, - shouldDisplayFunc: shouldDisplay, - valueNormalizerFunc: valueNormalizer, - displayTemplate: fn, - inverseTemplate: inverse, - property: property, - previousContext: ctx, - isEscaped: options.hash.escaped - }); +/** @private */ +Ember.Handlebars.ViewHelper = Ember.Object.create({ - view.appendChild(bindView); + viewClassFromHTMLOptions: function(viewClass, options, thisContext) { + var extensions = {}, + classes = options['class'], + dup = false; - /** @private */ - var observer = function() { - Ember.run.once(bindView, 'rerenderIfNeeded'); - }; + if (options.id) { + extensions.elementId = options.id; + dup = true; + } - // Observes the given property on the context and - // tells the Ember._BindableSpan to re-render. If property - // is an empty string, we are printing the current context - // object ({{this}}) so updating it is not our responsibility. - if (property !== '') { - Ember.addObserver(ctx, property, observer); - } - } else { - // The object is not observable, so just render it out and - // be done with it. - data.buffer.push(getPath(this, property)); + if (classes) { + classes = classes.split(' '); + extensions.classNames = classes; + dup = true; } - }; - /** - '_triageMustache' is used internally select between a binding and helper for - the given context. Until this point, it would be hard to determine if the - mustache is a property reference or a regular helper reference. This triage - helper resolves that. + if (options.classBinding) { + extensions.classNameBindings = options.classBinding.split(' '); + dup = true; + } - This would not be typically invoked by directly. + if (options.classNameBindings) { + extensions.classNameBindings = options.classNameBindings.split(' '); + dup = true; + } - @private - @name Handlebars.helpers._triageMustache - @param {String} property Property/helperID to triage - @param {Function} fn Context to provide for rendering - @returns {String} HTML string - */ - EmberHandlebars.registerHelper('_triageMustache', function(property, fn) { - ember_assert("You cannot pass more than one argument to the _triageMustache helper", arguments.length <= 2); - if (helpers[property]) { - return helpers[property].call(this, fn); + if (options.attributeBindings) { + ember_assert("Setting 'attributeBindings' via Handlebars is not allowed. Please subclass Ember.View and set it there instead."); + extensions.attributeBindings = null; + dup = true; } - else { - return helpers.bind.apply(this, arguments); + + if (dup) { + options = Ember.$.extend({}, options); + delete options.id; + delete options['class']; + delete options.classBinding; } - }); - /** - `bind` can be used to display a value, then update that value if it - changes. For example, if you wanted to print the `title` property of - `content`: + // Look for bindings passed to the helper and, if they are + // local, make them relative to the current context instead of the + // view. + var path; - {{bind "content.title"}} + for (var prop in options) { + if (!options.hasOwnProperty(prop)) { continue; } - This will return the `title` property as a string, then create a new - observer at the specified path. If it changes, it will update the value in - DOM. Note that if you need to support IE7 and IE8 you must modify the - model objects properties using Ember.get() and Ember.set() for this to work as - it relies on Ember's KVO system. For all other browsers this will be handled - for you automatically. + // Test if the property ends in "Binding" + if (Ember.IS_BINDING.test(prop)) { + path = options[prop]; + if (!Ember.isGlobalPath(path)) { + if (path === 'this') { + options[prop] = 'bindingContext'; + } else { + options[prop] = 'bindingContext.'+path; + } + } + } + } - @private - @name Handlebars.helpers.bind - @param {String} property Property to bind - @param {Function} fn Context to provide for rendering - @returns {String} HTML string - */ - EmberHandlebars.registerHelper('bind', function(property, fn) { - ember_assert("You cannot pass more than one argument to the bind helper", arguments.length <= 2); + // Make the current template context available to the view + // for the bindings set up above. + extensions.bindingContext = thisContext; - var context = (fn.contexts && fn.contexts[0]) || this; + return viewClass.extend(options, extensions); + }, - return bind.call(context, property, fn, false, function(result) { - return !Ember.none(result); - }); - }); + helper: function(thisContext, path, options) { + var inverse = options.inverse, + data = options.data, + view = data.view, + fn = options.fn, + hash = options.hash, + newView; - /** - Use the `boundIf` helper to create a conditional that re-evaluates - whenever the bound value changes. + if ('string' === typeof path) { + newView = Ember.Handlebars.getPath(thisContext, path); + ember_assert("Unable to find view at path '" + path + "'", !!newView); + } else { + newView = path; + } - {{#boundIf "content.shouldDisplayTitle"}} - {{content.title}} - {{/boundIf}} + ember_assert(Ember.String.fmt('You must pass a view class to the #view helper, not %@ (%@)', [path, newView]), Ember.View.detect(newView)); - @private - @name Handlebars.helpers.boundIf - @param {String} property Property to bind - @param {Function} fn Context to provide for rendering - @returns {String} HTML string - */ - EmberHandlebars.registerHelper('boundIf', function(property, fn) { - var context = (fn.contexts && fn.contexts[0]) || this; - var func = function(result) { - if (Ember.typeOf(result) === 'array') { - return get(result, 'length') !== 0; - } else { - return !!result; - } - }; + newView = this.viewClassFromHTMLOptions(newView, hash, thisContext); + var currentView = data.view; + var viewOptions = {}; - return bind.call(context, property, fn, true, func, func); - }); -})(); + if (fn) { + ember_assert("You cannot provide a template block if you also specified a templateName", !(get(viewOptions, 'templateName')) && (indexOf(newView.PrototypeMixin.keys(), 'templateName') >= 0)); + viewOptions.template = fn; + } + + currentView.appendChild(newView, viewOptions); + } +}); /** - @name Handlebars.helpers.with - @param {Function} context + @name Handlebars.helpers.view + @param {String} path @param {Hash} options @returns {String} HTML string */ -EmberHandlebars.registerHelper('with', function(context, options) { - ember_assert("You must pass exactly one argument to the with helper", arguments.length === 2); - ember_assert("You must pass a block to the with helper", options.fn && options.fn !== Handlebars.VM.noop); +Ember.Handlebars.registerHelper('view', function(path, options) { + ember_assert("The view helper only takes a single argument", arguments.length <= 2); - return helpers.bind.call(options.contexts[0], context, options); + // If no path is provided, treat path param as options. + if (path && path.data && path.data.isRenderData) { + options = path; + path = "Ember.View"; + } + + return Ember.Handlebars.ViewHelper.helper(this, path, options); }); -/** - @name Handlebars.helpers.if - @param {Function} context - @param {Hash} options - @returns {String} HTML string -*/ -EmberHandlebars.registerHelper('if', function(context, options) { - ember_assert("You must pass exactly one argument to the if helper", arguments.length === 2); - ember_assert("You must pass a block to the if helper", options.fn && options.fn !== Handlebars.VM.noop); +})({}); - return helpers.boundIf.call(options.contexts[0], context, options); -}); +(function(exports) { +// ========================================================================== +// Project: Ember Handlebar Views +// Copyright: ©2011 Strobe Inc. and contributors. +// License: Licensed under MIT license (see license.js) +// ========================================================================== +/*globals Handlebars ember_assert */ + +// TODO: Don't require all of this module +var get = Ember.get, getPath = Ember.Handlebars.getPath, fmt = Ember.String.fmt; /** - @name Handlebars.helpers.unless - @param {Function} context + @name Handlebars.helpers.collection + @param {String} path @param {Hash} options @returns {String} HTML string */ -EmberHandlebars.registerHelper('unless', function(context, options) { - ember_assert("You must pass exactly one argument to the unless helper", arguments.length === 2); - ember_assert("You must pass a block to the unless helper", options.fn && options.fn !== Handlebars.VM.noop); +Ember.Handlebars.registerHelper('collection', function(path, options) { + // If no path is provided, treat path param as options. + if (path && path.data && path.data.isRenderData) { + options = path; + path = undefined; + ember_assert("You cannot pass more than one argument to the collection helper", arguments.length === 1); + } else { + ember_assert("You cannot pass more than one argument to the collection helper", arguments.length === 2); + } - var fn = options.fn, inverse = options.inverse; + var fn = options.fn; + var data = options.data; + var inverse = options.inverse; - options.fn = inverse; - options.inverse = fn; + // If passed a path string, convert that into an object. + // Otherwise, just default to the standard class. + var collectionClass; + collectionClass = path ? getPath(this, path) : Ember.CollectionView; + ember_assert(fmt("%@ #collection: Could not find %@", data.view, path), !!collectionClass); - return helpers.boundIf.call(options.contexts[0], context, options); -}); + var hash = options.hash, itemHash = {}, match; -/** - `bindAttr` allows you to create a binding between DOM element attributes and - Ember objects. For example: + // Extract item view class if provided else default to the standard class + var itemViewClass, itemViewPath = hash.itemViewClass; + var collectionPrototype = collectionClass.proto(); + delete hash.itemViewClass; + itemViewClass = itemViewPath ? getPath(collectionPrototype, itemViewPath) : collectionPrototype.itemViewClass; + ember_assert(fmt("%@ #collection: Could not find %@", data.view, itemViewPath), !!itemViewClass); - imageTitle + // Go through options passed to the {{collection}} helper and extract options + // that configure item views instead of the collection itself. + for (var prop in hash) { + if (hash.hasOwnProperty(prop)) { + match = prop.match(/^item(.)(.*)$/); - @name Handlebars.helpers.bindAttr - @param {Hash} options - @returns {String} HTML string -*/ -EmberHandlebars.registerHelper('bindAttr', function(options) { + if(match) { + // Convert itemShouldFoo -> shouldFoo + itemHash[match[1].toLowerCase() + match[2]] = hash[prop]; + // Delete from hash as this will end up getting passed to the + // {{view}} helper method. + delete hash[prop]; + } + } + } - var attrs = options.hash; + var tagName = hash.tagName || collectionPrototype.tagName; - ember_assert("You must specify at least one hash argument to bindAttr", !!Ember.keys(attrs).length); + if (fn) { + itemHash.template = fn; + delete options.fn; + } - var view = options.data.view; - var ret = []; - var ctx = this; + if (inverse && inverse !== Handlebars.VM.noop) { + var emptyViewClass = Ember.View; - // Generate a unique id for this element. This will be added as a - // data attribute to the element so it can be looked up when - // the bound property changes. - var dataId = ++Ember.$.uuid; + if (hash.emptyViewClass) { + emptyViewClass = Ember.View.detect(hash.emptyViewClass) ? + hash.emptyViewClass : getPath(this, hash.emptyViewClass); + } - // Handle classes differently, as we can bind multiple classes - var classBindings = attrs['class']; - if (classBindings !== null && classBindings !== undefined) { - var classResults = EmberHandlebars.bindClasses(this, classBindings, view, dataId); - ret.push('class="' + classResults.join(' ') + '"'); - delete attrs['class']; + hash.emptyView = emptyViewClass.extend({ + template: inverse, + tagName: itemHash.tagName + }); } - var attrKeys = Ember.keys(attrs); + if (hash.preserveContext) { + itemHash.templateContext = Ember.computed(function() { + return get(this, 'content'); + }).property('content'); + delete hash.preserveContext; + } - // For each attribute passed, create an observer and emit the - // current value of the property as an attribute. - forEach(attrKeys, function(attr) { - var property = attrs[attr]; + hash.itemViewClass = Ember.Handlebars.ViewHelper.viewClassFromHTMLOptions(itemViewClass, itemHash, this); - ember_assert(fmt("You must provide a String for a bound attribute, not %@", [property]), typeof property === 'string'); + return Ember.Handlebars.helpers.view.call(this, collectionClass, options); +}); - var value = (property === 'this') ? ctx : getPath(ctx, property), - type = Ember.typeOf(value); - ember_assert(fmt("Attributes must be numbers, strings or booleans, not %@", [value]), value === null || value === undefined || type === 'number' || type === 'string' || type === 'boolean'); - var observer, invoker; - /** @private */ - observer = function observer() { - var result = getPath(ctx, property); +})({}); - ember_assert(fmt("Attributes must be numbers, strings or booleans, not %@", [result]), result === null || result === undefined || typeof result === 'number' || typeof result === 'string' || typeof result === 'boolean'); +(function(exports) { +// ========================================================================== +// Project: Ember Handlebar Views +// Copyright: ©2011 Strobe Inc. and contributors. +// License: Licensed under MIT license (see license.js) +// ========================================================================== +/*globals Handlebars */ +var getPath = Ember.Handlebars.getPath; - var elem = view.$("[data-bindAttr-" + dataId + "='" + dataId + "']"); +/** + `unbound` allows you to output a property without binding. *Important:* The + output will not be updated if the property changes. Use with caution. - // If we aren't able to find the element, it means the element - // to which we were bound has been removed from the view. - // In that case, we can assume the template has been re-rendered - // and we need to clean up the observer. - if (elem.length === 0) { - Ember.removeObserver(ctx, property, invoker); - return; - } +
{{unbound somePropertyThatDoesntChange}}
+ + @name Handlebars.helpers.unbound + @param {String} property + @returns {String} HTML string +*/ +Ember.Handlebars.registerHelper('unbound', function(property, fn) { + var context = (fn.contexts && fn.contexts[0]) || this; + return getPath(context, property); +}); + +})({}); + +(function(exports) { +// ========================================================================== +// Project: Ember Handlebar Views +// Copyright: ©2011 Strobe Inc. and contributors. +// License: Licensed under MIT license (see license.js) +// ========================================================================== + +/*jshint debug:true*/ +var getPath = Ember.getPath; + +/** + `log` allows you to output the value of a value in the current rendering + context. + + {{log myVariable}} + + @name Handlebars.helpers.log + @param {String} property +*/ +Ember.Handlebars.registerHelper('log', function(property, fn) { + var context = (fn.contexts && fn.contexts[0]) || this; + Ember.Logger.log(getPath(context, property)); +}); + +/** + The `debugger` helper executes the `debugger` statement in the current + context. + + {{debugger}} + + @name Handlebars.helpers.debugger + @param {String} property +*/ +Ember.Handlebars.registerHelper('debugger', function() { + debugger; +}); - Ember.View.applyAttributeBindings(elem, attr, result); - }; +})({}); - /** @private */ - invoker = function() { - Ember.run.once(observer); - }; +(function(exports) { +Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember.Metamorph, { + itemViewClass: Ember.View.extend(Ember.Metamorph) +}); - // 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 (property !== 'this') { - Ember.addObserver(ctx, property, invoker); - } +Ember.Handlebars.registerHelper('each', function(path, options) { + options.hash.contentBinding = path; + options.hash.preserveContext = true; - // if this changes, also change the logic in ember-views/lib/views/view.js - if ((type === 'string' || (type === 'number' && !isNaN(value)))) { - ret.push(attr + '="' + value + '"'); - } else if (value && type === 'boolean') { - ret.push(attr + '="' + attr + '"'); - } - }, this); + // Set up emptyView as a metamorph with no tag + options.hash.itemTagName = ''; + options.hash.emptyViewClass = Ember.View.extend(Ember.Metamorph); - // Add the unique identifier - ret.push('data-bindAttr-' + dataId + '="' + dataId + '"'); - return new EmberHandlebars.SafeString(ret.join(' ')); + return Ember.Handlebars.helpers.collection.call(this, 'Ember.Handlebars.EachView', options); }); +})({}); + +(function(exports) { /** - Helper that, given a space-separated string of property paths and a context, - returns an array of class names. Calling this method also has the side - effect of setting up observers at those property paths, such that if they - change, the correct class name will be reapplied to the DOM element. + `template` allows you to render a template from inside another template. + This allows you to re-use the same template in multiple places. For example: - For example, if you pass the string "fooBar", it will first look up the - "fooBar" value of the context. If that value is true, it will add the - "foo-bar" class to the current element (i.e., the dasherized form of - "fooBar"). If the value is a string, it will add that string as the class. - Otherwise, it will not add any new class name. + - @param {Ember.Object} context - The context from which to lookup properties + - @param {String} classBindings - A string, space-separated, of class bindings to use + This helper looks for templates in the global Ember.TEMPLATES hash. If you + add <script> tags to your page with the `data-template-name` attribute set, + they will be compiled and placed in this hash automatically. - @param {Ember.View} view - The view in which observers should look for the element to update + You can also manually register templates by adding them to the hash: - @param {Srting} bindAttrId - Optional bindAttr id used to lookup elements + Ember.TEMPLATES["my_cool_template"] = Ember.Handlebars.compile('{{user}}'); - @returns {Array} An array of class names to add + @name Handlebars.helpers.template + @param {String} templateName the template to render */ -EmberHandlebars.bindClasses = function(context, classBindings, view, bindAttrId) { - var ret = [], newClass, value, elem; - // Helper method to retrieve the property from the context and - // determine which class string to return, based on whether it is - // a Boolean or not. - var classStringForProperty = function(property) { - var split = property.split(':'), - className = split[1]; +Ember.Handlebars.registerHelper('template', function(name, options) { + var template = Ember.TEMPLATES[name]; - property = split[0]; + ember_assert("Unable to find template with name '"+name+"'.", !!template); - var val = getPath(context, property); + Ember.TEMPLATES[name](this, { data: options.data }); +}); - // If value is a Boolean and true, return the dasherized property - // name. - if (val === true) { - if (className) { return className; } +})({}); - // Normalize property path to be suitable for use - // as a class name. For exaple, content.foo.barBaz - // becomes bar-baz. - var parts = property.split('.'); - return Ember.String.dasherize(parts[parts.length-1]); +(function(exports) { +var EmberHandlebars = Ember.Handlebars, getPath = Ember.Handlebars.getPath; - // If the value is not false, undefined, or null, return the current - // value of the property. - } else if (val !== false && val !== undefined && val !== null) { - return val; +var ActionHelper = EmberHandlebars.ActionHelper = { + registeredActions: {} +}; - // Nothing to display. Return null so that the old class is removed - // but no new class is added. - } else { - return null; +ActionHelper.registerAction = function(actionName, eventName, target, view, context) { + var actionId = (++Ember.$.uuid).toString(); + + ActionHelper.registeredActions[actionId] = { + eventName: eventName, + handler: function(event) { + event.view = view; + event.context = context; + + if ('function' === typeof target.send) { + return target.send(actionName, event); + } else { + return target[actionName].call(target, event); + } } }; - // For each property passed, loop through and setup - // an observer. - forEach(classBindings.split(' '), function(binding) { - - // Variable in which the old class value is saved. The observer function - // closes over this variable, so it knows which string to remove when - // the property changes. - var oldClass; + view.on('willRerender', function() { + delete ActionHelper.registeredActions[actionId]; + }); - var observer, invoker; + return actionId; +}; - // Set up an observer on the context. If the property changes, toggle the - // class name. - /** @private */ - observer = function() { - // Get the current value of the property - newClass = classStringForProperty(binding); - elem = bindAttrId ? view.$("[data-bindAttr-" + bindAttrId + "='" + bindAttrId + "']") : view.$(); +EmberHandlebars.registerHelper('action', function(actionName, options) { + var hash = options.hash || {}, + eventName = options.hash.on || "click", + view = options.data.view, + target, context; - // If we can't find the element anymore, a parent template has been - // re-rendered and we've been nuked. Remove the observer. - if (elem.length === 0) { - Ember.removeObserver(context, binding, invoker); - } else { - // If we had previously added a class to the element, remove it. - if (oldClass) { - elem.removeClass(oldClass); - } + if (view.isVirtual) { view = view.get('parentView'); } + target = options.hash.target ? getPath(this, options.hash.target) : view; + context = options.contexts[0]; - // If necessary, add a new class. Make sure we keep track of it so - // it can be removed in the future. - if (newClass) { - elem.addClass(newClass); - oldClass = newClass; - } else { - oldClass = null; - } - } - }; + var actionId = ActionHelper.registerAction(actionName, eventName, target, view, context); + return new EmberHandlebars.SafeString('data-ember-action="' + actionId + '"'); +}); - /** @private */ - invoker = function() { - Ember.run.once(observer); - }; +})({}); - var property = binding.split(':')[0]; - Ember.addObserver(context, property, invoker); +(function(exports) { +var get = Ember.get, set = Ember.set; - // We've already setup the observer; now we just need to figure out the - // correct behavior right now on the first pass through. - value = classStringForProperty(binding); +Ember.Handlebars.registerHelper('yield', function(options) { + var view = options.data.view, template; - if (value) { - ret.push(value); + while (view && !get(view, 'layout')) { + view = get(view, 'parentView'); + } - // Make sure we save the current value so that it can be removed if the - // observer fires. - oldClass = value; - } - }); + ember_assert("You called yield in a template that was not a layout", !!view); - return ret; -}; + template = get(view, 'template'); + ember_assert("You called yield on " + view.toString() + " without supplying a template", !!template); + template(this, options); +}); })({}); - (function(exports) { // ========================================================================== // Project: Ember Handlebar Views // Copyright: ©2011 Strobe Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== -/*globals Handlebars ember_assert */ -// TODO: Don't require the entire module -var get = Ember.get, set = Ember.set; -var indexOf = Ember.ArrayUtils.indexOf; -var PARENT_VIEW_PATH = /^parentView\./; +})({}); -/** @private */ -Ember.Handlebars.ViewHelper = Ember.Object.create({ +(function(exports) { +// ========================================================================== +// Project: Ember Handlebar Views +// Copyright: ©2011 Strobe Inc. and contributors. +// License: Licensed under MIT license (see license.js) +// ========================================================================== - viewClassFromHTMLOptions: function(viewClass, options, thisContext) { - var extensions = {}, - classes = options['class'], - dup = false; +})({}); - if (options.id) { - extensions.elementId = options.id; - dup = true; - } +(function(exports) { +// ========================================================================== +// Project: Ember Handlebar Views +// Copyright: ©2011 Strobe Inc. and contributors. +// License: Licensed under MIT license (see license.js) +// ========================================================================== +var set = Ember.set, get = Ember.get; - if (classes) { - classes = classes.split(' '); - extensions.classNames = classes; - dup = true; - } +// TODO: Be explicit in the class documentation that you +// *MUST* set the value of a checkbox through Ember. +// Updating the value of a checkbox directly via jQuery objects +// will not work. - if (options.classBinding) { - extensions.classNameBindings = options.classBinding.split(' '); - dup = true; - } +Ember.Checkbox = Ember.View.extend({ + title: null, + value: false, + disabled: false, - if (options.attributeBindings) { - ember_assert("Setting 'attributeBindings' via Handlebars is not allowed. Please subclass Ember.View and set it there instead."); - extensions.attributeBindings = null; - dup = true; - } + classNames: ['ember-checkbox'], - if (options.classNameBindings) { - ember_assert("Setting 'classNameBindings' via Handlebars is not allowed. Consider setting 'classNames' instead."); - extensions.classNameBindings = null; - dup = true; - } + defaultTemplate: Ember.Handlebars.compile(''), - if (dup) { - options = Ember.$.extend({}, options); - delete options.id; - delete options['class']; - delete options.classBinding; - } + change: function() { + Ember.run.once(this, this._updateElementValue); + // returning false will cause IE to not change checkbox state + }, - // Look for bindings passed to the helper and, if they are - // local, make them relative to the current context instead of the - // view. - var path; + _updateElementValue: function() { + var input = this.$('input:checkbox'); + set(this, 'value', input.prop('checked')); + } +}); - for (var prop in options) { - if (!options.hasOwnProperty(prop)) { continue; } - // Test if the property ends in "Binding" - if (Ember.IS_BINDING.test(prop)) { - path = options[prop]; - if (!Ember.isGlobalPath(path)) { - if (path === 'this') { - options[prop] = 'bindingContext'; - } else { - options[prop] = 'bindingContext.'+path; - } - } - } - } +})({}); - // Make the current template context available to the view - // for the bindings set up above. - extensions.bindingContext = thisContext; +(function(exports) { +// ========================================================================== +// Project: Ember Handlebar Views +// Copyright: ©2011 Strobe Inc. and contributors. +// License: Licensed under MIT license (see license.js) +// ========================================================================== +var get = Ember.get, set = Ember.set; - return viewClass.extend(options, extensions); - }, +/** @class */ +Ember.TextSupport = Ember.Mixin.create( +/** @scope Ember.TextSupport.prototype */ { - helper: function(thisContext, path, options) { - var inverse = options.inverse, - data = options.data, - view = data.view, - fn = options.fn, - hash = options.hash, - newView; + value: "", - if ('string' === typeof path) { - newView = Ember.Handlebars.getPath(thisContext, path); - ember_assert("Unable to find view at path '" + path + "'", !!newView); - } else { - newView = path; - } + attributeBindings: ['placeholder', 'disabled', 'maxlength'], + placeholder: null, + disabled: false, + maxlength: null, - ember_assert(Ember.String.fmt('You must pass a view class to the #view helper, not %@ (%@)', [path, newView]), Ember.View.detect(newView)); + insertNewline: Ember.K, + cancel: Ember.K, - newView = this.viewClassFromHTMLOptions(newView, hash, thisContext); - var currentView = data.view; - var viewOptions = {}; + focusOut: function(event) { + this._elementValueDidChange(); + }, - if (fn) { - ember_assert("You cannot provide a template block if you also specified a templateName", !(get(viewOptions, 'templateName')) && (indexOf(newView.PrototypeMixin.keys(), 'templateName') >= 0)); - viewOptions.template = fn; - } + change: function(event) { + this._elementValueDidChange(); + }, - currentView.appendChild(newView, viewOptions); - } -}); + keyUp: function(event) { + this.interpretKeyEvents(event); + }, -/** - @name Handlebars.helpers.view - @param {String} path - @param {Hash} options - @returns {String} HTML string -*/ -Ember.Handlebars.registerHelper('view', function(path, options) { - ember_assert("The view helper only takes a single argument", arguments.length <= 2); + /** + @private + */ + interpretKeyEvents: function(event) { + var map = Ember.TextSupport.KEY_EVENTS; + var method = map[event.keyCode]; - // If no path is provided, treat path param as options. - if (path && path.data && path.data.isRenderData) { - options = path; - path = "Ember.View"; + this._elementValueDidChange(); + if (method) { return this[method](event); } + }, + + _elementValueDidChange: function() { + set(this, 'value', this.$().val()); } - return Ember.Handlebars.ViewHelper.helper(this, path, options); }); +Ember.TextSupport.KEY_EVENTS = { + 13: 'insertNewline', + 27: 'cancel' +}; })({}); - (function(exports) { // ========================================================================== // Project: Ember Handlebar Views // Copyright: ©2011 Strobe Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== -/*globals Handlebars ember_assert */ - -// TODO: Don't require all of this module -var get = Ember.get, getPath = Ember.Handlebars.getPath, fmt = Ember.String.fmt; +var get = Ember.get, set = Ember.set; /** - @name Handlebars.helpers.collection - @param {String} path - @param {Hash} options - @returns {String} HTML string + @class + @extends Ember.TextSupport */ -Ember.Handlebars.registerHelper('collection', function(path, options) { - // If no path is provided, treat path param as options. - if (path && path.data && path.data.isRenderData) { - options = path; - path = undefined; - ember_assert("You cannot pass more than one argument to the collection helper", arguments.length === 1); - } else { - ember_assert("You cannot pass more than one argument to the collection helper", arguments.length === 2); - } - - var fn = options.fn; - var data = options.data; - var inverse = options.inverse; - - // If passed a path string, convert that into an object. - // Otherwise, just default to the standard class. - var collectionClass; - collectionClass = path ? getPath(this, path) : Ember.CollectionView; - ember_assert(fmt("%@ #collection: Could not find %@", data.view, path), !!collectionClass); +Ember.TextField = Ember.View.extend(Ember.TextSupport, + /** @scope Ember.TextField.prototype */ { - var hash = options.hash, itemHash = {}, match; + classNames: ['ember-text-field'], - // Extract item view class if provided else default to the standard class - var itemViewClass, itemViewPath = hash.itemViewClass; - var collectionPrototype = get(collectionClass, 'proto'); - delete hash.itemViewClass; - itemViewClass = itemViewPath ? getPath(collectionPrototype, itemViewPath) : collectionPrototype.itemViewClass; - ember_assert(fmt("%@ #collection: Could not find %@", data.view, itemViewPath), !!itemViewClass); + tagName: "input", + attributeBindings: ['type', 'value', 'size'], + type: "text", + size: null +}); - // Go through options passed to the {{collection}} helper and extract options - // that configure item views instead of the collection itself. - for (var prop in hash) { - if (hash.hasOwnProperty(prop)) { - match = prop.match(/^item(.)(.*)$/); +})({}); - if(match) { - // Convert itemShouldFoo -> shouldFoo - itemHash[match[1].toLowerCase() + match[2]] = hash[prop]; - // Delete from hash as this will end up getting passed to the - // {{view}} helper method. - delete hash[prop]; - } - } - } +(function(exports) { +// ========================================================================== +// Project: Ember Handlebar Views +// Copyright: ©2011 Strobe Inc. and contributors. +// License: Licensed under MIT license (see license.js) +// ========================================================================== +var get = Ember.get, set = Ember.set; - var tagName = hash.tagName || get(collectionClass, 'proto').tagName; +Ember.Button = Ember.View.extend(Ember.TargetActionSupport, { + classNames: ['ember-button'], + classNameBindings: ['isActive'], - if (fn) { - itemHash.template = fn; - delete options.fn; - } + tagName: 'button', - if (inverse && inverse !== Handlebars.VM.noop) { - var emptyViewClass = Ember.View; + propagateEvents: false, - if (hash.emptyViewClass) { - emptyViewClass = Ember.View.detect(hash.emptyViewClass) ? - hash.emptyViewClass : getPath(this, hash.emptyViewClass); - } + attributeBindings: ['type', 'disabled', 'href'], - hash.emptyView = emptyViewClass.extend({ - template: inverse, - tagName: itemHash.tagName - }); - } + // Defaults to 'button' if tagName is 'input' or 'button' + type: Ember.computed(function(key, value) { + var tagName = this.get('tagName'); + if (value !== undefined) { this._type = value; } + if (this._type !== undefined) { return this._type; } + if (tagName === 'input' || tagName === 'button') { return 'button'; } + }).property('tagName').cacheable(), - if (hash.preserveContext) { - itemHash.templateContext = Ember.computed(function() { - return get(this, 'content'); - }).property('content'); - delete hash.preserveContext; - } + disabled: false, - hash.itemViewClass = Ember.Handlebars.ViewHelper.viewClassFromHTMLOptions(itemViewClass, itemHash, this); + // Allow 'a' tags to act like buttons + href: Ember.computed(function() { + return this.get('tagName') === 'a' ? '#' : null; + }).property('tagName').cacheable(), - return Ember.Handlebars.helpers.view.call(this, collectionClass, options); -}); + mouseDown: function() { + if (!get(this, 'disabled')) { + set(this, 'isActive', true); + this._mouseDown = true; + this._mouseEntered = true; + } + return get(this, 'propagateEvents'); + }, + mouseLeave: function() { + if (this._mouseDown) { + set(this, 'isActive', false); + this._mouseEntered = false; + } + }, + mouseEnter: function() { + if (this._mouseDown) { + set(this, 'isActive', true); + this._mouseEntered = true; + } + }, + mouseUp: function(event) { + if (get(this, 'isActive')) { + // Actually invoke the button's target and action. + // This method comes from the Ember.TargetActionSupport mixin. + this.triggerAction(); + set(this, 'isActive', false); + } -})({}); + this._mouseDown = false; + this._mouseEntered = false; + return get(this, 'propagateEvents'); + }, + keyDown: function(event) { + // Handle space or enter + if (event.keyCode === 13 || event.keyCode === 32) { + this.mouseDown(); + } + }, -(function(exports) { -// ========================================================================== -// Project: Ember Handlebar Views -// Copyright: ©2011 Strobe Inc. and contributors. -// License: Licensed under MIT license (see license.js) -// ========================================================================== -/*globals Handlebars */ -var getPath = Ember.Handlebars.getPath; + keyUp: function(event) { + // Handle space or enter + if (event.keyCode === 13 || event.keyCode === 32) { + this.mouseUp(); + } + }, -/** - `unbound` allows you to output a property without binding. *Important:* The - output will not be updated if the property changes. Use with caution. + // TODO: Handle proper touch behavior. Including should make inactive when + // finger moves more than 20x outside of the edge of the button (vs mouse + // which goes inactive as soon as mouse goes out of edges.) -
{{unbound somePropertyThatDoesntChange}}
+ touchStart: function(touch) { + return this.mouseDown(touch); + }, - @name Handlebars.helpers.unbound - @param {String} property - @returns {String} HTML string -*/ -Ember.Handlebars.registerHelper('unbound', function(property, fn) { - var context = (fn.contexts && fn.contexts[0]) || this; - return getPath(context, property); + touchEnd: function(touch) { + return this.mouseUp(touch); + } }); })({}); - (function(exports) { // ========================================================================== // Project: Ember Handlebar Views // Copyright: ©2011 Strobe Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== - -/*jshint debug:true*/ -var getPath = Ember.getPath; +var get = Ember.get, set = Ember.set; /** - `log` allows you to output the value of a value in the current rendering - context. + @class + @extends Ember.TextSupport +*/ +Ember.TextArea = Ember.View.extend(Ember.TextSupport, +/** @scope Ember.TextArea.prototype */ { - {{log myVariable}} + classNames: ['ember-text-area'], - @name Handlebars.helpers.log - @param {String} property -*/ -Ember.Handlebars.registerHelper('log', function(property, fn) { - var context = (fn.contexts && fn.contexts[0]) || this; - Ember.Logger.log(getPath(context, property)); -}); + tagName: "textarea", + attributeBindings: ['rows', 'cols'], + rows: null, + cols: null, -/** - The `debugger` helper executes the `debugger` statement in the current - context. + /** + @private + */ + didInsertElement: function() { + this._updateElementValue(); + }, - {{debugger}} + _updateElementValue: Ember.observer(function() { + this.$().val(get(this, 'value')); + }, 'value') - @name Handlebars.helpers.debugger - @param {String} property -*/ -Ember.Handlebars.registerHelper('debugger', function() { - debugger; }); })({}); - (function(exports) { -Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember.Metamorph, { - itemViewClass: Ember.View.extend(Ember.Metamorph) -}); +Ember.TabContainerView = Ember.View.extend(); -Ember.Handlebars.registerHelper('each', function(path, options) { - options.hash.contentBinding = path; - options.hash.preserveContext = true; +})({}); - // Set up emptyView as a metamorph with no tag - options.hash.itemTagName = ''; - options.hash.emptyViewClass = Ember.View.extend(Ember.Metamorph); +(function(exports) { +var get = Ember.get, getPath = Ember.getPath; - return Ember.Handlebars.helpers.collection.call(this, 'Ember.Handlebars.EachView', options); +Ember.TabPaneView = Ember.View.extend({ + tabsContainer: Ember.computed(function() { + return this.nearestInstanceOf(Ember.TabContainerView); + }).property(), + + isVisible: Ember.computed(function() { + return get(this, 'viewName') === getPath(this, 'tabsContainer.currentView'); + }).property('tabsContainer.currentView') }); })({}); - (function(exports) { -/** - `template` allows you to render a template from inside another template. - This allows you to re-use the same template in multiple places. For example: +var get = Ember.get, setPath = Ember.setPath; - +Ember.TabView = Ember.View.extend({ + tabsContainer: Ember.computed(function() { + return this.nearestInstanceOf(Ember.TabContainerView); + }).property(), - + mouseUp: function() { + setPath(this, 'tabsContainer.currentView', get(this, 'value')); + } +}); - This helper looks for templates in the global Ember.TEMPLATES hash. If you - add <script> tags to your page with the `data-template-name` attribute set, - they will be compiled and placed in this hash automatically. +})({}); - You can also manually register templates by adding them to the hash: +(function(exports) { - Ember.TEMPLATES["my_cool_template"] = Ember.Handlebars.compile('{{user}}'); +})({}); - @name Handlebars.helpers.template - @param {String} templateName the template to render -*/ +(function(exports) { +/*jshint eqeqeq:false */ -Ember.Handlebars.registerHelper('template', function(name, options) { - var template = Ember.TEMPLATES[name]; +var set = Ember.set, get = Ember.get, getPath = Ember.getPath; +var indexOf = Ember.ArrayUtils.indexOf; - ember_assert("Unable to find template with name '"+name+"'.", !!template); +Ember.Select = Ember.View.extend({ + tagName: 'select', + template: Ember.Handlebars.compile( + '{{#if prompt}}{{/if}}' + + '{{#each content}}{{view Ember.SelectOption contentBinding="this"}}{{/each}}' + ), - Ember.TEMPLATES[name](this, { data: options.data }); -}); + content: null, + selection: null, + prompt: null, -})({}); + optionLabelPath: 'content', + optionValuePath: 'content', + didInsertElement: function() { + var selection = get(this, 'selection'); -(function(exports) { -var EmberHandlebars = Ember.Handlebars, getPath = Ember.Handlebars.getPath; + if (selection) { this.selectionDidChange(); } -var ActionHelper = EmberHandlebars.ActionHelper = { - registeredActions: {} -}; + this.change(); + }, -ActionHelper.registerAction = function(actionName, eventName, target, view, context) { - var actionId = (++Ember.$.uuid).toString(); + change: function() { + var selectedIndex = this.$()[0].selectedIndex, + content = get(this, 'content'), + prompt = get(this, 'prompt'); - ActionHelper.registeredActions[actionId] = { - eventName: eventName, - handler: function(event) { - if ('function' === typeof target.send) { - return target.send(actionName, { view: view, event: event, context: context }); - } else { - return target[actionName].call(target, view, event, context); - } - } - }; + if (!content) { return; } + if (prompt && selectedIndex === 0) { set(this, 'selection', null); return; } - view.on('willRerender', function() { - delete ActionHelper.registeredActions[actionId]; - }); + if (prompt) { selectedIndex -= 1; } + set(this, 'selection', content.objectAt(selectedIndex)); + }, - return actionId; -}; + selectionDidChange: Ember.observer(function() { + var el = this.$()[0], + content = get(this, 'content'), + selection = get(this, 'selection'), + selectionIndex = indexOf(content, selection), + prompt = get(this, 'prompt'); -EmberHandlebars.registerHelper('action', function(actionName, options) { - var hash = options.hash || {}, - eventName = options.hash.on || "click", - view = options.data.view, - target, context; + if (prompt) { selectionIndex += 1; } + if (el) { el.selectedIndex = selectionIndex; } + }, 'selection') +}); - if (view.isVirtual) { view = view.get('parentView'); } - target = options.hash.target ? getPath(this, options.hash.target) : view; - context = options.contexts[0]; +Ember.SelectOption = Ember.View.extend({ + tagName: 'option', + template: Ember.Handlebars.compile("{{label}}"), + attributeBindings: ['value', 'selected'], - var actionId = ActionHelper.registerAction(actionName, eventName, target, view, context); - return new EmberHandlebars.SafeString('data-ember-action="' + actionId + '"'); -}); + init: function() { + this.labelPathDidChange(); + this.valuePathDidChange(); -})({}); + this._super(); + }, + selected: Ember.computed(function() { + // Primitives get passed through bindings as objects... since `new Number(4) !== 4`, we use `==` below + return get(this, 'content') == getPath(this, 'parentView.selection'); + }).property('content', 'parentView.selection'), -(function(exports) { -var get = Ember.get, set = Ember.set; + labelPathDidChange: Ember.observer(function() { + var labelPath = getPath(this, 'parentView.optionLabelPath'); -Ember.Handlebars.registerHelper('yield', function(options) { - var view = options.data.view, template; + if (!labelPath) { return; } - while (view && !get(view, 'layout')) { - view = get(view, 'parentView'); - } + Ember.defineProperty(this, 'label', Ember.computed(function() { + return getPath(this, labelPath); + }).property(labelPath).cacheable()); + }, 'parentView.optionLabelPath'), - ember_assert("You called yield in a template that was not a layout", !!view); + valuePathDidChange: Ember.observer(function() { + var valuePath = getPath(this, 'parentView.optionValuePath'); - template = get(view, 'template'); + if (!valuePath) { return; } - ember_assert("You called yield on " + view.toString() + " without supplying a template", !!template); - template(this, options); + Ember.defineProperty(this, 'value', Ember.computed(function() { + return getPath(this, valuePath); + }).property(valuePath).cacheable()); + }, 'parentView.optionValuePath') }); -})({}); +})({}); (function(exports) { // ========================================================================== @@ -16230,8 +16653,8 @@ Ember.Handlebars.registerHelper('yield', function(options) { // Copyright: ©2011 Strobe Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== -})({}); +})({}); (function(exports) { // ========================================================================== @@ -16266,6 +16689,9 @@ Ember.Handlebars.bootstrap = function(ctx) { var compile = (script.attr('type') === 'text/x-raw-handlebars') ? Ember.$.proxy(Handlebars.compile, Handlebars) : Ember.$.proxy(Ember.Handlebars.compile, Ember.Handlebars), + // Get the id of the script, used by Ember.View's elementId property, + // Look for data-element-id attribute. + elementId = script.attr('data-element-id'), // Get the name of the script, used by Ember.View's templateName property. // First look for data-template-name attribute, then fall back to its // id if no name is found. @@ -16301,6 +16727,7 @@ Ember.Handlebars.bootstrap = function(ctx) { tagName = script.attr('data-tag-name'); view = view.create({ + elementId: elementId, template: template, tagName: (tagName) ? tagName : undefined }); @@ -16323,20 +16750,21 @@ Ember.$(document).ready( })({}); - (function(exports) { // ========================================================================== // Project: Ember Handlebar Views // Copyright: ©2011 Strobe Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== -})({}); +})({}); (function(exports) { // ========================================================================== -// Project: Ember Handlebar Views +// Project: Ember // Copyright: ©2011 Strobe Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== + })({}); +