dist/ember.prod.js in ember-source-1.0.0.rc2.3 vs dist/ember.prod.js in ember-source-1.0.0.rc3

- old
+ new

@@ -53,11 +53,11 @@ The core Runtime framework is based on the jQuery API with a number of performance optimizations. @class Ember @static - @version 1.0.0-rc.2 + @version 1.0.0-rc.3 */ if ('undefined' === typeof Ember) { // Create core object. Make it act like an instance of Ember.Namespace so that // objects assigned to it are given a sane string representation. @@ -80,14 +80,14 @@ /** @property VERSION @type String - @default '1.0.0-rc.2' + @default '1.0.0-rc.3' @final */ -Ember.VERSION = '1.0.0-rc.2'; +Ember.VERSION = '1.0.0-rc.3'; /** Standard environmental variables. You can define these in a global `ENV` variable before loading Ember to control various configuration settings. @@ -476,15 +476,112 @@ })(); (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 +*/ + + var o_defineProperty = Ember.platform.defineProperty, o_create = Ember.create, // Used for guid generation... GUID_KEY = '__ember'+ (+ new Date()), uuid = 0, @@ -877,11 +974,11 @@ @method tryInvoke @for Ember @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)) { return obj[methodName].apply(obj, args || []); } @@ -908,11 +1005,11 @@ @method tryFinally @for Ember @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 */ if (needsFinallyFix) { @@ -959,11 +1056,11 @@ @for Ember @param {Function} tryable The function to run the try callback @param {Function} catchable The function to run the catchable callback @param {Function} finalizer The function to run the finally callback @param [binding] - @return {anything} The return value is the that of the finalizer, + @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) { @@ -1003,10 +1100,82 @@ return (finalResult === undefined) ? result : finalResult; }; } +// ........................................ +// 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; +}; })(); (function() { @@ -1183,21 +1352,27 @@ })(); (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) { return elements === undefined ? [] : utils.map(elements, function(item) { return utils.indexOf(obj, item); @@ -1216,20 +1391,20 @@ replace: function(array, idx, amt, objects) { 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); } }, 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); } }); return intersection; @@ -1239,111 +1414,14 @@ })(); (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 -*/ - /* JavaScript (before ES6) does not have a Map implementation. Objects, which are often used as dictionaries, may only have Strings as keys. Because Ember has a way to get a unique identifier for every object @@ -1474,11 +1552,11 @@ @param {Function} fn @param self */ 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]); } }, @@ -1497,11 +1575,11 @@ */ copy: function() { var set = new OrderedSet(); set.presenceSet = copy(this.presenceSet); - set.list = this.list.slice(); + set.list = this.toArray(); return set; } }; @@ -1541,12 +1619,12 @@ Map.prototype = { /** 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, guid = guidFor(key); @@ -1556,12 +1634,12 @@ /** Adds a value to the map. If a value for the given key has already been 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, values = this.values, guid = guidFor(key); @@ -1572,24 +1650,22 @@ /** 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) { // don't use ES6 "delete" because it will be annoying // 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]; return true; } else { return false; } @@ -1597,11 +1673,11 @@ /** 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) { var values = this.values, guid = guidFor(key); @@ -1615,11 +1691,11 @@ The keys are guaranteed to be iterated over in insertion order. @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) { var keys = this.keys, values = this.values; @@ -1644,22 +1720,22 @@ @namespace Ember @extends Ember.Map @private @constructor @param [options] - @param {anything} [options.defaultValue] + @param {*} [options.defaultValue] */ var MapWithDefault = Ember.MapWithDefault = function(options) { Map.call(this); this.defaultValue = options.defaultValue; }; /** @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` */ MapWithDefault.create = function(options) { if (options) { @@ -1673,12 +1749,12 @@ /** 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); if (hasValue) { @@ -1707,15 +1783,14 @@ (function() { /** @module ember-metal */ -var META_KEY = Ember.META_KEY, get, set; +var META_KEY = Ember.META_KEY, get; 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 = /^([^\.\*]+)/; // .......................................................... @@ -1783,11 +1858,774 @@ return ret; } }; +// Currently used only by Ember Data tests +if (Ember.config.overrideAccessors) { + Ember.get = get; + Ember.config.overrideAccessors(); + get = Ember.get; +} + +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 ]; +} + +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; + } + + 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; +}; + /** + @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; + +/* + 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] = []; + } + + 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], + once = actions[i][2], + suspended = actions[i][3], + actionIndex = indexOf(otherActions, target, method); + + if (actionIndex === -1) { + otherActions.push([target, method, once, suspended]); + } + } +} + +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]); + } + + 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) { + + + if (!method && 'function' === typeof target) { + method = target; + target = null; + } + + var actions = actionsFor(obj, eventName), + actionIndex = indexOf(actions, target, method); + + if (actionIndex !== -1) { return; } + + actions.push([target, method, once, undefined]); + + if ('function' === typeof obj.didAddListener) { + obj.didAddListener(eventName, target, method); + } +} + +/** + Remove an event listener + + Arguments should match those passed to {{#crossLink "Ember/addListener"}}{{/crossLink}} + + @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) { + + + if (!method && 'function' === typeof target) { + method = target; + target = null; + } + + 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); + } + } + + 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[3] = true; // 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[3] = undefined; } } + + 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[3] = true; + 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][3] = undefined; + } + } + + 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 + if (!actions[i] || actions[i][3] === true) { continue; } + + var target = actions[i][0], + method = actions[i][1], + once = actions[i][2]; + + 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; +} + +/** + @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, onceFlag, suspendedFlag] + ] + }, + ... + ] +*/ +var ObserverSet = Ember._ObserverSet = function() { + this.clear(); +}; + +ObserverSet.prototype.add = function(sender, keyName, eventName) { + var observerSet = this.observerSet, + observers = this.observers, + senderGuid = 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; } + sendEvent(sender, observer.eventName, [sender, observer.keyName], observer.listeners); + } +}; + +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 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); + } +}; + +Ember.overrideChains = function(obj, keyName, m) { + chainsDidChange(obj, keyName, m, true); +}; + +/** + @method beginPropertyChanges + @chainable +*/ +var beginPropertyChanges = Ember.beginPropertyChanges = function() { + deferred++; +}; + +/** + @method endPropertyChanges +*/ +var 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 changePropertiess + @param {Function} callback + @param [binding] +*/ +var changeProperties = 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]); + } +}; + +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. @@ -1806,11 +2644,11 @@ @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) { +var set = function set(obj, keyName, value, tolerant) { if (typeof obj === 'string') { value = keyName; keyName = obj; obj = null; @@ -1861,69 +2699,15 @@ 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); @@ -1947,39 +2731,10 @@ } 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 @@ -1997,25 +2752,10 @@ 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() { @@ -2032,11 +2772,11 @@ // .......................................................... // DESCRIPTOR // /** - 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. @class Descriptor @@ -2102,11 +2842,11 @@ @param {Object} obj the object to define this property on. This may be a prototype. @param {String} keyName the name of the property @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) { var descs, existingDesc, watching, value; @@ -2173,344 +2913,135 @@ })(); (function() { -// Ember.tryFinally -/** -@module ember-metal -*/ +var changeProperties = Ember.changeProperties, + set = Ember.set; -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); -}; - -/** Set a list of properties on an object. These properties are set inside a single `beginPropertyChanges` and `endPropertyChanges` batch, so observers will be buffered. @method setProperties @param target @param {Hash} properties @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; -} +(function() { +var metaFor = Ember.meta, // utils.js + typeOf = Ember.typeOf, // utils.js + MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, + o_defineProperty = Ember.platform.defineProperty; -/** - @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.watchKey = function(obj, keyName) { + // can't watch length on Array - it is special... + if (keyName === 'length' && typeOf(obj) === 'array') { return; } -Ember.observersFor = function(obj, path) { - return Ember.listenersFor(obj, changeEvent(path)); -}; + var m = metaFor(obj), watching = m.watching, desc; -/** - @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; -}; + // activate watching first time + if (!watching[keyName]) { + watching[keyName] = 1; + desc = m.descs[keyName]; + if (desc && desc.willWatch) { desc.willWatch(obj, keyName); } -/** - @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; -}; + if ('function' === typeof obj.willWatchProperty) { + obj.willWatchProperty(keyName); + } -// 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 (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._suspendObserver = function(obj, path, target, method, callback) { - return Ember._suspendListener(obj, changeEvent(path), target, method, callback); -}; -var map = Ember.ArrayPolyfills.map; +Ember.unwatchKey = function(obj, keyName) { + var m = metaFor(obj), watching = m.watching, desc; -Ember._suspendBeforeObservers = function(obj, paths, target, method, callback) { - var events = map.call(paths, beforeEvent); - return Ember._suspendListeners(obj, events, target, method, callback); -}; + if (watching[keyName] === 1) { + watching[keyName] = 0; + desc = m.descs[keyName]; -Ember._suspendObservers = function(obj, paths, target, method, callback) { - var events = map.call(paths, changeEvent); - return Ember._suspendListeners(obj, events, target, method, callback); -}; + if (desc && desc.didUnwatch) { desc.didUnwatch(obj, keyName); } -Ember.beforeObserversFor = function(obj, path) { - return Ember.listenersFor(obj, beforeEvent(path)); -}; + if ('function' === typeof obj.didUnwatchProperty) { + obj.didUnwatchProperty(keyName); + } -/** - @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.notifyBeforeObservers = function(obj, keyName) { - if (obj.isDestroying) { return; } - - 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 (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]--; } }; - -Ember.notifyObservers = function(obj, keyName) { - if (obj.isDestroying) { return; } - - 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]); - } -}; - })(); (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 = /[\.\*]/; + warn = Ember.warn, + watchKey = Ember.watchKey, + unwatchKey = Ember.unwatchKey, + propertyWillChange = Ember.propertyWillChange, + propertyDidChange = Ember.propertyDidChange, + FIRST_KEY = /^([^\.\*]+)/; -var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, -o_defineProperty = Ember.platform.defineProperty; - function firstKey(path) { return path.match(FIRST_KEY)[0]; } -// returns true if the passed path is just a keyName -function isKeyName(path) { - return path==='*' || !IS_PATH.test(path); -} +var pendingQueue = []; -// .......................................................... -// DEPENDENT KEYS -// +// 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 -function iterDeps(method, obj, depKey, seen, meta) { + var queue = pendingQueue; + pendingQueue = []; - var guid = guidFor(obj); - if (!seen[guid]) seen[guid] = {}; - if (seen[guid][depKey]) return; - seen[guid][depKey] = true; + forEach.call(queue, function(q) { q[0].add(q[1]); }); - 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); - } - } -} + 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); +}; -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; } -} - -// .......................................................... -// CHAIN -// - function addChainWatcher(obj, keyName, node) { if (!obj || ('object' !== typeof obj)) { return; } // nothing to do var m = metaFor(obj), nodes = m.chainWatchers; @@ -2518,14 +3049,14 @@ nodes = m.chainWatchers = {}; } 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); if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do @@ -2535,37 +3066,21 @@ nodes = nodes[keyName]; for (var i = 0, l = nodes.length; i < l; i++) { if (nodes[i] === node) { nodes.splice(i, 1); } } } - Ember.unwatch(obj, keyName); -} + unwatchKey(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]); }); - -} - function isProto(pvalue) { return metaFor(pvalue, false).proto === 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; // _watching is true when calling get(this._parent, this._key) will // return the value of this node. @@ -2733,24 +3248,24 @@ if (this._key) { path = this._key + '.' + path; } 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); } } }; ChainNodePrototype.chainDidChange = function(chain, path, depth) { if (this._key) { path = this._key + '.' + path; } 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); } } }; ChainNodePrototype.didChange = function(suppressEvent) { // invalidate my own value first. @@ -2782,10 +3297,28 @@ // and finally tell parent about my path changing... 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. function chainsFor(obj) { var m = metaFor(obj), ret = m.chains; @@ -2795,45 +3328,60 @@ ret = m.chains = ret.copy(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, desc; - 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 = /[\.\*]/; + +// returns true if the passed path is just a keyName +function isKeyName(path) { + return path==='*' || !IS_PATH.test(path); } -// .......................................................... -// WATCH -// - /** @private Starts watching a property on an object. Whenever the property changes, invokes `Ember.propertyWillChange` and `Ember.propertyDidChange`. This is the @@ -2844,88 +3392,37 @@ @method watch @for Ember @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; } + if (keyPath === 'length' && typeOf(obj) === 'array') { return; } - 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); - } - - } 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) { var meta = obj[META_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; } + if (keyPath === 'length' && typeOf(obj) === 'array') { return; } - 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 (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; }; /** @private @@ -2940,100 +3437,19 @@ Ember.rewatch = function(obj) { var m = metaFor(obj, false), chains = m.chains; // 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 = []; /** Tears down the meta on an object so that it can be garbage collected. Multiple calls will have no effect. @@ -3108,11 +3524,11 @@ /* 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 // create a new empty list of dependencies for the key keys = depsMeta[depKey] = {}; @@ -3122,26 +3538,26 @@ keys = depsMeta[depKey] = o_create(keys); } return keys; } -function metaForDeps(obj, meta) { - return keysForDep(obj, meta, 'deps'); +function metaForDeps(meta) { + return keysForDep(meta, 'deps'); } function addDependentKeys(desc, obj, keyName, meta) { // the descriptor has a list of dependent keys, so // add all of its dependent keys. 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 watch(obj, depKey); } @@ -3151,16 +3567,16 @@ // the descriptor has a list of dependent keys, so // add all of its dependent keys. 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 unwatch(obj, depKey); } @@ -3187,29 +3603,33 @@ Ember.ComputedProperty = ComputedProperty; 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. +/* + Call on a computed property to explicitly change it's cacheable mode. + Please use `.volatile` over this method. + ```javascript MyApp.president = Ember.Object.create({ fullName: function() { return this.get('firstName') + ' ' + this.get('lastName'); - // After calculating the value of this function, Ember will - // return that value without re-executing this function until - // one of the dependent properties change. + // By default, Ember will return the value of this property + // without re-executing this function. }.property('firstName', 'lastName') + + initials: function() { + return this.get('firstName')[0] + this.get('lastName')[0]; + + // This function will be executed every time this property + // is requested. + }.property('firstName', 'lastName').cacheable(false) }); ``` - Properties are cacheable by default. - @method cacheable @param {Boolean} aFlag optional set to `false` to disable caching @return {Ember.ComputedProperty} this @chainable */ @@ -3506,11 +3926,11 @@ @method cacheFor @for Ember @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; if (cache && key in cache) { @@ -3769,11 +4189,10 @@ @return {Ember.ComputedProperty} computed property which acts like a standard getter and setter, but defaults to the value from `defaultPath`. */ Ember.computed.defaultTo = function(defaultPath) { return Ember.computed(function(key, newValue, cachedValue) { - var result; if (arguments.length === 1) { return cachedValue != null ? cachedValue : get(this, defaultPath); } return newValue != null ? newValue : get(this, defaultPath); }); @@ -3782,385 +4201,111 @@ })(); (function() { +// Ember.tryFinally /** @module ember-metal */ -var o_create = Ember.create, - metaFor = Ember.meta, - META_KEY = Ember.META_KEY; +var AFTER_OBSERVERS = ':change'; +var BEFORE_OBSERVERS = ':before'; -/* - 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. +var guidFor = Ember.guidFor; - 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 changeEvent(keyName) { + return keyName+AFTER_OBSERVERS; } -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 beforeEvent(keyName) { + return keyName+BEFORE_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 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]); - } - - return diffActions; -} - /** - Add an event listener - - @method addListener - @for Ember + @method addObserver @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.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)); +}; - if (!method && 'function' === typeof target) { - method = target; - target = null; - } - - var actions = actionsFor(obj, eventName), - actionIndex = indexOf(actions, target, method); - - if (actionIndex !== -1) { return; } - - actions.push([target, method, once, undefined]); - - if ('function' === typeof obj.didAddListener) { - obj.didAddListener(eventName, target, method); - } -} - /** - Remove an event listener - - Arguments should match those passed to {{#crossLink "Ember/addListener"}}{{/crossLink}} - - @method removeListener - @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 {String} path + @param {Object|Function} targetOrMethod + @param {Function|String} [method] */ -function removeListener(obj, eventName, target, method) { +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; - } - - 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); - } - } - - 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 + @method addBeforeObserver @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 + @param {String} path + @param {Object|Function} targetOrMethod + @param {Function|String} [method] */ -function suspendListener(obj, eventName, target, method, callback) { - if (!method && 'function' === typeof target) { - method = target; - target = null; - } +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), - action; +// 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) { - 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 - } +Ember._suspendObserver = function(obj, path, target, method, callback) { + return Ember._suspendListener(obj, changeEvent(path), target, method, callback); +}; - function tryable() { return callback.call(target); } - function finalizer() { if (action) { action[3] = undefined; } } +var map = Ember.ArrayPolyfills.map; - return Ember.tryFinally(tryable, finalizer); -} +Ember._suspendBeforeObservers = function(obj, paths, target, method, callback) { + var events = map.call(paths, beforeEvent); + return Ember._suspendListeners(obj, events, target, method, callback); +}; -/** - @private +Ember._suspendObservers = function(obj, paths, target, method, callback) { + var events = map.call(paths, changeEvent); + return Ember._suspendListeners(obj, events, target, method, callback); +}; - Suspend listener during callback. +Ember.beforeObserversFor = function(obj, path) { + return Ember.listenersFor(obj, beforeEvent(path)); +}; - 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[3] = true; - 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][3] = undefined; - } - } - - return Ember.tryFinally(tryable, finalizer); -} - /** - @private - - Return a list of currently watched events - - @method watchedEvents - @for Ember + @method removeBeforeObserver @param obj + @param {String} path + @param {Object|Function} targetOrMethod + @param {Function|String} [method] */ -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 - if (!actions[i] || actions[i][3] === true) { continue; } - - var target = actions[i][0], - method = actions[i][1], - once = actions[i][2]; - - 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; -} - -/** - @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; - +Ember.removeBeforeObserver = function(obj, path, target, method) { + Ember.unwatch(obj, path); + Ember.removeListener(obj, beforeEvent(path), target, method); + return this; +}; })(); (function() { @@ -4203,12 +4348,10 @@ // .......................................................... // RUNLOOP // -var timerMark; // used by timers... - /** Ember RunLoop (Private) @class RunLoop @namespace Ember @@ -4335,12 +4478,10 @@ idx++; } } - timerMark = null; - return this; } }; @@ -4452,11 +4593,11 @@ started a RunLoop when calling this method one will be started for you automatically. At the end of a RunLoop, any methods scheduled in this way will be invoked. Methods will be invoked in an order matching the named queues defined in - the `run.queues` property. + the `Ember.run.queues` property. ```javascript Ember.run.schedule('sync', this, function(){ // this will be executed in the first RunLoop queue, when bindings are synced console.log("scheduled on sync queue"); @@ -4683,54 +4824,70 @@ return guid; } /** - Schedules an item to run one time during the current RunLoop. Calling - this method with the same target/method combination will have no effect. + Schedule a function to run one time during the current RunLoop. This is equivalent + to calling `scheduleOnce` with the "actions" queue. + @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) { + return scheduleOnce('actions', target, method, slice.call(arguments, 2)); +}; + +/** + 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 calls. ```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) { +Ember.run.scheduleOnce = function(queue, target, method) { return scheduleOnce(queue, target, method, slice.call(arguments, 3)); }; /** Schedules an item to run from within a separate run loop, after @@ -4828,12 +4985,13 @@ (function() { // Ember.Logger -// get, set, trySet -// guidFor, isArray, meta +// get +// set +// guidFor, meta // addObserver, removeObserver // Ember.run.schedule /** @module ember-metal */ @@ -4855,13 +5013,26 @@ 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); } // .......................................................... @@ -5148,11 +5319,11 @@ detection. 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" ``` @@ -5448,11 +5619,11 @@ descs[key] = undefined; values[key] = value; } } -function mergeMixins(mixins, m, descs, values, base) { +function mergeMixins(mixins, m, descs, values, base, keys) { var mixin, props, key, concats, meta; function removeKeys(keyName) { delete descs[keyName]; delete values[keyName]; @@ -5469,17 +5640,18 @@ meta = Ember.meta(base); concats = concatenatedProperties(props, 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); } } } } @@ -5571,22 +5743,23 @@ updateObservers(obj, key, observer, '__ember_observes__', 'addObserver'); } 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: // // * Handle concatenated properties // * 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]; if (desc === REQUIRED) { continue; } @@ -5636,11 +5809,11 @@ }, isEditing: false }); // 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}}') }); commentView = App.CommentView.create(); @@ -5655,10 +5828,16 @@ */ 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) { var args = a_slice.call(arguments, 1); return applyMixin(obj, args, true); @@ -5879,11 +6058,11 @@ */ 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. ```javascript @@ -6494,11 +6673,11 @@ function instantiate(container, fullName) { 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) { return factory; } @@ -6545,84 +6724,11 @@ @submodule ember-runtime */ 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: - -1 if the first is smaller than the second, - 0 if both are equal, @@ -6975,11 +7081,12 @@ "Hello %@ %@".fmt('John', 'Doe'); // "Hello John Doe" "Hello %@2, %@1".fmt('John', 'Doe'); // "Hello Doe, John" ``` @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) { // first, replace any ORDERED replacements. var idx = 0; // the current index for non-numerical replacements @@ -8378,11 +8485,11 @@ arr.objectAt(5); // undefined ``` @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 ; return get(this, idx); }, @@ -8952,20 +9059,20 @@ ## Adding Objects 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); ``` ## 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 set.removeObject(contact); ``` @@ -8988,11 +9095,11 @@ Attempts to add the passed object to the receiver if the object is not already present in the collection. If the object is present, this method has no effect. - 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 @param {Object} object The object to add to the enumerable. @return {Object} the passed object @@ -9015,25 +9122,25 @@ /** __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 @param {Object} object The object to remove from the enumerable. @return {Object} the passed object */ removeObject: Ember.required(Function), /** - 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 @return {Object} receiver */ @@ -9187,12 +9294,12 @@ colors.pushObject("black"); // ["red", "green", "blue", "black"] colors.pushObject(["yellow", "orange"]); // ["red", "green", "blue", "black", ["yellow", "orange"]] ``` @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) ; return obj ; }, @@ -9267,12 +9374,12 @@ colors.unshiftObject("yellow"); // ["yellow", "red", "green", "blue"] colors.unshiftObject(["black", "white"]); // [["black", "white"], "yellow", "red", "green", "blue"] ``` @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) ; return obj ; }, @@ -9885,17 +9992,27 @@ */ 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 */ Ember.TargetActionSupport = Ember.Mixin.create({ target: null, action: null, + actionContext: null, targetObject: Ember.computed(function() { var target = get(this, 'target'); if (Ember.typeOf(target) === "string") { @@ -9905,25 +10022,90 @@ } else { return target; } }).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); + + ret = target[action].apply(target, [actionContext]); } + if (ret !== false) ret = true; return ret; } else { return false; @@ -10229,10 +10411,12 @@ var concatenatedProperties = this.concatenatedProperties; for (var i = 0, l = props.length; i < l; i++) { var properties = props[i]; + + for (var keyName in properties) { if (!properties.hasOwnProperty(keyName)) { continue; } var value = properties[keyName], IS_BINDING = Ember.IS_BINDING; @@ -10349,12 +10533,10 @@ `Ember.ArrayController`, be sure to call `this._super()` in your `init` declaration! If you don't, Ember may not have an opportunity to do important setup work, and you'll see strange behavior in your application. - ``` - @method init */ init: function() {}, /** @@ -10461,28 +10643,28 @@ if (this._didCallDestroy) { return; } this.isDestroying = true; this._didCallDestroy = true; - if (this.willDestroy) { this.willDestroy(); } - schedule('destroy', this, this._scheduledDestroy); return this; }, + willDestroy: Ember.K, + /** @private Invoked by the run loop to actually destroy the object. This is scheduled for execution by the `destroy` method. @method _scheduledDestroy */ _scheduledDestroy: function() { + if (this.willDestroy) { this.willDestroy(); } destroy(this); - set(this, 'isDestroyed', true); - + this.isDestroyed = true; if (this.didDestroy) { this.didDestroy(); } }, bind: function(to, from) { if (!(from instanceof Ember.Binding)) { from = Ember.Binding.from(from); } @@ -10825,11 +11007,11 @@ if (Namespace.PROCESSED) { return; } 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 if (prop === "globalStorage" && lookup.StorageList && lookup.globalStorage instanceof lookup.StorageList) { continue; } // Unfortunately, some versions of IE don't support window.hasOwnProperty @@ -11511,11 +11693,11 @@ You can directly access mapped properties by simply requesting them. The `unknownProperty` handler will generate an EachArray of each item. @method unknownProperty @param keyName {String} - @param value {anything} + @param value {*} */ unknownProperty: function(keyName, value) { var ret; ret = new EachArray(this._content, keyName, this); Ember.defineProperty(this, keyName, null, ret); @@ -11735,13 +11917,12 @@ @class NativeArray @namespace Ember @extends Ember.Mixin @uses Ember.MutableArray - @uses Ember.MutableEnumerable + @uses Ember.Observable @uses Ember.Copyable - @uses Ember.Freezable */ Ember.NativeArray = NativeArray; /** Creates an `Ember.NativeArray` from an Array like object. @@ -12747,13 +12928,13 @@ } } }); ``` - @method - @type String - @default null + @method lookupItemController + @param {Object} object + @return {String} */ lookupItemController: function(object) { return get(this, 'itemController'); }, @@ -12798,12 +12979,12 @@ // calling `objectAt` this._super(idx, removedCnt, addedCnt); }, init: function() { - this._super(); if (!this.get('content')) { Ember.defineProperty(this, 'content', undefined, Ember.A()); } + this._super(); this.set('_subControllers', Ember.A()); }, controllerAt: function(idx, object, controllerClass) { var container = get(this, 'container'), @@ -12827,15 +13008,16 @@ _subControllers: null, _resetSubControllers: function() { var subControllers = get(this, '_subControllers'); + if (subControllers) { + forEach(subControllers, function(subController) { + if (subController) { subController.destroy(); } + }); + } - forEach(subControllers, function(subController) { - if (subController) { subController.destroy(); } - }); - this.set('_subControllers', Ember.A()); } }); })(); @@ -13092,49 +13274,10 @@ toDOM: function() { return this.list.join(" "); } }; -var BAD_TAG_NAME_TEST_REGEXP = /[^a-zA-Z\-]/; -var BAD_TAG_NAME_REPLACE_REGEXP = /[^a-zA-Z\-]/g; - -function stripTagName(tagName) { - if (!tagName) { - return tagName; - } - - if (!BAD_TAG_NAME_TEST_REGEXP.test(tagName)) { - return tagName; - } - - return tagName.replace(BAD_TAG_NAME_REPLACE_REGEXP, ''); -} - -var BAD_CHARS_REGEXP = /&(?!\w+;)|[<>"'`]/g; -var POSSIBLE_CHARS_REGEXP = /[&<>"'`]/; - -function escapeAttribute(value) { - // Stolen shamelessly from Handlebars - - var escape = { - "<": "&lt;", - ">": "&gt;", - '"': "&quot;", - "'": "&#x27;", - "`": "&#x60;" - }; - - var escapeChar = function(chr) { - return escape[chr] || "&amp;"; - }; - - var string = value.toString(); - - if(!POSSIBLE_CHARS_REGEXP.test(string)) { return string; } - return string.replace(BAD_CHARS_REGEXP, escapeChar); -} - /** `Ember.RenderBuffer` gathers information regarding the a view and generates the final representation. `Ember.RenderBuffer` will generate HTML which can be pushed to the DOM. @@ -13146,19 +13289,21 @@ return new Ember._RenderBuffer(tagName); }; Ember._RenderBuffer = function(tagName) { this.tagNames = [tagName || null]; - this.buffer = []; + this.buffer = ""; }; Ember._RenderBuffer.prototype = /** @scope Ember.RenderBuffer.prototype */ { // The root view's element _element: null, + _hasElement: true, + /** @private An internal set used to de-dupe class names when `addClass()` is used. After each call to `addClass()`, the `classes` property @@ -13270,11 +13415,11 @@ @method push @param {String} string HTML to push into the buffer @chainable */ push: function(string) { - this.buffer.push(string); + this.buffer += string; return this; }, /** Adds a class to the buffer, which will be rendered to the class attribute. @@ -13403,11 +13548,11 @@ pushOpeningTag: function() { 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; } var buffer = this.buffer, @@ -13416,39 +13561,39 @@ attrs = this.elementAttributes, props = this.elementProperties, style = this.elementStyle, attr, prop; - buffer.push('<' + stripTagName(tagName)); + buffer += '<' + tagName; if (id) { - buffer.push(' id="' + escapeAttribute(id) + '"'); + buffer += ' id="' + this._escapeAttribute(id) + '"'; this.elementId = null; } if (classes) { - buffer.push(' class="' + 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 + ':' + escapeAttribute(style[prop]) + ';'); + buffer += prop + ':' + this._escapeAttribute(style[prop]) + ';'; } } - buffer.push('"'); + buffer += '"'; this.elementStyle = null; } if (attrs) { for (attr in attrs) { if (attrs.hasOwnProperty(attr)) { - buffer.push(' ' + attr + '="' + escapeAttribute(attrs[attr]) + '"'); + buffer += ' ' + attr + '="' + this._escapeAttribute(attrs[attr]) + '"'; } } this.elementAttributes = null; } @@ -13457,27 +13602,28 @@ for (prop in props) { if (props.hasOwnProperty(prop)) { var value = props[prop]; if (value || typeof(value) === 'number') { if (value === true) { - buffer.push(' ' + prop + '="' + prop + '"'); + buffer += ' ' + prop + '="' + prop + '"'; } else { - buffer.push(' ' + prop + '="' + escapeAttribute(props[prop]) + '"'); + buffer += ' ' + prop + '="' + this._escapeAttribute(props[prop]) + '"'; } } } } this.elementProperties = null; } - buffer.push('>'); + buffer += '>'; + this.buffer = buffer; }, pushClosingTag: function() { var tagName = this.tagNames.pop(); - if (tagName) { this.buffer.push('</' + stripTagName(tagName) + '>'); } + if (tagName) { this.buffer += '</' + tagName + '>'; } }, currentTagName: function() { return this.tagNames[this.tagNames.length-1]; }, @@ -13557,22 +13703,50 @@ @method string @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) { + // Stolen shamelessly from Handlebars + + var escape = { + "<": "&lt;", + ">": "&gt;", + '"': "&quot;", + "'": "&#x27;", + "`": "&#x60;" + }; + + var badChars = /&(?!\w+;)|[<>"'`]/g; + var possible = /[&<>"'`]/; + + var escapeChar = function(chr) { + return escape[chr] || "&amp;"; + }; + + var string = value.toString(); + + if(!possible.test(string)) { return string; } + return string.replace(badChars, escapeChar); } + }; })(); @@ -13902,22 +14076,10 @@ states: states, 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.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'); }, /** If the view is currently inserted into the DOM of a parent view, this @@ -13943,11 +14105,11 @@ // return the current view, not including virtual views concreteView: Ember.computed(function() { if (!this.isVirtual) { return this; } else { return get(this, 'parentView'); } - }).property('parentView').volatile(), + }).property('parentView'), instrumentName: 'core_view', instrumentDetails: function(hash) { hash.object = this.toString(); @@ -13981,12 +14143,10 @@ return this._renderToBuffer(parentBuffer, bufferOperation); }, this); }, _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 // insert a new buffer after the "parent buffer"). var tagName = this.tagName; @@ -14028,35 +14188,98 @@ has: function(name) { 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(); } // 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'); + this.transitionTo('destroying', false); - // next remove view from global hash - if (!this.isVirtual) delete Ember.View.views[this.elementId]; + return this; }, clearRenderedChildren: Ember.K, triggerRecursively: Ember.K, invokeRecursively: Ember.K, transitionTo: Ember.K, 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 DOM, and registering and responding to user-initiated events. @@ -14679,18 +14902,10 @@ 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. @property controller @@ -14724,16 +14939,12 @@ }).property('layoutName'), templateForName: function(name, type) { if (!name) { return; } - - var container = get(this, 'container'); - - if (container) { - return container.lookup('template:' + name); - } + var container = this.container || (Ember.Container && Ember.Container.defaultContainer); + return container && container.lookup('template:' + name); }, /** The object from which templates should access properties. @@ -14820,11 +15031,11 @@ @type Array @default [] */ childViews: childViewsProperty, - _childViews: [], + _childViews: EMPTY_ARRAY, // When it's a virtual view, we need to notify the parent that their // childViews will change. _childViewsWillChange: Ember.beforeObserver(function() { if (this.isVirtual) { @@ -15117,11 +15328,11 @@ @method _applyAttributeBindings @param {Ember.RenderBuffer} buffer */ _applyAttributeBindings: function(buffer, attributeBindings) { - var attributeValue, elem, type; + var attributeValue, elem; a_forEach(attributeBindings, function(binding) { var split = binding.split(':'), property = split[0], attributeName = split[1] || property; @@ -15208,11 +15419,11 @@ idx = childViews.length, view; while(--idx >= 0) { view = childViews[idx]; - callback.call(this, view, idx); + callback(this, view, idx); } return this; }, @@ -15222,13 +15433,13 @@ if (!childViews) { return this; } 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; }, @@ -15420,25 +15631,27 @@ willClearRender: Ember.K, /** @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(); childViews = []; 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); } } } @@ -15459,10 +15672,23 @@ } } } }, + 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 will do nothing. @@ -15503,12 +15729,14 @@ `willClearRender` event recursively. @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() { this.forEachChildView(function(view) { Ember.propertyWillChange(view, 'element'); @@ -15550,12 +15778,12 @@ this.lengthAfterRender = this._childViews.length; return buffer; }, - renderToBufferIfNeeded: function () { - return this.currentState.renderToBufferIfNeeded(this, this); + renderToBufferIfNeeded: function (buffer) { + return this.currentState.renderToBufferIfNeeded(this, buffer); }, beforeRender: function(buffer) { this.applyAttributesToBuffer(buffer); buffer.pushOpeningTag(); @@ -15679,11 +15907,11 @@ @property classNameBindings @type Array @default [] */ - classNameBindings: [], + classNameBindings: EMPTY_ARRAY, /** A list of properties of the view to apply as attributes. If the property is a string value, the value of that string will be applied as the attribute. @@ -15707,11 +15935,11 @@ }); ``` @property attributeBindings */ - attributeBindings: [], + attributeBindings: EMPTY_ARRAY, // ....................................................... // CORE DISPLAY METHODS // @@ -15781,17 +16009,17 @@ @method removeAllChildren @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(); }); }, /** @@ -15815,50 +16043,37 @@ You must call `destroy` on a view to destroy the view (and all of its child views). This will remove the view from any parent node, then make 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--) { childViews[i].removedFromDOM = true; } // 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; }, /** Instantiates a view to be added to the childViews array during view initialization. You generally will not call this method directly unless @@ -15875,10 +16090,11 @@ if (view.isView && view._parentView === this) { return view; } 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); // don't set the property on a virtual view, as they are invisible to @@ -15969,13 +16185,17 @@ view.buffer = null; }); }, 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); }); } @@ -16315,19 +16535,22 @@ Ember.merge(preRender, { // a view leaves the preRender state once its element has been // 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, setElement: function(view, value) { @@ -16370,14 +16593,15 @@ // when a view is rendered in a buffer, appending a child // view will render that view and append the resulting // buffer into its buffer. appendChild: function(view, childView, options) { - var buffer = 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); view.propertyDidChange('childViews'); @@ -16387,22 +16611,22 @@ // when a view is rendered in a buffer, destroying the // element will simply destroy the buffer and put the // 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; }, empty: function() { }, - renderToBufferIfNeeded: function (view) { - return view.buffer; + renderToBufferIfNeeded: function (view, buffer) { + return false; }, // It should be impossible for a rendered view to be scheduled for // insertion. insertElement: function() { @@ -16517,10 +16741,27 @@ }); 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.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"; } }); @@ -16532,34 +16773,34 @@ /** @module ember @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 // the view has been destroyed between scheduling and execution insertElement: Ember.K @@ -16574,11 +16815,11 @@ Ember.View.cloneStates = function(from) { var into = {}; 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); for (var stateName in from) { @@ -16601,10 +16842,11 @@ @submodule ember-views */ 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` allowing programatic management of its child views. @@ -16805,10 +17047,11 @@ _childViews[idx] = view; }, this); 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) { @@ -16819,10 +17062,11 @@ if (addedCount === 0) { 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); } this.arrayContentDidChange(idx, removedCount, addedCount); this.childViewsDidChange(this._childViews, idx, removedCount, addedCount); @@ -16963,30 +17207,52 @@ childViewsDidChange: function(view, views, start, added) { Ember.run.scheduleOnce('render', view, '_ensureChildrenAreInDOM'); }, 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'); + }); +} + + })(); (function() { @@ -17223,19 +17489,21 @@ var len = content ? get(content, 'length') : 0; 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) { // If the contents were empty before and this template collection has an // empty view remove it now. @@ -17253,15 +17521,17 @@ var removingAll = removedCount === len; 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(); } }, /** @@ -17355,10 +17625,73 @@ })(); (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 @module ember @@ -17864,10 +18197,21 @@ @class Handlebars @namespace Ember */ Ember.Handlebars = objectCreate(Handlebars); +Ember.Handlebars.helper = function(name, value) { + if (Ember.View.detect(value)) { + Ember.Handlebars.registerHelper(name, function(name, options) { + + return Ember.Handlebars.helpers.view.call(this, value, options); + }); + } else { + Ember.Handlebars.registerBoundHelper.apply(null, arguments); + } +} + /** @class helpers @namespace Ember.Handlebars */ Ember.Handlebars.helpers = objectCreate(Handlebars.helpers); @@ -18332,10 +18676,11 @@ /** @private Renders the unbound form of an otherwise bound helper function. + @method evaluateMultiPropertyBoundHelper @param {Function} fn @param {Object} context @param {Array} normalizedProperties @param {String} options */ @@ -18348,11 +18693,11 @@ watchedProperties, boundOption, bindView, loc, property, len; 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) { if (!boundOptions.hasOwnProperty(boundOption)) { continue; } property = normalizePath(context, boundOptions[boundOption], data); @@ -18393,10 +18738,11 @@ /** @private Renders the unbound form of an otherwise bound helper function. + @method evaluateUnboundHelper @param {Function} fn @param {Object} context @param {Array} normalizedProperties @param {String} options */ @@ -18531,17 +18877,22 @@ view.clearRenderedChildren(); 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(); }); }, empty: function(view) { @@ -18646,10 +18997,12 @@ this.updateId = null; } this.morph = null; }, + propertyWillChange: Ember.K, + propertyDidChange: Ember.K, normalizedValue: function() { var path = this.path, pathRoot = this.pathRoot, @@ -18696,11 +19049,11 @@ }, 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."); case 'hasElement': case 'inDOM': @@ -18727,11 +19080,11 @@ rerenderIfNeeded: Ember.K }); merge(states.inDOM, { rerenderIfNeeded: function(view) { - if (get(view, 'normalizedValue') !== view._lastNormalizedValue) { + if (view.normalizedValue() !== view._lastNormalizedValue) { view.rerender(); } } }); @@ -18831,11 +19184,11 @@ @property pathRoot @type Object */ pathRoot: null, - normalizedValue: Ember.computed(function() { + normalizedValue: function() { var path = get(this, 'path'), pathRoot = get(this, 'pathRoot'), valueNormalizer = get(this, 'valueNormalizerFunc'), result, templateData; @@ -18849,11 +19202,11 @@ templateData = get(this, 'templateData'); result = handlebarsGet(pathRoot, path, { data: templateData }); } return valueNormalizer ? valueNormalizer(result) : result; - }).property('path', 'pathRoot', 'valueNormalizerFunc').volatile(), + }, rerenderIfNeeded: function() { this.currentState.rerenderIfNeeded(this); }, @@ -18884,11 +19237,11 @@ context = get(this, 'previousContext'); 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 // render the template or not. if (shouldDisplay(result)) { @@ -18947,10 +19300,14 @@ var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.normalizePath; 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) { var data = options.data, fn = options.fn, @@ -19125,13 +19482,11 @@ if (!options.fn) { 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); }); /** @private @@ -19197,13 +19552,11 @@ // if the path is '' ("this"), just bind directly to the current context var contextPath = path ? contextKey + '.' + path : contextKey; 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 { return helpers.bind.call(options.contexts[0], context, options); } @@ -19351,11 +19704,11 @@ ``` 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 hard-coded value – can be combined in a single declaration: @@ -19686,15 +20039,12 @@ return '_parentView.context.' + path; } }, 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) { newView = EmberHandlebars.get(thisContext, path, options); @@ -19703,11 +20053,11 @@ } 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) { viewOptions.template = fn; @@ -19830,21 +20180,20 @@ {{#view "MyApp.CustomView"}} hello. {{/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({}); MyApp.OuterView = Ember.View.extend({ 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'); ``` @@ -20085,12 +20434,10 @@ delete hash[prop]; } } } - var tagName = hash.tagName || collectionPrototype.tagName; - if (fn) { itemHash.template = fn; delete options.fn; } @@ -20108,12 +20455,10 @@ if(!hash.keyword){ 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); return Ember.Handlebars.helpers.view.call(this, collectionClass, options); }); @@ -20299,18 +20644,20 @@ } return view; }, - willDestroy: function() { + destroy: function() { + if (!this._super()) { return; } + var arrayController = get(this, '_arrayController'); if (arrayController) { arrayController.destroy(); } - return this._super(); + return this; } }); var GroupedEach = Ember.Handlebars.GroupedEach = function(context, path, options) { var self = this, @@ -20622,10 +20969,11 @@ <script type="text/x-handlebars" data-template-name="header_bar"> {{#with currentUser}} {{partial user_info}} {{/with}} </script> + ``` The `data-template-name` attribute of a partial template is prefixed with an underscore. ```html @@ -20908,11 +21256,11 @@ ## HTML Attributes 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 `Ember.TextField` or `Ember.TextSupport`. @@ -20983,10 +21331,24 @@ @default null */ action: null, /** + The event that should send the action. + + Options are: + + * `enter`: the user pressed enter + * `keypress`: the user pressed a key + + @property on + @type String + @default enter + */ + onEvent: 'enter', + + /** Whether they `keyUp` event that triggers an `action` to be sent continues propagating to other views. By default, when the user presses the return key on their keyboard and the text field has an `action` set, the action will be sent to the view's @@ -21000,23 +21362,34 @@ @default false */ bubbles: false, insertNewline: function(event) { - var controller = get(this, 'controller'), - action = get(this, 'action'); + sendAction('enter', this, event); + }, - if (action) { - controller.send(action, get(this, 'value'), this); - - 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(); + } + } +} })(); (function() { @@ -21230,10 +21603,59 @@ indexesOf = Ember.EnumerableUtils.indexesOf, replace = Ember.EnumerableUtils.replace, 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, allowing the user to choose from a list of options. @@ -21406,11 +21828,11 @@ ``` 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 A `null` value for the `Ember.Select`'s `value` or `selection` property results in there being no `<option>` with a `selected` attribute: @@ -21501,11 +21923,11 @@ function program3(depth0,data) { var hashTypes; 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}))); } hashTypes = {}; @@ -21610,10 +22032,19 @@ @type String @default 'content' */ 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(); } else { this._changeSingle(); @@ -21730,64 +22161,55 @@ this.on("didInsertElement", this, this._triggerChange); this.on("change", this, this._change); } }); -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'); +Ember.Handlebars.registerHelper('input', function(options) { - if (!labelPath) { return; } - 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); + } }); -})(); +Ember.Handlebars.registerHelper('textarea', function(options) { + var hash = options.hash, + types = options.hashTypes; -(function() { + normalizeHash(hash, types); + return Ember.Handlebars.helpers.view.call(this, Ember.TextArea, options); +}); })(); @@ -22401,20 +22823,14 @@ ## `RecognizedHandler` * `{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 */ @@ -22457,12 +22873,11 @@ @param {String} url a URL to process @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 + "'"); } @@ -22554,26 +22969,26 @@ var handlers = this.recognizer.handlersFor(handlerName), params = {}, 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--) { if (handlers[i].names.length) { objectsToMatch--; startIdx = i; } } 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; objectChanged = false; @@ -22614,24 +23029,36 @@ setContext(handler, object); } 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 }; }, isActive: function(handlerName) { var contexts = [].slice.call(arguments, 1); var currentHandlerInfos = this.currentHandlerInfos, - found = false, names, object, handlerInfo, handlerObj; + found = false, object, handlerInfo; for (var i=currentHandlerInfos.length-1; i>=0; i--) { handlerInfo = currentHandlerInfos[i]; if (handlerInfo.name === handlerName) { found = true; } @@ -22756,13 +23183,25 @@ resolved. It will use the resolved value as the context of `HandlerInfo`. */ 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]; var handler = router.getHandler(result.handler); var object = handler.deserialize && handler.deserialize(result.params); @@ -22783,22 +23222,24 @@ setContext(handler, object); } 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); } } /** @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. ``` @@ -22828,15 +23269,13 @@ 2. Triggers the `serialize` callback on `about` 3. Triggers the `enter` callback on `about` 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; @@ -22860,11 +23299,11 @@ aborted = true; } } }); - if (router.didTransition) { + if (!aborted && router.didTransition) { router.didTransition(handlerInfos); } } /** @@ -22887,32 +23326,10 @@ } /** @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); - } - } - - /** - @private - This function is called when transitioning from one URL to another to determine which handlers are not longer active, which handlers are newly active, and which handlers remain active but have their context changed. @@ -23007,10 +23424,11 @@ if (handler.contextDidChange) { handler.contextDidChange(); } } return Router; }); + })(); (function() { @@ -23107,11 +23525,11 @@ 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` */ @@ -23157,20 +23575,21 @@ var get = Ember.get, set = Ember.set; 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); - } } } /** The `Ember.Router` class manages the application state and URLs. Refer to @@ -23212,15 +23631,10 @@ this.handleURL(location.getURL()); }, 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); set(appController, 'currentPath', path); this.notifyPropertyChange('url'); @@ -23401,11 +23815,12 @@ @module ember @submodule ember-routing */ 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 the [routing guide](http://emberjs.com/guides/routing/) for documentation. @@ -23476,13 +23891,20 @@ @method transitionTo @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. @@ -23490,34 +23912,94 @@ @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); }, send: function() { return this.router.send.apply(this.router, arguments); }, /** + @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 @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; } + // 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 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); if (controller) { this.controller = controller; set(controller, 'model', context); @@ -23564,10 +24046,12 @@ /** @private Called when the context is changed by router.js. + + @method contextDidChange */ contextDidChange: function() { this.currentModel = this.context; }, @@ -23921,10 +24405,14 @@ var parentView = route.router._lookupActiveView(options.into); route.teardownView = teardownOutlet(parentView, options.outlet); 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); } } @@ -23986,11 +24474,11 @@ /** @module ember @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, isSimpleClick = Ember.ViewUtils.isSimpleClick; @@ -24018,23 +24506,159 @@ var ret = [ routeName ]; return ret.concat(resolvedPaths(linkView.parameters)); } /** - Renders a link to the supplied route. + 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: - 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. + ```handlebars + {{#linkTo photoGallery}} + Great Hamster Photos + {{/linkTo}} + ``` - You may re-open LinkView in order to change the default active class: + ```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: + + ``` + {{#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}}`: + + ``` + {{#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: + + ``` + {{#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" + activeClass: "is-active", + tagName: 'li' }) ``` - + @class LinkView @namespace Ember @extends Ember.View **/ var LinkView = Ember.LinkView = Ember.View.extend({ @@ -24049,11 +24673,11 @@ // 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'), active: Ember.computed(function() { var router = this.get('router'), params = resolvedPaths(this.parameters), currentWithIndex = this.currentWhen + '.index', @@ -24081,10 +24705,12 @@ 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)); }) }); @@ -24702,17 +25328,44 @@ */ 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'), method = target.transitionToRoute || target.transitionTo; return method.apply(target, arguments); }, + /** + @deprecated + @for Ember.ControllerMixin + @method transitionTo + */ transitionTo: function() { return this.transitionToRoute.apply(this, arguments); }, @@ -24721,10 +25374,15 @@ var target = get(this, 'target'), method = target.replaceRoute || target.replaceWith; return method.apply(target, arguments); }, + /** + @deprecated + @for Ember.ControllerMixin + @method replaceWith + */ replaceWith: function() { return this.replaceRoute.apply(this, arguments); } }); @@ -25009,11 +25667,10 @@ */ Ember.HistoryLocation = Ember.Object.extend({ init: function() { set(this, 'location', get(this, 'location') || window.location); - this._initialUrl = this.getURL(); this.initState(); }, /** @private @@ -25021,12 +25678,12 @@ Used to set state on first call to setURL @method initState */ initState: function() { + set(this, 'history', get(this, 'history') || window.history); this.replaceState(this.formatURL(this.getURL())); - set(this, 'history', window.history); }, /** Will be pre-pended to path upon state change @@ -25103,11 +25760,13 @@ @method pushState @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(); }, /** @private @@ -25115,11 +25774,13 @@ @method replaceState @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(); }, /** @private @@ -25135,11 +25796,11 @@ Ember.$(window).bind('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()); }); }, @@ -25516,14 +26177,11 @@ /** @module ember @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; /** An instance of `Ember.Application` is the starting point for every Ember application. It helps to instantiate, initialize and coordinate the many objects that make up your app. @@ -25959,16 +26617,23 @@ return this; }, reset: function() { - get(this, '__container__').destroy(); - this.buildContainer(); - Ember.run.schedule('actions', this, function(){ - this._initialize(); - this.startRouting(); + Ember.run(this, function(){ + Ember.run(get(this,'__container__'), 'destroy'); + + this.buildContainer(); + + this._readinessDeferrals = 1; + this.$(this.rootElement).removeClass('ember-application'); + + Ember.run.schedule('actions', this, function(){ + this._initialize(); + this.startRouting(); + }); }); }, /** @private @@ -26171,12 +26836,13 @@ * if the default lookup fails, look for registered classes on the container 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; var resolver = resolverClass.create({ namespace: namespace @@ -26412,14 +27078,32 @@ for (name in states) { this.setupChild(states, name, states[name]); } } - 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) { if (!value) { return false; } if (value.isState) { set(value, 'name', name); @@ -27401,11 +28085,11 @@ this.enterState(transition); this.triggerSetupContext(transition); }, 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), exitStates = [], resolveState = currentState; @@ -27487,16 +28171,18 @@ exitStates.shift(); } // 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; }, triggerSetupContext: function(transitions) { var contexts = transitions.contexts, @@ -27548,9 +28234,106 @@ @module ember @submodule ember-states @requires ember-runtime */ + +})(); + +(function() { +/*globals EMBER_APP_BEING_TESTED */ + +var Promise = Ember.RSVP.Promise, + pendingAjaxRequests = 0, + originalFind; + +function visit(url) { + var promise = new Promise(); + Ember.run(EMBER_APP_BEING_TESTED, EMBER_APP_BEING_TESTED.handleURL, url); + wait(promise, promise.resolve); + return promise; +} + +function click(selector) { + var promise = new Promise(); + Ember.run(function() { + Ember.$(selector).click(); + }); + wait(promise, promise.resolve); + return promise; +} + +function fillIn(selector, text) { + var promise = new Promise(); + var $el = find(selector); + Ember.run(function() { + $el.val(text); + }); + + wait(promise, promise.resolve); + return promise; +} + +function find(selector) { + return Ember.$('.ember-application').find(selector); +} + +function wait(target, method) { + if (!method) { + method = target; + target = null; + } + stop(); + var watcher = setInterval(function() { + var routerIsLoading = EMBER_APP_BEING_TESTED.__container__.lookup('router:main').router.isLoading; + if (routerIsLoading) { return; } + if (pendingAjaxRequests) { return; } + if (Ember.run.hasScheduledTimers() || Ember.run.currentRunLoop) { return; } + clearInterval(watcher); + start(); + Ember.run(target, method); + }, 200); +} + +Ember.Application.reopen({ + setupForTesting: function() { + this.deferReadiness(); + + this.Router.reopen({ + location: 'none' + }); + + window.EMBER_APP_BEING_TESTED = this; + }, + + injectTestHelpers: function() { + Ember.$(document).ajaxStart(function() { + pendingAjaxRequests++; + }); + + Ember.$(document).ajaxStop(function() { + pendingAjaxRequests--; + }); + + window.visit = visit; + window.click = click; + window.fillIn = fillIn; + originalFind = window.find; + window.find = find; + }, + + removeTestHelpers: function() { + window.visit = null; + window.click = null; + window.fillIn = null; + window.find = originalFind; + } +}); +})(); + + + +(function() { })(); })();