diff options
Diffstat (limited to 'public/js/libs/ember.js')
-rw-r--r-- | public/js/libs/ember.js | 6819 |
1 files changed, 4508 insertions, 2311 deletions
diff --git a/public/js/libs/ember.js b/public/js/libs/ember.js index 7f44724..2bfa603 100644 --- a/public/js/libs/ember.js +++ b/public/js/libs/ember.js @@ -1,5 +1,5 @@ -// Version: v1.0.0-rc.2 -// Last commit: 656fa6e (2013-03-29 13:40:38 -0700) +// Version: v1.0.0-rc.5-1-gf84c193 +// Last commit: f84c193 (2013-06-01 13:57:19 -0400) (function() { @@ -151,8 +151,8 @@ Ember.deprecateFunc = function(message, func) { })(); -// Version: v1.0.0-rc.2 -// Last commit: 656fa6e (2013-03-29 13:40:38 -0700) +// Version: v1.0.0-rc.5-1-gf84c193 +// Last commit: f84c193 (2013-06-01 13:57:19 -0400) (function() { @@ -169,11 +169,18 @@ var define, requireModule; if (seen[name]) { return seen[name]; } seen[name] = {}; - var mod = registry[name], - deps = mod.deps, - callback = mod.callback, - reified = [], - exports; + var mod, deps, callback, reified, exports; + + mod = registry[name]; + + if (!mod) { + throw new Error("Module '" + name + "' not found."); + } + + deps = mod.deps; + callback = mod.callback; + reified = []; + exports; for (var i=0, l=deps.length; i<l; i++) { if (deps[i] === 'exports') { @@ -212,7 +219,7 @@ var define, requireModule; @class Ember @static - @version 1.0.0-rc.2 + @version 1.0.0-rc.5 */ if ('undefined' === typeof Ember) { @@ -239,10 +246,10 @@ Ember.toString = function() { return "Ember"; }; /** @property VERSION @type String - @default '1.0.0-rc.2' + @default '1.0.0-rc.5' @final */ -Ember.VERSION = '1.0.0-rc.2'; +Ember.VERSION = '1.0.0-rc.5'; /** Standard environmental variables. You can define these in a global `ENV` @@ -391,7 +398,7 @@ Ember.onerror = null; /** @private - Wrap code block in a try/catch if {{#crossLink "Ember/onerror"}}{{/crossLink}} is set. + Wrap code block in a try/catch if `Ember.onerror` is set. @method handleErrors @for Ember @@ -467,7 +474,7 @@ Ember.none = Ember.deprecateFunc("Ember.none is deprecated. Please use Ember.isN @return {Boolean} */ Ember.isEmpty = function(obj) { - return obj === null || obj === undefined || (obj.length === 0 && typeof obj !== 'function') || (typeof obj === 'object' && Ember.get(obj, 'length') === 0); + return Ember.isNone(obj) || (obj.length === 0 && typeof obj !== 'function') || (typeof obj === 'object' && Ember.get(obj, 'length') === 0); }; Ember.empty = Ember.deprecateFunc("Ember.empty is deprecated. Please use Ember.isEmpty instead.", Ember.isEmpty) ; @@ -500,6 +507,13 @@ var platform = Ember.platform = {}; */ Ember.create = Object.create; +// IE8 has Object.create but it couldn't treat property descripters. +if (Ember.create) { + if (Ember.create({a: 1}, {a: {value: 2}}).a !== 2) { + Ember.create = null; + } +} + // STUB_OBJECT_CREATE allows us to override other libraries that stub // Object.create different than we would prefer if (!Ember.create || Ember.ENV.STUB_OBJECT_CREATE) { @@ -635,6 +649,103 @@ if (Ember.ENV.MANDATORY_SETTER && !platform.hasPropertyAccessors) { (function() { +/*jshint newcap:false*/ +/** +@module ember-metal +*/ + +// NOTE: There is a bug in jshint that doesn't recognize `Object()` without `new` +// as being ok unless both `newcap:false` and not `use strict`. +// https://github.com/jshint/jshint/issues/392 + +// Testing this is not ideal, but we want to use native functions +// if available, but not to use versions created by libraries like Prototype +var isNativeFunc = function(func) { + // This should probably work in all browsers likely to have ES5 array methods + return func && Function.prototype.toString.call(func).indexOf('[native code]') > -1; +}; + +// From: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/map +var arrayMap = isNativeFunc(Array.prototype.map) ? Array.prototype.map : function(fun /*, thisp */) { + //"use strict"; + + if (this === void 0 || this === null) { + throw new TypeError(); + } + + var t = Object(this); + var len = t.length >>> 0; + if (typeof fun !== "function") { + throw new TypeError(); + } + + var res = new Array(len); + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (i in t) { + res[i] = fun.call(thisp, t[i], i, t); + } + } + + return res; +}; + +// From: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/foreach +var arrayForEach = isNativeFunc(Array.prototype.forEach) ? Array.prototype.forEach : function(fun /*, thisp */) { + //"use strict"; + + if (this === void 0 || this === null) { + throw new TypeError(); + } + + var t = Object(this); + var len = t.length >>> 0; + if (typeof fun !== "function") { + throw new TypeError(); + } + + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (i in t) { + fun.call(thisp, t[i], i, t); + } + } +}; + +var arrayIndexOf = isNativeFunc(Array.prototype.indexOf) ? Array.prototype.indexOf : function (obj, fromIndex) { + if (fromIndex === null || fromIndex === undefined) { fromIndex = 0; } + else if (fromIndex < 0) { fromIndex = Math.max(0, this.length + fromIndex); } + for (var i = fromIndex, j = this.length; i < j; i++) { + if (this[i] === obj) { return i; } + } + return -1; +}; + +Ember.ArrayPolyfills = { + map: arrayMap, + forEach: arrayForEach, + indexOf: arrayIndexOf +}; + +if (Ember.SHIM_ES5) { + if (!Array.prototype.map) { + Array.prototype.map = arrayMap; + } + + if (!Array.prototype.forEach) { + Array.prototype.forEach = arrayForEach; + } + + if (!Array.prototype.indexOf) { + Array.prototype.indexOf = arrayIndexOf; + } +} + +})(); + + + +(function() { /** @module ember-metal */ @@ -723,7 +834,7 @@ Ember.guidFor = function guidFor(obj) { if (obj === undefined) return "(undefined)"; if (obj === null) return "(null)"; - var cache, ret; + var ret; var type = typeof obj; // Don't allow prototype changes to String etc. to change the guidFor @@ -872,6 +983,7 @@ Ember.setMeta = function setMeta(obj, property, value) { }; /** + @deprecated @private In order to store defaults for a class, a prototype may need to create @@ -904,6 +1016,7 @@ Ember.setMeta = function setMeta(obj, property, value) { shared with its constructor */ Ember.metaPath = function metaPath(obj, path, writable) { + Ember.deprecate("Ember.metaPath is deprecated and will be removed from future releases."); var meta = Ember.meta(obj, writable), keyName, value; for (var i=0, l=path.length; i<l; i++) { @@ -1036,7 +1149,7 @@ Ember.canInvoke = canInvoke; @param {Object} obj The object to check for the method @param {String} methodName The method name to check for @param {Array} [args] The arguments to pass to the method - @return {anything} the return value of the invoked method or undefined if it cannot be invoked + @return {*} the return value of the invoked method or undefined if it cannot be invoked */ Ember.tryInvoke = function(obj, methodName, args) { if (canInvoke(obj, methodName)) { @@ -1067,7 +1180,7 @@ var needsFinallyFix = (function() { @param {Function} tryable The function to run the try callback @param {Function} finalizer The function to run the finally callback @param [binding] - @return {anything} The return value is the that of the finalizer, + @return {*} The return value is the that of the finalizer, unless that valueis undefined, in which case it is the return value of the tryable */ @@ -1118,13 +1231,13 @@ if (needsFinallyFix) { @param {Function} catchable The function to run the catchable callback @param {Function} finalizer The function to run the finally callback @param [binding] - @return {anything} The return value is the that of the finalizer, + @return {*} The return value is the that of the finalizer, unless that value is undefined, in which case it is the return value of the tryable. */ if (needsFinallyFix) { Ember.tryCatchFinally = function(tryable, catchable, finalizer, binding) { - var result, finalResult, finalError, finalReturn; + var result, finalResult, finalError; binding = binding || this; @@ -1162,6 +1275,79 @@ if (needsFinallyFix) { }; } +// ........................................ +// TYPING & ARRAY MESSAGING +// + +var TYPE_MAP = {}; +var t = "Boolean Number String Function Array Date RegExp Object".split(" "); +Ember.ArrayPolyfills.forEach.call(t, function(name) { + TYPE_MAP[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +var toString = Object.prototype.toString; + +/** + Returns a consistent type for the passed item. + + Use this instead of the built-in `typeof` to get the type of an item. + It will return the same result across all browsers and includes a bit + more detail. Here is what will be returned: + + | Return Value | Meaning | + |---------------|------------------------------------------------------| + | 'string' | String primitive | + | 'number' | Number primitive | + | 'boolean' | Boolean primitive | + | 'null' | Null value | + | 'undefined' | Undefined value | + | 'function' | A function | + | 'array' | An instance of Array | + | 'class' | An Ember class (created using Ember.Object.extend()) | + | 'instance' | An Ember object instance | + | 'error' | An instance of the Error object | + | 'object' | A JavaScript object not inheriting from Ember.Object | + + Examples: + + ```javascript + Ember.typeOf(); // 'undefined' + Ember.typeOf(null); // 'null' + Ember.typeOf(undefined); // 'undefined' + Ember.typeOf('michael'); // 'string' + Ember.typeOf(101); // 'number' + Ember.typeOf(true); // 'boolean' + Ember.typeOf(Ember.makeArray); // 'function' + Ember.typeOf([1,2,90]); // 'array' + Ember.typeOf(Ember.Object.extend()); // 'class' + Ember.typeOf(Ember.Object.create()); // 'instance' + Ember.typeOf(new Error('teamocil')); // 'error' + + // "normal" JavaScript object + Ember.typeOf({a: 'b'}); // 'object' + ``` + + @method typeOf + @for Ember + @param {Object} item the item to check + @return {String} the type +*/ +Ember.typeOf = function(item) { + var ret; + + ret = (item === null || item === undefined) ? String(item) : TYPE_MAP[toString.call(item)] || 'object'; + + if (ret === 'function') { + if (Ember.Object && Ember.Object.detect(item)) ret = 'class'; + } else if (ret === 'object') { + if (item instanceof Error) ret = 'error'; + else if (Ember.Object && item instanceof Ember.Object) ret = 'instance'; + else ret = 'object'; + } + + return ret; +}; + })(); @@ -1342,17 +1528,23 @@ Ember.subscribe = Ember.Instrumentation.subscribe; (function() { +var map, forEach, indexOf, concat; +concat = Array.prototype.concat; +map = Array.prototype.map || Ember.ArrayPolyfills.map; +forEach = Array.prototype.forEach || Ember.ArrayPolyfills.forEach; +indexOf = Array.prototype.indexOf || Ember.ArrayPolyfills.indexOf; + var utils = Ember.EnumerableUtils = { map: function(obj, callback, thisArg) { - return obj.map ? obj.map.call(obj, callback, thisArg) : Array.prototype.map.call(obj, callback, thisArg); + return obj.map ? obj.map.call(obj, callback, thisArg) : map.call(obj, callback, thisArg); }, forEach: function(obj, callback, thisArg) { - return obj.forEach ? obj.forEach.call(obj, callback, thisArg) : Array.prototype.forEach.call(obj, callback, thisArg); + return obj.forEach ? obj.forEach.call(obj, callback, thisArg) : forEach.call(obj, callback, thisArg); }, indexOf: function(obj, element, index) { - return obj.indexOf ? obj.indexOf.call(obj, element, index) : Array.prototype.indexOf.call(obj, element, index); + return obj.indexOf ? obj.indexOf.call(obj, element, index) : indexOf.call(obj, element, index); }, indexesOf: function(obj, elements) { @@ -1375,7 +1567,7 @@ var utils = Ember.EnumerableUtils = { if (array.replace) { return array.replace(idx, amt, objects); } else { - var args = Array.prototype.concat.apply([idx, amt], objects); + var args = concat.apply([idx, amt], objects); return array.splice.apply(array, args); } }, @@ -1383,8 +1575,8 @@ var utils = Ember.EnumerableUtils = { intersection: function(array1, array2) { var intersection = []; - array1.forEach(function(element) { - if (array2.indexOf(element) >= 0) { + utils.forEach(array1, function(element) { + if (utils.indexOf(array2, element) >= 0) { intersection.push(element); } }); @@ -1398,98 +1590,982 @@ var utils = Ember.EnumerableUtils = { (function() { -/*jshint newcap:false*/ /** @module ember-metal */ -// NOTE: There is a bug in jshint that doesn't recognize `Object()` without `new` -// as being ok unless both `newcap:false` and not `use strict`. -// https://github.com/jshint/jshint/issues/392 +var META_KEY = Ember.META_KEY, get; -// Testing this is not ideal, but we want to use native functions -// if available, but not to use versions created by libraries like Prototype -var isNativeFunc = function(func) { - // This should probably work in all browsers likely to have ES5 array methods - return func && Function.prototype.toString.call(func).indexOf('[native code]') > -1; +var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; + +var IS_GLOBAL_PATH = /^([A-Z$]|([0-9][A-Z$])).*[\.\*]/; +var HAS_THIS = /^this[\.\*]/; +var FIRST_KEY = /^([^\.\*]+)/; + +// .......................................................... +// GET AND SET +// +// If we are on a platform that supports accessors we can use those. +// Otherwise simulate accessors by looking up the property directly on the +// object. + +/** + Gets the value of a property on an object. If the property is computed, + the function will be invoked. If the property is not defined but the + object implements the `unknownProperty` method then that will be invoked. + + If you plan to run on IE8 and older browsers then you should use this + method anytime you want to retrieve a property on an object that you don't + know for sure is private. (Properties beginning with an underscore '_' + are considered private.) + + On all newer browsers, you only need to use this method to retrieve + properties if the property might not be defined on the object and you want + to respect the `unknownProperty` handler. Otherwise you can ignore this + method. + + Note that if the object itself is `undefined`, this method will throw + an error. + + @method get + @for Ember + @param {Object} obj The object to retrieve from. + @param {String} keyName The property key to retrieve + @return {Object} the property value or `null`. +*/ +get = function get(obj, keyName) { + // Helpers that operate with 'this' within an #each + if (keyName === '') { + return obj; + } + + if (!keyName && 'string'===typeof obj) { + keyName = obj; + obj = null; + } + + Ember.assert("Cannot call get with '"+ keyName +"' on an undefined object.", obj !== undefined); + + if (obj === null || keyName.indexOf('.') !== -1) { + return getPath(obj, keyName); + } + + var meta = obj[META_KEY], desc = meta && meta.descs[keyName], ret; + if (desc) { + return desc.get(obj, keyName); + } else { + if (MANDATORY_SETTER && meta && meta.watching[keyName] > 0) { + ret = meta.values[keyName]; + } else { + ret = obj[keyName]; + } + + if (ret === undefined && + 'object' === typeof obj && !(keyName in obj) && 'function' === typeof obj.unknownProperty) { + return obj.unknownProperty(keyName); + } + + return ret; + } }; -// From: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/map -var arrayMap = isNativeFunc(Array.prototype.map) ? Array.prototype.map : function(fun /*, thisp */) { - //"use strict"; +// Currently used only by Ember Data tests +if (Ember.config.overrideAccessors) { + Ember.get = get; + Ember.config.overrideAccessors(); + get = Ember.get; +} - if (this === void 0 || this === null) { - throw new TypeError(); +function firstKey(path) { + return path.match(FIRST_KEY)[0]; +} + +// assumes path is already normalized +function normalizeTuple(target, path) { + var hasThis = HAS_THIS.test(path), + isGlobal = !hasThis && IS_GLOBAL_PATH.test(path), + key; + + if (!target || isGlobal) target = Ember.lookup; + if (hasThis) path = path.slice(5); + + if (target === Ember.lookup) { + key = firstKey(path); + target = get(target, key); + path = path.slice(key.length+1); } - var t = Object(this); - var len = t.length >>> 0; - if (typeof fun !== "function") { - throw new TypeError(); + // must return some kind of path to be valid else other things will break. + if (!path || path.length===0) throw new Error('Invalid Path'); + + return [ target, path ]; +} + +var getPath = Ember._getPath = function(root, path) { + var hasThis, parts, tuple, idx, len; + + // If there is no root and path is a key name, return that + // property from the global object. + // E.g. get('Ember') -> Ember + if (root === null && path.indexOf('.') === -1) { return get(Ember.lookup, path); } + + // detect complicated paths and normalize them + hasThis = HAS_THIS.test(path); + + if (!root || hasThis) { + tuple = normalizeTuple(root, path); + root = tuple[0]; + path = tuple[1]; + tuple.length = 0; } - var res = new Array(len); - var thisp = arguments[1]; - for (var i = 0; i < len; i++) { - if (i in t) { - res[i] = fun.call(thisp, t[i], i, t); + parts = path.split("."); + len = parts.length; + for (idx = 0; root != null && idx < len; idx++) { + root = get(root, parts[idx], true); + if (root && root.isDestroyed) { return undefined; } + } + return root; +}; + +/** + @private + + Normalizes a target/path pair to reflect that actual target/path that should + be observed, etc. This takes into account passing in global property + paths (i.e. a path beginning with a captial letter not defined on the + target) and * separators. + + @method normalizeTuple + @for Ember + @param {Object} target The current target. May be `null`. + @param {String} path A path on the target or a global property path. + @return {Array} a temporary array with the normalized target/path pair. +*/ +Ember.normalizeTuple = function(target, path) { + return normalizeTuple(target, path); +}; + +Ember.getWithDefault = function(root, key, defaultValue) { + var value = get(root, key); + + if (value === undefined) { return defaultValue; } + return value; +}; + + +Ember.get = get; +Ember.getPath = Ember.deprecateFunc('getPath is deprecated since get now supports paths', Ember.get); + +})(); + + + +(function() { +/** +@module ember-metal +*/ + +var o_create = Ember.create, + metaFor = Ember.meta, + META_KEY = Ember.META_KEY, + /* listener flags */ + ONCE = 1, SUSPENDED = 2; + +/* + The event system uses a series of nested hashes to store listeners on an + object. When a listener is registered, or when an event arrives, these + hashes are consulted to determine which target and action pair to invoke. + + The hashes are stored in the object's meta hash, and look like this: + + // Object's meta hash + { + listeners: { // variable name: `listenerSet` + "foo:changed": [ // variable name: `actions` + [target, method, flags] + ] + } + } + +*/ + +function indexOf(array, target, method) { + var index = -1; + for (var i = 0, l = array.length; i < l; i++) { + if (target === array[i][0] && method === array[i][1]) { index = i; break; } + } + return index; +} + +function actionsFor(obj, eventName) { + var meta = metaFor(obj, true), + actions; + + if (!meta.listeners) { meta.listeners = {}; } + + if (!meta.hasOwnProperty('listeners')) { + // setup inherited copy of the listeners object + meta.listeners = o_create(meta.listeners); + } + + actions = meta.listeners[eventName]; + + // if there are actions, but the eventName doesn't exist in our listeners, then copy them from the prototype + if (actions && !meta.listeners.hasOwnProperty(eventName)) { + actions = meta.listeners[eventName] = meta.listeners[eventName].slice(); + } else if (!actions) { + actions = meta.listeners[eventName] = []; + } + + return actions; +} + +function actionsUnion(obj, eventName, otherActions) { + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName]; + + if (!actions) { return; } + for (var i = actions.length - 1; i >= 0; i--) { + var target = actions[i][0], + method = actions[i][1], + flags = actions[i][2], + actionIndex = indexOf(otherActions, target, method); + + if (actionIndex === -1) { + otherActions.push([target, method, flags]); } } +} - return res; +function actionsDiff(obj, eventName, otherActions) { + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName], + diffActions = []; + + if (!actions) { return; } + for (var i = actions.length - 1; i >= 0; i--) { + var target = actions[i][0], + method = actions[i][1], + flags = actions[i][2], + actionIndex = indexOf(otherActions, target, method); + + if (actionIndex !== -1) { continue; } + + otherActions.push([target, method, flags]); + diffActions.push([target, method, flags]); + } + + return diffActions; +} + +/** + Add an event listener + + @method addListener + @for Ember + @param obj + @param {String} eventName + @param {Object|Function} targetOrMethod A target object or a function + @param {Function|String} method A function or the name of a function to be called on `target` + @param {Boolean} once A flag whether a function should only be called once +*/ +function addListener(obj, eventName, target, method, once) { + Ember.assert("You must pass at least an object and event name to Ember.addListener", !!obj && !!eventName); + + if (!method && 'function' === typeof target) { + method = target; + target = null; + } + + var actions = actionsFor(obj, eventName), + actionIndex = indexOf(actions, target, method), + flags = 0; + + if (once) flags |= ONCE; + + if (actionIndex !== -1) { return; } + + actions.push([target, method, flags]); + + if ('function' === typeof obj.didAddListener) { + obj.didAddListener(eventName, target, method); + } +} + +/** + Remove an event listener + + Arguments should match those passed to `Ember.addListener`. + + @method removeListener + @for Ember + @param obj + @param {String} eventName + @param {Object|Function} targetOrMethod A target object or a function + @param {Function|String} method A function or the name of a function to be called on `target` +*/ +function removeListener(obj, eventName, target, method) { + Ember.assert("You must pass at least an object and event name to Ember.removeListener", !!obj && !!eventName); + + if (!method && 'function' === typeof target) { + method = target; + target = null; + } + + function _removeListener(target, method) { + var actions = actionsFor(obj, eventName), + actionIndex = indexOf(actions, target, method); + + // action doesn't exist, give up silently + if (actionIndex === -1) { return; } + + actions.splice(actionIndex, 1); + + if ('function' === typeof obj.didRemoveListener) { + obj.didRemoveListener(eventName, target, method); + } + } + + if (method) { + _removeListener(target, method); + } else { + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName]; + + if (!actions) { return; } + for (var i = actions.length - 1; i >= 0; i--) { + _removeListener(actions[i][0], actions[i][1]); + } + } +} + +/** + @private + + Suspend listener during callback. + + This should only be used by the target of the event listener + when it is taking an action that would cause the event, e.g. + an object might suspend its property change listener while it is + setting that property. + + @method suspendListener + @for Ember + @param obj + @param {String} eventName + @param {Object|Function} targetOrMethod A target object or a function + @param {Function|String} method A function or the name of a function to be called on `target` + @param {Function} callback +*/ +function suspendListener(obj, eventName, target, method, callback) { + if (!method && 'function' === typeof target) { + method = target; + target = null; + } + + var actions = actionsFor(obj, eventName), + actionIndex = indexOf(actions, target, method), + action; + + if (actionIndex !== -1) { + action = actions[actionIndex].slice(); // copy it, otherwise we're modifying a shared object + action[2] |= SUSPENDED; // mark the action as suspended + actions[actionIndex] = action; // replace the shared object with our copy + } + + function tryable() { return callback.call(target); } + function finalizer() { if (action) { action[2] &= ~SUSPENDED; } } + + return Ember.tryFinally(tryable, finalizer); +} + +/** + @private + + Suspend listener during callback. + + This should only be used by the target of the event listener + when it is taking an action that would cause the event, e.g. + an object might suspend its property change listener while it is + setting that property. + + @method suspendListener + @for Ember + @param obj + @param {Array} eventName Array of event names + @param {Object|Function} targetOrMethod A target object or a function + @param {Function|String} method A function or the name of a function to be called on `target` + @param {Function} callback +*/ +function suspendListeners(obj, eventNames, target, method, callback) { + if (!method && 'function' === typeof target) { + method = target; + target = null; + } + + var suspendedActions = [], + eventName, actions, action, i, l; + + for (i=0, l=eventNames.length; i<l; i++) { + eventName = eventNames[i]; + actions = actionsFor(obj, eventName); + var actionIndex = indexOf(actions, target, method); + + if (actionIndex !== -1) { + action = actions[actionIndex].slice(); + action[2] |= SUSPENDED; + actions[actionIndex] = action; + suspendedActions.push(action); + } + } + + function tryable() { return callback.call(target); } + + function finalizer() { + for (i = 0, l = suspendedActions.length; i < l; i++) { + suspendedActions[i][2] &= ~SUSPENDED; + } + } + + return Ember.tryFinally(tryable, finalizer); +} + +/** + @private + + Return a list of currently watched events + + @method watchedEvents + @for Ember + @param obj +*/ +function watchedEvents(obj) { + var listeners = obj[META_KEY].listeners, ret = []; + + if (listeners) { + for(var eventName in listeners) { + if (listeners[eventName]) { ret.push(eventName); } + } + } + return ret; +} + +/** + @method sendEvent + @for Ember + @param obj + @param {String} eventName + @param {Array} params + @param {Array} actions + @return true +*/ +function sendEvent(obj, eventName, params, actions) { + // first give object a chance to handle it + if (obj !== Ember && 'function' === typeof obj.sendEvent) { + obj.sendEvent(eventName, params); + } + + if (!actions) { + var meta = obj[META_KEY]; + actions = meta && meta.listeners && meta.listeners[eventName]; + } + + if (!actions) { return; } + + for (var i = actions.length - 1; i >= 0; i--) { // looping in reverse for once listeners + var action = actions[i]; + if (!action) { continue; } + var target = action[0], method = action[1], flags = action[2]; + if (flags & SUSPENDED) { continue; } + if (flags & ONCE) { removeListener(obj, eventName, target, method); } + if (!target) { target = obj; } + if ('string' === typeof method) { method = target[method]; } + if (params) { + method.apply(target, params); + } else { + method.call(target); + } + } + return true; +} + +/** + @private + @method hasListeners + @for Ember + @param obj + @param {String} eventName +*/ +function hasListeners(obj, eventName) { + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName]; + + return !!(actions && actions.length); +} + +/** + @private + @method listenersFor + @for Ember + @param obj + @param {String} eventName +*/ +function listenersFor(obj, eventName) { + var ret = []; + var meta = obj[META_KEY], + actions = meta && meta.listeners && meta.listeners[eventName]; + + if (!actions) { return ret; } + + for (var i = 0, l = actions.length; i < l; i++) { + var target = actions[i][0], + method = actions[i][1]; + ret.push([target, method]); + } + + return ret; +} + +Ember.addListener = addListener; +Ember.removeListener = removeListener; +Ember._suspendListener = suspendListener; +Ember._suspendListeners = suspendListeners; +Ember.sendEvent = sendEvent; +Ember.hasListeners = hasListeners; +Ember.watchedEvents = watchedEvents; +Ember.listenersFor = listenersFor; +Ember.listenersDiff = actionsDiff; +Ember.listenersUnion = actionsUnion; + +})(); + + + +(function() { +var guidFor = Ember.guidFor, + sendEvent = Ember.sendEvent; + +/* + this.observerSet = { + [senderGuid]: { // variable name: `keySet` + [keyName]: listIndex + } + }, + this.observers = [ + { + sender: obj, + keyName: keyName, + eventName: eventName, + listeners: [ + [target, method, flags] + ] + }, + ... + ] +*/ +var ObserverSet = Ember._ObserverSet = function() { + this.clear(); }; -// From: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/foreach -var arrayForEach = isNativeFunc(Array.prototype.forEach) ? Array.prototype.forEach : function(fun /*, thisp */) { - //"use strict"; +ObserverSet.prototype.add = function(sender, keyName, eventName) { + var observerSet = this.observerSet, + observers = this.observers, + senderGuid = guidFor(sender), + keySet = observerSet[senderGuid], + index; - if (this === void 0 || this === null) { - throw new TypeError(); + if (!keySet) { + observerSet[senderGuid] = keySet = {}; } + index = keySet[keyName]; + if (index === undefined) { + index = observers.push({ + sender: sender, + keyName: keyName, + eventName: eventName, + listeners: [] + }) - 1; + keySet[keyName] = index; + } + return observers[index].listeners; +}; - var t = Object(this); - var len = t.length >>> 0; - if (typeof fun !== "function") { - throw new TypeError(); +ObserverSet.prototype.flush = function() { + var observers = this.observers, i, len, observer, sender; + this.clear(); + for (i=0, len=observers.length; i < len; ++i) { + observer = observers[i]; + sender = observer.sender; + if (sender.isDestroying || sender.isDestroyed) { continue; } + sendEvent(sender, observer.eventName, [sender, observer.keyName], observer.listeners); } +}; - var thisp = arguments[1]; - for (var i = 0; i < len; i++) { - if (i in t) { - fun.call(thisp, t[i], i, t); +ObserverSet.prototype.clear = function() { + this.observerSet = {}; + this.observers = []; +}; +})(); + + + +(function() { +var metaFor = Ember.meta, + guidFor = Ember.guidFor, + tryFinally = Ember.tryFinally, + sendEvent = Ember.sendEvent, + listenersUnion = Ember.listenersUnion, + listenersDiff = Ember.listenersDiff, + ObserverSet = Ember._ObserverSet, + beforeObserverSet = new ObserverSet(), + observerSet = new ObserverSet(), + deferred = 0; + +// .......................................................... +// PROPERTY CHANGES +// + +/** + This function is called just before an object property is about to change. + It will notify any before observers and prepare caches among other things. + + Normally you will not need to call this method directly but if for some + reason you can't directly watch a property you can invoke this method + manually along with `Ember.propertyDidChange()` which you should call just + after the property value changes. + + @method propertyWillChange + @for Ember + @param {Object} obj The object with the property that will change + @param {String} keyName The property key (or path) that will change. + @return {void} +*/ +var propertyWillChange = Ember.propertyWillChange = function(obj, keyName) { + var m = metaFor(obj, false), + watching = m.watching[keyName] > 0 || keyName === 'length', + proto = m.proto, + desc = m.descs[keyName]; + + if (!watching) { return; } + if (proto === obj) { return; } + if (desc && desc.willChange) { desc.willChange(obj, keyName); } + dependentKeysWillChange(obj, keyName, m); + chainsWillChange(obj, keyName, m); + notifyBeforeObservers(obj, keyName); +}; + +/** + This function is called just after an object property has changed. + It will notify any observers and clear caches among other things. + + Normally you will not need to call this method directly but if for some + reason you can't directly watch a property you can invoke this method + manually along with `Ember.propertyWilLChange()` which you should call just + before the property value changes. + + @method propertyDidChange + @for Ember + @param {Object} obj The object with the property that will change + @param {String} keyName The property key (or path) that will change. + @return {void} +*/ +var propertyDidChange = Ember.propertyDidChange = function(obj, keyName) { + var m = metaFor(obj, false), + watching = m.watching[keyName] > 0 || keyName === 'length', + proto = m.proto, + desc = m.descs[keyName]; + + if (proto === obj) { return; } + + // shouldn't this mean that we're watching this key? + if (desc && desc.didChange) { desc.didChange(obj, keyName); } + if (!watching && keyName !== 'length') { return; } + + dependentKeysDidChange(obj, keyName, m); + chainsDidChange(obj, keyName, m); + notifyObservers(obj, keyName); +}; + +var WILL_SEEN, DID_SEEN; + +// called whenever a property is about to change to clear the cache of any dependent keys (and notify those properties of changes, etc...) +function dependentKeysWillChange(obj, depKey, meta) { + if (obj.isDestroying) { return; } + + var seen = WILL_SEEN, top = !seen; + if (top) { seen = WILL_SEEN = {}; } + iterDeps(propertyWillChange, obj, depKey, seen, meta); + if (top) { WILL_SEEN = null; } +} + +// called whenever a property has just changed to update dependent keys +function dependentKeysDidChange(obj, depKey, meta) { + if (obj.isDestroying) { return; } + + var seen = DID_SEEN, top = !seen; + if (top) { seen = DID_SEEN = {}; } + iterDeps(propertyDidChange, obj, depKey, seen, meta); + if (top) { DID_SEEN = null; } +} + +function iterDeps(method, obj, depKey, seen, meta) { + var guid = guidFor(obj); + if (!seen[guid]) seen[guid] = {}; + if (seen[guid][depKey]) return; + seen[guid][depKey] = true; + + var deps = meta.deps; + deps = deps && deps[depKey]; + if (deps) { + for(var key in deps) { + var desc = meta.descs[key]; + if (desc && desc._suspended === obj) continue; + method(obj, key); } } +} + +var chainsWillChange = function(obj, keyName, m, arg) { + if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do + + var nodes = m.chainWatchers; + + nodes = nodes[keyName]; + if (!nodes) { return; } + + for(var i = 0, l = nodes.length; i < l; i++) { + nodes[i].willChange(arg); + } }; -var arrayIndexOf = isNativeFunc(Array.prototype.indexOf) ? Array.prototype.indexOf : function (obj, fromIndex) { - if (fromIndex === null || fromIndex === undefined) { fromIndex = 0; } - else if (fromIndex < 0) { fromIndex = Math.max(0, this.length + fromIndex); } - for (var i = fromIndex, j = this.length; i < j; i++) { - if (this[i] === obj) { return i; } +var chainsDidChange = function(obj, keyName, m, arg) { + if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do + + var nodes = m.chainWatchers; + + nodes = nodes[keyName]; + if (!nodes) { return; } + + // looping in reverse because the chainWatchers array can be modified inside didChange + for (var i = nodes.length - 1; i >= 0; i--) { + nodes[i].didChange(arg); } - return -1; }; -Ember.ArrayPolyfills = { - map: arrayMap, - forEach: arrayForEach, - indexOf: arrayIndexOf +Ember.overrideChains = function(obj, keyName, m) { + chainsDidChange(obj, keyName, m, true); }; -if (Ember.SHIM_ES5) { - if (!Array.prototype.map) { - Array.prototype.map = arrayMap; +/** + @method beginPropertyChanges + @chainable +*/ +var beginPropertyChanges = Ember.beginPropertyChanges = function() { + deferred++; +}; + +/** + @method endPropertyChanges +*/ +var endPropertyChanges = Ember.endPropertyChanges = function() { + deferred--; + if (deferred<=0) { + beforeObserverSet.clear(); + observerSet.flush(); } +}; - if (!Array.prototype.forEach) { - Array.prototype.forEach = arrayForEach; +/** + Make a series of property changes together in an + exception-safe way. + + ```javascript + Ember.changeProperties(function() { + obj1.set('foo', mayBlowUpWhenSet); + obj2.set('bar', baz); + }); + ``` + + @method changeProperties + @param {Function} callback + @param [binding] +*/ +Ember.changeProperties = function(cb, binding){ + beginPropertyChanges(); + tryFinally(cb, endPropertyChanges, binding); +}; + +var notifyBeforeObservers = function(obj, keyName) { + if (obj.isDestroying) { return; } + + var eventName = keyName + ':before', listeners, diff; + if (deferred) { + listeners = beforeObserverSet.add(obj, keyName, eventName); + diff = listenersDiff(obj, eventName, listeners); + sendEvent(obj, eventName, [obj, keyName], diff); + } else { + sendEvent(obj, eventName, [obj, keyName]); } +}; - if (!Array.prototype.indexOf) { - Array.prototype.indexOf = arrayIndexOf; +var notifyObservers = function(obj, keyName) { + if (obj.isDestroying) { return; } + + var eventName = keyName + ':change', listeners; + if (deferred) { + listeners = observerSet.add(obj, keyName, eventName); + listenersUnion(obj, eventName, listeners); + } else { + sendEvent(obj, eventName, [obj, keyName]); + } +}; +})(); + + + +(function() { +// META_KEY +// _getPath +// propertyWillChange, propertyDidChange + +var META_KEY = Ember.META_KEY, + MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, + IS_GLOBAL = /^([A-Z$]|([0-9][A-Z$]))/, + getPath = Ember._getPath; + +/** + Sets the value of a property on an object, respecting computed properties + and notifying observers and other listeners of the change. If the + property is not defined but the object implements the `unknownProperty` + method then that will be invoked as well. + + If you plan to run on IE8 and older browsers then you should use this + method anytime you want to set a property on an object that you don't + know for sure is private. (Properties beginning with an underscore '_' + are considered private.) + + On all newer browsers, you only need to use this method to set + properties if the property might not be defined on the object and you want + to respect the `unknownProperty` handler. Otherwise you can ignore this + method. + + @method set + @for Ember + @param {Object} obj The object to modify. + @param {String} keyName The property key to set + @param {Object} value The value to set + @return {Object} the passed value. +*/ +var set = function set(obj, keyName, value, tolerant) { + if (typeof obj === 'string') { + Ember.assert("Path '" + obj + "' must be global if no obj is given.", IS_GLOBAL.test(obj)); + value = keyName; + keyName = obj; + obj = null; + } + + if (!obj || keyName.indexOf('.') !== -1) { + return setPath(obj, keyName, value, tolerant); + } + + Ember.assert("You need to provide an object and key to `set`.", !!obj && keyName !== undefined); + Ember.assert('calling set on destroyed object', !obj.isDestroyed); + + var meta = obj[META_KEY], desc = meta && meta.descs[keyName], + isUnknown, currentValue; + if (desc) { + desc.set(obj, keyName, value); + } else { + isUnknown = 'object' === typeof obj && !(keyName in obj); + + // setUnknownProperty is called if `obj` is an object, + // the property does not already exist, and the + // `setUnknownProperty` method exists on the object + if (isUnknown && 'function' === typeof obj.setUnknownProperty) { + obj.setUnknownProperty(keyName, value); + } else if (meta && meta.watching[keyName] > 0) { + if (MANDATORY_SETTER) { + currentValue = meta.values[keyName]; + } else { + currentValue = obj[keyName]; + } + // only trigger a change if the value has changed + if (value !== currentValue) { + Ember.propertyWillChange(obj, keyName); + if (MANDATORY_SETTER) { + if (currentValue === undefined && !(keyName in obj)) { + Ember.defineProperty(obj, keyName, null, value); // setup mandatory setter + } else { + meta.values[keyName] = value; + } + } else { + obj[keyName] = value; + } + Ember.propertyDidChange(obj, keyName); + } + } else { + obj[keyName] = value; + } } + return value; +}; + +// Currently used only by Ember Data tests +if (Ember.config.overrideAccessors) { + Ember.set = set; + Ember.config.overrideAccessors(); + set = Ember.set; } +function setPath(root, path, value, tolerant) { + var keyName; + + // get the last part of the path + keyName = path.slice(path.lastIndexOf('.') + 1); + + // get the first part of the part + path = path.slice(0, path.length-(keyName.length+1)); + + // unless the path is this, look up the first part to + // get the root + if (path !== 'this') { + root = getPath(root, path); + } + + if (!keyName || keyName.length === 0) { + throw new Error('You passed an empty path'); + } + + if (!root) { + if (tolerant) { return; } + else { throw new Error('Object in path '+path+' could not be found or was destroyed.'); } + } + + return set(root, keyName, value); +} + +Ember.set = set; +Ember.setPath = Ember.deprecateFunc('setPath is deprecated since set now supports paths', Ember.set); + +/** + Error-tolerant form of `Ember.set`. Will not blow up if any part of the + chain is `undefined`, `null`, or destroyed. + + This is primarily used when syncing bindings, which may try to update after + an object has been destroyed. + + @method trySet + @for Ember + @param {Object} obj The object to modify. + @param {String} path The property path to set + @param {Object} value The value to set +*/ +Ember.trySet = function(root, path, value) { + return set(root, path, value, true); +}; +Ember.trySetPath = Ember.deprecateFunc('trySetPath has been renamed to trySet', Ember.trySet); + })(); @@ -1518,7 +2594,9 @@ if (Ember.SHIM_ES5) { Map is mocked out to look like an Ember object, so you can do `Ember.Map.create()` for symmetry with other Ember classes. */ -var guidFor = Ember.guidFor, +var get = Ember.get, + set = Ember.set, + guidFor = Ember.guidFor, indexOf = Ember.ArrayPolyfills.indexOf; var copy = function(obj) { @@ -1537,6 +2615,7 @@ var copyMap = function(original, newObject) { newObject.keys = keys; newObject.values = values; + newObject.length = original.length; return newObject; }; @@ -1633,7 +2712,7 @@ OrderedSet.prototype = { */ forEach: function(fn, self) { // allow mutation during iteration - var list = this.list.slice(); + var list = this.toArray(); for (var i = 0, j = list.length; i < j; i++) { fn.call(self, list[i]); @@ -1656,7 +2735,7 @@ OrderedSet.prototype = { var set = new OrderedSet(); set.presenceSet = copy(this.presenceSet); - set.list = this.list.slice(); + set.list = this.toArray(); return set; } @@ -1697,11 +2776,21 @@ Map.create = function() { Map.prototype = { /** + This property will change as the number of objects in the map changes. + + @property length + @type number + @default 0 + */ + length: 0, + + + /** Retrieve the value associated with a given key. @method get - @param {anything} key - @return {anything} the value associated with the key, or `undefined` + @param {*} key + @return {*} the value associated with the key, or `undefined` */ get: function(key) { var values = this.values, @@ -1715,8 +2804,8 @@ Map.prototype = { provided, the new value will replace the old value. @method set - @param {anything} key - @param {anything} value + @param {*} key + @param {*} value */ set: function(key, value) { var keys = this.keys, @@ -1725,13 +2814,14 @@ Map.prototype = { keys.add(key); values[guid] = value; + set(this, 'length', keys.list.length); }, /** Removes a value from the map for an associated key. @method remove - @param {anything} key + @param {*} key @return {Boolean} true if an item was removed, false otherwise */ remove: function(key) { @@ -1739,13 +2829,12 @@ Map.prototype = { // to use in browsers that are not ES6 friendly; var keys = this.keys, values = this.values, - guid = guidFor(key), - value; + guid = guidFor(key); if (values.hasOwnProperty(guid)) { keys.remove(key); - value = values[guid]; delete values[guid]; + set(this, 'length', keys.list.length); return true; } else { return false; @@ -1756,7 +2845,7 @@ Map.prototype = { Check whether a key is present. @method has - @param {anything} key + @param {*} key @return {Boolean} true if the item was present, false otherwise */ has: function(key) { @@ -1774,7 +2863,7 @@ Map.prototype = { @method forEach @param {Function} callback - @param {anything} self if passed, the `this` value inside the + @param {*} self if passed, the `this` value inside the callback. By default, `this` is the map. */ forEach: function(callback, self) { @@ -1803,7 +2892,7 @@ Map.prototype = { @private @constructor @param [options] - @param {anything} [options.defaultValue] + @param {*} [options.defaultValue] */ var MapWithDefault = Ember.MapWithDefault = function(options) { Map.call(this); @@ -1814,7 +2903,7 @@ var MapWithDefault = Ember.MapWithDefault = function(options) { @method create @static @param [options] - @param {anything} [options.defaultValue] + @param {*} [options.defaultValue] @return {Ember.MapWithDefault|Ember.Map} If options are passed, returns `Ember.MapWithDefault` otherwise returns `Ember.Map` */ @@ -1832,8 +2921,8 @@ MapWithDefault.prototype = Ember.create(Map.prototype); Retrieve the value associated with a given key. @method get - @param {anything} key - @return {anything} the value associated with the key, or the default value + @param {*} key + @return {*} the value associated with the key, or the default value */ MapWithDefault.prototype.get = function(key) { var hasValue = this.has(key); @@ -1866,322 +2955,6 @@ MapWithDefault.prototype.copy = function() { @module ember-metal */ -var META_KEY = Ember.META_KEY, get, set; - -var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; - -var IS_GLOBAL = /^([A-Z$]|([0-9][A-Z$]))/; -var IS_GLOBAL_PATH = /^([A-Z$]|([0-9][A-Z$])).*[\.\*]/; -var HAS_THIS = /^this[\.\*]/; -var FIRST_KEY = /^([^\.\*]+)/; - -// .......................................................... -// GET AND SET -// -// If we are on a platform that supports accessors we can use those. -// Otherwise simulate accessors by looking up the property directly on the -// object. - -/** - Gets the value of a property on an object. If the property is computed, - the function will be invoked. If the property is not defined but the - object implements the `unknownProperty` method then that will be invoked. - - If you plan to run on IE8 and older browsers then you should use this - method anytime you want to retrieve a property on an object that you don't - know for sure is private. (Properties beginning with an underscore '_' - are considered private.) - - On all newer browsers, you only need to use this method to retrieve - properties if the property might not be defined on the object and you want - to respect the `unknownProperty` handler. Otherwise you can ignore this - method. - - Note that if the object itself is `undefined`, this method will throw - an error. - - @method get - @for Ember - @param {Object} obj The object to retrieve from. - @param {String} keyName The property key to retrieve - @return {Object} the property value or `null`. -*/ -get = function get(obj, keyName) { - // Helpers that operate with 'this' within an #each - if (keyName === '') { - return obj; - } - - if (!keyName && 'string'===typeof obj) { - keyName = obj; - obj = null; - } - - if (!obj || keyName.indexOf('.') !== -1) { - Ember.assert("Cannot call get with '"+ keyName +"' on an undefined object.", obj !== undefined); - return getPath(obj, keyName); - } - - Ember.assert("You need to provide an object and key to `get`.", !!obj && keyName); - - var meta = obj[META_KEY], desc = meta && meta.descs[keyName], ret; - if (desc) { - return desc.get(obj, keyName); - } else { - if (MANDATORY_SETTER && meta && meta.watching[keyName] > 0) { - ret = meta.values[keyName]; - } else { - ret = obj[keyName]; - } - - if (ret === undefined && - 'object' === typeof obj && !(keyName in obj) && 'function' === typeof obj.unknownProperty) { - return obj.unknownProperty(keyName); - } - - return ret; - } -}; - -/** - Sets the value of a property on an object, respecting computed properties - and notifying observers and other listeners of the change. If the - property is not defined but the object implements the `unknownProperty` - method then that will be invoked as well. - - If you plan to run on IE8 and older browsers then you should use this - method anytime you want to set a property on an object that you don't - know for sure is private. (Properties beginning with an underscore '_' - are considered private.) - - On all newer browsers, you only need to use this method to set - properties if the property might not be defined on the object and you want - to respect the `unknownProperty` handler. Otherwise you can ignore this - method. - - @method set - @for Ember - @param {Object} obj The object to modify. - @param {String} keyName The property key to set - @param {Object} value The value to set - @return {Object} the passed value. -*/ -set = function set(obj, keyName, value, tolerant) { - if (typeof obj === 'string') { - Ember.assert("Path '" + obj + "' must be global if no obj is given.", IS_GLOBAL.test(obj)); - value = keyName; - keyName = obj; - obj = null; - } - - if (!obj || keyName.indexOf('.') !== -1) { - return setPath(obj, keyName, value, tolerant); - } - - Ember.assert("You need to provide an object and key to `set`.", !!obj && keyName !== undefined); - Ember.assert('calling set on destroyed object', !obj.isDestroyed); - - var meta = obj[META_KEY], desc = meta && meta.descs[keyName], - isUnknown, currentValue; - if (desc) { - desc.set(obj, keyName, value); - } else { - isUnknown = 'object' === typeof obj && !(keyName in obj); - - // setUnknownProperty is called if `obj` is an object, - // the property does not already exist, and the - // `setUnknownProperty` method exists on the object - if (isUnknown && 'function' === typeof obj.setUnknownProperty) { - obj.setUnknownProperty(keyName, value); - } else if (meta && meta.watching[keyName] > 0) { - if (MANDATORY_SETTER) { - currentValue = meta.values[keyName]; - } else { - currentValue = obj[keyName]; - } - // only trigger a change if the value has changed - if (value !== currentValue) { - Ember.propertyWillChange(obj, keyName); - if (MANDATORY_SETTER) { - if (currentValue === undefined && !(keyName in obj)) { - Ember.defineProperty(obj, keyName, null, value); // setup mandatory setter - } else { - meta.values[keyName] = value; - } - } else { - obj[keyName] = value; - } - Ember.propertyDidChange(obj, keyName); - } - } else { - obj[keyName] = value; - } - } - return value; -}; - -// Currently used only by Ember Data tests -if (Ember.config.overrideAccessors) { - Ember.get = get; - Ember.set = set; - Ember.config.overrideAccessors(); - get = Ember.get; - set = Ember.set; -} - -function firstKey(path) { - return path.match(FIRST_KEY)[0]; -} - -// assumes path is already normalized -function normalizeTuple(target, path) { - var hasThis = HAS_THIS.test(path), - isGlobal = !hasThis && IS_GLOBAL_PATH.test(path), - key; - - if (!target || isGlobal) target = Ember.lookup; - if (hasThis) path = path.slice(5); - - if (target === Ember.lookup) { - key = firstKey(path); - target = get(target, key); - path = path.slice(key.length+1); - } - - // must return some kind of path to be valid else other things will break. - if (!path || path.length===0) throw new Error('Invalid Path'); - - return [ target, path ]; -} - -function getPath(root, path) { - var hasThis, parts, tuple, idx, len; - - // If there is no root and path is a key name, return that - // property from the global object. - // E.g. get('Ember') -> Ember - if (root === null && path.indexOf('.') === -1) { return get(Ember.lookup, path); } - - // detect complicated paths and normalize them - hasThis = HAS_THIS.test(path); - - if (!root || hasThis) { - tuple = normalizeTuple(root, path); - root = tuple[0]; - path = tuple[1]; - tuple.length = 0; - } - - parts = path.split("."); - len = parts.length; - for (idx=0; root && idx<len; idx++) { - root = get(root, parts[idx], true); - if (root && root.isDestroyed) { return undefined; } - } - return root; -} - -function setPath(root, path, value, tolerant) { - var keyName; - - // get the last part of the path - keyName = path.slice(path.lastIndexOf('.') + 1); - - // get the first part of the part - path = path.slice(0, path.length-(keyName.length+1)); - - // unless the path is this, look up the first part to - // get the root - if (path !== 'this') { - root = getPath(root, path); - } - - if (!keyName || keyName.length === 0) { - throw new Error('You passed an empty path'); - } - - if (!root) { - if (tolerant) { return; } - else { throw new Error('Object in path '+path+' could not be found or was destroyed.'); } - } - - return set(root, keyName, value); -} - -/** - @private - - Normalizes a target/path pair to reflect that actual target/path that should - be observed, etc. This takes into account passing in global property - paths (i.e. a path beginning with a captial letter not defined on the - target) and * separators. - - @method normalizeTuple - @for Ember - @param {Object} target The current target. May be `null`. - @param {String} path A path on the target or a global property path. - @return {Array} a temporary array with the normalized target/path pair. -*/ -Ember.normalizeTuple = function(target, path) { - return normalizeTuple(target, path); -}; - -Ember.getWithDefault = function(root, key, defaultValue) { - var value = get(root, key); - - if (value === undefined) { return defaultValue; } - return value; -}; - - -Ember.get = get; -Ember.getPath = Ember.deprecateFunc('getPath is deprecated since get now supports paths', Ember.get); - -Ember.set = set; -Ember.setPath = Ember.deprecateFunc('setPath is deprecated since set now supports paths', Ember.set); - -/** - Error-tolerant form of `Ember.set`. Will not blow up if any part of the - chain is `undefined`, `null`, or destroyed. - - This is primarily used when syncing bindings, which may try to update after - an object has been destroyed. - - @method trySet - @for Ember - @param {Object} obj The object to modify. - @param {String} path The property path to set - @param {Object} value The value to set -*/ -Ember.trySet = function(root, path, value) { - return set(root, path, value, true); -}; -Ember.trySetPath = Ember.deprecateFunc('trySetPath has been renamed to trySet', Ember.trySet); - -/** - Returns true if the provided path is global (e.g., `MyApp.fooController.bar`) - instead of local (`foo.bar.baz`). - - @method isGlobalPath - @for Ember - @private - @param {String} path - @return Boolean -*/ -Ember.isGlobalPath = function(path) { - return IS_GLOBAL.test(path); -}; - - -})(); - - - -(function() { -/** -@module ember-metal -*/ - var META_KEY = Ember.META_KEY, metaFor = Ember.meta, objectDefineProperty = Ember.platform.defineProperty; @@ -2193,7 +2966,7 @@ var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; // /** - Objects of this type can implement an interface to responds requests to + Objects of this type can implement an interface to respond to requests to get and set. The default implementation handles simple properties. You generally won't need to create or subclass this directly. @@ -2203,7 +2976,7 @@ var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; @private @constructor */ -var Descriptor = Ember.Descriptor = function() {}; +Ember.Descriptor = function() {}; // .......................................................... // DEFINING PROPERTIES API @@ -2263,7 +3036,7 @@ var DEFAULT_GETTER_FUNCTION = Ember.DEFAULT_GETTER_FUNCTION = function(name) { @param {Ember.Descriptor} [desc] an instance of `Ember.Descriptor` (typically a computed property) or an ES5 descriptor. You must provide this or `data` but not both. - @param {anything} [data] something other than a descriptor, that will + @param {*} [data] something other than a descriptor, that will become the explicit value of this property. */ Ember.defineProperty = function(obj, keyName, desc, data, meta) { @@ -2334,119 +3107,8 @@ Ember.defineProperty = function(obj, keyName, desc, data, meta) { (function() { -// Ember.tryFinally -/** -@module ember-metal -*/ - -var AFTER_OBSERVERS = ':change'; -var BEFORE_OBSERVERS = ':before'; - -var guidFor = Ember.guidFor; - -var deferred = 0; - -/* - this.observerSet = { - [senderGuid]: { // variable name: `keySet` - [keyName]: listIndex - } - }, - this.observers = [ - { - sender: obj, - keyName: keyName, - eventName: eventName, - listeners: [ - [target, method, onceFlag, suspendedFlag] - ] - }, - ... - ] -*/ -function ObserverSet() { - this.clear(); -} - -ObserverSet.prototype.add = function(sender, keyName, eventName) { - var observerSet = this.observerSet, - observers = this.observers, - senderGuid = Ember.guidFor(sender), - keySet = observerSet[senderGuid], - index; - - if (!keySet) { - observerSet[senderGuid] = keySet = {}; - } - index = keySet[keyName]; - if (index === undefined) { - index = observers.push({ - sender: sender, - keyName: keyName, - eventName: eventName, - listeners: [] - }) - 1; - keySet[keyName] = index; - } - return observers[index].listeners; -}; - -ObserverSet.prototype.flush = function() { - var observers = this.observers, i, len, observer, sender; - this.clear(); - for (i=0, len=observers.length; i < len; ++i) { - observer = observers[i]; - sender = observer.sender; - if (sender.isDestroying || sender.isDestroyed) { continue; } - Ember.sendEvent(sender, observer.eventName, [sender, observer.keyName], observer.listeners); - } -}; - -ObserverSet.prototype.clear = function() { - this.observerSet = {}; - this.observers = []; -}; - -var beforeObserverSet = new ObserverSet(), observerSet = new ObserverSet(); - -/** - @method beginPropertyChanges - @chainable -*/ -Ember.beginPropertyChanges = function() { - deferred++; -}; - -/** - @method endPropertyChanges -*/ -Ember.endPropertyChanges = function() { - deferred--; - if (deferred<=0) { - beforeObserverSet.clear(); - observerSet.flush(); - } -}; - -/** - Make a series of property changes together in an - exception-safe way. - - ```javascript - Ember.changeProperties(function() { - obj1.set('foo', mayBlowUpWhenSet); - obj2.set('bar', baz); - }); - ``` - - @method changeProperties - @param {Function} callback - @param [binding] -*/ -Ember.changeProperties = function(cb, binding){ - Ember.beginPropertyChanges(); - Ember.tryFinally(cb, Ember.endPropertyChanges, binding); -}; +var changeProperties = Ember.changeProperties, + set = Ember.set; /** Set a list of properties on an object. These properties are set inside @@ -2459,214 +3121,116 @@ Ember.changeProperties = function(cb, binding){ @return target */ Ember.setProperties = function(self, hash) { - Ember.changeProperties(function(){ + changeProperties(function(){ for(var prop in hash) { - if (hash.hasOwnProperty(prop)) Ember.set(self, prop, hash[prop]); + if (hash.hasOwnProperty(prop)) { set(self, prop, hash[prop]); } } }); return self; }; +})(); -function changeEvent(keyName) { - return keyName+AFTER_OBSERVERS; -} - -function beforeEvent(keyName) { - return keyName+BEFORE_OBSERVERS; -} - -/** - @method addObserver - @param obj - @param {String} path - @param {Object|Function} targetOrMethod - @param {Function|String} [method] -*/ -Ember.addObserver = function(obj, path, target, method) { - Ember.addListener(obj, changeEvent(path), target, method); - Ember.watch(obj, path); - return this; -}; -Ember.observersFor = function(obj, path) { - return Ember.listenersFor(obj, changeEvent(path)); -}; - -/** - @method removeObserver - @param obj - @param {String} path - @param {Object|Function} targetOrMethod - @param {Function|String} [method] -*/ -Ember.removeObserver = function(obj, path, target, method) { - Ember.unwatch(obj, path); - Ember.removeListener(obj, changeEvent(path), target, method); - return this; -}; +(function() { +var metaFor = Ember.meta, // utils.js + typeOf = Ember.typeOf, // utils.js + MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, + o_defineProperty = Ember.platform.defineProperty; -/** - @method addBeforeObserver - @param obj - @param {String} path - @param {Object|Function} targetOrMethod - @param {Function|String} [method] -*/ -Ember.addBeforeObserver = function(obj, path, target, method) { - Ember.addListener(obj, beforeEvent(path), target, method); - Ember.watch(obj, path); - return this; -}; +Ember.watchKey = function(obj, keyName) { + // can't watch length on Array - it is special... + if (keyName === 'length' && typeOf(obj) === 'array') { return; } -// Suspend observer during callback. -// -// This should only be used by the target of the observer -// while it is setting the observed path. -Ember._suspendBeforeObserver = function(obj, path, target, method, callback) { - return Ember._suspendListener(obj, beforeEvent(path), target, method, callback); -}; + var m = metaFor(obj), watching = m.watching, desc; -Ember._suspendObserver = function(obj, path, target, method, callback) { - return Ember._suspendListener(obj, changeEvent(path), target, method, callback); -}; + // activate watching first time + if (!watching[keyName]) { + watching[keyName] = 1; + desc = m.descs[keyName]; + if (desc && desc.willWatch) { desc.willWatch(obj, keyName); } -var map = Ember.ArrayPolyfills.map; + if ('function' === typeof obj.willWatchProperty) { + obj.willWatchProperty(keyName); + } -Ember._suspendBeforeObservers = function(obj, paths, target, method, callback) { - var events = map.call(paths, beforeEvent); - return Ember._suspendListeners(obj, events, target, method, callback); + if (MANDATORY_SETTER && keyName in obj) { + m.values[keyName] = obj[keyName]; + o_defineProperty(obj, keyName, { + configurable: true, + enumerable: true, + set: Ember.MANDATORY_SETTER_FUNCTION, + get: Ember.DEFAULT_GETTER_FUNCTION(keyName) + }); + } + } else { + watching[keyName] = (watching[keyName] || 0) + 1; + } }; -Ember._suspendObservers = function(obj, paths, target, method, callback) { - var events = map.call(paths, changeEvent); - return Ember._suspendListeners(obj, events, target, method, callback); -}; -Ember.beforeObserversFor = function(obj, path) { - return Ember.listenersFor(obj, beforeEvent(path)); -}; - -/** - @method removeBeforeObserver - @param obj - @param {String} path - @param {Object|Function} targetOrMethod - @param {Function|String} [method] -*/ -Ember.removeBeforeObserver = function(obj, path, target, method) { - Ember.unwatch(obj, path); - Ember.removeListener(obj, beforeEvent(path), target, method); - return this; -}; +Ember.unwatchKey = function(obj, keyName) { + var m = metaFor(obj), watching = m.watching, desc; -Ember.notifyBeforeObservers = function(obj, keyName) { - if (obj.isDestroying) { return; } + if (watching[keyName] === 1) { + watching[keyName] = 0; + desc = m.descs[keyName]; - var eventName = beforeEvent(keyName), listeners, listenersDiff; - if (deferred) { - listeners = beforeObserverSet.add(obj, keyName, eventName); - listenersDiff = Ember.listenersDiff(obj, eventName, listeners); - Ember.sendEvent(obj, eventName, [obj, keyName], listenersDiff); - } else { - Ember.sendEvent(obj, eventName, [obj, keyName]); - } -}; + if (desc && desc.didUnwatch) { desc.didUnwatch(obj, keyName); } -Ember.notifyObservers = function(obj, keyName) { - if (obj.isDestroying) { return; } + if ('function' === typeof obj.didUnwatchProperty) { + obj.didUnwatchProperty(keyName); + } - var eventName = changeEvent(keyName), listeners; - if (deferred) { - listeners = observerSet.add(obj, keyName, eventName); - Ember.listenersUnion(obj, eventName, listeners); - } else { - Ember.sendEvent(obj, eventName, [obj, keyName]); + if (MANDATORY_SETTER && keyName in obj) { + o_defineProperty(obj, keyName, { + configurable: true, + enumerable: true, + writable: true, + value: m.values[keyName] + }); + delete m.values[keyName]; + } + } else if (watching[keyName] > 1) { + watching[keyName]--; } }; - })(); (function() { -/** -@module ember-metal -*/ - -var guidFor = Ember.guidFor, // utils.js - metaFor = Ember.meta, // utils.js - get = Ember.get, // accessors.js - set = Ember.set, // accessors.js - normalizeTuple = Ember.normalizeTuple, // accessors.js - GUID_KEY = Ember.GUID_KEY, // utils.js - META_KEY = Ember.META_KEY, // utils.js - // circular reference observer depends on Ember.watch - // we should move change events to this file or its own property_events.js +var metaFor = Ember.meta, // utils.js + get = Ember.get, // property_get.js + normalizeTuple = Ember.normalizeTuple, // property_get.js forEach = Ember.ArrayPolyfills.forEach, // array.js - FIRST_KEY = /^([^\.\*]+)/, - IS_PATH = /[\.\*]/; - -var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, -o_defineProperty = Ember.platform.defineProperty; + warn = Ember.warn, + watchKey = Ember.watchKey, + unwatchKey = Ember.unwatchKey, + propertyWillChange = Ember.propertyWillChange, + propertyDidChange = Ember.propertyDidChange, + FIRST_KEY = /^([^\.\*]+)/; function firstKey(path) { return path.match(FIRST_KEY)[0]; } -// returns true if the passed path is just a keyName -function isKeyName(path) { - return path==='*' || !IS_PATH.test(path); -} - -// .......................................................... -// DEPENDENT KEYS -// - -function iterDeps(method, obj, depKey, seen, meta) { - - var guid = guidFor(obj); - if (!seen[guid]) seen[guid] = {}; - if (seen[guid][depKey]) return; - seen[guid][depKey] = true; - - var deps = meta.deps; - deps = deps && deps[depKey]; - if (deps) { - for(var key in deps) { - var desc = meta.descs[key]; - if (desc && desc._suspended === obj) continue; - method(obj, key); - } - } -} - - -var WILL_SEEN, DID_SEEN; +var pendingQueue = []; -// called whenever a property is about to change to clear the cache of any dependent keys (and notify those properties of changes, etc...) -function dependentKeysWillChange(obj, depKey, meta) { - if (obj.isDestroying) { return; } +// attempts to add the pendingQueue chains again. If some of them end up +// back in the queue and reschedule is true, schedules a timeout to try +// again. +Ember.flushPendingChains = function() { + if (pendingQueue.length === 0) { return; } // nothing to do - var seen = WILL_SEEN, top = !seen; - if (top) { seen = WILL_SEEN = {}; } - iterDeps(propertyWillChange, obj, depKey, seen, meta); - if (top) { WILL_SEEN = null; } -} + var queue = pendingQueue; + pendingQueue = []; -// called whenever a property has just changed to update dependent keys -function dependentKeysDidChange(obj, depKey, meta) { - if (obj.isDestroying) { return; } + forEach.call(queue, function(q) { q[0].add(q[1]); }); - var seen = DID_SEEN, top = !seen; - if (top) { seen = DID_SEEN = {}; } - iterDeps(propertyDidChange, obj, depKey, seen, meta); - if (top) { DID_SEEN = null; } -} + warn('Watching an undefined global, Ember expects watched globals to be setup by the time the run loop is flushed, check for typos', pendingQueue.length === 0); +}; -// .......................................................... -// CHAIN -// function addChainWatcher(obj, keyName, node) { if (!obj || ('object' !== typeof obj)) { return; } // nothing to do @@ -2679,10 +3243,10 @@ function addChainWatcher(obj, keyName, node) { if (!nodes[keyName]) { nodes[keyName] = []; } nodes[keyName].push(node); - Ember.watch(obj, keyName); + watchKey(obj, keyName); } -function removeChainWatcher(obj, keyName, node) { +var removeChainWatcher = Ember.removeChainWatcher = function(obj, keyName, node) { if (!obj || 'object' !== typeof obj) { return; } // nothing to do var m = metaFor(obj, false); @@ -2696,24 +3260,8 @@ function removeChainWatcher(obj, keyName, node) { if (nodes[i] === node) { nodes.splice(i, 1); } } } - Ember.unwatch(obj, keyName); -} - -var pendingQueue = []; - -// attempts to add the pendingQueue chains again. If some of them end up -// back in the queue and reschedule is true, schedules a timeout to try -// again. -function flushPendingChains() { - if (pendingQueue.length === 0) { return; } // nothing to do - - var queue = pendingQueue; - pendingQueue = []; - - forEach.call(queue, function(q) { q[0].add(q[1]); }); - - Ember.warn('Watching an undefined global, Ember expects watched globals to be setup by the time the run loop is flushed, check for typos', pendingQueue.length === 0); -} + unwatchKey(obj, keyName); +}; function isProto(pvalue) { return metaFor(pvalue, false).proto === pvalue; @@ -2722,8 +3270,7 @@ function isProto(pvalue) { // A ChainNode watches a single key on an object. If you provide a starting // value for the key then the node won't actually watch it. For a root node // pass null for parent and key and object for value. -var ChainNode = function(parent, key, value) { - var obj; +var ChainNode = Ember._ChainNode = function(parent, key, value) { this._parent = parent; this._key = key; @@ -2895,9 +3442,9 @@ ChainNodePrototype.chainWillChange = function(chain, path, depth) { if (this._parent) { this._parent.chainWillChange(this, path, depth+1); } else { - if (depth > 1) { Ember.propertyWillChange(this.value(), path); } + if (depth > 1) { propertyWillChange(this.value(), path); } path = 'this.' + path; - if (this._paths[path] > 0) { Ember.propertyWillChange(this.value(), path); } + if (this._paths[path] > 0) { propertyWillChange(this.value(), path); } } }; @@ -2906,9 +3453,9 @@ ChainNodePrototype.chainDidChange = function(chain, path, depth) { if (this._parent) { this._parent.chainDidChange(this, path, depth+1); } else { - if (depth > 1) { Ember.propertyDidChange(this.value(), path); } + if (depth > 1) { propertyDidChange(this.value(), path); } path = 'this.' + path; - if (this._paths[path] > 0) { Ember.propertyDidChange(this.value(), path); } + if (this._paths[path] > 0) { propertyDidChange(this.value(), path); } } }; @@ -2944,6 +3491,24 @@ ChainNodePrototype.didChange = function(suppressEvent) { if (this._parent) { this._parent.chainDidChange(this, this._key, 1); } }; +Ember.finishChains = function(obj) { + var m = metaFor(obj, false), chains = m.chains; + if (chains) { + if (chains.value() !== obj) { + m.chains = chains = chains.copy(obj); + } + chains.didChange(true); + } +}; +})(); + + + +(function() { +var metaFor = Ember.meta, // utils.js + typeOf = Ember.typeOf, // utils.js + ChainNode = Ember._ChainNode; // chains.js + // get the chains for the current object. If the current object has // chains inherited from the proto they will be cloned and reconfigured for // the current object. @@ -2957,40 +3522,55 @@ function chainsFor(obj) { return ret; } -Ember.overrideChains = function(obj, keyName, m) { - chainsDidChange(obj, keyName, m, true); -}; +Ember.watchPath = function(obj, keyPath) { + // can't watch length on Array - it is special... + if (keyPath === 'length' && typeOf(obj) === 'array') { return; } -function chainsWillChange(obj, keyName, m, arg) { - if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do + var m = metaFor(obj), watching = m.watching; - var nodes = m.chainWatchers; + if (!watching[keyPath]) { // activate watching first time + watching[keyPath] = 1; + chainsFor(obj).add(keyPath); + } else { + watching[keyPath] = (watching[keyPath] || 0) + 1; + } +}; - nodes = nodes[keyName]; - if (!nodes) { return; } +Ember.unwatchPath = function(obj, keyPath) { + var m = metaFor(obj), watching = m.watching; - for(var i = 0, l = nodes.length; i < l; i++) { - nodes[i].willChange(arg); + if (watching[keyPath] === 1) { + watching[keyPath] = 0; + chainsFor(obj).remove(keyPath); + } else if (watching[keyPath] > 1) { + watching[keyPath]--; } -} +}; +})(); -function chainsDidChange(obj, keyName, m, arg) { - if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do - var nodes = m.chainWatchers; - nodes = nodes[keyName]; - if (!nodes) { return; } +(function() { +/** +@module ember-metal +*/ - // looping in reverse because the chainWatchers array can be modified inside didChange - for (var i = nodes.length - 1; i >= 0; i--) { - nodes[i].didChange(arg); - } -} +var metaFor = Ember.meta, // utils.js + GUID_KEY = Ember.GUID_KEY, // utils.js + META_KEY = Ember.META_KEY, // utils.js + removeChainWatcher = Ember.removeChainWatcher, + watchKey = Ember.watchKey, // watch_key.js + unwatchKey = Ember.unwatchKey, + watchPath = Ember.watchPath, // watch_path.js + unwatchPath = Ember.unwatchPath, + typeOf = Ember.typeOf, // utils.js + generateGuid = Ember.generateGuid, + IS_PATH = /[\.\*]/; -// .......................................................... -// WATCH -// +// returns true if the passed path is just a keyName +function isKeyName(path) { + return path==='*' || !IS_PATH.test(path); +} /** @private @@ -3006,40 +3586,15 @@ function chainsDidChange(obj, keyName, m, arg) { @param obj @param {String} keyName */ -Ember.watch = function(obj, keyName) { +Ember.watch = function(obj, keyPath) { // can't watch length on Array - it is special... - if (keyName === 'length' && Ember.typeOf(obj) === 'array') { return this; } - - var m = metaFor(obj), watching = m.watching, desc; - - // activate watching first time - if (!watching[keyName]) { - watching[keyName] = 1; - if (isKeyName(keyName)) { - desc = m.descs[keyName]; - if (desc && desc.willWatch) { desc.willWatch(obj, keyName); } - - if ('function' === typeof obj.willWatchProperty) { - obj.willWatchProperty(keyName); - } - - if (MANDATORY_SETTER && keyName in obj) { - m.values[keyName] = obj[keyName]; - o_defineProperty(obj, keyName, { - configurable: true, - enumerable: true, - set: Ember.MANDATORY_SETTER_FUNCTION, - get: Ember.DEFAULT_GETTER_FUNCTION(keyName) - }); - } - } else { - chainsFor(obj).add(keyName); - } + if (keyPath === 'length' && typeOf(obj) === 'array') { return; } - } else { - watching[keyName] = (watching[keyName] || 0) + 1; + if (isKeyName(keyPath)) { + watchKey(obj, keyPath); + } else { + watchPath(obj, keyPath); } - return this; }; Ember.isWatching = function isWatching(obj, key) { @@ -3047,43 +3602,17 @@ Ember.isWatching = function isWatching(obj, key) { return (meta && meta.watching[key]) > 0; }; -Ember.watch.flushPending = flushPendingChains; +Ember.watch.flushPending = Ember.flushPendingChains; -Ember.unwatch = function(obj, keyName) { +Ember.unwatch = function(obj, keyPath) { // can't watch length on Array - it is special... - if (keyName === 'length' && Ember.typeOf(obj) === 'array') { return this; } - - var m = metaFor(obj), watching = m.watching, desc; - - if (watching[keyName] === 1) { - watching[keyName] = 0; - - if (isKeyName(keyName)) { - desc = m.descs[keyName]; - if (desc && desc.didUnwatch) { desc.didUnwatch(obj, keyName); } - - if ('function' === typeof obj.didUnwatchProperty) { - obj.didUnwatchProperty(keyName); - } + if (keyPath === 'length' && typeOf(obj) === 'array') { return; } - if (MANDATORY_SETTER && keyName in obj) { - o_defineProperty(obj, keyName, { - configurable: true, - enumerable: true, - writable: true, - value: m.values[keyName] - }); - delete m.values[keyName]; - } - } else { - chainsFor(obj).remove(keyName); - } - - } else if (watching[keyName]>1) { - watching[keyName]--; + if (isKeyName(keyPath)) { + unwatchKey(obj, keyPath); + } else { + unwatchPath(obj, keyPath); } - - return this; }; /** @@ -3102,96 +3631,15 @@ Ember.rewatch = function(obj) { // make sure the object has its own guid. if (GUID_KEY in obj && !obj.hasOwnProperty(GUID_KEY)) { - Ember.generateGuid(obj, 'ember'); + generateGuid(obj, 'ember'); } // make sure any chained watchers update. if (chains && chains.value() !== obj) { m.chains = chains.copy(obj); } - - return this; }; -Ember.finishChains = function(obj) { - var m = metaFor(obj, false), chains = m.chains; - if (chains) { - if (chains.value() !== obj) { - m.chains = chains = chains.copy(obj); - } - chains.didChange(true); - } -}; - -// .......................................................... -// PROPERTY CHANGES -// - -/** - This function is called just before an object property is about to change. - It will notify any before observers and prepare caches among other things. - - Normally you will not need to call this method directly but if for some - reason you can't directly watch a property you can invoke this method - manually along with `Ember.propertyDidChange()` which you should call just - after the property value changes. - - @method propertyWillChange - @for Ember - @param {Object} obj The object with the property that will change - @param {String} keyName The property key (or path) that will change. - @return {void} -*/ -function propertyWillChange(obj, keyName) { - var m = metaFor(obj, false), - watching = m.watching[keyName] > 0 || keyName === 'length', - proto = m.proto, - desc = m.descs[keyName]; - - if (!watching) { return; } - if (proto === obj) { return; } - if (desc && desc.willChange) { desc.willChange(obj, keyName); } - dependentKeysWillChange(obj, keyName, m); - chainsWillChange(obj, keyName, m); - Ember.notifyBeforeObservers(obj, keyName); -} - -Ember.propertyWillChange = propertyWillChange; - -/** - This function is called just after an object property has changed. - It will notify any observers and clear caches among other things. - - Normally you will not need to call this method directly but if for some - reason you can't directly watch a property you can invoke this method - manually along with `Ember.propertyWilLChange()` which you should call just - before the property value changes. - - @method propertyDidChange - @for Ember - @param {Object} obj The object with the property that will change - @param {String} keyName The property key (or path) that will change. - @return {void} -*/ -function propertyDidChange(obj, keyName) { - var m = metaFor(obj, false), - watching = m.watching[keyName] > 0 || keyName === 'length', - proto = m.proto, - desc = m.descs[keyName]; - - if (proto === obj) { return; } - - // shouldn't this mean that we're watching this key? - if (desc && desc.didChange) { desc.didChange(obj, keyName); } - if (!watching && keyName !== 'length') { return; } - - dependentKeysDidChange(obj, keyName, m); - chainsDidChange(obj, keyName, m); - Ember.notifyObservers(obj, keyName); -} - -Ember.propertyDidChange = propertyDidChange; - var NODE_STACK = []; /** @@ -3271,7 +3719,7 @@ var get = Ember.get, This function returns a map of unique dependencies for a given object and key. */ -function keysForDep(obj, depsMeta, depKey) { +function keysForDep(depsMeta, depKey) { var keys = depsMeta[depKey]; if (!keys) { // if there are no dependencies yet for a the given key @@ -3285,8 +3733,8 @@ function keysForDep(obj, depsMeta, depKey) { return keys; } -function metaForDeps(obj, meta) { - return keysForDep(obj, meta, 'deps'); +function metaForDeps(meta) { + return keysForDep(meta, 'deps'); } function addDependentKeys(desc, obj, keyName, meta) { @@ -3295,12 +3743,12 @@ function addDependentKeys(desc, obj, keyName, meta) { var depKeys = desc._dependentKeys, depsMeta, idx, len, depKey, keys; if (!depKeys) return; - depsMeta = metaForDeps(obj, meta); + depsMeta = metaForDeps(meta); for(idx = 0, len = depKeys.length; idx < len; idx++) { depKey = depKeys[idx]; // Lookup keys meta for depKey - keys = keysForDep(obj, depsMeta, depKey); + keys = keysForDep(depsMeta, depKey); // Increment the number of times depKey depends on keyName. keys[keyName] = (keys[keyName] || 0) + 1; // Watch the depKey @@ -3314,12 +3762,12 @@ function removeDependentKeys(desc, obj, keyName, meta) { var depKeys = desc._dependentKeys, depsMeta, idx, len, depKey, keys; if (!depKeys) return; - depsMeta = metaForDeps(obj, meta); + depsMeta = metaForDeps(meta); for(idx = 0, len = depKeys.length; idx < len; idx++) { depKey = depKeys[idx]; // Lookup keys meta for depKey - keys = keysForDep(obj, depsMeta, depKey); + keys = keysForDep(depsMeta, depKey); // Increment the number of times depKey depends on keyName. keys[keyName] = (keys[keyName] || 0) - 1; // Watch the depKey @@ -3350,24 +3798,15 @@ ComputedProperty.prototype = new Ember.Descriptor(); var ComputedPropertyPrototype = ComputedProperty.prototype; -/** - Call on a computed property to set it into cacheable mode. When in this - mode the computed property will automatically cache the return value of - your function until one of the dependent keys changes. - - ```javascript - MyApp.president = Ember.Object.create({ - fullName: function() { - return this.get('firstName') + ' ' + this.get('lastName'); +/* + Properties are cacheable by default. Computed property will automatically + cache the return value of your function until one of the dependent keys changes. - // After calculating the value of this function, Ember will - // return that value without re-executing this function until - // one of the dependent properties change. - }.property('firstName', 'lastName') - }); - ``` + Call `volatile()` to set it into non-cached mode. When in this mode + the computed property will not automatically cache the return value. - Properties are cacheable by default. + However, if a property is properly observable, there is no reason to disable + caching. @method cacheable @param {Boolean} aFlag optional set to `false` to disable caching @@ -3631,7 +4070,6 @@ ComputedPropertyPrototype.teardown = function(obj, keyName) { The function should accept two parameters, key and value. If value is not undefined you should set the value first. In either case return the current value of the property. - @method computed @for Ember @param {Function} func The computed property function. @@ -3669,7 +4107,7 @@ Ember.computed = function(func) { @param {Object} obj the object whose property you want to check @param {String} key the name of the property whose cached value you want to return - @return {any} the cached value + @return {*} the cached value */ Ember.cacheFor = function cacheFor(obj, key) { var cache = metaFor(obj, false).cache; @@ -3873,7 +4311,7 @@ registerComputedWithProperties('or', function(properties) { @for Ember @param {String} dependentKey, [dependentKey...] @return {Ember.ComputedProperty} computed property which returns - the first trouthy value of given list of properties. + the first truthy value of given list of properties. */ registerComputedWithProperties('any', function(properties) { for (var key in properties) { @@ -3924,6 +4362,48 @@ Ember.computed.alias = function(dependentKey) { }; /** + @method computed.oneWay + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which creates an + one way computed property to the original value for property. + + Where `computed.alias` aliases `get` and `set`, and allows for bidirectional + data flow, `computed.oneWay` only provides an aliased `get`. The `set` will + not mutate the upstream property, rather causes the current property to + become the value set. This causes the downstream property to permentantly + diverge from the upstream property. + + ```javascript + User = Ember.Object.extend({ + firstName: null, + lastName: null, + nickName: Ember.computed.oneWay('firstName') + }); + + user = User.create({ + firstName: 'Teddy', + lastName: 'Zeenny' + }); + + user.get('nickName'); + # 'Teddy' + + user.set('nickName', 'TeddyBear'); + # 'TeddyBear' + + user.get('firstName'); + # 'Teddy' + ``` +*/ +Ember.computed.oneWay = function(dependentKey) { + return Ember.computed(dependentKey, function() { + return get(this, dependentKey); + }); +}; + + +/** @method computed.defaultTo @for Ember @param {String} defaultPath @@ -3932,7 +4412,6 @@ Ember.computed.alias = function(dependentKey) { */ Ember.computed.defaultTo = function(defaultPath) { return Ember.computed(function(key, newValue, cachedValue) { - var result; if (arguments.length === 1) { return cachedValue != null ? cachedValue : get(this, defaultPath); } @@ -3945,567 +4424,611 @@ Ember.computed.defaultTo = function(defaultPath) { (function() { +// Ember.tryFinally /** @module ember-metal */ -var o_create = Ember.create, - metaFor = Ember.meta, - META_KEY = Ember.META_KEY; - -/* - The event system uses a series of nested hashes to store listeners on an - object. When a listener is registered, or when an event arrives, these - hashes are consulted to determine which target and action pair to invoke. - - The hashes are stored in the object's meta hash, and look like this: - - // Object's meta hash - { - listeners: { // variable name: `listenerSet` - "foo:changed": [ // variable name: `actions` - [target, method, onceFlag, suspendedFlag] - ] - } - } - -*/ - -function indexOf(array, target, method) { - var index = -1; - for (var i = 0, l = array.length; i < l; i++) { - if (target === array[i][0] && method === array[i][1]) { index = i; break; } - } - return index; -} - -function actionsFor(obj, eventName) { - var meta = metaFor(obj, true), - actions; - - if (!meta.listeners) { meta.listeners = {}; } - - if (!meta.hasOwnProperty('listeners')) { - // setup inherited copy of the listeners object - meta.listeners = o_create(meta.listeners); - } - - actions = meta.listeners[eventName]; - - // if there are actions, but the eventName doesn't exist in our listeners, then copy them from the prototype - if (actions && !meta.listeners.hasOwnProperty(eventName)) { - actions = meta.listeners[eventName] = meta.listeners[eventName].slice(); - } else if (!actions) { - actions = meta.listeners[eventName] = []; - } +var AFTER_OBSERVERS = ':change'; +var BEFORE_OBSERVERS = ':before'; - return actions; +function changeEvent(keyName) { + return keyName+AFTER_OBSERVERS; } -function actionsUnion(obj, eventName, otherActions) { - var meta = obj[META_KEY], - actions = meta && meta.listeners && meta.listeners[eventName]; - - if (!actions) { return; } - for (var i = actions.length - 1; i >= 0; i--) { - var target = actions[i][0], - method = actions[i][1], - once = actions[i][2], - suspended = actions[i][3], - actionIndex = indexOf(otherActions, target, method); - - if (actionIndex === -1) { - otherActions.push([target, method, once, suspended]); - } - } +function beforeEvent(keyName) { + return keyName+BEFORE_OBSERVERS; } -function actionsDiff(obj, eventName, otherActions) { - var meta = obj[META_KEY], - actions = meta && meta.listeners && meta.listeners[eventName], - diffActions = []; - - if (!actions) { return; } - for (var i = actions.length - 1; i >= 0; i--) { - var target = actions[i][0], - method = actions[i][1], - once = actions[i][2], - suspended = actions[i][3], - actionIndex = indexOf(otherActions, target, method); - - if (actionIndex !== -1) { continue; } - - otherActions.push([target, method, once, suspended]); - diffActions.push([target, method, once, suspended]); - } +/** + @method addObserver + @param obj + @param {String} path + @param {Object|Function} targetOrMethod + @param {Function|String} [method] +*/ +Ember.addObserver = function(obj, path, target, method) { + Ember.addListener(obj, changeEvent(path), target, method); + Ember.watch(obj, path); + return this; +}; - return diffActions; -} +Ember.observersFor = function(obj, path) { + return Ember.listenersFor(obj, changeEvent(path)); +}; /** - Add an event listener - - @method addListener - @for Ember + @method removeObserver @param obj - @param {String} eventName - @param {Object|Function} targetOrMethod A target object or a function - @param {Function|String} method A function or the name of a function to be called on `target` - @param {Boolean} once A flag whether a function should only be called once + @param {String} path + @param {Object|Function} targetOrMethod + @param {Function|String} [method] */ -function addListener(obj, eventName, target, method, once) { - Ember.assert("You must pass at least an object and event name to Ember.addListener", !!obj && !!eventName); +Ember.removeObserver = function(obj, path, target, method) { + Ember.unwatch(obj, path); + Ember.removeListener(obj, changeEvent(path), target, method); + return this; +}; - if (!method && 'function' === typeof target) { - method = target; - target = null; - } +/** + @method addBeforeObserver + @param obj + @param {String} path + @param {Object|Function} targetOrMethod + @param {Function|String} [method] +*/ +Ember.addBeforeObserver = function(obj, path, target, method) { + Ember.addListener(obj, beforeEvent(path), target, method); + Ember.watch(obj, path); + return this; +}; - var actions = actionsFor(obj, eventName), - actionIndex = indexOf(actions, target, method); +// Suspend observer during callback. +// +// This should only be used by the target of the observer +// while it is setting the observed path. +Ember._suspendBeforeObserver = function(obj, path, target, method, callback) { + return Ember._suspendListener(obj, beforeEvent(path), target, method, callback); +}; - if (actionIndex !== -1) { return; } +Ember._suspendObserver = function(obj, path, target, method, callback) { + return Ember._suspendListener(obj, changeEvent(path), target, method, callback); +}; - actions.push([target, method, once, undefined]); +var map = Ember.ArrayPolyfills.map; - if ('function' === typeof obj.didAddListener) { - obj.didAddListener(eventName, target, method); - } -} +Ember._suspendBeforeObservers = function(obj, paths, target, method, callback) { + var events = map.call(paths, beforeEvent); + return Ember._suspendListeners(obj, events, target, method, callback); +}; -/** - Remove an event listener +Ember._suspendObservers = function(obj, paths, target, method, callback) { + var events = map.call(paths, changeEvent); + return Ember._suspendListeners(obj, events, target, method, callback); +}; - Arguments should match those passed to {{#crossLink "Ember/addListener"}}{{/crossLink}} +Ember.beforeObserversFor = function(obj, path) { + return Ember.listenersFor(obj, beforeEvent(path)); +}; - @method removeListener - @for Ember +/** + @method removeBeforeObserver @param obj - @param {String} eventName - @param {Object|Function} targetOrMethod A target object or a function - @param {Function|String} method A function or the name of a function to be called on `target` + @param {String} path + @param {Object|Function} targetOrMethod + @param {Function|String} [method] */ -function removeListener(obj, eventName, target, method) { - Ember.assert("You must pass at least an object and event name to Ember.removeListener", !!obj && !!eventName); - - if (!method && 'function' === typeof target) { - method = target; - target = null; - } +Ember.removeBeforeObserver = function(obj, path, target, method) { + Ember.unwatch(obj, path); + Ember.removeListener(obj, beforeEvent(path), target, method); + return this; +}; +})(); - function _removeListener(target, method, once) { - var actions = actionsFor(obj, eventName), - actionIndex = indexOf(actions, target, method); - // action doesn't exist, give up silently - if (actionIndex === -1) { return; } - actions.splice(actionIndex, 1); - - if ('function' === typeof obj.didRemoveListener) { - obj.didRemoveListener(eventName, target, method); +(function() { +define("backburner", + ["backburner/deferred_action_queues","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var DeferredActionQueues = __dependency1__.DeferredActionQueues; + + var slice = [].slice, + pop = [].pop, + debouncees = [], + timers = [], + autorun, laterTimer, laterTimerExpiresAt; + + function Backburner(queueNames, options) { + this.queueNames = queueNames; + this.options = options || {}; + if (!this.options.defaultQueue) { + this.options.defaultQueue = queueNames[0]; + } + this.instanceStack = []; } - } - if (method) { - _removeListener(target, method); - } else { - var meta = obj[META_KEY], - actions = meta && meta.listeners && meta.listeners[eventName]; + Backburner.prototype = { + queueNames: null, + options: null, + currentInstance: null, + instanceStack: null, - if (!actions) { return; } - for (var i = actions.length - 1; i >= 0; i--) { - _removeListener(actions[i][0], actions[i][1]); - } - } -} + begin: function() { + var onBegin = this.options && this.options.onBegin, + previousInstance = this.currentInstance; -/** - @private + if (previousInstance) { + this.instanceStack.push(previousInstance); + } - Suspend listener during callback. + this.currentInstance = new DeferredActionQueues(this.queueNames, this.options); + if (onBegin) { + onBegin(this.currentInstance, previousInstance); + } + }, - This should only be used by the target of the event listener - when it is taking an action that would cause the event, e.g. - an object might suspend its property change listener while it is - setting that property. + end: function() { + var onEnd = this.options && this.options.onEnd, + currentInstance = this.currentInstance, + nextInstance = null; - @method suspendListener - @for Ember - @param obj - @param {String} eventName - @param {Object|Function} targetOrMethod A target object or a function - @param {Function|String} method A function or the name of a function to be called on `target` - @param {Function} callback -*/ -function suspendListener(obj, eventName, target, method, callback) { - if (!method && 'function' === typeof target) { - method = target; - target = null; - } + try { + currentInstance.flush(); + } finally { + this.currentInstance = null; - var actions = actionsFor(obj, eventName), - actionIndex = indexOf(actions, target, method), - action; + if (this.instanceStack.length) { + nextInstance = this.instanceStack.pop(); + this.currentInstance = nextInstance; + } - if (actionIndex !== -1) { - action = actions[actionIndex].slice(); // copy it, otherwise we're modifying a shared object - action[3] = true; // mark the action as suspended - actions[actionIndex] = action; // replace the shared object with our copy - } + if (onEnd) { + onEnd(currentInstance, nextInstance); + } + } + }, - function tryable() { return callback.call(target); } - function finalizer() { if (action) { action[3] = undefined; } } + run: function(target, method /*, args */) { + var ret; + this.begin(); - return Ember.tryFinally(tryable, finalizer); -} + if (!method) { + method = target; + target = null; + } -/** - @private + if (typeof method === 'string') { + method = target[method]; + } - Suspend listener during callback. + // Prevent Safari double-finally. + var finallyAlreadyCalled = false; + try { + if (arguments.length > 2) { + ret = method.apply(target, slice.call(arguments, 2)); + } else { + ret = method.call(target); + } + } finally { + if (!finallyAlreadyCalled) { + finallyAlreadyCalled = true; + this.end(); + } + } + return ret; + }, - This should only be used by the target of the event listener - when it is taking an action that would cause the event, e.g. - an object might suspend its property change listener while it is - setting that property. + defer: function(queueName, target, method /* , args */) { + if (!method) { + method = target; + target = null; + } - @method suspendListener - @for Ember - @param obj - @param {Array} eventName Array of event names - @param {Object|Function} targetOrMethod A target object or a function - @param {Function|String} method A function or the name of a function to be called on `target` - @param {Function} callback -*/ -function suspendListeners(obj, eventNames, target, method, callback) { - if (!method && 'function' === typeof target) { - method = target; - target = null; - } + if (typeof method === 'string') { + method = target[method]; + } - var suspendedActions = [], - eventName, actions, action, i, l; + var stack = this.DEBUG ? new Error().stack : undefined, + args = arguments.length > 3 ? slice.call(arguments, 3) : undefined; + if (!this.currentInstance) { createAutorun(this); } + return this.currentInstance.schedule(queueName, target, method, args, false, stack); + }, - for (i=0, l=eventNames.length; i<l; i++) { - eventName = eventNames[i]; - actions = actionsFor(obj, eventName); - var actionIndex = indexOf(actions, target, method); + deferOnce: function(queueName, target, method /* , args */) { + if (!method) { + method = target; + target = null; + } - if (actionIndex !== -1) { - action = actions[actionIndex].slice(); - action[3] = true; - actions[actionIndex] = action; - suspendedActions.push(action); - } - } + if (typeof method === 'string') { + method = target[method]; + } - function tryable() { return callback.call(target); } + var stack = this.DEBUG ? new Error().stack : undefined, + args = arguments.length > 3 ? slice.call(arguments, 3) : undefined; + if (!this.currentInstance) { createAutorun(this); } + return this.currentInstance.schedule(queueName, target, method, args, true, stack); + }, - function finalizer() { - for (i = 0, l = suspendedActions.length; i < l; i++) { - suspendedActions[i][3] = undefined; - } - } + setTimeout: function() { + var self = this, + wait = pop.call(arguments), + target = arguments[0], + method = arguments[1], + executeAt = (+new Date()) + wait; - return Ember.tryFinally(tryable, finalizer); -} + if (!method) { + method = target; + target = null; + } -/** - @private + if (typeof method === 'string') { + method = target[method]; + } - Return a list of currently watched events + var fn, args; + if (arguments.length > 2) { + args = slice.call(arguments, 2); - @method watchedEvents - @for Ember - @param obj -*/ -function watchedEvents(obj) { - var listeners = obj[META_KEY].listeners, ret = []; + fn = function() { + method.apply(target, args); + }; + } else { + fn = function() { + method.call(target); + }; + } - if (listeners) { - for(var eventName in listeners) { - if (listeners[eventName]) { ret.push(eventName); } - } - } - return ret; -} + // find position to insert - TODO: binary search + var i, l; + for (i = 0, l = timers.length; i < l; i += 2) { + if (executeAt < timers[i]) { break; } + } -/** - @method sendEvent - @for Ember - @param obj - @param {String} eventName - @param {Array} params - @param {Array} actions - @return true -*/ -function sendEvent(obj, eventName, params, actions) { - // first give object a chance to handle it - if (obj !== Ember && 'function' === typeof obj.sendEvent) { - obj.sendEvent(eventName, params); - } + timers.splice(i, 0, executeAt, fn); - if (!actions) { - var meta = obj[META_KEY]; - actions = meta && meta.listeners && meta.listeners[eventName]; - } + if (laterTimer && laterTimerExpiresAt < executeAt) { return fn; } - if (!actions) { return; } + if (laterTimer) { + clearTimeout(laterTimer); + laterTimer = null; + } + laterTimer = window.setTimeout(function() { + executeTimers(self); + laterTimer = null; + laterTimerExpiresAt = null; + }, wait); + laterTimerExpiresAt = executeAt; + + return fn; + }, - for (var i = actions.length - 1; i >= 0; i--) { // looping in reverse for once listeners - if (!actions[i] || actions[i][3] === true) { continue; } + debounce: function(target, method /* , args, wait */) { + var self = this, + args = arguments, + wait = pop.call(args), + debouncee; - var target = actions[i][0], - method = actions[i][1], - once = actions[i][2]; + for (var i = 0, l = debouncees.length; i < l; i++) { + debouncee = debouncees[i]; + if (debouncee[0] === target && debouncee[1] === method) { return; } // do nothing + } - if (once) { removeListener(obj, eventName, target, method); } - if (!target) { target = obj; } - if ('string' === typeof method) { method = target[method]; } - if (params) { - method.apply(target, params); - } else { - method.call(target); - } - } - return true; -} + var timer = window.setTimeout(function() { + self.run.apply(self, args); -/** - @private - @method hasListeners - @for Ember - @param obj - @param {String} eventName -*/ -function hasListeners(obj, eventName) { - var meta = obj[META_KEY], - actions = meta && meta.listeners && meta.listeners[eventName]; + // remove debouncee + var index = -1; + for (var i = 0, l = debouncees.length; i < l; i++) { + debouncee = debouncees[i]; + if (debouncee[0] === target && debouncee[1] === method) { + index = i; + break; + } + } - return !!(actions && actions.length); -} + if (index > -1) { debouncees.splice(index, 1); } + }, wait); -/** - @private - @method listenersFor - @for Ember - @param obj - @param {String} eventName -*/ -function listenersFor(obj, eventName) { - var ret = []; - var meta = obj[META_KEY], - actions = meta && meta.listeners && meta.listeners[eventName]; + debouncees.push([target, method, timer]); + }, - if (!actions) { return ret; } + cancelTimers: function() { + for (var i = 0, l = debouncees.length; i < l; i++) { + clearTimeout(debouncees[i][2]); + } + debouncees = []; - for (var i = 0, l = actions.length; i < l; i++) { - var target = actions[i][0], - method = actions[i][1]; - ret.push([target, method]); - } + if (laterTimer) { + clearTimeout(laterTimer); + laterTimer = null; + } + timers = []; - return ret; -} + if (autorun) { + clearTimeout(autorun); + autorun = null; + } + }, -Ember.addListener = addListener; -Ember.removeListener = removeListener; -Ember._suspendListener = suspendListener; -Ember._suspendListeners = suspendListeners; -Ember.sendEvent = sendEvent; -Ember.hasListeners = hasListeners; -Ember.watchedEvents = watchedEvents; -Ember.listenersFor = listenersFor; -Ember.listenersDiff = actionsDiff; -Ember.listenersUnion = actionsUnion; + hasTimers: function() { + return !!timers.length || autorun; + }, -})(); + cancel: function(timer) { + if (typeof timer === 'object' && timer.queue && timer.method) { // we're cancelling a deferOnce + return timer.queue.cancel(timer); + } else if (typeof timer === 'function') { // we're cancelling a setTimeout + for (var i = 0, l = timers.length; i < l; i += 2) { + if (timers[i + 1] === timer) { + timers.splice(i, 2); // remove the two elements + return true; + } + } + } + } + }; + Backburner.prototype.schedule = Backburner.prototype.defer; + Backburner.prototype.scheduleOnce = Backburner.prototype.deferOnce; + Backburner.prototype.later = Backburner.prototype.setTimeout; + function createAutorun(backburner) { + backburner.begin(); + autorun = window.setTimeout(function() { + backburner.end(); + autorun = null; + }); + } -(function() { -// Ember.Logger -// Ember.watch.flushPending -// Ember.beginPropertyChanges, Ember.endPropertyChanges -// Ember.guidFor, Ember.tryFinally + function executeTimers(self) { + var now = +new Date(), + time, fns, i, l; -/** -@module ember-metal -*/ + self.run(function() { + // TODO: binary search + for (i = 0, l = timers.length; i < l; i += 2) { + time = timers[i]; + if (time > now) { break; } + } -// .......................................................... -// HELPERS -// + fns = timers.splice(0, i); -var slice = [].slice, - forEach = Ember.ArrayPolyfills.forEach; + for (i = 1, l = fns.length; i < l; i += 2) { + self.schedule(self.options.defaultQueue, null, fns[i]); + } + }); -// invokes passed params - normalizing so you can pass target/func, -// target/string or just func -function invoke(target, method, args, ignore) { + if (timers.length) { + laterTimer = window.setTimeout(function() { + executeTimers(self); + laterTimer = null; + laterTimerExpiresAt = null; + }, timers[0] - now); + laterTimerExpiresAt = timers[0]; + } + } - if (method === undefined) { - method = target; - target = undefined; - } - if ('string' === typeof method) { method = target[method]; } - if (args && ignore > 0) { - args = args.length > ignore ? slice.call(args, ignore) : null; - } + __exports__.Backburner = Backburner; + }); - return Ember.handleErrors(function() { - // IE8's Function.prototype.apply doesn't accept undefined/null arguments. - return method.apply(target || this, args || []); - }, this); -} +define("backburner/deferred_action_queues", + ["backburner/queue","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Queue = __dependency1__.Queue; + function DeferredActionQueues(queueNames, options) { + var queues = this.queues = {}; + this.queueNames = queueNames = queueNames || []; -// .......................................................... -// RUNLOOP -// + var queueName; + for (var i = 0, l = queueNames.length; i < l; i++) { + queueName = queueNames[i]; + queues[queueName] = new Queue(this, queueName, options[queueName]); + } + } -var timerMark; // used by timers... + DeferredActionQueues.prototype = { + queueNames: null, + queues: null, -/** -Ember RunLoop (Private) + schedule: function(queueName, target, method, args, onceFlag, stack) { + var queues = this.queues, + queue = queues[queueName]; -@class RunLoop -@namespace Ember -@private -@constructor -*/ -var RunLoop = function(prev) { - this._prev = prev || null; - this.onceTimers = {}; -}; + if (!queue) { throw new Error("You attempted to schedule an action in a queue (" + queueName + ") that doesn't exist"); } -RunLoop.prototype = { - /** - @method end - */ - end: function() { - this.flush(); - }, + if (onceFlag) { + return queue.pushUnique(target, method, args, stack); + } else { + return queue.push(target, method, args, stack); + } + }, - /** - @method prev - */ - prev: function() { - return this._prev; - }, + flush: function() { + var queues = this.queues, + queueNames = this.queueNames, + queueName, queue, queueItems, priorQueueNameIndex, + queueNameIndex = 0, numberOfQueues = queueNames.length; + + outerloop: + while (queueNameIndex < numberOfQueues) { + queueName = queueNames[queueNameIndex]; + queue = queues[queueName]; + queueItems = queue._queue.slice(); + queue._queue = []; + + var options = queue.options, + before = options && options.before, + after = options && options.after, + target, method, args, stack, + queueIndex = 0, numberOfQueueItems = queueItems.length; + + if (numberOfQueueItems && before) { before(); } + while (queueIndex < numberOfQueueItems) { + target = queueItems[queueIndex]; + method = queueItems[queueIndex+1]; + args = queueItems[queueIndex+2]; + stack = queueItems[queueIndex+3]; // Debugging assistance + + if (typeof method === 'string') { method = target[method]; } + + // TODO: error handling + if (args && args.length > 0) { + method.apply(target, args); + } else { + method.call(target); + } - // .......................................................... - // Delayed Actions - // + queueIndex += 4; + } + if (numberOfQueueItems && after) { after(); } - /** - @method schedule - @param {String} queueName - @param target - @param method - */ - schedule: function(queueName, target, method) { - var queues = this._queues, queue; - if (!queues) { queues = this._queues = {}; } - queue = queues[queueName]; - if (!queue) { queue = queues[queueName] = []; } + if ((priorQueueNameIndex = indexOfPriorQueueWithActions(this, queueNameIndex)) !== -1) { + queueNameIndex = priorQueueNameIndex; + continue outerloop; + } - var args = arguments.length > 3 ? slice.call(arguments, 3) : null; - queue.push({ target: target, method: method, args: args }); - return this; - }, + queueNameIndex++; + } + } + }; - /** - @method flush - @param {String} queueName - */ - flush: function(queueName) { - var queueNames, idx, len, queue, log; + function indexOfPriorQueueWithActions(daq, currentQueueIndex) { + var queueName, queue; - if (!this._queues) { return this; } // nothing to do + for (var i = 0, l = currentQueueIndex; i <= l; i++) { + queueName = daq.queueNames[i]; + queue = daq.queues[queueName]; + if (queue._queue.length) { return i; } + } - function iter(item) { - invoke(item.target, item.method, item.args); + return -1; } - function tryable() { - forEach.call(queue, iter); - } + __exports__.DeferredActionQueues = DeferredActionQueues; + }); - Ember.watch.flushPending(); // make sure all chained watchers are setup +define("backburner/queue", + ["exports"], + function(__exports__) { + "use strict"; + function Queue(daq, name, options) { + this.daq = daq; + this.name = name; + this.options = options; + this._queue = []; + } + + Queue.prototype = { + daq: null, + name: null, + options: null, + _queue: null, + + push: function(target, method, args, stack) { + var queue = this._queue; + queue.push(target, method, args, stack); + return {queue: this, target: target, method: method}; + }, - if (queueName) { - while (this._queues && (queue = this._queues[queueName])) { - this._queues[queueName] = null; + pushUnique: function(target, method, args, stack) { + var queue = this._queue, currentTarget, currentMethod, i, l; - // the sync phase is to allow property changes to propagate. don't - // invoke observers until that is finished. - if (queueName === 'sync') { - log = Ember.LOG_BINDINGS; - if (log) { Ember.Logger.log('Begin: Flush Sync Queue'); } + for (i = 0, l = queue.length; i < l; i += 4) { + currentTarget = queue[i]; + currentMethod = queue[i+1]; - Ember.beginPropertyChanges(); + if (currentTarget === target && currentMethod === method) { + queue[i+2] = args; // replace args + queue[i+3] = stack; // replace stack + return {queue: this, target: target, method: method}; // TODO: test this code path + } + } - Ember.tryFinally(tryable, Ember.endPropertyChanges); + this._queue.push(target, method, args, stack); + return {queue: this, target: target, method: method}; + }, - if (log) { Ember.Logger.log('End: Flush Sync Queue'); } + // TODO: remove me, only being used for Ember.run.sync + flush: function() { + var queue = this._queue, + options = this.options, + before = options && options.before, + after = options && options.after, + target, method, args, stack, i, l = queue.length; + + if (l && before) { before(); } + for (i = 0; i < l; i += 4) { + target = queue[i]; + method = queue[i+1]; + args = queue[i+2]; + stack = queue[i+3]; // Debugging assistance + + // TODO: error handling + if (args && args.length > 0) { + method.apply(target, args); + } else { + method.call(target); + } + } + if (l && after) { after(); } + // check if new items have been added + if (queue.length > l) { + this._queue = queue.slice(l); + this.flush(); } else { - forEach.call(queue, iter); + this._queue.length = 0; } - } - - } else { - queueNames = Ember.run.queues; - len = queueNames.length; - idx = 0; - - outerloop: - while (idx < len) { - queueName = queueNames[idx]; - queue = this._queues && this._queues[queueName]; - delete this._queues[queueName]; - - if (queue) { - // the sync phase is to allow property changes to propagate. don't - // invoke observers until that is finished. - if (queueName === 'sync') { - log = Ember.LOG_BINDINGS; - if (log) { Ember.Logger.log('Begin: Flush Sync Queue'); } + }, - Ember.beginPropertyChanges(); + cancel: function(actionToCancel) { + var queue = this._queue, currentTarget, currentMethod, i, l; - Ember.tryFinally(tryable, Ember.endPropertyChanges); + for (i = 0, l = queue.length; i < l; i += 4) { + currentTarget = queue[i]; + currentMethod = queue[i+1]; - if (log) { Ember.Logger.log('End: Flush Sync Queue'); } - } else { - forEach.call(queue, iter); + if (currentTarget === actionToCancel.target && currentMethod === actionToCancel.method) { + queue.splice(i, 4); + return true; } } + } + }; - // Loop through prior queues - for (var i = 0; i <= idx; i++) { - if (this._queues && this._queues[queueNames[i]]) { - // Start over at the first queue with contents - idx = i; - continue outerloop; - } - } + __exports__.Queue = Queue; + }); - idx++; - } - } +})(); - timerMark = null; - return this; - } +(function() { +var onBegin = function(current) { + Ember.run.currentRunLoop = current; }; -Ember.RunLoop = RunLoop; +var onEnd = function(current, next) { + Ember.run.currentRunLoop = next; +}; + +var Backburner = requireModule('backburner').Backburner, + backburner = new Backburner(['sync', 'actions', 'destroy'], { + sync: { + before: Ember.beginPropertyChanges, + after: Ember.endPropertyChanges + }, + defaultQueue: 'actions', + onBegin: onBegin, + onEnd: onEnd + }), + slice = [].slice; // .......................................................... // Ember.run - this is ideally the only public API the dev sees @@ -4539,20 +5062,76 @@ Ember.RunLoop = RunLoop; @return {Object} return value from invoking the passed function. */ Ember.run = function(target, method) { - var args = arguments; - run.begin(); + var ret; - function tryable() { - if (target || method) { - return invoke(target, method, args, 2); + if (Ember.onerror) { + try { + ret = backburner.run.apply(backburner, arguments); + } catch (e) { + Ember.onerror(e); } + } else { + ret = backburner.run.apply(backburner, arguments); + } + + return ret; +}; + +/** + + If no run-loop is present, it creates a new one. If a run loop is + present it will queue itself to run on the existing run-loops action + queue. + + Please note: This is not for normal usage, and should be used sparingly. + + If invoked when not within a run loop: + + ```javascript + Ember.run.join(function(){ + // creates a new run-loop + }); + ``` + + Alternatively, if called within an existing run loop: + + ```javascript + Ember.run(function(){ + // creates a new run-loop + Ember.run.join(function(){ + // joins with the existing run-loop, and queues for invocation on + // the existing run-loops action queue. + }); + }); + ``` + + @method join + @namespace Ember + @param {Object} [target] target of method to call + @param {Function|String} method Method to invoke. + May be a function or a string. If you pass a string + then it will be looked up on the passed target. + @param {Object} [args*] Any additional arguments you wish to pass to the method. + @return {Object} return value from invoking the passed function. Please note, + when called within an existing loop, no return value is possible. +*/ +Ember.run.join = function(target, method) { + if (!Ember.run.currentRunLoop) { + return Ember.run.apply(Ember.run, arguments); } - return Ember.tryFinally(tryable, run.end); + var args = slice.call(arguments); + args.unshift('actions'); + Ember.run.schedule.apply(Ember.run, args); }; +Ember.run.backburner = backburner; + var run = Ember.run; +Ember.run.currentRunLoop = null; + +Ember.run.queues = backburner.queueNames; /** Begins a new RunLoop. Any deferred actions invoked after the begin will @@ -4569,7 +5148,7 @@ var run = Ember.run; @return {void} */ Ember.run.begin = function() { - run.currentRunLoop = new RunLoop(run.currentRunLoop); + backburner.begin(); }; /** @@ -4587,12 +5166,7 @@ Ember.run.begin = function() { @return {void} */ Ember.run.end = function() { - Ember.assert('must have a current run loop', run.currentRunLoop); - - function tryable() { run.currentRunLoop.end(); } - function finalizer() { run.currentRunLoop = run.currentRunLoop.prev(); } - - Ember.tryFinally(tryable, finalizer); + backburner.end(); }; /** @@ -4605,7 +5179,6 @@ Ember.run.end = function() { @type Array @default ['sync', 'actions', 'destroy'] */ -Ember.run.queues = ['sync', 'actions', 'destroy']; /** Adds the passed target/method and any optional arguments to the named @@ -4615,7 +5188,7 @@ Ember.run.queues = ['sync', 'actions', 'destroy']; At the end of a RunLoop, any methods scheduled in this way will be invoked. Methods will be invoked in an order matching the named queues defined in - the `run.queues` property. + the `Ember.run.queues` property. ```javascript Ember.run.schedule('sync', this, function(){ @@ -4644,57 +5217,18 @@ Ember.run.queues = ['sync', 'actions', 'destroy']; @return {void} */ Ember.run.schedule = function(queue, target, method) { - var loop = run.autorun(); - loop.schedule.apply(loop, arguments); + checkAutoRun(); + backburner.schedule.apply(backburner, arguments); }; -var scheduledAutorun; -function autorun() { - scheduledAutorun = null; - if (run.currentRunLoop) { run.end(); } -} - // Used by global test teardown Ember.run.hasScheduledTimers = function() { - return !!(scheduledAutorun || scheduledLater); + return backburner.hasTimers(); }; // Used by global test teardown Ember.run.cancelTimers = function () { - if (scheduledAutorun) { - clearTimeout(scheduledAutorun); - scheduledAutorun = null; - } - if (scheduledLater) { - clearTimeout(scheduledLater); - scheduledLater = null; - } - timers = {}; -}; - -/** - Begins a new RunLoop if necessary and schedules a timer to flush the - RunLoop at a later time. This method is used by parts of Ember to - ensure the RunLoop always finishes. You normally do not need to call this - method directly. Instead use `Ember.run()` - - @method autorun - @example - Ember.run.autorun(); - @return {Ember.RunLoop} the new current RunLoop -*/ -Ember.run.autorun = function() { - if (!run.currentRunLoop) { - Ember.assert("You have turned on testing mode, which disabled the run-loop's autorun. You will need to wrap any code with asynchronous side-effects in an Ember.run", !Ember.testing); - - run.begin(); - - if (!scheduledAutorun) { - scheduledAutorun = setTimeout(autorun, 1); - } - } - - return run.currentRunLoop; + backburner.cancelTimers(); }; /** @@ -4714,42 +5248,9 @@ Ember.run.autorun = function() { @return {void} */ Ember.run.sync = function() { - run.autorun(); - run.currentRunLoop.flush('sync'); + backburner.currentInstance.queues.sync.flush(); }; -// .......................................................... -// TIMERS -// - -var timers = {}; // active timers... - -var scheduledLater, scheduledLaterExpires; -function invokeLaterTimers() { - scheduledLater = null; - run(function() { - var now = (+ new Date()), earliest = -1; - for (var key in timers) { - if (!timers.hasOwnProperty(key)) { continue; } - var timer = timers[key]; - if (timer && timer.expires) { - if (now >= timer.expires) { - delete timers[key]; - invoke(timer.target, timer.method, timer.args, 2); - } else { - if (earliest < 0 || (timer.expires < earliest)) { earliest = timer.expires; } - } - } - } - - // schedule next timeout to fire when the earliest timer expires - if (earliest > 0) { - scheduledLater = setTimeout(invokeLaterTimers, earliest - now); - scheduledLaterExpires = earliest; - } - }); -} - /** Invokes the passed target/method and optional arguments after a specified period if time. The last parameter of this method must always be a number @@ -4774,80 +5275,35 @@ function invokeLaterTimers() { @param {Object} [args*] Optional arguments to pass to the timeout. @param {Number} wait Number of milliseconds to wait. @return {String} a string you can use to cancel the timer in - {{#crossLink "Ember/run.cancel"}}{{/crossLink}} later. + `Ember.run.cancel` later. */ Ember.run.later = function(target, method) { - var args, expires, timer, guid, wait; - - // setTimeout compatibility... - if (arguments.length===2 && 'function' === typeof target) { - wait = method; - method = target; - target = undefined; - args = [target, method]; - } else { - args = slice.call(arguments); - wait = args.pop(); - } - - expires = (+ new Date()) + wait; - timer = { target: target, method: method, expires: expires, args: args }; - guid = Ember.guidFor(timer); - timers[guid] = timer; - - if(scheduledLater && expires < scheduledLaterExpires) { - // Cancel later timer (then reschedule earlier timer below) - clearTimeout(scheduledLater); - scheduledLater = null; - } - - if (!scheduledLater) { - // Schedule later timers to be run. - scheduledLater = setTimeout(invokeLaterTimers, wait); - scheduledLaterExpires = expires; - } - - return guid; + return backburner.later.apply(backburner, arguments); }; -function invokeOnceTimer(guid, onceTimers) { - if (onceTimers[this.tguid]) { delete onceTimers[this.tguid][this.mguid]; } - if (timers[guid]) { invoke(this.target, this.method, this.args); } - delete timers[guid]; -} - -function scheduleOnce(queue, target, method, args) { - var tguid = Ember.guidFor(target), - mguid = Ember.guidFor(method), - onceTimers = run.autorun().onceTimers, - guid = onceTimers[tguid] && onceTimers[tguid][mguid], - timer; - - if (guid && timers[guid]) { - timers[guid].args = args; // replace args - } else { - timer = { - target: target, - method: method, - args: args, - tguid: tguid, - mguid: mguid - }; - - guid = Ember.guidFor(timer); - timers[guid] = timer; - if (!onceTimers[tguid]) { onceTimers[tguid] = {}; } - onceTimers[tguid][mguid] = guid; // so it isn't scheduled more than once - - run.schedule(queue, timer, invokeOnceTimer, guid, onceTimers); - } +/** + Schedule a function to run one time during the current RunLoop. This is equivalent + to calling `scheduleOnce` with the "actions" queue. - return guid; -} + @method once + @param {Object} [target] The target of the method to invoke. + @param {Function|String} method The method to invoke. + If you pass a string it will be resolved on the + target at the time the method is invoked. + @param {Object} [args*] Optional arguments to pass to the timeout. + @return {Object} timer +*/ +Ember.run.once = function(target, method) { + checkAutoRun(); + var args = slice.call(arguments); + args.unshift('actions'); + return backburner.scheduleOnce.apply(backburner, args); +}; /** - Schedules an item to run one time during the current RunLoop. Calling - this method with the same target/method combination will have no effect. + Schedules a function to run one time in a given queue of the current RunLoop. + Calling this method with the same queue/target/method combination will have + no effect (past the initial call). Note that although you can pass optional arguments these will not be considered when looking for duplicates. New arguments will replace previous @@ -4855,47 +5311,47 @@ function scheduleOnce(queue, target, method, args) { ```javascript Ember.run(function(){ - var doFoo = function() { foo(); } - Ember.run.once(myContext, doFoo); - Ember.run.once(myContext, doFoo); - // doFoo will only be executed once at the end of the RunLoop + var sayHi = function() { console.log('hi'); } + Ember.run.scheduleOnce('afterRender', myContext, sayHi); + Ember.run.scheduleOnce('afterRender', myContext, sayHi); + // doFoo will only be executed once, in the afterRender queue of the RunLoop }); ``` - Also note that passing an anonymous function to `Ember.run.once` will + Also note that passing an anonymous function to `Ember.run.scheduleOnce` will not prevent additional calls with an identical anonymous function from scheduling the items multiple times, e.g.: ```javascript function scheduleIt() { - Ember.run.once(myContext, function() { console.log("Closure"); }); + Ember.run.scheduleOnce('actions', myContext, function() { console.log("Closure"); }); } scheduleIt(); scheduleIt(); - // "Closure" will print twice, even though we're using `Ember.run.once`, + // "Closure" will print twice, even though we're using `Ember.run.scheduleOnce`, // because the function we pass to it is anonymous and won't match the // previously scheduled operation. ``` - @method once - @param {Object} [target] target of method to invoke + Available queues, and their order, can be found at `Ember.run.queues` + + @method scheduleOnce + @param {String} [queue] The name of the queue to schedule against. Default queues are 'sync' and 'actions'. + @param {Object} [target] The target of the method to invoke. @param {Function|String} method The method to invoke. If you pass a string it will be resolved on the target at the time the method is invoked. @param {Object} [args*] Optional arguments to pass to the timeout. @return {Object} timer */ -Ember.run.once = function(target, method) { - return scheduleOnce('actions', target, method, slice.call(arguments, 2)); -}; - -Ember.run.scheduleOnce = function(queue, target, method, args) { - return scheduleOnce(queue, target, method, slice.call(arguments, 3)); +Ember.run.scheduleOnce = function(queue, target, method) { + checkAutoRun(); + return backburner.scheduleOnce.apply(backburner, arguments); }; /** - Schedules an item to run from within a separate run loop, after - control has been returned to the system. This is equivalent to calling + Schedules an item to run from within a separate run loop, after + control has been returned to the system. This is equivalent to calling `Ember.run.later` with a wait time of 1ms. ```javascript @@ -4907,7 +5363,7 @@ Ember.run.scheduleOnce = function(queue, target, method, args) { Multiple operations scheduled with `Ember.run.next` will coalesce into the same later run loop, along with any other operations scheduled by `Ember.run.later` that expire right around the same - time that `Ember.run.next` operations will fire. + time that `Ember.run.next` operations will fire. Note that there are often alternatives to using `Ember.run.next`. For instance, if you'd like to schedule an operation to happen @@ -4933,13 +5389,13 @@ Ember.run.scheduleOnce = function(queue, target, method, args) { One benefit of the above approach compared to using `Ember.run.next` is that you will be able to perform DOM/CSS operations before unprocessed - elements are rendered to the screen, which may prevent flickering or + elements are rendered to the screen, which may prevent flickering or other artifacts caused by delaying processing until after rendering. - The other major benefit to the above approach is that `Ember.run.next` - introduces an element of non-determinism, which can make things much - harder to test, due to its reliance on `setTimeout`; it's much harder - to guarantee the order of scheduled operations when they are scheduled + The other major benefit to the above approach is that `Ember.run.next` + introduces an element of non-determinism, which can make things much + harder to test, due to its reliance on `setTimeout`; it's much harder + to guarantee the order of scheduled operations when they are scheduled outside of the current run loop, i.e. with `Ember.run.next`. @method next @@ -4952,8 +5408,8 @@ Ember.run.scheduleOnce = function(queue, target, method, args) { */ Ember.run.next = function() { var args = slice.call(arguments); - args.push(1); // 1 millisecond wait - return run.later.apply(this, args); + args.push(1); + return backburner.later.apply(backburner, args); }; /** @@ -4982,17 +5438,25 @@ Ember.run.next = function() { @return {void} */ Ember.run.cancel = function(timer) { - delete timers[timer]; + return backburner.cancel(timer); }; +// Make sure it's not an autorun during testing +function checkAutoRun() { + if (!Ember.run.currentRunLoop) { + Ember.assert("You have turned on testing mode, which disabled the run-loop's autorun. You will need to wrap any code with asynchronous side-effects in an Ember.run", !Ember.testing); + } +} + })(); (function() { // Ember.Logger -// get, set, trySet -// guidFor, isArray, meta +// get +// set +// guidFor, meta // addObserver, removeObserver // Ember.run.schedule /** @@ -5018,8 +5482,21 @@ Ember.LOG_BINDINGS = false || !!Ember.ENV.LOG_BINDINGS; var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, - isGlobalPath = Ember.isGlobalPath; + IS_GLOBAL = /^([A-Z$]|([0-9][A-Z$]))/; +/** + Returns true if the provided path is global (e.g., `MyApp.fooController.bar`) + instead of local (`foo.bar.baz`). + + @method isGlobalPath + @for Ember + @private + @param {String} path + @return Boolean +*/ +var isGlobalPath = Ember.isGlobalPath = function(path) { + return IS_GLOBAL.test(path); +}; function getWithGlobals(obj, path) { return get(isGlobalPath(path) ? Ember.lookup : obj, path); @@ -5255,7 +5732,7 @@ function mixinProperties(to, from) { mixinProperties(Binding, { /** - See {{#crossLink "Ember.Binding/from"}}{{/crossLink}} + See `Ember.Binding.from`. @method from @static @@ -5266,7 +5743,7 @@ mixinProperties(Binding, { }, /** - See {{#crossLink "Ember.Binding/to"}}{{/crossLink}} + See `Ember.Binding.to`. @method to @static @@ -5283,7 +5760,7 @@ mixinProperties(Binding, { This means that if you change the "to" side directly, the "from" side may have a different value. - See {{#crossLink "Binding/oneWay"}}{{/crossLink}} + See `Binding.oneWay`. @method oneWay @param {String} from from path. @@ -5311,7 +5788,7 @@ mixinProperties(Binding, { Properties ending in a `Binding` suffix will be converted to `Ember.Binding` instances. The value of this property should be a string representing a path to another object or a custom binding instanced created using Binding helpers - (see "Customizing Your Bindings"): + (see "One Way Bindings"): ``` valueBinding: "MyApp.someController.title" @@ -5344,7 +5821,7 @@ mixinProperties(Binding, { You should consider using one way bindings anytime you have an object that may be created frequently and you do not intend to change a property; only - to monitor it for changes. (such as in the example above). + to monitor it for changes (such as in the example above). ## Adding Bindings Manually @@ -5611,7 +6088,7 @@ function addNormalizedProperty(base, key, value, meta, descs, values, concats) { } } -function mergeMixins(mixins, m, descs, values, base) { +function mergeMixins(mixins, m, descs, values, base, keys) { var mixin, props, key, concats, meta; function removeKeys(keyName) { @@ -5632,26 +6109,19 @@ function mergeMixins(mixins, m, descs, values, base) { for (key in props) { if (!props.hasOwnProperty(key)) { continue; } + keys.push(key); addNormalizedProperty(base, key, props[key], meta, descs, values, concats); } // manually copy toString() because some JS engines do not enumerate it if (props.hasOwnProperty('toString')) { base.toString = props.toString; } } else if (mixin.mixins) { - mergeMixins(mixin.mixins, m, descs, values, base); + mergeMixins(mixin.mixins, m, descs, values, base, keys); if (mixin._without) { a_forEach.call(mixin._without, removeKeys); } } } } -function writableReq(obj) { - var m = Ember.meta(obj), req = m.required; - if (!req || !m.hasOwnProperty('required')) { - req = m.required = req ? o_create(req) : {}; - } - return req; -} - var IS_BINDING = Ember.IS_BINDING = /^.+Binding$/; function detectBinding(obj, key, value, m) { @@ -5734,7 +6204,7 @@ function replaceObservers(obj, key, observer) { function applyMixin(obj, mixins, partial) { var descs = {}, values = {}, m = Ember.meta(obj), - key, value, desc; + key, value, desc, keys = []; // Go through all mixins and hashes passed in, and: // @@ -5742,10 +6212,11 @@ function applyMixin(obj, mixins, partial) { // * Set up _super wrapping if necessary // * Set up computed property descriptors // * Copying `toString` in broken browsers - mergeMixins(mixins, mixinsMeta(obj), descs, values, obj); + mergeMixins(mixins, mixinsMeta(obj), descs, values, obj, keys); - for(key in values) { - if (key === 'contructor' || !values.hasOwnProperty(key)) { continue; } + for(var i = 0, l = keys.length; i < l; i++) { + key = keys[i]; + if (key === 'constructor' || !values.hasOwnProperty(key)) { continue; } desc = descs[key]; value = values[key]; @@ -5799,7 +6270,7 @@ Ember.mixin = function(obj) { }); // Mix mixins into classes by passing them as the first arguments to - // .extend or .create. + // .extend. App.CommentView = Ember.View.extend(App.Editable, { template: Ember.Handlebars.compile('{{#if isEditing}}...{{else}}...{{/if}}') }); @@ -5818,6 +6289,12 @@ Ember.Mixin = function() { return initMixin(this, arguments); }; Mixin = Ember.Mixin; +Mixin.prototype = { + properties: null, + mixins: null, + ownerConstructor: null +}; + Mixin._apply = applyMixin; Mixin.applyPartial = function(obj) { @@ -6042,7 +6519,7 @@ Ember.alias = function(methodName) { return new Alias(methodName); }; -Ember.deprecateFunc("Ember.alias is deprecated. Please use Ember.aliasMethod or Ember.computed.alias instead.", Ember.alias); +Ember.alias = Ember.deprecateFunc("Ember.alias is deprecated. Please use Ember.aliasMethod or Ember.computed.alias instead.", Ember.alias); /** Makes a method available via an additional name. @@ -6103,6 +6580,29 @@ Ember.immediateObserver = function() { }; /** + When observers fire, they are called with the arguments `obj`, `keyName` + and `value`. In a typical observer, value is the new, post-change value. + + A `beforeObserver` fires before a property changes. The `value` argument contains + the pre-change value. + + A `beforeObserver` is an alternative form of `.observesBefore()`. + + ```javascript + App.PersonView = Ember.View.extend({ + valueWillChange: function (obj, keyName, value) { + this.changingFrom = value; + }.observesBefore('content.value'), + valueDidChange: function(obj, keyName, value) { + // only run if updating a value already in the DOM + if(this.get('state') === 'inDOM') { + var color = value > this.changingFrom ? 'green' : 'red'; + // logic + } + }.observes('content.value') + }); + ``` + @method beforeObserver @for Ember @param {Function} func @@ -6130,14 +6630,57 @@ Ember Metal })(); (function() { -define("rsvp", - [], - function() { +define("rsvp/all", + ["rsvp/defer","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var defer = __dependency1__.defer; + + function all(promises) { + var results = [], deferred = defer(), remaining = promises.length; + + if (remaining === 0) { + deferred.resolve([]); + } + + var resolver = function(index) { + return function(value) { + resolveAll(index, value); + }; + }; + + var resolveAll = function(index, value) { + results[index] = value; + if (--remaining === 0) { + deferred.resolve(results); + } + }; + + var rejectAll = function(error) { + deferred.reject(error); + }; + + for (var i = 0; i < promises.length; i++) { + if (promises[i] && typeof promises[i].then === 'function') { + promises[i].then(resolver(i), rejectAll); + } else { + resolveAll(i, promises[i]); + } + } + return deferred.promise; + } + + __exports__.all = all; + }); + +define("rsvp/async", + ["exports"], + function(__exports__) { "use strict"; var browserGlobal = (typeof window !== 'undefined') ? window : {}; - var MutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; - var RSVP, async; + var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; + var async; if (typeof process !== 'undefined' && {}.toString.call(process) === '[object process]') { @@ -6146,10 +6689,10 @@ define("rsvp", callback.call(binding); }); }; - } else if (MutationObserver) { + } else if (BrowserMutationObserver) { var queue = []; - var observer = new MutationObserver(function() { + var observer = new BrowserMutationObserver(function() { var toProcess = queue.slice(); queue = []; @@ -6180,6 +6723,47 @@ define("rsvp", }; } + + __exports__.async = async; + }); + +define("rsvp/config", + ["rsvp/async","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var async = __dependency1__.async; + + var config = {}; + config.async = async; + + __exports__.config = config; + }); + +define("rsvp/defer", + ["rsvp/promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__.Promise; + + function defer() { + var deferred = {}; + + var promise = new Promise(function(resolve, reject) { + deferred.resolve = resolve; + deferred.reject = reject; + }); + + deferred.promise = promise; + return deferred; + } + + __exports__.defer = defer; + }); + +define("rsvp/events", + ["exports"], + function(__exports__) { + "use strict"; var Event = function(type, options) { this.type = type; @@ -6274,7 +6858,148 @@ define("rsvp", } }; - var Promise = function() { + + __exports__.EventTarget = EventTarget; + }); + +define("rsvp/hash", + ["rsvp/defer","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var defer = __dependency1__.defer; + + function size(object) { + var size = 0; + + for (var prop in object) { + size++; + } + + return size; + } + + function hash(promises) { + var results = {}, deferred = defer(), remaining = size(promises); + + if (remaining === 0) { + deferred.resolve({}); + } + + var resolver = function(prop) { + return function(value) { + resolveAll(prop, value); + }; + }; + + var resolveAll = function(prop, value) { + results[prop] = value; + if (--remaining === 0) { + deferred.resolve(results); + } + }; + + var rejectAll = function(error) { + deferred.reject(error); + }; + + for (var prop in promises) { + if (promises[prop] && typeof promises[prop].then === 'function') { + promises[prop].then(resolver(prop), rejectAll); + } else { + resolveAll(prop, promises[prop]); + } + } + + return deferred.promise; + } + + __exports__.hash = hash; + }); + +define("rsvp/node", + ["rsvp/promise","rsvp/all","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var Promise = __dependency1__.Promise; + var all = __dependency2__.all; + + function makeNodeCallbackFor(resolve, reject) { + return function (error, value) { + if (error) { + reject(error); + } else if (arguments.length > 2) { + resolve(Array.prototype.slice.call(arguments, 1)); + } else { + resolve(value); + } + }; + } + + function denodeify(nodeFunc) { + return function() { + var nodeArgs = Array.prototype.slice.call(arguments), resolve, reject; + + var promise = new Promise(function(nodeResolve, nodeReject) { + resolve = nodeResolve; + reject = nodeReject; + }); + + all(nodeArgs).then(function(nodeArgs) { + nodeArgs.push(makeNodeCallbackFor(resolve, reject)); + + try { + nodeFunc.apply(this, nodeArgs); + } catch(e) { + reject(e); + } + }); + + return promise; + }; + } + + __exports__.denodeify = denodeify; + }); + +define("rsvp/promise", + ["rsvp/config","rsvp/events","exports"], + function(__dependency1__, __dependency2__, __exports__) { + "use strict"; + var config = __dependency1__.config; + var EventTarget = __dependency2__.EventTarget; + + function objectOrFunction(x) { + return isFunction(x) || (typeof x === "object" && x !== null); + } + + function isFunction(x){ + return typeof x === "function"; + } + + var Promise = function(resolver) { + var promise = this, + resolved = false; + + if (typeof resolver !== 'function') { + throw new TypeError('You must pass a resolver function as the sole argument to the promise constructor'); + } + + if (!(promise instanceof Promise)) { + return new Promise(resolver); + } + + var resolvePromise = function(value) { + if (resolved) { return; } + resolved = true; + resolve(promise, value); + }; + + var rejectPromise = function(value) { + if (resolved) { return; } + resolved = true; + reject(promise, value); + }; + this.on('promise:resolved', function(event) { this.trigger('success', { detail: event.detail }); }, this); @@ -6282,12 +7007,16 @@ define("rsvp", this.on('promise:failed', function(event) { this.trigger('error', { detail: event.detail }); }, this); - }; - var noop = function() {}; + try { + resolver(resolvePromise, rejectPromise); + } catch(e) { + rejectPromise(e); + } + }; var invokeCallback = function(type, promise, callback, event) { - var hasCallback = typeof callback === 'function', + var hasCallback = isFunction(callback), value, error, succeeded, failed; if (hasCallback) { @@ -6303,34 +7032,34 @@ define("rsvp", succeeded = true; } - if (value && typeof value.then === 'function') { - value.then(function(value) { - promise.resolve(value); - }, function(error) { - promise.reject(error); - }); + if (handleThenable(promise, value)) { + return; } else if (hasCallback && succeeded) { - promise.resolve(value); + resolve(promise, value); } else if (failed) { - promise.reject(error); - } else { - promise[type](value); + reject(promise, error); + } else if (type === 'resolve') { + resolve(promise, value); + } else if (type === 'reject') { + reject(promise, value); } }; Promise.prototype = { + constructor: Promise, + then: function(done, fail) { - var thenPromise = new Promise(); + var thenPromise = new Promise(function() {}); - if (this.isResolved) { - RSVP.async(function() { - invokeCallback('resolve', thenPromise, done, { detail: this.resolvedValue }); + if (this.isFulfilled) { + config.async(function() { + invokeCallback('resolve', thenPromise, done, { detail: this.fulfillmentValue }); }, this); } if (this.isRejected) { - RSVP.async(function() { - invokeCallback('reject', thenPromise, fail, { detail: this.rejectedValue }); + config.async(function() { + invokeCallback('reject', thenPromise, fail, { detail: this.rejectedReason }); }, this); } @@ -6343,75 +7072,162 @@ define("rsvp", }); return thenPromise; - }, + } + }; - resolve: function(value) { - resolve(this, value); + EventTarget.mixin(Promise.prototype); - this.resolve = noop; - this.reject = noop; - }, + function resolve(promise, value) { + if (promise === value) { + fulfill(promise, value); + } else if (!handleThenable(promise, value)) { + fulfill(promise, value); + } + } - reject: function(value) { - reject(this, value); + function handleThenable(promise, value) { + var then = null; - this.resolve = noop; - this.reject = noop; + if (objectOrFunction(value)) { + try { + then = value.then; + } catch(e) { + reject(promise, e); + return true; + } + + if (isFunction(then)) { + try { + then.call(value, function(val) { + if (value !== val) { + resolve(promise, val); + } else { + fulfill(promise, val); + } + }, function(val) { + reject(promise, val); + }); + } catch (e) { + reject(promise, e); + } + return true; + } } - }; - function resolve(promise, value) { - RSVP.async(function() { + return false; + } + + function fulfill(promise, value) { + config.async(function() { promise.trigger('promise:resolved', { detail: value }); - promise.isResolved = true; - promise.resolvedValue = value; + promise.isFulfilled = true; + promise.fulfillmentValue = value; }); } function reject(promise, value) { - RSVP.async(function() { + config.async(function() { promise.trigger('promise:failed', { detail: value }); promise.isRejected = true; - promise.rejectedValue = value; + promise.rejectedReason = value; }); } - function all(promises) { - var i, results = []; - var allPromise = new Promise(); - var remaining = promises.length; - if (remaining === 0) { - allPromise.resolve([]); - } + __exports__.Promise = Promise; + }); - var resolver = function(index) { - return function(value) { - resolve(index, value); - }; - }; +define("rsvp/reject", + ["rsvp/promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__.Promise; - var resolve = function(index, value) { - results[index] = value; - if (--remaining === 0) { - allPromise.resolve(results); - } - }; - var reject = function(error) { - allPromise.reject(error); - }; + function objectOrFunction(x) { + return typeof x === "function" || (typeof x === "object" && x !== null); + } - for (i = 0; i < remaining; i++) { - promises[i].then(resolver(i), reject); - } - return allPromise; + + function reject(reason) { + return new Promise(function (resolve, reject) { + reject(reason); + }); } - EventTarget.mixin(Promise.prototype); - RSVP = { async: async, Promise: Promise, Event: Event, EventTarget: EventTarget, all: all, raiseOnUncaughtExceptions: true }; - return RSVP; + __exports__.reject = reject; + }); + +define("rsvp/resolve", + ["rsvp/promise","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Promise = __dependency1__.Promise; + + + function objectOrFunction(x) { + return typeof x === "function" || (typeof x === "object" && x !== null); + } + + function resolve(thenable){ + var promise = new Promise(function(resolve, reject){ + var then; + + try { + if ( objectOrFunction(thenable) ) { + then = thenable.then; + + if (typeof then === "function") { + then.call(thenable, resolve, reject); + } else { + resolve(thenable); + } + + } else { + resolve(thenable); + } + + } catch(error) { + reject(error); + } + }); + + return promise; + } + + + __exports__.resolve = resolve; + }); + +define("rsvp", + ["rsvp/events","rsvp/promise","rsvp/node","rsvp/all","rsvp/hash","rsvp/defer","rsvp/config","rsvp/resolve","rsvp/reject","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __exports__) { + "use strict"; + var EventTarget = __dependency1__.EventTarget; + var Promise = __dependency2__.Promise; + var denodeify = __dependency3__.denodeify; + var all = __dependency4__.all; + var hash = __dependency5__.hash; + var defer = __dependency6__.defer; + var config = __dependency7__.config; + var resolve = __dependency8__.resolve; + var reject = __dependency9__.reject; + + function configure(name, value) { + config[name] = value; + } + + + __exports__.Promise = Promise; + __exports__.EventTarget = EventTarget; + __exports__.all = all; + __exports__.hash = hash; + __exports__.defer = defer; + __exports__.denodeify = denodeify; + __exports__.configure = configure; + __exports__.resolve = resolve; + __exports__.reject = reject; }); })(); @@ -6588,10 +7404,6 @@ define("container", this.children = []; eachDestroyable(this, function(item) { - item.isDestroying = true; - }); - - eachDestroyable(this, function(item) { item.destroy(); }); @@ -6657,7 +7469,7 @@ define("container", var factory = factoryFor(container, fullName); var splitName = fullName.split(":"), - type = splitName[0], name = splitName[1], + type = splitName[0], value; if (option(container, fullName, 'instantiate') === false) { @@ -6708,79 +7520,6 @@ define("container", var indexOf = Ember.EnumerableUtils.indexOf; -// ........................................ -// TYPING & ARRAY MESSAGING -// - -var TYPE_MAP = {}; -var t = "Boolean Number String Function Array Date RegExp Object".split(" "); -Ember.ArrayPolyfills.forEach.call(t, function(name) { - TYPE_MAP[ "[object " + name + "]" ] = name.toLowerCase(); -}); - -var toString = Object.prototype.toString; - -/** - Returns a consistent type for the passed item. - - Use this instead of the built-in `typeof` to get the type of an item. - It will return the same result across all browsers and includes a bit - more detail. Here is what will be returned: - - | Return Value | Meaning | - |---------------|------------------------------------------------------| - | 'string' | String primitive | - | 'number' | Number primitive | - | 'boolean' | Boolean primitive | - | 'null' | Null value | - | 'undefined' | Undefined value | - | 'function' | A function | - | 'array' | An instance of Array | - | 'class' | An Ember class (created using Ember.Object.extend()) | - | 'instance' | An Ember object instance | - | 'error' | An instance of the Error object | - | 'object' | A JavaScript object not inheriting from Ember.Object | - - Examples: - - ```javascript - Ember.typeOf(); // 'undefined' - Ember.typeOf(null); // 'null' - Ember.typeOf(undefined); // 'undefined' - Ember.typeOf('michael'); // 'string' - Ember.typeOf(101); // 'number' - Ember.typeOf(true); // 'boolean' - Ember.typeOf(Ember.makeArray); // 'function' - Ember.typeOf([1,2,90]); // 'array' - Ember.typeOf(Ember.Object.extend()); // 'class' - Ember.typeOf(Ember.Object.create()); // 'instance' - Ember.typeOf(new Error('teamocil')); // 'error' - - // "normal" JavaScript object - Ember.typeOf({a: 'b'}); // 'object' - ``` - - @method typeOf - @for Ember - @param {Object} item the item to check - @return {String} the type -*/ -Ember.typeOf = function(item) { - var ret; - - ret = (item === null || item === undefined) ? String(item) : TYPE_MAP[toString.call(item)] || 'object'; - - if (ret === 'function') { - if (Ember.Object && Ember.Object.detect(item)) ret = 'class'; - } else if (ret === 'object') { - if (item instanceof Error) ret = 'error'; - else if (Ember.Object && item instanceof Ember.Object) ret = 'instance'; - else ret = 'object'; - } - - return ret; -}; - /** This will compare two javascript values of possibly different types. It will tell you which one is greater than the other by returning: @@ -7035,10 +7774,15 @@ Ember.ORDER_DEFINITION = Ember.ENV.ORDER_DEFINITION || [ */ Ember.keys = Object.keys; -if (!Ember.keys) { +if (!Ember.keys || Ember.create.isSimulated) { Ember.keys = function(obj) { var ret = []; for(var key in obj) { + // Prevents browsers that don't respect non-enumerability from + // copying internal Ember properties + if (key.substring(0,2) === '__') continue; + if (key === '_super') continue; + if (obj.hasOwnProperty(key)) { ret.push(key); } } return ret; @@ -7060,7 +7804,7 @@ var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'n @constructor */ Ember.Error = function() { - var tmp = Error.prototype.constructor.apply(this, arguments); + var tmp = Error.apply(this, arguments); // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. for (var idx = 0; idx < errorProps.length; idx++) { @@ -7139,7 +7883,8 @@ Ember.String = { ``` @method fmt - @param {Object...} [args] + @param {String} str The string to format + @param {Array} formats An array of parameters to interpolate into string. @return {String} formatted string */ fmt: function(str, formats) { @@ -7321,10 +8066,12 @@ Ember.String = { /** Returns the Capitalized form of a string - 'innerHTML'.capitalize() // 'InnerHTML' - 'action_name'.capitalize() // 'Action_name' - 'css-class-name'.capitalize() // 'Css-class-name' - 'my favorite items'.capitalize() // 'My favorite items' + ```javascript + 'innerHTML'.capitalize() // 'InnerHTML' + 'action_name'.capitalize() // 'Action_name' + 'css-class-name'.capitalize() // 'Css-class-name' + 'my favorite items'.capitalize() // 'My favorite items' + ``` @method capitalize @param {String} str @@ -7361,7 +8108,7 @@ var fmt = Ember.String.fmt, if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { /** - See {{#crossLink "Ember.String/fmt"}}{{/crossLink}} + See `Ember.String.fmt`. @method fmt @for String @@ -7371,7 +8118,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/w"}}{{/crossLink}} + See `Ember.String.w`. @method w @for String @@ -7381,7 +8128,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/loc"}}{{/crossLink}} + See `Ember.String.loc`. @method loc @for String @@ -7391,7 +8138,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/camelize"}}{{/crossLink}} + See `Ember.String.camelize`. @method camelize @for String @@ -7401,7 +8148,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/decamelize"}}{{/crossLink}} + See `Ember.String.decamelize`. @method decamelize @for String @@ -7411,7 +8158,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/dasherize"}}{{/crossLink}} + See `Ember.String.dasherize`. @method dasherize @for String @@ -7421,7 +8168,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/underscore"}}{{/crossLink}} + See `Ember.String.underscore`. @method underscore @for String @@ -7431,7 +8178,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/classify"}}{{/crossLink}} + See `Ember.String.classify`. @method classify @for String @@ -7441,7 +8188,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { }; /** - See {{#crossLink "Ember.String/capitalize"}}{{/crossLink}} + See `Ember.String.capitalize`. @method capitalize @for String @@ -7517,8 +8264,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Function) { will instead clear the cache so that it is updated when the next `get` is called on the property. - See {{#crossLink "Ember.ComputedProperty"}}{{/crossLink}}, - {{#crossLink "Ember/computed"}}{{/crossLink}} + See `Ember.ComputedProperty`, `Ember.computed`. @method property @for Function @@ -7545,7 +8291,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Function) { }); ``` - See {{#crossLink "Ember.Observable/observes"}}{{/crossLink}} + See `Ember.Observable.observes`. @method observes @for Function @@ -7572,7 +8318,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Function) { }); ``` - See {{#crossLink "Ember.Observable/observesBefore"}}{{/crossLink}} + See `Ember.Observable.observesBefore`. @method observesBefore @for Function @@ -8469,9 +9215,7 @@ Ember.Enumerable = Ember.Mixin.create({ // HELPERS // -var get = Ember.get, set = Ember.set, map = Ember.EnumerableUtils.map, cacheFor = Ember.cacheFor; - -function none(obj) { return obj===null || obj===undefined; } +var get = Ember.get, set = Ember.set, isNone = Ember.isNone, map = Ember.EnumerableUtils.map, cacheFor = Ember.cacheFor; // .......................................................... // ARRAY @@ -8494,7 +9238,7 @@ function none(obj) { return obj===null || obj===undefined; } You can use the methods defined in this module to access and modify array contents in a KVO-friendly way. You can also be notified whenever the - membership if an array changes by changing the syntax of the property to + membership of an array changes by changing the syntax of the property to `.observes('*myProperty.[]')`. To support `Ember.Array` in your own class, you must override two @@ -8511,9 +9255,6 @@ function none(obj) { return obj===null || obj===undefined; } */ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.prototype */ { - // compatibility - isSCArray: true, - /** Your array must support the `length` property. Your replace methods should set this property whenever it changes. @@ -8542,7 +9283,7 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot @method objectAt @param {Number} idx The index of the item to return. - @return {any} item at index or undefined + @return {*} item at index or undefined */ objectAt: function(idx) { if ((idx < 0) || (idx>=get(this, 'length'))) return undefined ; @@ -8621,8 +9362,8 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot slice: function(beginIndex, endIndex) { var ret = Ember.A([]); var length = get(this, 'length') ; - if (none(beginIndex)) beginIndex = 0 ; - if (none(endIndex) || (endIndex > length)) endIndex = length ; + if (isNone(beginIndex)) beginIndex = 0 ; + if (isNone(endIndex) || (endIndex > length)) endIndex = length ; if (beginIndex < 0) beginIndex = length + beginIndex; if (endIndex < 0) endIndex = length + endIndex; @@ -8782,7 +9523,7 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot @param {Number} startIdx The starting index in the array that will change. @param {Number} removeAmt The number of items that will be removed. If you pass `null` assumes 0 - @param {Number} addAmt The number of items that will be added If you + @param {Number} addAmt The number of items that will be added. If you pass `null` assumes 0. @return {Ember.Array} receiver */ @@ -8816,6 +9557,20 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot return this; }, + /** + If you are implementing an object that supports `Ember.Array`, call this + method just after the array content changes to notify any observers and + invalidate any related properties. Pass the starting index of the change + as well as a delta of the amounts to change. + + @method arrayContentDidChange + @param {Number} startIdx The starting index in the array that did change. + @param {Number} removeAmt The number of items that were removed. If you + pass `null` assumes 0 + @param {Number} addAmt The number of items that were added. If you + pass `null` assumes 0. + @return {Ember.Array} receiver + */ arrayContentDidChange: function(startIdx, removeAmt, addAmt) { // if no args are passed assume everything changes @@ -8956,8 +9711,7 @@ var get = Ember.get, set = Ember.set; @extends Ember.Mixin @since Ember 0.9 */ -Ember.Copyable = Ember.Mixin.create( -/** @scope Ember.Copyable.prototype */ { +Ember.Copyable = Ember.Mixin.create(/** @scope Ember.Copyable.prototype */ { /** Override to return a copy of the receiver. Default implementation raises @@ -9062,8 +9816,7 @@ var get = Ember.get, set = Ember.set; @extends Ember.Mixin @since Ember 0.9 */ -Ember.Freezable = Ember.Mixin.create( -/** @scope Ember.Freezable.prototype */ { +Ember.Freezable = Ember.Mixin.create(/** @scope Ember.Freezable.prototype */ { /** Set to `true` when the object is frozen. Use this property to detect @@ -9116,7 +9869,7 @@ var forEach = Ember.EnumerableUtils.forEach; To add an object to an enumerable, use the `addObject()` method. This method will only add the object to the enumerable if the object is not - already present and the object if of a type supported by the enumerable. + already present and is of a type supported by the enumerable. ```javascript set.addObject(contact); @@ -9124,8 +9877,8 @@ var forEach = Ember.EnumerableUtils.forEach; ## Removing Objects - To remove an object form an enumerable, use the `removeObject()` method. This - will only remove the object if it is already in the enumerable, otherwise + To remove an object from an enumerable, use the `removeObject()` method. This + will only remove the object if it is present in the enumerable, otherwise this method has no effect. ```javascript @@ -9152,7 +9905,7 @@ Ember.MutableEnumerable = Ember.Mixin.create(Ember.Enumerable, { already present in the collection. If the object is present, this method has no effect. - If the passed object is of a type not supported by the receiver + If the passed object is of a type not supported by the receiver, then this method should raise an exception. @method addObject @@ -9179,10 +9932,10 @@ Ember.MutableEnumerable = Ember.Mixin.create(Ember.Enumerable, { __Required.__ You must implement this method to apply this mixin. Attempts to remove the passed object from the receiver collection if the - object is in present in the collection. If the object is not present, + object is present in the collection. If the object is not present, this method has no effect. - If the passed object is of a type not supported by the receiver + If the passed object is of a type not supported by the receiver, then this method should raise an exception. @method removeObject @@ -9193,7 +9946,7 @@ Ember.MutableEnumerable = Ember.Mixin.create(Ember.Enumerable, { /** - Removes each objects in the passed enumerable from the receiver. + Removes each object in the passed enumerable from the receiver. @method removeObjects @param {Ember.Enumerable} objects the objects to remove @@ -9244,8 +9997,7 @@ var get = Ember.get, set = Ember.set; @uses Ember.Array @uses Ember.MutableEnumerable */ -Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, - /** @scope Ember.MutableArray.prototype */ { +Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable,/** @scope Ember.MutableArray.prototype */ { /** __Required.__ You must implement this method to apply this mixin. @@ -9351,8 +10103,8 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, ``` @method pushObject - @param {anything} obj object to push - @return {any} the same obj passed as param + @param {*} obj object to push + @return {*} the same obj passed as param */ pushObject: function(obj) { this.insertAt(get(this, 'length'), obj) ; @@ -9431,8 +10183,8 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, ``` @method unshiftObject - @param {anything} obj object to unshift - @return {any} the same obj passed as param + @param {*} obj object to unshift + @return {*} the same obj passed as param */ unshiftObject: function(obj) { this.insertAt(0, obj) ; @@ -9516,7 +10268,6 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, }); - })(); @@ -9564,7 +10315,7 @@ var get = Ember.get, set = Ember.set; For example: ```javascript - Ember.Object.create({ + Ember.Object.extend({ valueObserver: function() { // Executes whenever the "value" property changes }.observes('value') @@ -9583,8 +10334,8 @@ var get = Ember.get, set = Ember.set; object.addObserver('propertyKey', targetObject, targetAction) ``` - This will call the `targetAction` method on the `targetObject` to be called - whenever the value of the `propertyKey` changes. + This will call the `targetAction` method on the `targetObject` whenever + the value of the `propertyKey` changes. Note that if `propertyKey` is a computed property, the observer will be called when any of the property dependencies are changed, even if the @@ -9844,8 +10595,8 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { This is the core method used to register an observer for a property. - Once you call this method, anytime the key's value is set, your observer - will be notified. Note that the observers are triggered anytime the + Once you call this method, any time the key's value is set, your observer + will be notified. Note that the observers are triggered any time the value is set, regardless of whether it has actually changed. Your observer should be prepared to handle that. @@ -9970,11 +10721,11 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { @method incrementProperty @param {String} keyName The name of the property to increment - @param {Object} increment The amount to increment by. Defaults to 1 - @return {Object} The new property value + @param {Number} increment The amount to increment by. Defaults to 1 + @return {Number} The new property value */ incrementProperty: function(keyName, increment) { - if (!increment) { increment = 1; } + if (Ember.isNone(increment)) { increment = 1; } set(this, keyName, (get(this, keyName) || 0)+increment); return get(this, keyName); }, @@ -9989,12 +10740,12 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { @method decrementProperty @param {String} keyName The name of the property to decrement - @param {Object} increment The amount to decrement by. Defaults to 1 - @return {Object} The new property value + @param {Number} decrement The amount to decrement by. Defaults to 1 + @return {Number} The new property value */ - decrementProperty: function(keyName, increment) { - if (!increment) { increment = 1; } - set(this, keyName, (get(this, keyName) || 0)-increment); + decrementProperty: function(keyName, decrement) { + if (Ember.isNone(decrement)) { decrement = 1; } + set(this, keyName, (get(this, keyName) || 0)-decrement); return get(this, keyName); }, @@ -10003,7 +10754,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { current value. ```javascript - starship.toggleProperty('warpDriveEnaged'); + starship.toggleProperty('warpDriveEngaged'); ``` @method toggleProperty @@ -10035,7 +10786,6 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { } }); - })(); @@ -10049,6 +10799,15 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { var get = Ember.get, set = Ember.set; /** +`Ember.TargetActionSupport` is a mixin that can be included in a class +to add a `triggerAction` method with semantics similar to the Handlebars +`{{action}}` helper. In normal Ember usage, the `{{action}}` helper is +usually the best choice. This mixin is most often useful when you are +doing more complex event handling in View objects. + +See also `Ember.ViewTargetActionSupport`, which has +view-aware defaults for target and actionContext. + @class TargetActionSupport @namespace Ember @extends Ember.Mixin @@ -10056,6 +10815,7 @@ var get = Ember.get, set = Ember.set; Ember.TargetActionSupport = Ember.Mixin.create({ target: null, action: null, + actionContext: null, targetObject: Ember.computed(function() { var target = get(this, 'target'); @@ -10069,21 +10829,86 @@ Ember.TargetActionSupport = Ember.Mixin.create({ } }).property('target'), - triggerAction: function() { - var action = get(this, 'action'), - target = get(this, 'targetObject'); + actionContextObject: Ember.computed(function() { + var actionContext = get(this, 'actionContext'); + + if (Ember.typeOf(actionContext) === "string") { + var value = get(this, actionContext); + if (value === undefined) { value = get(Ember.lookup, actionContext); } + return value; + } else { + return actionContext; + } + }).property('actionContext'), + + /** + Send an "action" with an "actionContext" to a "target". The action, actionContext + and target will be retrieved from properties of the object. For example: + + ```javascript + App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { + target: Ember.computed.alias('controller'), + action: 'save', + actionContext: Ember.computed.alias('context'), + click: function(){ + this.triggerAction(); // Sends the `save` action, along with the current context + // to the current controller + } + }); + ``` + + The `target`, `action`, and `actionContext` can be provided as properties of + an optional object argument to `triggerAction` as well. + + ```javascript + App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { + click: function(){ + this.triggerAction({ + action: 'save', + target: this.get('controller'), + actionContext: this.get('context'), + }); // Sends the `save` action, along with the current context + // to the current controller + } + }); + ``` + + The `actionContext` defaults to the object you mixing `TargetActionSupport` into. + But `target` and `action` must be specified either as properties or with the argument + to `triggerAction`, or a combination: + + ```javascript + App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { + target: Ember.computed.alias('controller'), + click: function(){ + this.triggerAction({ + action: 'save' + }); // Sends the `save` action, along with a reference to `this`, + // to the current controller + } + }); + ``` + + @method triggerAction + @param opts {Hash} (optional, with the optional keys action, target and/or actionContext) + @return {Boolean} true if the action was sent successfully and did not return false + */ + triggerAction: function(opts) { + opts = opts || {}; + var action = opts['action'] || get(this, 'action'), + target = opts['target'] || get(this, 'targetObject'), + actionContext = opts['actionContext'] || get(this, 'actionContextObject') || this; if (target && action) { var ret; - if (typeof target.send === 'function') { - ret = target.send(action, this); + if (target.send) { + ret = target.send.apply(target, [action, actionContext]); } else { - if (typeof action === 'string') { - action = target[action]; - } - ret = action.call(target, this); + Ember.assert("The action '" + action + "' did not exist on " + target, typeof target[action] === 'function'); + ret = target[action].apply(target, [actionContext]); } + if (ret !== false) ret = true; return ret; @@ -10255,9 +11080,9 @@ Ember.Evented = Ember.Mixin.create({ (function() { var RSVP = requireModule("rsvp"); -RSVP.async = function(callback, binding) { +RSVP.configure('async', function(callback, binding) { Ember.run.schedule('actions', binding, callback); -}; +}); /** @module ember @@ -10279,9 +11104,22 @@ Ember.DeferredMixin = Ember.Mixin.create({ @param {Function} doneCallback a callback function to be called when done @param {Function} failCallback a callback function to be called when failed */ - then: function(doneCallback, failCallback) { - var promise = get(this, 'promise'); - return promise.then.apply(promise, arguments); + then: function(resolve, reject) { + var deferred, promise, entity; + + entity = this; + deferred = get(this, '_deferred'); + promise = deferred.promise; + + function fulfillmentHandler(fulfillment) { + if (fulfillment === promise) { + return resolve(entity); + } else { + return resolve(fulfillment); + } + } + + return promise.then(resolve && fulfillmentHandler, reject); }, /** @@ -10290,7 +11128,16 @@ Ember.DeferredMixin = Ember.Mixin.create({ @method resolve */ resolve: function(value) { - get(this, 'promise').resolve(value); + var deferred, promise; + + deferred = get(this, '_deferred'); + promise = deferred.promise; + + if (value === this){ + deferred.resolve(promise); + } else { + deferred.resolve(value); + } }, /** @@ -10299,11 +11146,11 @@ Ember.DeferredMixin = Ember.Mixin.create({ @method reject */ reject: function(value) { - get(this, 'promise').reject(value); + get(this, '_deferred').reject(value); }, - promise: Ember.computed(function() { - return new RSVP.Promise(); + _deferred: Ember.computed(function() { + return RSVP.defer(); }) }); @@ -10393,6 +11240,9 @@ function makeCtor() { for (var i = 0, l = props.length; i < l; i++) { var properties = props[i]; + + Ember.assert("Ember.Object.create no longer supports mixing in other definitions, use createWithMixins instead.", !(properties instanceof Ember.Mixin)); + for (var keyName in properties) { if (!properties.hasOwnProperty(keyName)) { continue; } @@ -10514,8 +11364,6 @@ CoreObject.PrototypeMixin = Mixin.create({ do important setup work, and you'll see strange behavior in your application. - ``` - @method init */ init: function() {}, @@ -10615,24 +11463,26 @@ CoreObject.PrototypeMixin = Mixin.create({ raised. Note that destruction is scheduled for the end of the run loop and does not - happen immediately. + happen immediately. It will set an isDestroying flag immediately. @method destroy @return {Ember.Object} receiver */ destroy: function() { - if (this._didCallDestroy) { return; } - + if (this.isDestroying) { return; } this.isDestroying = true; - this._didCallDestroy = true; - - if (this.willDestroy) { this.willDestroy(); } + schedule('actions', this, this.willDestroy); schedule('destroy', this, this._scheduledDestroy); return this; }, /** + Override to implement teardown. + */ + willDestroy: Ember.K, + + /** @private Invoked by the run loop to actually destroy the object. This is @@ -10641,10 +11491,9 @@ CoreObject.PrototypeMixin = Mixin.create({ @method _scheduledDestroy */ _scheduledDestroy: function() { + if (this.isDestroyed) { return; } destroy(this); - set(this, 'isDestroyed', true); - - if (this.didDestroy) { this.didDestroy(); } + this.isDestroyed = true; }, bind: function(to, from) { @@ -10991,7 +11840,7 @@ function findNamespaces() { for (var prop in lookup) { // These don't raise exceptions but can cause warnings - if (prop === "parent" || prop === "top" || prop === "frameElement") { continue; } + if (prop === "parent" || prop === "top" || prop === "frameElement" || prop === "webkitStorageInfo") { continue; } // get(window.globalStorage, 'isNamespace') would try to read the storage for domain isNamespace and cause exception in Firefox. // globalStorage is a storage obsoleted by the WhatWG storage specification. See https://developer.mozilla.org/en/DOM/Storage#globalStorage @@ -11135,8 +11984,7 @@ var get = Ember.get, set = Ember.set; @extends Ember.Object @uses Ember.MutableArray */ -Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray, -/** @scope Ember.ArrayProxy.prototype */ { +Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray,/** @scope Ember.ArrayProxy.prototype */ { /** The content array. Must be an object that implements `Ember.Array` and/or @@ -11412,7 +12260,6 @@ Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray, } }); - })(); @@ -11512,8 +12359,7 @@ function contentPropertyDidChange(content, contentKey) { @namespace Ember @extends Ember.Object */ -Ember.ObjectProxy = Ember.Object.extend( -/** @scope Ember.ObjectProxy.prototype */ { +Ember.ObjectProxy = Ember.Object.extend(/** @scope Ember.ObjectProxy.prototype */ { /** The object whose properties will be forwarded. @@ -11679,7 +12525,7 @@ Ember.EachProxy = Ember.Object.extend({ @method unknownProperty @param keyName {String} - @param value {anything} + @param value {*} */ unknownProperty: function(keyName, value) { var ret; @@ -11903,9 +12749,8 @@ if (ignore.length>0) { @namespace Ember @extends Ember.Mixin @uses Ember.MutableArray - @uses Ember.MutableEnumerable + @uses Ember.Observable @uses Ember.Copyable - @uses Ember.Freezable */ Ember.NativeArray = NativeArray; @@ -11952,7 +12797,7 @@ if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) { @submodule ember-runtime */ -var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, none = Ember.isNone, fmt = Ember.String.fmt; +var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, isNone = Ember.isNone, fmt = Ember.String.fmt; /** An unordered collection of objects. @@ -12310,7 +13155,7 @@ Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Emb // implements Ember.MutableEnumerable addObject: function(obj) { if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); - if (none(obj)) return this; // nothing to do + if (isNone(obj)) return this; // nothing to do var guid = guidFor(obj), idx = this[guid], @@ -12338,7 +13183,7 @@ Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Emb // implements Ember.MutableEnumerable removeObject: function(obj) { if (get(this, 'isFrozen')) throw new Error(Ember.FROZEN_ERROR); - if (none(obj)) return this; // nothing to do + if (isNone(obj)) return this; // nothing to do var guid = guidFor(obj), idx = this[guid], @@ -12413,7 +13258,7 @@ Deferred.reopenClass({ promise: function(callback, binding) { var deferred = Deferred.create(); callback.call(binding, deferred); - return get(deferred, 'promise'); + return deferred; } }); @@ -12424,6 +13269,8 @@ Ember.Deferred = Deferred; (function() { +var forEach = Ember.ArrayPolyfills.forEach; + /** @module ember @submodule ember-runtime @@ -12456,12 +13303,10 @@ Ember.onLoad = function(name, callback) { @param object {Object} object to pass to callbacks */ Ember.runLoadHooks = function(name, object) { - var hooks; - loaded[name] = object; - if (hooks = loadHooks[name]) { - loadHooks[name].forEach(function(callback) { + if (loadHooks[name]) { + forEach.call(loadHooks[name], function(callback) { callback(object); }); } @@ -12535,6 +13380,8 @@ Ember.ControllerMixin = Ember.Mixin.create({ container: null, + parentController: null, + store: null, model: Ember.computed.alias('content'), @@ -12877,6 +13724,10 @@ var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach, }); ``` + The itemController instances will have a `parentController` property set to + either the the `parentController` property of the `ArrayController` + or to the `ArrayController` instance itself. + @class ArrayController @namespace Ember @extends Ember.ArrayProxy @@ -12916,9 +13767,9 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, }); ``` - @method - @type String - @default null + @method lookupItemController + @param {Object} object + @return {String} */ lookupItemController: function(object) { return get(this, 'itemController'); @@ -12967,8 +13818,8 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, }, init: function() { - this._super(); if (!this.get('content')) { Ember.defineProperty(this, 'content', undefined, Ember.A()); } + this._super(); this.set('_subControllers', Ember.A()); }, @@ -12987,6 +13838,7 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, } subController.set('target', this); + subController.set('parentController', get(this, 'parentController') || this); subController.set('content', object); return subController; @@ -12996,10 +13848,11 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, _resetSubControllers: function() { var subControllers = get(this, '_subControllers'); - - forEach(subControllers, function(subController) { - if (subController) { subController.destroy(); } - }); + if (subControllers) { + forEach(subControllers, function(subController) { + if (subController) { subController.destroy(); } + }); + } this.set('_subControllers', Ember.A()); } @@ -13059,7 +13912,7 @@ Ember Runtime */ var jQuery = Ember.imports.jQuery; -Ember.assert("Ember Views require jQuery 1.8, 1.9 or 2.0", jQuery && (jQuery().jquery.match(/^((1\.(8|9))|2.0)(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_JQUERY)); +Ember.assert("Ember Views require jQuery 1.8, 1.9, 1.10, or 2.0", jQuery && (jQuery().jquery.match(/^((1\.(8|9|10))|2.0)(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_JQUERY)); /** Alias for jQuery @@ -13099,7 +13952,7 @@ if (Ember.$) { @submodule ember-views */ -/*** BEGIN METAMORPH HELPERS ***/ +/* BEGIN METAMORPH HELPERS */ // Internet Explorer prior to 9 does not allow setting innerHTML if the first element // is a "zero-scope" element. This problem can be worked around by making @@ -13172,7 +14025,7 @@ var setInnerHTMLWithoutFix = function(element, html) { } }; -/*** END METAMORPH HELPERS */ +/* END METAMORPH HELPERS */ var innerHTMLTags = {}; @@ -13276,7 +14129,7 @@ Ember.RenderBuffer = function(tagName) { Ember._RenderBuffer = function(tagName) { this.tagNames = [tagName || null]; - this.buffer = []; + this.buffer = ""; }; Ember._RenderBuffer.prototype = @@ -13285,6 +14138,8 @@ Ember._RenderBuffer.prototype = // The root view's element _element: null, + _hasElement: true, + /** @private @@ -13400,7 +14255,7 @@ Ember._RenderBuffer.prototype = @chainable */ push: function(string) { - this.buffer.push(string); + this.buffer += string; return this; }, @@ -13533,7 +14388,7 @@ Ember._RenderBuffer.prototype = var tagName = this.currentTagName(); if (!tagName) { return; } - if (!this._element && this.buffer.length === 0) { + if (this._hasElement && !this._element && this.buffer.length === 0) { this._element = this.generateElement(); return; } @@ -13546,27 +14401,27 @@ Ember._RenderBuffer.prototype = style = this.elementStyle, attr, prop; - buffer.push('<' + tagName); + buffer += '<' + tagName; if (id) { - buffer.push(' id="' + this._escapeAttribute(id) + '"'); + buffer += ' id="' + this._escapeAttribute(id) + '"'; this.elementId = null; } if (classes) { - buffer.push(' class="' + this._escapeAttribute(classes.join(' ')) + '"'); + buffer += ' class="' + this._escapeAttribute(classes.join(' ')) + '"'; this.classes = null; } if (style) { - buffer.push(' style="'); + buffer += ' style="'; for (prop in style) { if (style.hasOwnProperty(prop)) { - buffer.push(prop + ':' + this._escapeAttribute(style[prop]) + ';'); + buffer += prop + ':' + this._escapeAttribute(style[prop]) + ';'; } } - buffer.push('"'); + buffer += '"'; this.elementStyle = null; } @@ -13574,7 +14429,7 @@ Ember._RenderBuffer.prototype = if (attrs) { for (attr in attrs) { if (attrs.hasOwnProperty(attr)) { - buffer.push(' ' + attr + '="' + this._escapeAttribute(attrs[attr]) + '"'); + buffer += ' ' + attr + '="' + this._escapeAttribute(attrs[attr]) + '"'; } } @@ -13587,9 +14442,9 @@ Ember._RenderBuffer.prototype = var value = props[prop]; if (value || typeof(value) === 'number') { if (value === true) { - buffer.push(' ' + prop + '="' + prop + '"'); + buffer += ' ' + prop + '="' + prop + '"'; } else { - buffer.push(' ' + prop + '="' + this._escapeAttribute(props[prop]) + '"'); + buffer += ' ' + prop + '="' + this._escapeAttribute(props[prop]) + '"'; } } } @@ -13598,12 +14453,13 @@ Ember._RenderBuffer.prototype = this.elementProperties = null; } - buffer.push('>'); + buffer += '>'; + this.buffer = buffer; }, pushClosingTag: function() { var tagName = this.tagNames.pop(); - if (tagName) { this.buffer.push('</' + tagName + '>'); } + if (tagName) { this.buffer += '</' + tagName + '>'; } }, currentTagName: function() { @@ -13687,17 +14543,20 @@ Ember._RenderBuffer.prototype = @return {String} The generated HTML */ string: function() { - if (this._element) { + if (this._hasElement && this._element) { // Firefox versions < 11 do not have support for element.outerHTML. - return this.element().outerHTML || - new XMLSerializer().serializeToString(this.element()); + var thisElement = this.element(), outerHTML = thisElement.outerHTML; + if (typeof outerHTML === 'undefined'){ + return Ember.$('<div/>').append(thisElement).html(); + } + return outerHTML; } else { return this.innerString(); } }, innerString: function() { - return this.buffer.join(''); + return this.buffer; }, _escapeAttribute: function(value) { @@ -13749,8 +14608,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; @private @extends Ember.Object */ -Ember.EventDispatcher = Ember.Object.extend( -/** @scope Ember.EventDispatcher.prototype */{ +Ember.EventDispatcher = Ember.Object.extend(/** @scope Ember.EventDispatcher.prototype */{ /** @private @@ -13782,7 +14640,7 @@ Ember.EventDispatcher = Ember.Object.extend( @method setup @param addedEvents {Hash} */ - setup: function(addedEvents) { + setup: function(addedEvents, rootElement) { var event, events = { touchstart : 'touchStart', touchmove : 'touchMove', @@ -13815,7 +14673,12 @@ Ember.EventDispatcher = Ember.Object.extend( Ember.$.extend(events, addedEvents || {}); - var rootElement = Ember.$(get(this, 'rootElement')); + + if (!Ember.isNone(rootElement)) { + set(this, 'rootElement', rootElement); + } + + rootElement = Ember.$(get(this, 'rootElement')); Ember.assert(fmt('You cannot use the same root element (%@) multiple times in an Ember.Application', [rootElement.selector || rootElement[0].tagName]), !rootElement.is('.ember-application')); Ember.assert('You cannot make a new Ember.Application using a root element that is a descendent of an existing Ember.Application', !rootElement.closest('.ember-application').length); @@ -13857,7 +14720,7 @@ Ember.EventDispatcher = Ember.Object.extend( setupHandler: function(rootElement, event, eventName) { var self = this; - rootElement.delegate('.ember-view', event + '.ember', function(evt, triggeringManager) { + rootElement.on(event + '.ember', '.ember-view', function(evt, triggeringManager) { return Ember.handleErrors(function() { var view = Ember.View.views[this.id], result = true, manager = null; @@ -13876,7 +14739,7 @@ Ember.EventDispatcher = Ember.Object.extend( }, this); }); - rootElement.delegate('[data-ember-action]', event + '.ember', function(evt) { + rootElement.on(event + '.ember', '[data-ember-action]', function(evt) { return Ember.handleErrors(function() { var actionId = Ember.$(evt.currentTarget).attr('data-ember-action'), action = Ember.Handlebars.ActionHelper.registeredActions[actionId]; @@ -13928,7 +14791,7 @@ Ember.EventDispatcher = Ember.Object.extend( destroy: function() { var rootElement = get(this, 'rootElement'); - Ember.$(rootElement).undelegate('.ember').removeClass('ember-application'); + Ember.$(rootElement).off('.ember', '**').removeClass('ember-application'); return this._super(); } }); @@ -14060,18 +14923,6 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { init: function() { this._super(); - - // Register the view for event handling. This hash is used by - // Ember.EventDispatcher to dispatch incoming events. - if (!this.isVirtual) { - Ember.assert("Attempted to register a view with an id already in use: "+this.elementId, !Ember.View.views[this.elementId]); - Ember.View.views[this.elementId] = this; - } - - this.addBeforeObserver('elementId', function() { - throw new Error("Changing a view's elementId after creation is not allowed"); - }); - this.transitionTo('preRender'); }, @@ -14101,7 +14952,7 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { concreteView: Ember.computed(function() { if (!this.isVirtual) { return this; } else { return get(this, 'parentView'); } - }).property('parentView').volatile(), + }).property('parentView'), instrumentName: 'core_view', @@ -14139,8 +14990,6 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { }, _renderToBuffer: function(parentBuffer, bufferOperation) { - Ember.run.sync(); - // If this is the top-most view, start a new buffer. Otherwise, // create a new buffer relative to the original using the // provided buffer operation (for example, `insertAfter` will @@ -14186,9 +15035,11 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { return Ember.typeOf(this[name]) === 'function' || this._super(name); }, - willDestroy: function() { + destroy: function() { var parent = this._parentView; + if (!this._super()) { return; } + // destroy the element -- this will avoid each child view destroying // the element over and over again... if (!this.removedFromDOM) { this.destroyElement(); } @@ -14198,10 +15049,9 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { // the DOM again. if (parent) { parent.removeChild(this); } - this.transitionTo('destroyed'); + this.transitionTo('destroying', false); - // next remove view from global hash - if (!this.isVirtual) delete Ember.View.views[this.elementId]; + return this; }, clearRenderedChildren: Ember.K, @@ -14211,6 +15061,68 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { destroyElement: Ember.K }); +var ViewCollection = Ember._ViewCollection = function(initialViews) { + var views = this.views = initialViews || []; + this.length = views.length; +}; + +ViewCollection.prototype = { + length: 0, + + trigger: function(eventName) { + var views = this.views, view; + for (var i = 0, l = views.length; i < l; i++) { + view = views[i]; + if (view.trigger) { view.trigger(eventName); } + } + }, + + triggerRecursively: function(eventName) { + var views = this.views; + for (var i = 0, l = views.length; i < l; i++) { + views[i].triggerRecursively(eventName); + } + }, + + invokeRecursively: function(fn) { + var views = this.views, view; + + for (var i = 0, l = views.length; i < l; i++) { + view = views[i]; + fn(view); + } + }, + + transitionTo: function(state, children) { + var views = this.views; + for (var i = 0, l = views.length; i < l; i++) { + views[i].transitionTo(state, children); + } + }, + + push: function() { + this.length += arguments.length; + var views = this.views; + return views.push.apply(views, arguments); + }, + + objectAt: function(idx) { + return this.views[idx]; + }, + + forEach: function(callback) { + var views = this.views; + return a_forEach(views, callback); + }, + + clear: function() { + this.length = 0; + this.views.length = 0; + } +}; + +var EMPTY_ARRAY = []; + /** `Ember.View` is the class in Ember responsible for encapsulating templates of HTML content, combining templates with data to render as sections of a page's @@ -14682,7 +15594,7 @@ class: eventManager: Ember.Object.create({ mouseEnter: function(event, view){ // view might be instance of either - // OutsideView or InnerView depending on + // OuterView or InnerView depending on // where on the page the user interaction occured } }) @@ -14838,14 +15750,6 @@ Ember.View = Ember.CoreView.extend( return template || get(this, 'defaultTemplate'); }).property('templateName'), - container: Ember.computed(function() { - var parentView = get(this, '_parentView'); - - if (parentView) { return get(parentView, 'container'); } - - return Ember.Container && Ember.Container.defaultContainer; - }), - /** The controller managing this view. If this property is set, it will be made available for use by the template. @@ -14883,14 +15787,11 @@ Ember.View = Ember.CoreView.extend( templateForName: function(name, type) { if (!name) { return; } - Ember.assert("templateNames are not allowed to contain periods: "+name, name.indexOf('.') === -1); - var container = get(this, 'container'); - - if (container) { - return container.lookup('template:' + name); - } + // the defaultContainer is deprecated + var container = this.container || (Ember.Container && Ember.Container.defaultContainer); + return container && container.lookup('template:' + name); }, /** @@ -14981,7 +15882,7 @@ Ember.View = Ember.CoreView.extend( */ childViews: childViewsProperty, - _childViews: [], + _childViews: EMPTY_ARRAY, // When it's a virtual view, we need to notify the parent that their // childViews will change. @@ -15279,7 +16180,7 @@ Ember.View = Ember.CoreView.extend( @param {Ember.RenderBuffer} buffer */ _applyAttributeBindings: function(buffer, attributeBindings) { - var attributeValue, elem, type; + var attributeValue, elem; a_forEach(attributeBindings, function(binding) { var split = binding.split(':'), @@ -15370,7 +16271,7 @@ Ember.View = Ember.CoreView.extend( while(--idx >= 0) { view = childViews[idx]; - callback.call(this, view, idx); + callback(this, view, idx); } return this; @@ -15384,9 +16285,9 @@ Ember.View = Ember.CoreView.extend( var len = childViews.length, view, idx; - for(idx = 0; idx < len; idx++) { + for (idx = 0; idx < len; idx++) { view = childViews[idx]; - callback.call(this, view); + callback(view); } return this; @@ -15582,13 +16483,15 @@ Ember.View = Ember.CoreView.extend( /** @private - Run this callback on the current view and recursively on child views. + Run this callback on the current view (unless includeSelf is false) and recursively on child views. @method invokeRecursively @param fn {Function} + @param includeSelf (optional, default true) */ - invokeRecursively: function(fn) { - var childViews = [this], currentViews, view; + invokeRecursively: function(fn, includeSelf) { + var childViews = (includeSelf === false) ? this._childViews : [this]; + var currentViews, view; while (childViews.length) { currentViews = childViews.slice(); @@ -15596,7 +16499,7 @@ Ember.View = Ember.CoreView.extend( for (var i=0, l=currentViews.length; i<l; i++) { view = currentViews[i]; - fn.call(view, view); + fn(view); if (view._childViews) { childViews.push.apply(childViews, view._childViews); } @@ -15621,6 +16524,19 @@ Ember.View = Ember.CoreView.extend( } }, + viewHierarchyCollection: function() { + var currentView, viewCollection = new ViewCollection([this]); + + for (var i = 0; i < viewCollection.length; i++) { + currentView = viewCollection.objectAt(i); + if (currentView._childViews) { + viewCollection.push.apply(viewCollection, currentView._childViews); + } + } + + return viewCollection; + }, + /** Destroys any existing element along with the element for any child views as well. If the view does not currently have a element, then this method @@ -15665,8 +16581,10 @@ Ember.View = Ember.CoreView.extend( @method _notifyWillDestroyElement */ _notifyWillDestroyElement: function() { - this.triggerRecursively('willClearRender'); - this.triggerRecursively('willDestroyElement'); + var viewCollection = this.viewHierarchyCollection(); + viewCollection.trigger('willClearRender'); + viewCollection.trigger('willDestroyElement'); + return viewCollection; }, _elementWillChange: Ember.beforeObserver(function() { @@ -15712,8 +16630,8 @@ Ember.View = Ember.CoreView.extend( return buffer; }, - renderToBufferIfNeeded: function () { - return this.currentState.renderToBufferIfNeeded(this, this); + renderToBufferIfNeeded: function (buffer) { + return this.currentState.renderToBufferIfNeeded(this, buffer); }, beforeRender: function(buffer) { @@ -15841,7 +16759,7 @@ Ember.View = Ember.CoreView.extend( @type Array @default [] */ - classNameBindings: [], + classNameBindings: EMPTY_ARRAY, /** A list of properties of the view to apply as attributes. If the property is @@ -15869,7 +16787,7 @@ Ember.View = Ember.CoreView.extend( @property attributeBindings */ - attributeBindings: [], + attributeBindings: EMPTY_ARRAY, // ....................................................... // CORE DISPLAY METHODS @@ -15945,13 +16863,13 @@ Ember.View = Ember.CoreView.extend( @return {Ember.View} receiver */ removeAllChildren: function() { - return this.mutateChildViews(function(view) { - this.removeChild(view); + return this.mutateChildViews(function(parentView, view) { + parentView.removeChild(view); }); }, destroyAllChildren: function() { - return this.mutateChildViews(function(view) { + return this.mutateChildViews(function(parentView, view) { view.destroy(); }); }, @@ -15979,18 +16897,16 @@ Ember.View = Ember.CoreView.extend( sure that the DOM element managed by the view can be released by the memory manager. - @method willDestroy + @method destroy */ - willDestroy: function() { - // calling this._super() will nuke computed properties and observers, - // so collect any information we need before calling super. + destroy: function() { var childViews = this._childViews, - parent = this._parentView, + // get parentView before calling super because it'll be destroyed + nonVirtualParentView = get(this, 'parentView'), + viewName = this.viewName, childLen, i; - // destroy the element -- this will avoid each child view destroying - // the element over and over again... - if (!this.removedFromDOM) { this.destroyElement(); } + if (!this._super()) { return; } childLen = childViews.length; for (i=childLen-1; i>=0; i--) { @@ -15998,27 +16914,16 @@ Ember.View = Ember.CoreView.extend( } // remove from non-virtual parent view if viewName was specified - if (this.viewName) { - var nonVirtualParentView = get(this, 'parentView'); - if (nonVirtualParentView) { - set(nonVirtualParentView, this.viewName, null); - } + if (viewName && nonVirtualParentView) { + nonVirtualParentView.set(viewName, null); } - // remove from parent if found. Don't call removeFromParent, - // as removeFromParent will try to remove the element from - // the DOM again. - if (parent) { parent.removeChild(this); } - - this.transitionTo('destroyed'); - childLen = childViews.length; for (i=childLen-1; i>=0; i--) { childViews[i].destroy(); } - // next remove view from global hash - if (!this.isVirtual) delete Ember.View.views[get(this, 'elementId')]; + return this; }, /** @@ -16039,6 +16944,7 @@ Ember.View = Ember.CoreView.extend( if (Ember.CoreView.detect(view)) { attrs = attrs || {}; attrs._parentView = this; + attrs.container = this.container; attrs.templateData = attrs.templateData || get(this, 'templateData'); view = view.create(attrs); @@ -16133,9 +17039,13 @@ Ember.View = Ember.CoreView.extend( }, transitionTo: function(state, children) { - this.currentState = this.states[state]; + var priorState = this.currentState, + currentState = this.currentState = this.states[state]; this.state = state; + if (priorState && priorState.exit) { priorState.exit(this); } + if (currentState.enter) { currentState.enter(this); } + if (children !== false) { this.forEachChildView(function(view) { view.transitionTo(state); @@ -16350,7 +17260,7 @@ Ember.View.reopenClass({ // If the value is not false, undefined, or null, return the current // value of the property. - } else if (val !== false && val !== undefined && val !== null) { + } else if (val !== false && val != null) { return val; // Nothing to display. Return null so that the old class is removed @@ -16479,15 +17389,18 @@ Ember.merge(preRender, { // created (createElement). insertElement: function(view, fn) { view.createElement(); - view.triggerRecursively('willInsertElement'); + var viewCollection = view.viewHierarchyCollection(); + + viewCollection.trigger('willInsertElement'); // after createElement, the view will be in the hasElement state. fn.call(view); - view.transitionTo('inDOM'); - view.triggerRecursively('didInsertElement'); + viewCollection.transitionTo('inDOM', false); + viewCollection.trigger('didInsertElement'); }, - renderToBufferIfNeeded: function(view) { - return view.renderToBuffer(); + renderToBufferIfNeeded: function(view, buffer) { + view.renderToBuffer(buffer); + return true; }, empty: Ember.K, @@ -16534,10 +17447,11 @@ Ember.merge(inBuffer, { // view will render that view and append the resulting // buffer into its buffer. appendChild: function(view, childView, options) { - var buffer = view.buffer; + var buffer = view.buffer, _childViews = view._childViews; childView = view.createChildView(childView, options); - view._childViews.push(childView); + if (!_childViews.length) { _childViews = view._childViews = _childViews.slice(); } + _childViews.push(childView); childView.renderToBuffer(buffer); @@ -16551,8 +17465,8 @@ Ember.merge(inBuffer, { // state back into the preRender state. destroyElement: function(view) { view.clearBuffer(); - view._notifyWillDestroyElement(); - view.transitionTo('preRender'); + var viewCollection = view._notifyWillDestroyElement(); + viewCollection.transitionTo('preRender', false); return view; }, @@ -16561,8 +17475,8 @@ Ember.merge(inBuffer, { Ember.assert("Emptying a view in the inBuffer state is not allowed and should not happen under normal circumstances. Most likely there is a bug in your application. This may be due to excessive property change notifications."); }, - renderToBufferIfNeeded: function (view) { - return view.buffer; + renderToBufferIfNeeded: function (view, buffer) { + return false; }, // It should be impossible for a rendered view to be scheduled for @@ -16681,6 +17595,23 @@ Ember.merge(hasElement, { var inDOM = Ember.View.states.inDOM = Ember.create(hasElement); Ember.merge(inDOM, { + enter: function(view) { + // Register the view for event handling. This hash is used by + // Ember.EventDispatcher to dispatch incoming events. + if (!view.isVirtual) { + Ember.assert("Attempted to register a view with an id already in use: "+view.elementId, !Ember.View.views[view.elementId]); + Ember.View.views[view.elementId] = view; + } + + view.addBeforeObserver('elementId', function() { + throw new Error("Changing a view's elementId after creation is not allowed"); + }); + }, + + exit: function(view) { + if (!this.isVirtual) delete Ember.View.views[view.elementId]; + }, + insertElement: function(view, fn) { throw "You can't insert an element into the DOM that has already been inserted"; } @@ -16696,30 +17627,30 @@ Ember.merge(inDOM, { @submodule ember-views */ -var destroyedError = "You can't call %@ on a destroyed view", fmt = Ember.String.fmt; +var destroyingError = "You can't call %@ on a view being destroyed", fmt = Ember.String.fmt; -var destroyed = Ember.View.states.destroyed = Ember.create(Ember.View.states._default); +var destroying = Ember.View.states.destroying = Ember.create(Ember.View.states._default); -Ember.merge(destroyed, { +Ember.merge(destroying, { appendChild: function() { - throw fmt(destroyedError, ['appendChild']); + throw fmt(destroyingError, ['appendChild']); }, rerender: function() { - throw fmt(destroyedError, ['rerender']); + throw fmt(destroyingError, ['rerender']); }, destroyElement: function() { - throw fmt(destroyedError, ['destroyElement']); + throw fmt(destroyingError, ['destroyElement']); }, empty: function() { - throw fmt(destroyedError, ['empty']); + throw fmt(destroyingError, ['empty']); }, setElement: function() { - throw fmt(destroyedError, ["set('element', ...)"]); + throw fmt(destroyingError, ["set('element', ...)"]); }, renderToBufferIfNeeded: function() { - throw fmt(destroyedError, ["renderToBufferIfNeeded"]); + return false; }, // Since element insertion is scheduled, don't do anything if @@ -16738,7 +17669,7 @@ Ember.View.cloneStates = function(from) { into._default = {}; into.preRender = Ember.create(into._default); - into.destroyed = Ember.create(into._default); + into.destroying = Ember.create(into._default); into.inBuffer = Ember.create(into._default); into.hasElement = Ember.create(into._default); into.inDOM = Ember.create(into.hasElement); @@ -16765,6 +17696,7 @@ var states = Ember.View.cloneStates(Ember.View.states); var get = Ember.get, set = Ember.set; var forEach = Ember.EnumerableUtils.forEach; +var ViewCollection = Ember._ViewCollection; /** A `ContainerView` is an `Ember.View` subclass that implements `Ember.MutableArray` @@ -16969,12 +17901,15 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { var currentView = get(this, 'currentView'); if (currentView) { + if (!_childViews.length) { _childViews = this._childViews = this._childViews.slice(); } _childViews.push(this.createChildView(currentView)); } }, replace: function(idx, removedCount, addedViews) { var addedCount = addedViews ? get(addedViews, 'length') : 0; + var self = this; + Ember.assert("You can't add a child to a container that is already a child of another view", Ember.A(addedViews).every(function(item) { return !get(item, '_parentView') || get(item, '_parentView') === self; })); this.arrayContentWillChange(idx, removedCount, addedCount); this.childViewsWillChange(this._childViews, idx, removedCount); @@ -16983,6 +17918,7 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { this._childViews.splice(idx, removedCount) ; } else { var args = [idx, removedCount].concat(addedViews); + if (addedViews.length && !this._childViews.length) { this._childViews = this._childViews.slice(); } this._childViews.splice.apply(this._childViews, args); } @@ -17014,7 +17950,7 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { }); }, - instrumentName: 'render.container', + instrumentName: 'container', /** @private @@ -17094,6 +18030,7 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { _currentViewDidChange: Ember.observer(function() { var currentView = get(this, 'currentView'); if (currentView) { + Ember.assert("You tried to set a current view that already has a parent. Make sure you don't have multiple outlets in the same view.", !get(currentView, '_parentView')); this.pushObject(currentView); } }, 'currentView'), @@ -17127,26 +18064,48 @@ Ember.merge(states.hasElement, { }, ensureChildrenAreInDOM: function(view) { - var childViews = view._childViews, i, len, childView, previous, buffer; + var childViews = view._childViews, i, len, childView, previous, buffer, viewCollection = new ViewCollection(); + for (i = 0, len = childViews.length; i < len; i++) { childView = childViews[i]; - buffer = childView.renderToBufferIfNeeded(); - if (buffer) { - childView.triggerRecursively('willInsertElement'); - if (previous) { - previous.domManager.after(previous, buffer.string()); - } else { - view.domManager.prepend(view, buffer.string()); - } - childView.transitionTo('inDOM'); - childView.propertyDidChange('element'); - childView.triggerRecursively('didInsertElement'); + + if (!buffer) { buffer = Ember.RenderBuffer(); buffer._hasElement = false; } + + if (childView.renderToBufferIfNeeded(buffer)) { + viewCollection.push(childView); + } else if (viewCollection.length) { + insertViewCollection(view, viewCollection, previous, buffer); + buffer = null; + previous = childView; + viewCollection.clear(); + } else { + previous = childView; } - previous = childView; + } + + if (viewCollection.length) { + insertViewCollection(view, viewCollection, previous, buffer); } } }); +function insertViewCollection(view, viewCollection, previous, buffer) { + viewCollection.triggerRecursively('willInsertElement'); + + if (previous) { + previous.domManager.after(previous, buffer.string()); + } else { + view.domManager.prepend(view, buffer.string()); + } + + viewCollection.forEach(function(v) { + v.transitionTo('inDOM'); + v.propertyDidChange('element'); + v.triggerRecursively('didInsertElement'); + }); +} + + })(); @@ -17161,7 +18120,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; /** `Ember.CollectionView` is an `Ember.View` descendent responsible for managing - a collection (an array or array-like object) by maintaing a child view object + a collection (an array or array-like object) by maintaining a child view object and associated DOM representation for each item in the array and ensuring that child views and their associated rendered HTML are updated when items in the array are added, removed, or replaced. @@ -17312,8 +18271,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; @extends Ember.ContainerView @since Ember 0.9 */ -Ember.CollectionView = Ember.ContainerView.extend( -/** @scope Ember.CollectionView.prototype */ { +Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionView.prototype */ { /** A list of items to be displayed by the `Ember.CollectionView`. @@ -17387,15 +18345,17 @@ Ember.CollectionView = Ember.ContainerView.extend( this.arrayDidChange(content, 0, null, len); }, 'content'), - willDestroy: function() { + destroy: function() { + if (!this._super()) { return; } + var content = get(this, 'content'); if (content) { content.removeArrayObserver(this); } - this._super(); - if (this._createdEmptyView) { this._createdEmptyView.destroy(); } + + return this; }, arrayWillChange: function(content, start, removedCount) { @@ -17417,11 +18377,13 @@ Ember.CollectionView = Ember.ContainerView.extend( if (removingAll) { this.currentState.empty(this); + this.invokeRecursively(function(view) { + view.removedFromDOM = true; + }, false); } for (idx = start + removedCount - 1; idx >= start; idx--) { childView = childViews[idx]; - if (removingAll) { childView.removedFromDOM = true; } childView.destroy(); } }, @@ -17435,9 +18397,10 @@ Ember.CollectionView = Ember.ContainerView.extend( This array observer is added in `contentDidChange`. @method arrayDidChange - @param {Array} addedObjects the objects that were added to the content - @param {Array} removedObjects the objects that were removed from the content - @param {Number} changeIndex the index at which the changes occurred + @param {Array} content the managed collection of objects + @param {Number} start the index at which the changes occurred + @param {Number} removed number of object removed from content + @param {Number} added number of object added to content */ arrayDidChange: function(content, start, removed, added) { var itemViewClass = get(this, 'itemViewClass'), @@ -17520,6 +18483,69 @@ Ember.CollectionView.CONTAINER_MAP = { (function() { +/** +`Ember.ViewTargetActionSupport` is a mixin that can be included in a +view class to add a `triggerAction` method with semantics similar to +the Handlebars `{{action}}` helper. It provides intelligent defaults +for the action's target: the view's controller; and the context that is +sent with the action: the view's context. + +Note: In normal Ember usage, the `{{action}}` helper is usually the best +choice. This mixin is most often useful when you are doing more complex +event handling in custom View subclasses. + +For example: + +```javascript +App.SaveButtonView = Ember.View.extend(Ember.ViewTargetActionSupport, { + action: 'save', + click: function(){ + this.triggerAction(); // Sends the `save` action, along with the current context + // to the current controller + } +}); +``` + +The `action` can be provided as properties of an optional object argument +to `triggerAction` as well. + +```javascript +App.SaveButtonView = Ember.View.extend(Ember.ViewTargetActionSupport, { + click: function(){ + this.triggerAction({ + action: 'save' + }); // Sends the `save` action, along with the current context + // to the current controller + } +}); +``` + +@class ViewTargetActionSupport +@namespace Ember +@extends Ember.TargetActionSupport +*/ +Ember.ViewTargetActionSupport = Ember.Mixin.create(Ember.TargetActionSupport, { + /** + @property target + */ + target: Ember.computed.alias('controller'), + /** + @property actionContext + */ + actionContext: Ember.computed.alias('context') +}); + +})(); + + + +(function() { + +})(); + + + +(function() { /*globals jQuery*/ /** Ember Views @@ -18012,7 +19038,8 @@ if(!Handlebars && typeof require === 'function') { Handlebars = require('handlebars'); } -Ember.assert("Ember Handlebars requires Handlebars 1.0.0-rc.3 or greater. Include a SCRIPT tag in the HTML HEAD linking to the Handlebars file before you link to Ember.", Handlebars && Handlebars.COMPILER_REVISION === 2); +Ember.assert("Ember Handlebars requires Handlebars version 1.0.0-rc.4. Include a SCRIPT tag in the HTML HEAD linking to the Handlebars file before you link to Ember.", Handlebars) +Ember.assert("Ember Handlebars requires Handlebars version 1.0.0-rc.4, COMPILER_REVISION expected: 3, got: " + Handlebars.COMPILER_REVISION + " – Please note: Builds of master may have other COMPILER_REVISION values.", Handlebars.COMPILER_REVISION === 3); /** Prepares the Handlebars templating library for use inside Ember's view @@ -18030,6 +19057,32 @@ Ember.assert("Ember Handlebars requires Handlebars 1.0.0-rc.3 or greater. Includ */ Ember.Handlebars = objectCreate(Handlebars); +function makeBindings(options) { + var hash = options.hash, + hashType = options.hashTypes; + + for (var prop in hash) { + if (hashType[prop] === 'ID') { + hash[prop + 'Binding'] = hash[prop]; + hashType[prop + 'Binding'] = 'STRING'; + delete hash[prop]; + delete hashType[prop]; + } + } +} + +Ember.Handlebars.helper = function(name, value) { + if (Ember.View.detect(value)) { + Ember.Handlebars.registerHelper(name, function(options) { + Ember.assert("You can only pass attributes as parameters (not values) to a application-defined helper", arguments.length < 2); + makeBindings(options); + return Ember.Handlebars.helpers.view.call(this, value, options); + }); + } else { + Ember.Handlebars.registerBoundHelper.apply(null, arguments); + } +} + /** @class helpers @namespace Ember.Handlebars @@ -18398,15 +19451,15 @@ Ember.Handlebars.registerHelper('helperMissing', function(path, options) { }); ``` - Which allows for template syntax such as {{concatenate prop1 prop2}} or - {{concatenate prop1 prop2 prop3}}. If any of the properties change, + Which allows for template syntax such as `{{concatenate prop1 prop2}}` or + `{{concatenate prop1 prop2 prop3}}`. If any of the properties change, the helpr will re-render. Note that dependency keys cannot be using in conjunction with multi-property helpers, since it is ambiguous which property the dependent keys would belong to. ## Use with unbound helper - The {{unbound}} helper can be used with bound helper invocations + The `{{unbound}}` helper can be used with bound helper invocations to render them in their unbound form, e.g. ```handlebars @@ -18416,6 +19469,10 @@ Ember.Handlebars.registerHelper('helperMissing', function(path, options) { In this example, if the name property changes, the helper will not re-render. + ## Use with blocks not supported + + Bound helpers do not support use with Handlebars blocks or + the addition of child views of any kind. @method registerBoundHelper @for Ember.Handlebars @@ -18439,6 +19496,8 @@ Ember.Handlebars.registerBoundHelper = function(name, fn) { pathRoot, path, loc, hashOption; + Ember.assert("registerBoundHelper-generated helpers do not support use with Handlebars blocks.", !options.fn); + // Detect bound options (e.g. countBinding="otherCount") hash.boundOptions = {}; for (hashOption in hash) { @@ -18499,6 +19558,7 @@ Ember.Handlebars.registerBoundHelper = function(name, fn) { Renders the unbound form of an otherwise bound helper function. + @method evaluateMultiPropertyBoundHelper @param {Function} fn @param {Object} context @param {Array} normalizedProperties @@ -18515,7 +19575,7 @@ function evaluateMultiPropertyBoundHelper(context, fn, normalizedProperties, opt bindView = new Ember._SimpleHandlebarsView(null, null, !hash.unescaped, data); bindView.normalizedValue = function() { - var args = [], value, boundOption; + var args = [], boundOption; // Copy over bound options. for (boundOption in boundOptions) { @@ -18560,6 +19620,7 @@ function evaluateMultiPropertyBoundHelper(context, fn, normalizedProperties, opt Renders the unbound form of an otherwise bound helper function. + @method evaluateUnboundHelper @param {Function} fn @param {Object} context @param {Array} normalizedProperties @@ -18597,7 +19658,6 @@ Ember.Handlebars.template = function(spec){ return t; }; - })(); @@ -18617,7 +19677,7 @@ var htmlSafe = Ember.String.htmlSafe; if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { /** - See {{#crossLink "Ember.String/htmlSafe"}}{{/crossLink}} + See `Ember.String.htmlSafe`. @method htmlSafe @for String @@ -18698,13 +19758,18 @@ var DOMManager = { var buffer = view.renderToBuffer(); view.invokeRecursively(function(view) { - view.propertyDidChange('element'); + view.propertyWillChange('element'); }); - view.triggerRecursively('willInsertElement'); + morph.replaceWith(buffer.string()); view.transitionTo('inDOM'); + + view.invokeRecursively(function(view) { + view.propertyDidChange('element'); + }); view.triggerRecursively('didInsertElement'); + notifyMutationListeners(); }); }, @@ -18728,7 +19793,7 @@ Ember._Metamorph = Ember.Mixin.create({ isVirtual: true, tagName: '', - instrumentName: 'render.metamorph', + instrumentName: 'metamorph', init: function() { this._super(); @@ -18813,6 +19878,8 @@ SimpleHandlebarsView.prototype = { this.morph = null; }, + propertyWillChange: Ember.K, + propertyDidChange: Ember.K, normalizedValue: function() { @@ -18863,7 +19930,7 @@ SimpleHandlebarsView.prototype = { rerender: function() { switch(this.state) { case 'preRender': - case 'destroyed': + case 'destroying': break; case 'inBuffer': throw new Ember.Error("Something you did tried to replace an {{expression}} before it was inserted into the DOM."); @@ -18894,7 +19961,7 @@ merge(states._default, { merge(states.inDOM, { rerenderIfNeeded: function(view) { - if (get(view, 'normalizedValue') !== view._lastNormalizedValue) { + if (view.normalizedValue() !== view._lastNormalizedValue) { view.rerender(); } } @@ -18915,7 +19982,7 @@ merge(states.inDOM, { @private */ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ - instrumentName: 'render.boundHandlebars', + instrumentName: 'boundHandlebars', states: states, /** @@ -18998,7 +20065,7 @@ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ */ pathRoot: null, - normalizedValue: Ember.computed(function() { + normalizedValue: function() { var path = get(this, 'path'), pathRoot = get(this, 'pathRoot'), valueNormalizer = get(this, 'valueNormalizerFunc'), @@ -19016,7 +20083,7 @@ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ } return valueNormalizer ? valueNormalizer(result) : result; - }).property('path', 'pathRoot', 'valueNormalizerFunc').volatile(), + }, rerenderIfNeeded: function() { this.currentState.rerenderIfNeeded(this); @@ -19051,7 +20118,7 @@ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ var inverseTemplate = get(this, 'inverseTemplate'), displayTemplate = get(this, 'displayTemplate'); - var result = get(this, 'normalizedValue'); + var result = this.normalizedValue(); this._lastNormalizedValue = result; // First, test the conditional to see if we should @@ -19114,6 +20181,10 @@ var forEach = Ember.ArrayPolyfills.forEach; var EmberHandlebars = Ember.Handlebars, helpers = EmberHandlebars.helpers; +function exists(value){ + return !Ember.isNone(value); +} + // Binds a property into the DOM. This will create a hook in DOM that the // KVO system will look for and update if the property changes. function bind(property, options, preserveContext, shouldDisplay, valueNormalizer, childProperties) { @@ -19292,9 +20363,7 @@ EmberHandlebars.registerHelper('bind', function(property, options) { return simpleBind.call(context, property, options); } - return bind.call(context, property, options, false, function(result) { - return !Ember.isNone(result); - }); + return bind.call(context, property, options, false, exists); }); /** @@ -19366,9 +20435,7 @@ EmberHandlebars.registerHelper('with', function(context, options) { Ember.bind(options.data.keywords, keywordName, contextPath); } - return bind.call(this, path, options, true, function(result) { - return !Ember.isNone(result); - }); + return bind.call(this, path, options, true, exists); } else { Ember.assert("You must pass exactly one argument to the with helper", arguments.length === 2); Ember.assert("You must pass a block to the with helper", options.fn && options.fn !== Handlebars.VM.noop); @@ -19520,7 +20587,7 @@ EmberHandlebars.registerHelper('unless', function(context, options) { Results in the following rendered output: ```html - <img class=":class-name-to-always-apply"> + <img class="class-name-to-always-apply"> ``` All three strategies - string return value, boolean return value, and @@ -19552,7 +20619,7 @@ EmberHandlebars.registerHelper('bindAttr', function(options) { // Handle classes differently, as we can bind multiple classes var classBindings = attrs['class']; - if (classBindings !== null && classBindings !== undefined) { + if (classBindings != null) { var classResults = EmberHandlebars.bindClasses(this, classBindings, view, dataId, options); ret.push('class="' + Handlebars.Utils.escapeExpression(classResults.join(' ')) + '"'); @@ -19859,11 +20926,8 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ }, helper: function(thisContext, path, options) { - var inverse = options.inverse, - data = options.data, - view = data.view, + var data = options.data, fn = options.fn, - hash = options.hash, newView; if ('string' === typeof path) { @@ -19877,7 +20941,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ var viewOptions = this.propertiesFromHTMLOptions(options, thisContext); var currentView = data.view; - viewOptions.templateData = options.data; + viewOptions.templateData = data; var newViewProto = newView.proto ? newView.proto() : newView; if (fn) { @@ -20004,9 +21068,8 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ {{/view}} ``` - The first argument can also be a relative path. Ember will search for the - view class starting at the `Ember.View` of the template where `{{view}}` was - used as the root object: + The first argument can also be a relative path accessible from the current + context. ```javascript MyApp = Ember.Application.create({}); @@ -20014,7 +21077,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ innerViewClass: Ember.View.extend({ classNames: ['a-custom-view-class-as-property'] }), - template: Ember.Handlebars.compile('{{#view "innerViewClass"}} hi {{/view}}') + template: Ember.Handlebars.compile('{{#view "view.innerViewClass"}} hi {{/view}}') }); MyApp.OuterView.create().appendTo('body'); @@ -20259,8 +21322,6 @@ Ember.Handlebars.registerHelper('collection', function(path, options) { } } - var tagName = hash.tagName || collectionPrototype.tagName; - if (fn) { itemHash.template = fn; delete options.fn; @@ -20282,8 +21343,6 @@ Ember.Handlebars.registerHelper('collection', function(path, options) { itemHash._context = Ember.computed.alias('content'); } - var viewString = view.toString(); - var viewOptions = Ember.Handlebars.ViewHelper.propertiesFromHTMLOptions({ data: data, hash: itemHash }, this); hash.itemViewClass = itemViewClass.extend(viewOptions); @@ -20412,6 +21471,7 @@ Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { set(controller, 'container', get(this, 'controller.container')); set(controller, '_eachView', this); set(controller, 'target', get(this, 'controller')); + set(controller, 'parentController', get(this, 'controller')); this.disableContentObservers(function() { set(this, 'content', controller); @@ -20473,14 +21533,16 @@ Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { return view; }, - willDestroy: function() { + destroy: function() { + if (!this._super()) { return; } + var arrayController = get(this, '_arrayController'); if (arrayController) { arrayController.destroy(); } - return this._super(); + return this; } }); @@ -20683,6 +21745,9 @@ GroupedEach.prototype = { {{/each}} ``` + Each itemController will receive a reference to the current controller as + a `parentController` property. + @method each @for Ember.Handlebars.helpers @param [name] {String} name for item (used with `in`) @@ -20704,6 +21769,11 @@ Ember.Handlebars.registerHelper('each', function(path, options) { options.hash.keyword = keywordName; } + if (arguments.length === 1) { + options = path; + path = 'this'; + } + options.hash.dataSourceBinding = path; // Set up emptyView as a metamorph with no tag //options.hash.emptyViewClass = Ember._MetamorphView; @@ -20797,6 +21867,7 @@ Ember.Handlebars.registerHelper('template', function(name, options) { {{partial user_info}} {{/with}} </script> + ``` The `data-template-name` attribute of a partial template is prefixed with an underscore. @@ -20821,7 +21892,7 @@ Ember.Handlebars.registerHelper('partial', function(name, options) { var view = options.data.view, underscoredName = nameParts.join("/"), template = view.templateForName(underscoredName), - deprecatedTemplate = view.templateForName(name); + deprecatedTemplate = !template && view.templateForName(name); Ember.deprecate("You tried to render the partial " + name + ", which should be at '" + underscoredName + "', but Ember found '" + name + "'. Please use a leading underscore in your partials", template); Ember.assert("Unable to find partial with name '"+name+"'.", template || deprecatedTemplate); @@ -20947,7 +22018,7 @@ var set = Ember.set, get = Ember.get; You can add a `label` tag yourself in the template where the `Ember.Checkbox` is being used. - ```html + ```handlebars <label> {{view Ember.Checkbox classNames="applicaton-specific-checkbox"}} Some Title @@ -21085,7 +22156,7 @@ var get = Ember.get, set = Ember.set; By default `Ember.TextField` provides support for `type`, `value`, `size`, `pattern`, `placeholder`, `disabled`, `maxlength` and `tabindex` attributes - on a test field. If you need to support more attributes have a look at the + on a text field. If you need to support more attributes have a look at the `attributeBindings` property in `Ember.View`'s HTML Attributes section. To globally add support for additional attributes you can reopen @@ -21160,6 +22231,20 @@ Ember.TextField = Ember.View.extend(Ember.TextSupport, action: null, /** + The event that should send the action. + + Options are: + + * `enter`: the user pressed enter + * `keypress`: the user pressed a key + + @property onEvent + @type String + @default enter + */ + onEvent: 'enter', + + /** Whether they `keyUp` event that triggers an `action` to be sent continues propagating to other views. @@ -21177,19 +22262,30 @@ Ember.TextField = Ember.View.extend(Ember.TextSupport, bubbles: false, insertNewline: function(event) { - var controller = get(this, 'controller'), - action = get(this, 'action'); - - if (action) { - controller.send(action, get(this, 'value'), this); + sendAction('enter', this, event); + }, - if (!get(this, 'bubbles')) { - event.stopPropagation(); - } - } + keyPress: function(event) { + sendAction('keyPress', this, event); } }); +function sendAction(eventName, view, event) { + var action = get(view, 'action'), + on = get(view, 'onEvent'); + + if (action && on === eventName) { + var controller = get(view, 'controller'), + value = get(view, 'value'), + bubbles = get(view, 'bubbles'); + + controller.send(action, value, view); + + if (!bubbles) { + event.stopPropagation(); + } + } +} })(); @@ -21407,6 +22503,55 @@ var set = Ember.set, isArray = Ember.isArray, precompileTemplate = Ember.Handlebars.compile; +Ember.SelectOption = Ember.View.extend({ + tagName: 'option', + attributeBindings: ['value', 'selected'], + + defaultTemplate: function(context, options) { + options = { data: options.data, hash: {} }; + Ember.Handlebars.helpers.bind.call(context, "view.label", options); + }, + + init: function() { + this.labelPathDidChange(); + this.valuePathDidChange(); + + this._super(); + }, + + selected: Ember.computed(function() { + var content = get(this, 'content'), + selection = get(this, 'parentView.selection'); + if (get(this, 'parentView.multiple')) { + return selection && indexOf(selection, content.valueOf()) > -1; + } else { + // Primitives get passed through bindings as objects... since + // `new Number(4) !== 4`, we use `==` below + return content == selection; + } + }).property('content', 'parentView.selection'), + + labelPathDidChange: Ember.observer(function() { + var labelPath = get(this, 'parentView.optionLabelPath'); + + if (!labelPath) { return; } + + Ember.defineProperty(this, 'label', Ember.computed(function() { + return get(this, labelPath); + }).property(labelPath)); + }, 'parentView.optionLabelPath'), + + valuePathDidChange: Ember.observer(function() { + var valuePath = get(this, 'parentView.optionValuePath'); + + if (!valuePath) { return; } + + Ember.defineProperty(this, 'value', Ember.computed(function() { + return get(this, valuePath); + }).property(valuePath)); + }, 'parentView.optionValuePath') +}); + /** The `Ember.Select` view class renders a [select](https://developer.mozilla.org/en/HTML/Element/select) HTML element, @@ -21583,7 +22728,7 @@ var set = Ember.set, Interacting with the rendered element by selecting the first option ('Yehuda') will update the `selectedPerson` value of `App.controller` to match the content object of the newly selected `<option>`. In this - case it is the first object in the `App.content.content` + case it is the first object in the `App.controller.content` ### Supplying a Prompt @@ -21660,34 +22805,38 @@ Ember.Select = Ember.View.extend( tagName: 'select', classNames: ['ember-select'], defaultTemplate: Ember.Handlebars.template(function anonymous(Handlebars,depth0,helpers,partials,data) { -this.compilerInfo = [2,'>= 1.0.0-rc.3']; +this.compilerInfo = [3,'>= 1.0.0-rc.4']; helpers = helpers || Ember.Handlebars.helpers; data = data || {}; - var buffer = '', stack1, hashTypes, escapeExpression=this.escapeExpression, self=this; + var buffer = '', stack1, hashTypes, hashContexts, escapeExpression=this.escapeExpression, self=this; function program1(depth0,data) { - var buffer = '', hashTypes; + var buffer = '', hashTypes, hashContexts; data.buffer.push("<option value=\"\">"); hashTypes = {}; - data.buffer.push(escapeExpression(helpers._triageMustache.call(depth0, "view.prompt", {hash:{},contexts:[depth0],types:["ID"],hashTypes:hashTypes,data:data}))); + hashContexts = {}; + data.buffer.push(escapeExpression(helpers._triageMustache.call(depth0, "view.prompt", {hash:{},contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}))); data.buffer.push("</option>"); return buffer; } function program3(depth0,data) { - var hashTypes; + var hashContexts, hashTypes; + hashContexts = {'contentBinding': depth0}; hashTypes = {'contentBinding': "STRING"}; - data.buffer.push(escapeExpression(helpers.view.call(depth0, "Ember.SelectOption", {hash:{ + data.buffer.push(escapeExpression(helpers.view.call(depth0, "view.optionView", {hash:{ 'contentBinding': ("this") - },contexts:[depth0],types:["ID"],hashTypes:hashTypes,data:data}))); + },contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}))); } hashTypes = {}; - stack1 = helpers['if'].call(depth0, "view.prompt", {hash:{},inverse:self.noop,fn:self.program(1, program1, data),contexts:[depth0],types:["ID"],hashTypes:hashTypes,data:data}); + hashContexts = {}; + stack1 = helpers['if'].call(depth0, "view.prompt", {hash:{},inverse:self.noop,fn:self.program(1, program1, data),contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}); if(stack1 || stack1 === 0) { data.buffer.push(stack1); } hashTypes = {}; - stack1 = helpers.each.call(depth0, "view.content", {hash:{},inverse:self.noop,fn:self.program(3, program3, data),contexts:[depth0],types:["ID"],hashTypes:hashTypes,data:data}); + hashContexts = {}; + stack1 = helpers.each.call(depth0, "view.content", {hash:{},inverse:self.noop,fn:self.program(3, program3, data),contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}); if(stack1 || stack1 === 0) { data.buffer.push(stack1); } return buffer; @@ -21787,6 +22936,15 @@ function program3(depth0,data) { */ optionValuePath: 'content', + /** + The view class for option. + + @property optionView + @type Ember.View + @default Ember.SelectOption + */ + optionView: Ember.SelectOption, + _change: function() { if (get(this, 'multiple')) { this._changeMultiple(); @@ -21816,9 +22974,9 @@ function program3(depth0,data) { selection; if (value !== selectedValue) { - selection = content.find(function(obj) { + selection = content ? content.find(function(obj) { return value === (valuePath ? get(obj, valuePath) : obj); - }); + }) : null; this.set('selection', selection); } @@ -21840,7 +22998,7 @@ function program3(depth0,data) { content = get(this, 'content'), prompt = get(this, 'prompt'); - if (!get(content, 'length')) { return; } + if (!content || !get(content, 'length')) { return; } if (prompt && selectedIndex === 0) { set(this, 'selection', null); return; } if (prompt) { selectedIndex -= 1; } @@ -21907,60 +23065,68 @@ function program3(depth0,data) { } }); -Ember.SelectOption = Ember.View.extend({ - tagName: 'option', - attributeBindings: ['value', 'selected'], +})(); - defaultTemplate: function(context, options) { - options = { data: options.data, hash: {} }; - Ember.Handlebars.helpers.bind.call(context, "view.label", options); - }, - init: function() { - this.labelPathDidChange(); - this.valuePathDidChange(); - this._super(); - }, - - selected: Ember.computed(function() { - var content = get(this, 'content'), - selection = get(this, 'parentView.selection'); - if (get(this, 'parentView.multiple')) { - return selection && indexOf(selection, content.valueOf()) > -1; - } else { - // Primitives get passed through bindings as objects... since - // `new Number(4) !== 4`, we use `==` below - return content == selection; +(function() { +function normalizeHash(hash, hashTypes) { + for (var prop in hash) { + if (hashTypes[prop] === 'ID') { + hash[prop + 'Binding'] = hash[prop]; + delete hash[prop]; } - }).property('content', 'parentView.selection').volatile(), - - labelPathDidChange: Ember.observer(function() { - var labelPath = get(this, 'parentView.optionLabelPath'); + } +} - if (!labelPath) { return; } +/** + * `{{input}}` inserts a new instance of either Ember.TextField or + * Ember.Checkbox, depending on the `type` option passed in. If no `type` + * is supplied it defaults to Ember.TextField. + * + * @method input + * @for Ember.Handlebars.helpers + * @param {Hash} options + */ +Ember.Handlebars.registerHelper('input', function(options) { + Ember.assert('You can only pass attributes to the `input` helper, not arguments', arguments.length < 2); - Ember.defineProperty(this, 'label', Ember.computed(function() { - return get(this, labelPath); - }).property(labelPath)); - }, 'parentView.optionLabelPath'), + var hash = options.hash, + types = options.hashTypes, + inputType = hash.type, + onEvent = hash.on; - valuePathDidChange: Ember.observer(function() { - var valuePath = get(this, 'parentView.optionValuePath'); + delete hash.type; + delete hash.on; - if (!valuePath) { return; } + normalizeHash(hash, types); - Ember.defineProperty(this, 'value', Ember.computed(function() { - return get(this, valuePath); - }).property(valuePath)); - }, 'parentView.optionValuePath') + if (inputType === 'checkbox') { + return Ember.Handlebars.helpers.view.call(this, Ember.Checkbox, options); + } else { + hash.type = inputType; + hash.onEvent = onEvent || 'enter'; + return Ember.Handlebars.helpers.view.call(this, Ember.TextField, options); + } }); -})(); - +/** + * `{{textarea}}` inserts a new instance of Ember.TextArea into the template + * passing its options to `Ember.TextArea`'s `create` method. + * + * @method textarea + * @for Ember.Handlebars.helpers + * @param {Hash} options + */ +Ember.Handlebars.registerHelper('textarea', function(options) { + Ember.assert('You can only pass attributes to the `textarea` helper, not arguments', arguments.length < 2); + var hash = options.hash, + types = options.hashTypes; -(function() { + normalizeHash(hash, types); + return Ember.Handlebars.helpers.view.call(this, Ember.TextArea, options); +}); })(); @@ -22425,14 +23591,14 @@ define("route-recognizer", }, recognize: function(path) { - var states = [ this.rootState ], i, l; + var states = [ this.rootState ], + pathLen, i, l; // DEBUG GROUP path - var pathLen = path.length; - if (path.charAt(0) !== "/") { path = "/" + path; } + pathLen = path.length; if (pathLen > 1 && path.charAt(pathLen - 1) === "/") { path = path.substr(0, pathLen - 1); } @@ -22578,16 +23744,10 @@ define("router", * `{String} handler`: A handler name * `{Object} params`: A hash of recognized parameters - ## `UnresolvedHandlerInfo` - - * `{Boolean} isDynamic`: whether a handler has any dynamic segments - * `{String} name`: the name of a handler - * `{Object} context`: the active context for the handler - ## `HandlerInfo` * `{Boolean} isDynamic`: whether a handler has any dynamic segments - * `{String} name`: the original unresolved handler name + * `{String} name`: the name of a handler * `{Object} handler`: a handler object * `{Object} context`: the active context for the handler */ @@ -22623,6 +23783,21 @@ define("router", }, /** + Clears the current and target route handlers and triggers exit + on each of them starting at the leaf and traversing up through + its ancestors. + */ + reset: function() { + eachHandler(this.currentHandlerInfos || [], function(handler) { + if (handler.exit) { + handler.exit(); + } + }); + this.currentHandlerInfos = null; + this.targetHandlerInfos = null; + }, + + /** The entry point for handling a change to the URL (usually via the back and forward button). @@ -22634,8 +23809,7 @@ define("router", @return {Array} an Array of `[handler, parameter]` tuples */ handleURL: function(url) { - var results = this.recognizer.recognize(url), - objects = []; + var results = this.recognizer.recognize(url); if (!results) { throw new Error("No route matched the URL '" + url + "'"); @@ -22731,7 +23905,7 @@ define("router", toSetup = [], startIdx = handlers.length, objectsToMatch = objects.length, - object, objectChanged, handlerObj, handler, names, i, len; + object, objectChanged, handlerObj, handler, names, i; // Find out which handler to start matching at for (i=handlers.length-1; i>=0 && objectsToMatch>0; i--) { @@ -22742,11 +23916,11 @@ define("router", } if (objectsToMatch > 0) { - throw "More objects were passed than dynamic segments"; + throw "More context objects were passed than there are dynamic segments for the route: "+handlerName; } // Connect the objects to the routes - for (i=0, len=handlers.length; i<len; i++) { + for (i=0; i<handlers.length; i++) { handlerObj = handlers[i]; handler = this.getHandler(handlerObj.handler); names = handlerObj.names; @@ -22791,10 +23965,22 @@ define("router", toSetup.push({ isDynamic: !!handlerObj.names.length, - handler: handlerObj.handler, - name: handlerObj.name, + name: handlerObj.handler, + handler: handler, context: object }); + + if (i === handlers.length - 1) { + var lastHandler = toSetup[toSetup.length - 1], + additionalHandler; + + if (additionalHandler = lastHandler.handler.additionalHandler) { + handlers.push({ + handler: additionalHandler.call(lastHandler.handler), + names: [] + }); + } + } } return { params: params, toSetup: toSetup }; @@ -22803,11 +23989,13 @@ define("router", isActive: function(handlerName) { var contexts = [].slice.call(arguments, 1); - var currentHandlerInfos = this.currentHandlerInfos, + var targetHandlerInfos = this.targetHandlerInfos, found = false, names, object, handlerInfo, handlerObj; - for (var i=currentHandlerInfos.length-1; i>=0; i--) { - handlerInfo = currentHandlerInfos[i]; + if (!targetHandlerInfos) { return; } + + for (var i=targetHandlerInfos.length-1; i>=0; i--) { + handlerInfo = targetHandlerInfos[i]; if (handlerInfo.name === handlerName) { found = true; } if (found) { @@ -22898,7 +24086,10 @@ define("router", function failure(router, error) { loaded(router); var handler = router.getHandler('failure'); - if (handler && handler.setup) { handler.setup(error); } + if (handler) { + if (handler.enter) { handler.enter(); } + if (handler.setup) { handler.setup(error); } + } } /** @@ -22933,9 +24124,21 @@ define("router", */ function collectObjects(router, results, index, objects) { if (results.length === index) { - loaded(router); - setupContexts(router, objects); - return; + var lastObject = objects[objects.length - 1], + lastHandler = lastObject && lastObject.handler; + + if (lastHandler && lastHandler.additionalHandler) { + var additionalResult = { + handler: lastHandler.additionalHandler(), + params: {}, + isDynamic: false + }; + results.push(additionalResult); + } else { + loaded(router); + setupContexts(router, objects); + return; + } } var result = results[index]; @@ -22960,7 +24163,8 @@ define("router", var updatedObjects = objects.concat([{ context: value, - handler: result.handler, + name: result.handler, + handler: router.getHandler(result.handler), isDynamic: result.isDynamic }]); collectObjects(router, results, index + 1, updatedObjects); @@ -22970,8 +24174,9 @@ define("router", /** @private - Takes an Array of `UnresolvedHandlerInfo`s, resolves the handler names - into handlers, and then figures out what to do with each of the handlers. + Takes an Array of `HandlerInfo`s, figures out which ones are + exiting, entering, or changing contexts, and calls the + proper handler hooks. For example, consider the following tree of handlers. Each handler is followed by the URL segment it handles. @@ -23005,28 +24210,30 @@ define("router", 4. Triggers the `setup` callback on `about` @param {Router} router - @param {Array[UnresolvedHandlerInfo]} handlerInfos + @param {Array[HandlerInfo]} handlerInfos */ function setupContexts(router, handlerInfos) { - resolveHandlers(router, handlerInfos); - var partition = partitionHandlers(router.currentHandlerInfos || [], handlerInfos); - router.currentHandlerInfos = handlerInfos; + router.targetHandlerInfos = handlerInfos; eachHandler(partition.exited, function(handler, context) { delete handler.context; if (handler.exit) { handler.exit(); } }); - eachHandler(partition.updatedContext, function(handler, context) { + var currentHandlerInfos = partition.unchanged.slice(); + router.currentHandlerInfos = currentHandlerInfos; + + eachHandler(partition.updatedContext, function(handler, context, handlerInfo) { setContext(handler, context); if (handler.setup) { handler.setup(context); } + currentHandlerInfos.push(handlerInfo); }); var aborted = false; - eachHandler(partition.entered, function(handler, context) { + eachHandler(partition.entered, function(handler, context, handlerInfo) { if (aborted) { return; } if (handler.enter) { handler.enter(); } setContext(handler, context); @@ -23035,9 +24242,13 @@ define("router", aborted = true; } } + + if (!aborted) { + currentHandlerInfos.push(handlerInfo); + } }); - if (router.didTransition) { + if (!aborted && router.didTransition) { router.didTransition(handlerInfos); } } @@ -23057,29 +24268,7 @@ define("router", handler = handlerInfo.handler, context = handlerInfo.context; - callback(handler, context); - } - } - - /** - @private - - Updates the `handler` field in each element in an Array of - `UnresolvedHandlerInfo`s from a handler name to a resolved handler. - - When done, the Array will contain `HandlerInfo` structures. - - @param {Router} router - @param {Array[UnresolvedHandlerInfo]} handlerInfos - */ - function resolveHandlers(router, handlerInfos) { - var handlerInfo; - - for (var i=0, l=handlerInfos.length; i<l; i++) { - handlerInfo = handlerInfos[i]; - - handlerInfo.name = handlerInfo.handler; - handlerInfo.handler = router.getHandler(handlerInfo.handler); + callback(handler, context, handlerInfo); } } @@ -23105,7 +24294,7 @@ define("router", * entered: the handler was not active in the old URL, but is now active. - The PartitionedHandlers structure has three fields: + The PartitionedHandlers structure has four fields: * `updatedContext`: a list of `HandlerInfo` objects that represent handlers that remain active but have a changed @@ -23114,6 +24303,7 @@ define("router", handlers that are newly active * `exited`: a list of `HandlerInfo` objects that are no longer active. + * `unchanged`: a list of `HanderInfo` objects that remain active. @param {Array[HandlerInfo]} oldHandlers a list of the handler information for the previous URL (or `[]` if this is the @@ -23127,7 +24317,8 @@ define("router", var handlers = { updatedContext: [], exited: [], - entered: [] + entered: [], + unchanged: [] }; var handlerChanged, contextChanged, i, l; @@ -23145,6 +24336,8 @@ define("router", } else if (contextChanged || oldHandler.context !== newHandler.context) { contextChanged = true; handlers.updatedContext.push(newHandler); + } else { + handlers.unchanged.push(oldHandler); } } @@ -23164,17 +24357,24 @@ define("router", throw new Error("Could not trigger event '" + name + "'. There are no active handlers"); } + var eventWasHandled = false; + for (var i=currentHandlerInfos.length-1; i>=0; i--) { var handlerInfo = currentHandlerInfos[i], handler = handlerInfo.handler; if (handler.events && handler.events[name]) { - handler.events[name].apply(handler, args); - return; + if (handler.events[name].apply(handler, args) === true) { + eventWasHandled = true; + } else { + return; + } } } - throw new Error("Nothing handled the event '" + name + "'."); + if (!eventWasHandled) { + throw new Error("Nothing handled the event '" + name + "'."); + } } function setContext(handler, context) { @@ -23184,6 +24384,7 @@ define("router", return Router; }); + })(); @@ -23275,6 +24476,8 @@ Ember.RouterDSL = DSL; (function() { +var get = Ember.get; + /** @module ember @submodule ember-routing @@ -23284,14 +24487,14 @@ Ember.controllerFor = function(container, controllerName, context, lookupOptions return container.lookup('controller:' + controllerName, lookupOptions) || Ember.generateController(container, controllerName, context); }; -/** +/* Generates a controller automatically if none was provided. The type of generated controller depends on the context. You can customize your generated controllers by defining `App.ObjectController` and `App.ArrayController` */ Ember.generateController = function(container, controllerName, context) { - var controller, DefaultController, fullName; + var controller, DefaultController, fullName, instance; if (context && Ember.isArray(context)) { DefaultController = container.resolve('controller:array'); @@ -23312,10 +24515,16 @@ Ember.generateController = function(container, controllerName, context) { return "(generated " + controllerName + " controller)"; }; - fullName = 'controller:' + controllerName; container.register(fullName, controller); - return container.lookup(fullName); + + instance = container.lookup(fullName); + + if (get(instance, 'namespace.LOG_ACTIVE_GENERATION')) { + Ember.Logger.info("generated -> " + fullName, { fullName: fullName }); + } + + return instance; }; })(); @@ -23330,20 +24539,22 @@ Ember.generateController = function(container, controllerName, context) { var Router = requireModule("router"); var get = Ember.get, set = Ember.set; +var defineProperty = Ember.defineProperty; var DefaultView = Ember._MetamorphView; function setupLocation(router) { var location = get(router, 'location'), - rootURL = get(router, 'rootURL'); + rootURL = get(router, 'rootURL'), + options = {}; + + if (typeof rootURL === 'string') { + options.rootURL = rootURL; + } if ('string' === typeof location) { - location = set(router, 'location', Ember.Location.create({ - implementation: location - })); + options.implementation = location; + location = set(router, 'location', Ember.Location.create(options)); - if (typeof rootURL === 'string') { - set(location, 'rootURL', rootURL); - } } } @@ -23389,14 +24600,12 @@ Ember.Router = Ember.Object.extend({ }, didTransition: function(infos) { - // Don't do any further action here if we redirected - for (var i=0, l=infos.length; i<l; i++) { - if (infos[i].handler.redirected) { return; } - } - var appController = this.container.lookup('controller:application'), path = routePath(infos); + if (!('currentPath' in appController)) { + defineProperty(appController, 'currentPath'); + } set(appController, 'currentPath', path); this.notifyPropertyChange('url'); @@ -23410,6 +24619,22 @@ Ember.Router = Ember.Object.extend({ this.notifyPropertyChange('url'); }, + /** + Transition to another route via the `routeTo` event which + will by default be handled by ApplicationRoute. + + @method routeTo + @param {TransitionEvent} transitionEvent + */ + routeTo: function(transitionEvent) { + var handlerInfos = this.router.currentHandlerInfos; + if (handlerInfos) { + transitionEvent.sourceRoute = handlerInfos[handlerInfos.length - 1].handler; + } + + this.send('routeTo', transitionEvent); + }, + transitionTo: function(name) { var args = [].slice.call(arguments); doTransition(this, 'transitionTo', args); @@ -23438,6 +24663,18 @@ Ember.Router = Ember.Object.extend({ return this.router.hasRoute(route); }, + /** + @private + + Resets the state of the router by clearing the current route + handlers and deactivating them. + + @method reset + */ + reset: function() { + this.router.reset(); + }, + _lookupActiveView: function(templateName) { var active = this._activeViews[templateName]; return active && active[0]; @@ -23488,6 +24725,16 @@ function getHandlerFunction(router) { container.register(routeName, DefaultRoute.extend()); handler = container.lookup(routeName); + + if (get(router, 'namespace.LOG_ACTIVE_GENERATION')) { + Ember.Logger.info("generated -> " + routeName, { fullName: routeName }); + } + } + + if (name === 'application') { + // Inject default `routeTo` handler. + handler.events = handler.events || {}; + handler.events.routeTo = handler.events.routeTo || Ember.TransitionEvent.defaultHandler; } handler.routeName = name; @@ -23579,7 +24826,8 @@ Ember.Router.reopenClass({ */ var get = Ember.get, set = Ember.set, - classify = Ember.String.classify; + classify = Ember.String.classify, + fmt = Ember.String.fmt; /** The `Ember.Route` class is used to define individual routes. Refer to @@ -23610,15 +24858,16 @@ Ember.Route = Ember.Object.extend({ }, /** - The collection of functions keyed by name available on this route as + The collection of functions, keyed by name, available on this route as action targets. These functions will be invoked when a matching `{{action}}` is triggered from within a template and the application's current route is this route. - Events can also be invoked from other parts of your application via `Route#send`. + Events can also be invoked from other parts of your application via `Route#send` + or `Controller#send`. - The context of event will be the this route. + The context of the event will be this route. @see {Ember.Route#send} @see {Handlebars.helpers.action} @@ -23646,6 +24895,17 @@ Ember.Route = Ember.Object.extend({ activate: Ember.K, /** + Transition to another route via the `routeTo` event which + will by default be handled by ApplicationRoute. + + @method routeTo + @param {TransitionEvent} transitionEvent + */ + routeTo: function(transitionEvent) { + this.router.routeTo(transitionEvent); + }, + + /** Transition into another route. Optionally supply a model for the route in question. The model will be serialized into the URL using the `serialize` hook. @@ -23654,21 +24914,38 @@ Ember.Route = Ember.Object.extend({ @param {String} name the name of the route @param {...Object} models the */ - transitionTo: function() { - if (this._checkingRedirect) { this.redirected = true; } - return this.router.transitionTo.apply(this.router, arguments); + transitionTo: function(name, context) { + var router = this.router; + + // If the transition is a no-op, just bail. + if (router.isActive.apply(router, arguments)) { + return; + } + + if (this._checkingRedirect) { this._redirected[this._redirectDepth] = true; } + return router.transitionTo.apply(router, arguments); }, /** Transition into another route while replacing the current URL if possible. Identical to `transitionTo` in all other respects. + Of the bundled location types, only `history` currently supports + this behavior. + @method replaceWith @param {String} name the name of the route @param {...Object} models the */ replaceWith: function() { - if (this._checkingRedirect) { this.redirected = true; } + var router = this.router; + + // If the transition is a no-op, just bail. + if (router.isActive.apply(router, arguments)) { + return; + } + + if (this._checkingRedirect) { this._redirected[this._redirectDepth] = true; } return this.router.replaceWith.apply(this.router, arguments); }, @@ -23677,6 +24954,15 @@ Ember.Route = Ember.Object.extend({ }, /** + @private + + Internal counter for tracking whether a route handler has + called transitionTo or replaceWith inside its redirect hook. + + */ + _redirectDepth: 0, + + /** @private This hook is the entry point for router.js @@ -23684,21 +24970,64 @@ Ember.Route = Ember.Object.extend({ @method setup */ setup: function(context) { - this.redirected = false; + // Determine if this is the top-most transition. + // If so, we'll set up a data structure to track + // whether `transitionTo` or replaceWith gets called + // inside our `redirect` hook. + // + // This is necessary because we set a flag on the route + // inside transitionTo/replaceWith to determine afterwards + // if they were called, but `setup` can be called + // recursively and we need to disambiguate where in the + // call stack the redirect happened. + + // Are we the first call to setup? If so, set up the + // redirect tracking data structure, and remember that + // we're the top-most so we can clean it up later. + var isTop; + if (!this._redirected) { + isTop = true; + this._redirected = []; + } + + // Set a flag on this route saying that we are interested in + // tracking redirects, and increment the depth count. this._checkingRedirect = true; + var depth = ++this._redirectDepth; - this.redirect(context); + // Check to see if context is set. This check preserves + // the correct arguments.length inside the `redirect` hook. + if (context === undefined) { + this.redirect(); + } else { + this.redirect(context); + } + // After the call to `redirect` returns, decrement the depth count. + this._redirectDepth--; this._checkingRedirect = false; - if (this.redirected) { return false; } - var controller = this.controllerFor(this.routeName, context); + // Save off the data structure so we can reset it on the route but + // still reference it later in this method. + var redirected = this._redirected; - if (controller) { - this.controller = controller; - set(controller, 'model', context); + // If this is the top `setup` call in the call stack, clear the + // redirect tracking data structure. + if (isTop) { this._redirected = null; } + + // If we were redirected, there is nothing left for us to do. + // Returning false tells router.js not to continue calling setup + // on any children route handlers. + if (redirected[depth]) { + return false; } + var controller = this.controllerFor(this.routeName, context); + + // Assign the route's controller so that it can more easily be + // referenced in event handlers + this.controller = controller; + if (this.setupControllers) { Ember.deprecate("Ember.Route.setupControllers is deprecated. Please use Ember.Route.setupController(controller, model) instead."); this.setupControllers(controller, context); @@ -23742,6 +25071,8 @@ Ember.Route = Ember.Object.extend({ @private Called when the context is changed by router.js. + + @method contextDidChange */ contextDidChange: function() { this.currentModel = this.context; @@ -23852,30 +25183,41 @@ Ember.Route = Ember.Object.extend({ This method is called with the controller for the current route and the model supplied by the `model` hook. - ```js - App.Router.map(function() { - this.resource('post', {path: '/posts/:post_id'}); - }); - ``` - - For the `post` route, the controller is `App.PostController`. - By default, the `setupController` hook sets the `content` property of the controller to the `model`. - If no explicit controller is defined, the route will automatically create - an appropriate controller for the model: + This means that your template will get a proxy for the model as its + context, and you can act as though the model itself was the context. + + The provided controller will be one resolved based on the name + of this route. + + If no explicit controller is defined, Ember will automatically create + an appropriate controller for the model. * if the model is an `Ember.Array` (including record arrays from Ember Data), the controller is an `Ember.ArrayController`. * otherwise, the controller is an `Ember.ObjectController`. - This means that your template will get a proxy for the model as its - context, and you can act as though the model itself was the context. + As an example, consider the router: + + ```js + App.Router.map(function() { + this.resource('post', {path: '/posts/:post_id'}); + }); + ``` + + For the `post` route, a controller named `App.PostController` would + be used if it is defined. If it is not defined, an `Ember.ObjectController` + instance would be used. @method setupController */ - setupController: Ember.K, + setupController: function(controller, context){ + if (controller && (context !== undefined)) { + set(controller, 'model', context); + } + }, /** Returns the controller for a particular route. @@ -24010,7 +25352,12 @@ Ember.Route = Ember.Object.extend({ view = container.lookup('view:' + name), template = container.lookup('template:' + name); - if (!view && !template) { return; } + if (!view && !template) { + if (get(this.router, 'namespace.LOG_VIEW_LOOKUPS')) { + Ember.Logger.info("Could not find \"" + name + "\" template or view. Nothing will be rendered", { fullName: 'template:' + name }); + } + return; + } options = normalizeOptions(this, name, template, options); view = setupView(view, container, options); @@ -24026,7 +25373,9 @@ Ember.Route = Ember.Object.extend({ }); function parentRoute(route) { - var handlerInfos = route.router.router.currentHandlerInfos; + var handlerInfos = route.router.router.targetHandlerInfos; + + if (!handlerInfos) { return; } var parent, current; @@ -24042,7 +25391,7 @@ function parentTemplate(route, isRecursive) { if (!parent) { return; } - Ember.warn("The immediate parent route did not render into the main outlet and the default 'into' option may not be expected", !isRecursive); + Ember.warn(fmt("The immediate parent route ('%@') did not render into the main outlet and the default 'into' option ('%@') may not be expected", [get(parent, 'routeName'), get(route, 'routeName')]), !isRecursive); if (template = parent.lastRenderedTemplate) { return template; @@ -24057,6 +25406,7 @@ function normalizeOptions(route, name, template, options) { options.outlet = options.outlet || 'main'; options.name = name; options.template = template; + options.LOG_VIEW_LOOKUPS = get(route.router, 'namespace.LOG_VIEW_LOOKUPS'); Ember.assert("An outlet ("+options.outlet+") was specified but this view will render at the root level.", options.outlet === 'main' || options.into); @@ -24080,9 +25430,17 @@ function normalizeOptions(route, name, template, options) { } function setupView(view, container, options) { - var defaultView = options.into ? 'view:default' : 'view:toplevel'; - - view = view || container.lookup(defaultView); + if (view) { + if (options.LOG_VIEW_LOOKUPS) { + Ember.Logger.info("Rendering " + options.name + " with " + view, { fullName: 'view:' + options.name }); + } + } else { + var defaultView = options.into ? 'view:default' : 'view:toplevel'; + view = container.lookup(defaultView); + if (options.LOG_VIEW_LOOKUPS) { + Ember.Logger.info("Rendering " + options.name + " with default view " + view, { fullName: 'view:' + options.name }); + } + } if (!get(view, 'templateName')) { set(view, 'template', options.template); @@ -24103,6 +25461,10 @@ function appendView(route, view, options) { parentView.connectOutlet(options.outlet, view); } else { var rootElement = get(route, 'router.namespace.rootElement'); + // tear down view if one is already rendered + if (route.teardownView) { + route.teardownView(); + } route.router._connectActiveView(options.name, view); route.teardownView = teardownTopLevel(view); view.appendTo(rootElement); @@ -24129,6 +25491,65 @@ function teardownView(route) { (function() { +/** +@module ember +@submodule ember-routing +*/ + + +/* + A TransitionEvent is passed as the argument for `transitionTo` + events and contains information about an attempted transition + that can be modified or decorated by leafier `transitionTo` event + handlers before the actual transition is committed by ApplicationRoute. + + @class TransitionEvent + @namespace Ember + @extends Ember.Deferred + */ +Ember.TransitionEvent = Ember.Object.extend({ + + /* + The Ember.Route method used to perform the transition. Presently, + the only valid values are 'transitionTo' and 'replaceWith'. + */ + transitionMethod: 'transitionTo', + destinationRouteName: null, + sourceRoute: null, + contexts: null, + + init: function() { + this._super(); + this.contexts = this.contexts || []; + }, + + /* + Convenience method that returns an array that can be used for + legacy `transitionTo` and `replaceWith`. + */ + transitionToArgs: function() { + return [this.destinationRouteName].concat(this.contexts); + } +}); + + +Ember.TransitionEvent.reopenClass({ + /* + This is the default transition event handler that will be injected + into ApplicationRoute. The context, like all route event handlers in + the events hash, will be an `Ember.Route`. + */ + defaultHandler: function(transitionEvent) { + var router = this.router; + router[transitionEvent.transitionMethod].apply(router, transitionEvent.transitionToArgs()); + } +}); + +})(); + + + +(function() { })(); @@ -24168,7 +25589,7 @@ Ember.onLoad('Ember.Handlebars', function() { @submodule ember-routing */ -var get = Ember.get, set = Ember.set; +var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; Ember.onLoad('Ember.Handlebars', function(Handlebars) { var resolveParams = Ember.Router.resolveParams, @@ -24194,26 +25615,13 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { routeName = fullRouteName(router, passedRouteName); - Ember.assert("The route " + passedRouteName + " was not found", router.hasRoute(routeName)); + Ember.assert(fmt("The attempt to linkTo route '%@' failed. The router did not find '%@' in its possible routes: '%@'", [passedRouteName, passedRouteName, Ember.keys(router.router.recognizer.names).join("', '")]), router.hasRoute(routeName)); var ret = [ routeName ]; return ret.concat(resolvedPaths(linkView.parameters)); } /** - Renders a link to the supplied route. - - When the rendered link matches the current route, and the same object instance is passed into the helper, - then the link is given class="active" by default. - - You may re-open LinkView in order to change the default active class: - - ``` javascript - Ember.LinkView.reopen({ - activeClass: "is-active" - }) - ``` - @class LinkView @namespace Ember @extends Ember.View @@ -24224,15 +25632,23 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { currentWhen: null, title: null, activeClass: 'active', + disabledClass: 'disabled', + _isDisabled: false, replace: false, attributeBindings: ['href', 'title'], - classNameBindings: 'active', + classNameBindings: ['active', 'disabled'], // Even though this isn't a virtual view, we want to treat it as if it is // so that you can access the parent with {{view.prop}} concreteView: Ember.computed(function() { return get(this, 'parentView'); - }).property('parentView').volatile(), + }).property('parentView'), + + disabled: Ember.computed(function(key, value) { + if (value !== undefined) { this.set('_isDisabled', value); } + + return value ? this.get('disabledClass') : false; + }), active: Ember.computed(function() { var router = this.get('router'), @@ -24253,17 +25669,32 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { event.preventDefault(); if (this.bubbles === false) { event.stopPropagation(); } + + if (get(this, '_isDisabled')) { return false; } var router = this.get('router'); - if (this.get('replace')) { - router.replaceWith.apply(router, args(this, router)); + if (Ember.ENV.ENABLE_ROUTE_TO) { + + var routeArgs = args(this, router); + + router.routeTo(Ember.TransitionEvent.create({ + transitionMethod: this.get('replace') ? 'replaceWith' : 'transitionTo', + destinationRouteName: routeArgs[0], + contexts: routeArgs.slice(1) + })); } else { - router.transitionTo.apply(router, args(this, router)); + if (this.get('replace')) { + router.replaceWith.apply(router, args(this, router)); + } else { + router.transitionTo.apply(router, args(this, router)); + } } }, href: Ember.computed(function() { + if (this.get('tagName') !== 'a') { return false; } + var router = this.get('router'); return router.generate.apply(router, args(this, router)); }) @@ -24272,6 +25703,155 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { LinkView.toString = function() { return "LinkView"; }; /** + The `{{linkTo}}` helper renders a link to the supplied + `routeName` passing an optionally supplied model to the + route as its `model` context of the route. The block + for `{{linkTo}}` becomes the innerHTML of the rendered + element: + + ```handlebars + {{#linkTo photoGallery}} + Great Hamster Photos + {{/linkTo}} + ``` + + ```html + <a href="/hamster-photos"> + Great Hamster Photos + </a> + ``` + + ### Supplying a tagName + By default `{{linkTo}}` renders an `<a>` element. This can + be overridden for a single use of `{{linkTo}}` by supplying + a `tagName` option: + + ```handlebars + {{#linkTo photoGallery tagName="li"}} + Great Hamster Photos + {{/linkTo}} + ``` + + ```html + <li> + Great Hamster Photos + </li> + ``` + + To override this option for your entire application, see + "Overriding Application-wide Defaults". + + ### Handling `href` + `{{linkTo}}` will use your application's Router to + fill the element's `href` property with a url that + matches the path to the supplied `routeName` for your + routers's configured `Location` scheme, which defaults + to Ember.HashLocation. + + ### Handling current route + `{{linkTo}}` will apply a CSS class name of 'active' + when the application's current route matches + the supplied routeName. For example, if the application's + current route is 'photoGallery.recent' the following + use of `{{linkTo}}`: + + ```handlebars + {{#linkTo photoGallery.recent}} + Great Hamster Photos from the last week + {{/linkTo}} + ``` + + will result in + + ```html + <a href="/hamster-photos/this-week" class="active"> + Great Hamster Photos + </a> + ``` + + The CSS class name used for active classes can be customized + for a single use of `{{linkTo}}` by passing an `activeClass` + option: + + ```handlebars + {{#linkTo photoGallery.recent activeClass="current-url"}} + Great Hamster Photos from the last week + {{/linkTo}} + ``` + + ```html + <a href="/hamster-photos/this-week" class="current-url"> + Great Hamster Photos + </a> + ``` + + To override this option for your entire application, see + "Overriding Application-wide Defaults". + + ### Supplying a model + An optional model argument can be used for routes whose + paths contain dynamic segments. This argument will become + the model context of the linked route: + + ```javascript + App.Router.map(function(){ + this.resource("photoGallery", {path: "hamster-photos/:photo_id"}); + }) + ``` + + ```handlebars + {{#linkTo photoGallery aPhoto}} + {{aPhoto.title}} + {{/linkTo}} + ``` + + ```html + <a href="/hamster-photos/42"> + Tomster + </a> + ``` + + ### Supplying multiple models + For deep-linking to route paths that contain multiple + dynamic segments, multiple model arguments can be used. + As the router transitions through the route path, each + supplied model argument will become the context for the + route with the dynamic segments: + + ```javascript + App.Router.map(function(){ + this.resource("photoGallery", {path: "hamster-photos/:photo_id"}, function(){ + this.route("comment", {path: "comments/:comment_id"}); + }); + }); + ``` + This argument will become the model context of the linked route: + + ```handlebars + {{#linkTo photoGallery.comment aPhoto comment}} + {{comment.body}} + {{/linkTo}} + ``` + + ```html + <a href="/hamster-photos/42/comment/718"> + A+++ would snuggle again. + </a> + ``` + + ### Overriding Application-wide Defaults + ``{{linkTo}}`` creates an instance of Ember.LinkView + for rendering. To override options for your entire + application, reopen Ember.LinkView and supply the + desired values: + + ``` javascript + Ember.LinkView.reopen({ + activeClass: "is-active", + tagName: 'li' + }) + ``` + @method linkTo @for Ember.Handlebars.helpers @param {String} routeName @@ -24286,6 +25866,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { hash.namedRoute = name; hash.currentWhen = hash.currentWhen || name; + hash.disabledBinding = hash.disabledWhen; hash.parameters = { context: this, @@ -24361,13 +25942,27 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { }); ``` + You can specify the view class that the outlet uses to contain and manage the + templates rendered into it. + + ``` handlebars + {{outlet viewClass=App.SectionContainer}} + ``` + + ``` javascript + App.SectionContainer = Ember.ContainerView.extend({ + tagName: 'section', + classNames: ['special'] + }); + ``` + @method outlet @for Ember.Handlebars.helpers @param {String} property the property on the controller that holds the view for this outlet */ Handlebars.registerHelper('outlet', function(property, options) { - var outletSource; + var outletSource, outletContainerClass; if (property && property.data && property.data.isRenderData) { options = property; @@ -24379,10 +25974,12 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { outletSource = outletSource.get('_parentView'); } + outletContainerClass = options.hash.viewClass || Handlebars.OutletView; + options.data.view.set('outletSource', outletSource); options.hash.currentViewBinding = '_view.outletSource._outlets.' + property; - return Handlebars.helpers.view.call(this, Handlebars.OutletView, options); + return Handlebars.helpers.view.call(this, outletContainerClass, options); }); }); @@ -24400,15 +25997,23 @@ var get = Ember.get, set = Ember.set; Ember.onLoad('Ember.Handlebars', function(Handlebars) { /** - Renders the named template in the current context using the singleton - instance of the same-named controller. + Renders the named template in the current context with the same-named + controller. - If a view class with the same name exists, uses the view class. + If a view class with the same name exists, the view class will be used. - If a `model` is specified, it becomes the model for that controller. + The optional second argument is a property path that will be bound + to the `model` property of the controller. + + If a `model` property path is specified, then a new instance of the + controller will be created. + + If no `model` property path is provided, then the helper will use the + singleton instance of the controller. A given controller may only be used + one time in your app in this manner. The default target for `{{action}}`s in the rendered template is the - named controller. + controller. @method render @for Ember.Handlebars.helpers @@ -24488,6 +26093,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { var EmberHandlebars = Ember.Handlebars, handlebarsGet = EmberHandlebars.get, SafeString = EmberHandlebars.SafeString, + forEach = Ember.ArrayPolyfills.forEach, get = Ember.get, a_slice = Array.prototype.slice; @@ -24514,7 +26120,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { var allowed = true; - keys.forEach(function(key) { + forEach.call(keys, function(key) { if (event[key + "Key"] && allowedKeys.indexOf(key) === -1) { allowed = false; } @@ -24600,7 +26206,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { aView.appendTo('body'); ``` - Will results in the following rendered HTML + Will result in the following rendered HTML ```html <div class="ember-view"> @@ -24685,7 +26291,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { Alternatively, a `target` option can be provided to the helper to change which object will receive the method call. This option must be a path - path to an object, accessible in the current context: + to an object, accessible in the current context: ```handlebars <script type="text/x-handlebars" data-template-name='a-template'> @@ -24887,6 +26493,28 @@ if (Ember.ENV.EXPERIMENTAL_CONTROL_HELPER) { var get = Ember.get, set = Ember.set; Ember.ControllerMixin.reopen({ + /** + Transition the application into another route. The route may + be either a single route or route path: + + ```javascript + aController.transitionToRoute('blogPosts'); + aController.transitionToRoute('blogPosts.recentEntries'); + ``` + + Optionally supply a model for the route in question. The model + will be serialized into the URL using the `serialize` hook of + the route: + + ```javascript + aController.transitionToRoute('blogPost', aPost); + ``` + + @param {String} name the name of the route + @param {...Object} models the + @for Ember.ControllerMixin + @method transitionToRoute + */ transitionToRoute: function() { // target may be either another controller or a router var target = get(this, 'target'), @@ -24894,6 +26522,11 @@ Ember.ControllerMixin.reopen({ return method.apply(target, arguments); }, + /** + @deprecated + @for Ember.ControllerMixin + @method transitionTo + */ transitionTo: function() { Ember.deprecate("transitionTo is deprecated. Please use transitionToRoute."); return this.transitionToRoute.apply(this, arguments); @@ -24906,6 +26539,11 @@ Ember.ControllerMixin.reopen({ return method.apply(target, arguments); }, + /** + @deprecated + @for Ember.ControllerMixin + @method replaceWith + */ replaceWith: function() { Ember.deprecate("replaceWith is deprecated. Please use replaceRoute."); return this.replaceRoute.apply(this, arguments); @@ -24931,6 +26569,15 @@ Ember.View.reopen({ }, connectOutlet: function(outletName, view) { + if (this._pendingDisconnections) { + delete this._pendingDisconnections[outletName]; + } + + if (this._hasEquivalentView(outletName, view)) { + view.destroy(); + return; + } + var outlets = get(this, '_outlets'), container = get(this, 'container'), router = container && container.lookup('router:main'), @@ -24943,10 +26590,30 @@ Ember.View.reopen({ } }, + _hasEquivalentView: function(outletName, view) { + var existingView = get(this, '_outlets.'+outletName); + return existingView && + existingView.prototype === view.prototype && + existingView.get('template') === view.get('template') && + existingView.get('context') === view.get('context'); + }, + disconnectOutlet: function(outletName) { + if (!this._pendingDisconnections) { + this._pendingDisconnections = {}; + } + this._pendingDisconnections[outletName] = true; + Ember.run.once(this, '_finishDisconnections'); + }, + + _finishDisconnections: function() { var outlets = get(this, '_outlets'); + var pendingDisconnections = this._pendingDisconnections; + this._pendingDisconnections = null; - set(outlets, outletName, null); + for (var outletName in pendingDisconnections) { + set(outlets, outletName, null); + } } }); @@ -25132,7 +26799,7 @@ Ember.HashLocation = Ember.Object.extend({ var self = this; var guid = Ember.guidFor(this); - Ember.$(window).bind('hashchange.ember-location-'+guid, function() { + Ember.$(window).on('hashchange.ember-location-'+guid, function() { Ember.run(function() { var path = location.hash.substr(1); if (get(self, 'lastSetURL') === path) { return; } @@ -25194,7 +26861,6 @@ Ember.HistoryLocation = Ember.Object.extend({ init: function() { set(this, 'location', get(this, 'location') || window.location); - this._initialUrl = this.getURL(); this.initState(); }, @@ -25206,8 +26872,8 @@ Ember.HistoryLocation = Ember.Object.extend({ @method initState */ initState: function() { + set(this, 'history', get(this, 'history') || window.history); this.replaceState(this.formatURL(this.getURL())); - set(this, 'history', window.history); }, /** @@ -25288,7 +26954,9 @@ Ember.HistoryLocation = Ember.Object.extend({ @param path {String} */ pushState: function(path) { - window.history.pushState({ path: path }, null, path); + get(this, 'history').pushState({ path: path }, null, path); + // used for webkit workaround + this._previousURL = this.getURL(); }, /** @@ -25300,7 +26968,9 @@ Ember.HistoryLocation = Ember.Object.extend({ @param path {String} */ replaceState: function(path) { - window.history.replaceState({ path: path }, null, path); + get(this, 'history').replaceState({ path: path }, null, path); + // used for webkit workaround + this._previousURL = this.getURL(); }, /** @@ -25316,11 +26986,11 @@ Ember.HistoryLocation = Ember.Object.extend({ var guid = Ember.guidFor(this), self = this; - Ember.$(window).bind('popstate.ember-location-'+guid, function(e) { + Ember.$(window).on('popstate.ember-location-'+guid, function(e) { // Ignore initial page load popstate event in Chrome if(!popstateFired) { popstateFired = true; - if (self.getURL() === self._initialUrl) { return; } + if (self.getURL() === self._previousURL) { return; } } callback(self.getURL()); }); @@ -25702,10 +27372,27 @@ Ember.DefaultResolver = Ember.Object.extend({ @submodule ember-application */ -var get = Ember.get, set = Ember.set, - classify = Ember.String.classify, - capitalize = Ember.String.capitalize, - decamelize = Ember.String.decamelize; +var get = Ember.get, set = Ember.set; + +function DeprecatedContainer(container) { + this._container = container; +} + +DeprecatedContainer.deprecate = function(method) { + return function() { + var container = this._container; + + Ember.deprecate('Using the defaultContainer is no longer supported. [defaultContainer#' + method + '] see: http://git.io/EKPpnA', false); + return container[method].apply(container, arguments); + }; +}; + +DeprecatedContainer.prototype = { + _container: null, + lookup: DeprecatedContainer.deprecate('lookup'), + resolve: DeprecatedContainer.deprecate('resolve'), + register: DeprecatedContainer.deprecate('register') +}; /** An instance of `Ember.Application` is the starting point for every Ember @@ -25834,17 +27521,6 @@ var get = Ember.get, set = Ember.set, If there is any setup required before routing begins, you can implement a `ready()` method on your app that will be invoked immediately before routing begins. - - To begin routing, you must have at a minimum a top-level controller and view. - You define these as `App.ApplicationController` and `App.ApplicationView`, - respectively. Your application will not work if you do not define these two - mandatory classes. For example: - - ```javascript - App.ApplicationView = Ember.View.extend({ - templateName: 'application' - }); - App.ApplicationController = Ember.Controller.extend(); ``` @class Application @@ -25966,10 +27642,10 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin This allows application developers to do: ```javascript - App = Ember.Application.create(); + var App = Ember.Application.create(); - App.Router.map(function(match) { - match("/").to("index"); + App.Router.map(function() { + this.resource('posts'); }); ``` @@ -26144,14 +27820,93 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin return this; }, - reset: function() { - get(this, '__container__').destroy(); - this.buildContainer(); + /** + Reset the application. This is typically used only in tests. It cleans up + the application in the following order: + + 1. Deactivate existing routes + 2. Destroy all objects in the container + 3. Create a new application container + 4. Re-route to the existing url - Ember.run.schedule('actions', this, function(){ - this._initialize(); - this.startRouting(); + Typical Example: + + ```javascript + + var App; + + Ember.run(function(){ + App = Ember.Application.create(); }); + + module("acceptance test", { + setup: function() { + App.reset(); + } + }); + + test("first test", function(){ + // App is freshly reset + }); + + test("first test", function(){ + // App is again freshly reset + }); + ``` + + Advanced Example: + + Occasionally you may want to prevent the app from initializing during + setup. This could enable extra configuration, or enable asserting prior + to the app becoming ready. + + ```javascript + + var App; + + Ember.run(function(){ + App = Ember.Application.create(); + }); + + module("acceptance test", { + setup: function() { + Ember.run(function() { + App.reset(); + App.deferReadiness(); + }); + } + }); + + test("first test", function(){ + ok(true, 'something before app is initialized'); + + Ember.run(function(){ + App.advanceReadiness(); + }); + ok(true, 'something after app is initialized'); + }); + ``` + + @method reset + **/ + reset: function() { + this._readinessDeferrals = 1; + + function handleReset() { + var router = this.__container__.lookup('router:main'); + router.reset(); + + Ember.run(this.__container__, 'destroy'); + + this.buildContainer(); + + Ember.run.schedule('actions', this, function(){ + this._initialize(); + this.startRouting(); + }); + } + + Ember.run.join(this, handleReset); }, /** @@ -26205,27 +27960,12 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin @method setupEventDispatcher */ setupEventDispatcher: function() { - var eventDispatcher = this.createEventDispatcher(), - customEvents = get(this, 'customEvents'); + var customEvents = get(this, 'customEvents'), + rootElement = get(this, 'rootElement'), + dispatcher = this.__container__.lookup('event_dispatcher:main'); - eventDispatcher.setup(customEvents); - }, - - /** - @private - - Create an event dispatcher for the application's `rootElement`. - - @method createEventDispatcher - */ - createEventDispatcher: function() { - var rootElement = get(this, 'rootElement'), - eventDispatcher = Ember.EventDispatcher.create({ - rootElement: rootElement - }); - - set(this, 'eventDispatcher', eventDispatcher); - return eventDispatcher; + set(this, 'eventDispatcher', dispatcher); + dispatcher.setup(customEvents, rootElement); }, /** @@ -26268,10 +28008,7 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin willDestroy: function() { Ember.BOOTED = false; - var eventDispatcher = get(this, 'eventDispatcher'); - if (eventDispatcher) { eventDispatcher.destroy(); } - - get(this, '__container__').destroy(); + this.__container__.destroy(); }, initializer: function(options) { @@ -26320,7 +28057,8 @@ Ember.Application.reopenClass({ */ buildContainer: function(namespace) { var container = new Ember.Container(); - Ember.Container.defaultContainer = Ember.Container.defaultContainer || container; + + Ember.Container.defaultContainer = new DeprecatedContainer(container); container.set = Ember.set; container.normalize = normalize; @@ -26333,13 +28071,14 @@ Ember.Application.reopenClass({ container.register('controller:object', Ember.ObjectController, { instantiate: false }); container.register('controller:array', Ember.ArrayController, { instantiate: false }); container.register('route:basic', Ember.Route, { instantiate: false }); + container.register('event_dispatcher:main', Ember.EventDispatcher); container.injection('router:main', 'namespace', 'application:main'); - container.typeInjection('controller', 'target', 'router:main'); - container.typeInjection('controller', 'namespace', 'application:main'); + container.injection('controller', 'target', 'router:main'); + container.injection('controller', 'namespace', 'application:main'); - container.typeInjection('route', 'router', 'router:main'); + container.injection('route', 'router', 'router:main'); return container; } @@ -26358,8 +28097,9 @@ Ember.Application.reopenClass({ This allows the application to register default injections in the container that could be overridden by the normal naming convention. + @method resolverFor @param {Ember.Namespace} namespace the namespace to look for classes - @return {any} the resolved value for a given lookup + @return {*} the resolved value for a given lookup */ function resolverFor(namespace) { var resolverClass = namespace.get('resolver') || Ember.DefaultResolver; @@ -26372,10 +28112,11 @@ function resolverFor(namespace) { } function normalize(fullName) { - var split = fullName.split(':'), + var split = fullName.split(':', 2), type = split[0], name = split[1]; + Ember.assert("Tried to normalize a container name without a colon (:) in it. You probably tried to lookup a name that did not contain a type, a colon, and a name. A proper lookup name would be `view:post`.", split.length === 2); if (type !== 'template') { var result = name; @@ -26453,6 +28194,31 @@ function verifyDependencies(controller) { Ember.ControllerMixin.reopen({ concatenatedProperties: ['needs'], + + /** + An array of other controller objects available inside + instances of this controller via the `controllers` + property: + + For example, when you define a controller: + + ```javascript + App.CommentsController = Ember.ArrayController.extend({ + needs: ['post'] + }); + ``` + + The application's single instance of these other + controllers are accessible by name through the + `controllers` property: + + ```javascript + this.get('controllers.post'); // instance of App.PostController + ``` + + @property {Array} needs + @default [] + */ needs: [], init: function() { @@ -26599,8 +28365,26 @@ Ember.State = Ember.Object.extend(Ember.Evented, } } - set(this, 'pathsCache', {}); - set(this, 'pathsCacheNoContext', {}); + // pathsCaches is a nested hash of the form: + // pathsCaches[stateManagerTypeGuid][path] == transitions_hash + set(this, 'pathsCaches', {}); + }, + + setPathsCache: function(stateManager, path, transitions) { + var stateManagerTypeGuid = Ember.guidFor(stateManager.constructor), + pathsCaches = get(this, 'pathsCaches'), + pathsCacheForManager = pathsCaches[stateManagerTypeGuid] || {}; + + pathsCacheForManager[path] = transitions; + pathsCaches[stateManagerTypeGuid] = pathsCacheForManager; + }, + + getPathsCache: function(stateManager, path) { + var stateManagerTypeGuid = Ember.guidFor(stateManager.constructor), + pathsCaches = get(this, 'pathsCaches'), + pathsCacheForManager = pathsCaches[stateManagerTypeGuid] || {}; + + return pathsCacheForManager[path]; }, setupChild: function(states, name, value) { @@ -27588,7 +29372,7 @@ Ember.StateManager = Ember.State.extend({ }, contextFreeTransition: function(currentState, path) { - var cache = currentState.pathsCache[path]; + var cache = currentState.getPathsCache(this, path); if (cache) { return cache; } var enterStates = this.getStatesInPath(currentState, path), @@ -27674,12 +29458,14 @@ Ember.StateManager = Ember.State.extend({ // Cache the enterStates, exitStates, and resolveState for the // current state and the `path`. - var transitions = currentState.pathsCache[path] = { + var transitions = { exitStates: exitStates, enterStates: enterStates, resolveState: resolveState }; + currentState.setPathsCache(this, path, transitions); + return transitions; }, @@ -27739,13 +29525,422 @@ Ember States })(); +(function() { +var slice = [].slice, + helpers = {}, + originalMethods = {}, + injectHelpersCallbacks = []; + +/** + @class Test + @namespace Ember +*/ +Ember.Test = { + + /** + @public + + `registerHelper` is used to register a + test helper that will be injected when + `App.injectTestHelpers` is called. + + The helper method will always be called + with the current Application as the first + parameter. + + For example: + ```javascript + Ember.Test.registerHelper('boot', function(app)) { + Ember.run(app, app.deferReadiness); + } + ``` + + This helper can later be called without arguments + because it will be called with `app` as the + first parameter. + + ```javascript + App = Ember.Application.create(); + App.injectTestHelpers(); + boot(); + ``` + + Whenever you register a helper that + performs async operations, + make sure you `return wait();` at the + end of the helper. + + If an async helper also needs to return a value, + pass it to the `wait` helper as a first argument: + `return wait(val);` + + @method registerHelper + @param name {String} + @param helperMethod {Function} + */ + registerHelper: function(name, helperMethod) { + helpers[name] = helperMethod; + }, + /** + @public + @method unregisterHelper + @param name {String} + */ + unregisterHelper: function(name) { + delete helpers[name]; + if (originalMethods[name]) { + window[name] = originalMethods[name]; + } + delete originalMethods[name]; + }, + + /** + @public + + Used to register callbacks to be fired + whenever `App.injectTestHelpers` is called + + The callback will receive the current application + as an argument. + + @method unregisterHelper + @param name {String} + */ + onInjectHelpers: function(callback) { + injectHelpersCallbacks.push(callback); + }, + + /** + @public + + This returns a thenable tailored + for testing. It catches failed + `onSuccess` callbacks and invokes + the `Ember.Test.adapter.exception` + callback in the last chained then. + + This method should be returned + by async helpers such as `wait`. + + @method promise + @param resolver {Function} + */ + promise: function(resolver) { + var promise = new Ember.RSVP.Promise(resolver); + var thenable = { + chained: false + }; + thenable.then = function(onSuccess, onFailure) { + var self = this, thenPromise, nextPromise; + thenable.chained = true; + thenPromise = promise.then(onSuccess, onFailure); + // this is to ensure all downstream fulfillment + // handlers are wrapped in the error handling + nextPromise = Ember.Test.promise(function(resolve) { + resolve(thenPromise); + }); + thenPromise.then(null, function(reason) { + // ensure this is the last promise in the chain + // if not, ignore and the exception will propagate + // this prevents the same error from being fired multiple times + if (!nextPromise.chained) { + Ember.Test.adapter.exception(reason); + } + }); + return nextPromise; + }; + return thenable; + }, + + /** + @public + + Used to allow ember-testing + to communicate with a specific + testing framework. + + You can manually set it before calling + `App.setupForTesting()`. + + Example: + 'Ember.Test.adapter = MyCustomAdapter.create()' + + If you do not set it, ember-testing + will default to `Ember.Test.QUnitAdapter`. + */ + adapter: null +}; + +function curry(app, fn) { + return function() { + var args = slice.call(arguments); + args.unshift(app); + return fn.apply(app, args); + }; +} + +Ember.Application.reopen({ + testHelpers: {}, + + setupForTesting: function() { + this.deferReadiness(); + + this.Router.reopen({ + location: 'none' + }); + + // if adapter is not manually set + // default to QUnit + if (!Ember.Test.adapter) { + Ember.Test.adapter = Ember.Test.QUnitAdapter.create(); + } + }, + + injectTestHelpers: function() { + this.testHelpers = {}; + for (var name in helpers) { + originalMethods[name] = window[name]; + this.testHelpers[name] = window[name] = curry(this, helpers[name]); + } + + for(var i = 0, l = injectHelpersCallbacks.length; i < l; i++) { + injectHelpersCallbacks[i](this); + } + }, + + removeTestHelpers: function() { + for (var name in helpers) { + window[name] = originalMethods[name]; + delete this.testHelpers[name]; + delete originalMethods[name]; + } + } +}); + +})(); + + + +(function() { +var Test = Ember.Test; + +/** + @class Adapter + @namespace Ember.Test +*/ +Test.Adapter = Ember.Object.extend({ + /** + @public + + This callback will be called + whenever an async operation + is about to start. + + Override this to call your + framework's methods + that handle async operations + + @method asyncStart + */ + asyncStart: Ember.K, + + /** + @public + + This callback will be called + whenever an async operation + has completed. + + @method asyncEnd + */ + asyncEnd: Ember.K, + + /** + @public + + Override this method with your + testing framework's false assertion + This function is called whenever + an exception occurs causing the testing + promise to fail. + + QUnit example: + + ```javascript + exception: function(error) { + ok(false, error); + } + ``` + + @method exception + @param reason {String} + */ + exception: function(error) { + setTimeout(function() { + throw error; + }); + } +}); + +/** + @class QUnitAdapter + @namespace Ember.Test +*/ +Test.QUnitAdapter = Test.Adapter.extend({ + asyncStart: function() { + stop(); + }, + asyncEnd: function() { + start(); + }, + exception: function(error) { + ok(false, error); + } +}); + +})(); + + + +(function() { +var get = Ember.get, + helper = Ember.Test.registerHelper, + pendingAjaxRequests = 0, + countAsync = 0; + + +Ember.Test.onInjectHelpers(function() { + Ember.$(document).ajaxStart(function() { + pendingAjaxRequests++; + }); + + Ember.$(document).ajaxStop(function() { + pendingAjaxRequests--; + }); +}); + + +function visit(app, url) { + Ember.run(app, app.handleURL, url); + app.__container__.lookup('router:main').location.setURL(url); + return wait(app); +} + +function click(app, selector, context) { + var $el = find(app, selector, context); + Ember.run(function() { + $el.click(); + }); + return wait(app); +} + +function fillIn(app, selector, context, text) { + var $el; + if (typeof text === 'undefined') { + text = context; + context = null; + } + $el = find(app, selector, context); + Ember.run(function() { + $el.val(text).change(); + }); + return wait(app); +} + +function find(app, selector, context) { + var $el; + context = context || get(app, 'rootElement'); + $el = app.$(selector, context); + if ($el.length === 0) { + throw("Element " + selector + " not found."); + } + return $el; +} + +function wait(app, value) { + var promise, obj = {}, helperName; + + promise = Ember.Test.promise(function(resolve) { + if (++countAsync === 1) { + Ember.Test.adapter.asyncStart(); + } + var watcher = setInterval(function() { + var routerIsLoading = app.__container__.lookup('router:main').router.isLoading; + if (routerIsLoading) { return; } + if (pendingAjaxRequests) { return; } + if (Ember.run.hasScheduledTimers() || Ember.run.currentRunLoop) { return; } + clearInterval(watcher); + if (--countAsync === 0) { + Ember.Test.adapter.asyncEnd(); + } + Ember.run(function() { + resolve(value); + }); + }, 10); + }); + + return buildChainObject(app, promise); +} + +/** + Builds an object that contains + all helper methods. This object will be + returned by helpers and then-promises. + + This allows us to chain helpers: + + ```javascript + visit('posts/new') + .click('.add-btn') + .fillIn('.title', 'Post') + .click('.submit') + .then(function() { + equal('.post-title', 'Post'); + }) + .visit('comments') + .then(function() { + equal(find('.comments'),length, 0); + }); + ``` +*/ +function buildChainObject(app, promise) { + var helperName, obj = {}; + for(helperName in app.testHelpers) { + obj[helperName] = chain(app, promise, app.testHelpers[helperName]); + } + obj.then = function(fn) { + var thenPromise = promise.then(fn); + return buildChainObject(app, thenPromise); + }; + return obj; +} + +function chain(app, promise, fn) { + return function() { + var args = arguments, chainedPromise; + chainedPromise = promise.then(function() { + return fn.apply(null, args); + }); + return buildChainObject(app, chainedPromise); + }; +} + +// expose these methods as test helpers +helper('visit', visit); +helper('click', click); +helper('fillIn', fillIn); +helper('find', find); +helper('wait', wait); })(); -// Version: v1.0.0-rc.2 -// Last commit: 656fa6e (2013-03-29 13:40:38 -0700) + (function() { + +})(); + +(function() { /** Ember @@ -27754,3 +29949,5 @@ Ember })(); + +})(); |