diff --git a/assets/scripts/vendor/ember.js b/assets/scripts/vendor/ember.js index 73543891..740a41b9 100644 --- a/assets/scripts/vendor/ember.js +++ b/assets/scripts/vendor/ember.js @@ -5,7 +5,7 @@ * Portions Copyright 2008-2011 Apple Inc. All rights reserved. * @license Licensed under MIT license * See https://raw.github.com/emberjs/ember.js/master/LICENSE - * @version 1.3.2 + * @version 1.4.0 */ @@ -70,11 +70,6 @@ if (!('MANDATORY_SETTER' in Ember.ENV)) { */ Ember.assert = function(desc, test) { if (!test) { - Ember.Logger.assert(test, desc); - } - - if (Ember.testing && !test) { - // when testing, ensure test failures when assertions fail throw new Ember.Error("Assertion Failed: " + desc); } }; @@ -185,10 +180,21 @@ Ember.deprecateFunc = function(message, func) { // Inform the developer about the Ember Inspector if not installed. if (!Ember.testing) { - if (typeof window !== 'undefined' && window.chrome && window.addEventListener) { + var isFirefox = typeof InstallTrigger !== 'undefined'; + var isChrome = !!window.chrome && !window.opera; + + if (typeof window !== 'undefined' && (isFirefox || isChrome) && window.addEventListener) { window.addEventListener("load", function() { if (document.body && document.body.dataset && !document.body.dataset.emberExtension) { - Ember.debug('For more advanced debugging, install the Ember Inspector from https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi'); + var downloadURL; + + if(isChrome) { + downloadURL = 'https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi'; + } else if(isFirefox) { + downloadURL = 'https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/' + } + + Ember.debug('For more advanced debugging, install the Ember Inspector from ' + downloadURL); } }, false); } @@ -203,7 +209,7 @@ if (!Ember.testing) { * Portions Copyright 2008-2011 Apple Inc. All rights reserved. * @license Licensed under MIT license * See https://raw.github.com/emberjs/ember.js/master/LICENSE - * @version 1.3.2 + * @version 1.4.0 */ @@ -286,7 +292,7 @@ var define, requireModule, require, requirejs; @class Ember @static - @version 1.3.2 + @version 1.4.0 */ if ('undefined' === typeof Ember) { @@ -313,10 +319,10 @@ Ember.toString = function() { return "Ember"; }; /** @property VERSION @type String - @default '1.3.2' + @default '1.4.0' @static */ -Ember.VERSION = '1.3.2'; +Ember.VERSION = '1.4.0'; /** Standard environmental variables. You can define these in a global `EmberENV` @@ -791,17 +797,19 @@ var arrayIndexOf = isNativeFunc(Array.prototype.indexOf) ? Array.prototype.index return -1; }; -/** - Array polyfills to support ES5 features in older browsers. - @namespace Ember - @property ArrayPolyfills -*/ -Ember.ArrayPolyfills = { - map: arrayMap, - forEach: arrayForEach, - indexOf: arrayIndexOf -}; + /** + Array polyfills to support ES5 features in older browsers. + + @namespace Ember + @property ArrayPolyfills + */ + Ember.ArrayPolyfills = { + map: arrayMap, + forEach: arrayForEach, + indexOf: arrayIndexOf + }; + if (Ember.SHIM_ES5) { if (!Array.prototype.map) { @@ -1029,7 +1037,7 @@ Ember.guidFor = function guidFor(obj) { // META // -var META_DESC = { +var META_DESC = Ember.META_DESC = { writable: true, configurable: false, enumerable: false, @@ -1069,7 +1077,8 @@ Meta.prototype = { bindings: null, chains: null, chainWatchers: null, - values: null + values: null, + proto: null }; if (isDefinePropertySimulated) { @@ -1576,6 +1585,41 @@ Ember.typeOf = function(item) { return ret; }; +/** + Convenience method to inspect an object. This method will attempt to + convert the object into a useful string description. + + It is a pretty simple implementation. If you want something more robust, + use something like JSDump: https://github.com/NV/jsDump + + @method inspect + @for Ember + @param {Object} obj The object you want to inspect. + @return {String} A description of the object +*/ +Ember.inspect = function(obj) { + var type = Ember.typeOf(obj); + if (type === 'array') { + return '[' + obj + ']'; + } + if (type !== 'object') { + return obj + ''; + } + + var v, ret = []; + for(var key in obj) { + if (obj.hasOwnProperty(key)) { + v = obj[key]; + if (v === 'toString') { continue; } // ignore useless items + if (Ember.typeOf(v) === 'function') { v = "function() { ... }"; } + ret.push(key + ": " + v); + } + } + return "{" + ret.join(", ") + "}"; +}; + + + })(); @@ -1790,36 +1834,130 @@ Ember.subscribe = Ember.Instrumentation.subscribe; (function() { -var map, forEach, indexOf, splice; +var map, forEach, indexOf, splice, filter; map = Array.prototype.map || Ember.ArrayPolyfills.map; forEach = Array.prototype.forEach || Ember.ArrayPolyfills.forEach; indexOf = Array.prototype.indexOf || Ember.ArrayPolyfills.indexOf; +filter = Array.prototype.filter || Ember.ArrayPolyfills.filter; splice = Array.prototype.splice; +/** + * Defines some convenience methods for working with Enumerables. + * `Ember.EnumerableUtils` uses `Ember.ArrayPolyfills` when necessary. + * + * @class EnumerableUtils + * @namespace Ember + * @static + * */ var utils = Ember.EnumerableUtils = { + /** + * Calls the map function on the passed object with a specified callback. This + * uses `Ember.ArrayPolyfill`'s-map method when necessary. + * + * @method map + * @param {Object} obj The object that should be mapped + * @param {Function} callback The callback to execute + * @param {Object} thisArg Value to use as this when executing *callback* + * + * @return {Array} An array of mapped values. + */ map: function(obj, callback, thisArg) { return obj.map ? obj.map.call(obj, callback, thisArg) : map.call(obj, callback, thisArg); }, + /** + * Calls the forEach function on the passed object with a specified callback. This + * uses `Ember.ArrayPolyfill`'s-forEach method when necessary. + * + * @method forEach + * @param {Object} obj The object to call forEach on + * @param {Function} callback The callback to execute + * @param {Object} thisArg Value to use as this when executing *callback* + * + */ forEach: function(obj, callback, thisArg) { return obj.forEach ? obj.forEach.call(obj, callback, thisArg) : forEach.call(obj, callback, thisArg); }, + /** + * Calls the filter function on the passed object with a specified callback. This + * uses `Ember.ArrayPolyfill`'s-filter method when necessary. + * + * @method filter + * @param {Object} obj The object to call filter on + * @param {Function} callback The callback to execute + * @param {Object} thisArg Value to use as this when executing *callback* + * + * @return {Array} An array containing the filtered values + */ + filter: function(obj, callback, thisArg) { + return obj.filter ? obj.filter.call(obj, callback, thisArg) : filter.call(obj, callback, thisArg); + }, + + /** + * Calls the indexOf function on the passed object with a specified callback. This + * uses `Ember.ArrayPolyfill`'s-indexOf method when necessary. + * + * @method indexOf + * @param {Object} obj The object to call indexOn on + * @param {Function} callback The callback to execute + * @param {Object} index The index to start searching from + * + */ indexOf: function(obj, element, index) { return obj.indexOf ? obj.indexOf.call(obj, element, index) : indexOf.call(obj, element, index); }, + /** + * Returns an array of indexes of the first occurrences of the passed elements + * on the passed object. + * + * ```javascript + * var array = [1, 2, 3, 4, 5]; + * Ember.EnumerableUtils.indexesOf(array, [2, 5]); // [1, 4] + * + * var fubar = "Fubarr"; + * Ember.EnumerableUtils.indexesOf(fubar, ['b', 'r']); // [2, 4] + * ``` + * + * @method indexesOf + * @param {Object} obj The object to check for element indexes + * @param {Array} elements The elements to search for on *obj* + * + * @return {Array} An array of indexes. + * + */ indexesOf: function(obj, elements) { return elements === undefined ? [] : utils.map(elements, function(item) { return utils.indexOf(obj, item); }); }, + /** + * Adds an object to an array. If the array already includes the object this + * method has no effect. + * + * @method addObject + * @param {Array} array The array the passed item should be added to + * @param {Object} item The item to add to the passed array + * + * @return 'undefined' + */ addObject: function(array, item) { var index = utils.indexOf(array, item); if (index === -1) { array.push(item); } }, + /** + * Removes an object from an array. If the array does not contain the passed + * object this method has no effect. + * + * @method removeObject + * @param {Array} array The array to remove the item from. + * @param {Object} item The item to remove from the passed array. + * + * @return 'undefined' + */ removeObject: function(array, item) { var index = utils.indexOf(array, item); if (index !== -1) { array.splice(index, 1); } @@ -1845,6 +1983,31 @@ var utils = Ember.EnumerableUtils = { return ret; }, + /** + * Replaces objects in an array with the passed objects. + * + * ```javascript + * var array = [1,2,3]; + * Ember.EnumerableUtils.replace(array, 1, 2, [4, 5]); // [1, 4, 5] + * + * var array = [1,2,3]; + * Ember.EnumerableUtils.replace(array, 1, 1, [4, 5]); // [1, 4, 5, 3] + * + * var array = [1,2,3]; + * Ember.EnumerableUtils.replace(array, 10, 1, [4, 5]); // [1, 2, 3, 4, 5] + * ``` + * + * @method replace + * @param {Array} array The array the objects should be inserted into. + * @param {Number} idx Starting index in the array to replace. If *idx* >= + * length, then append to the end of the array. + * @param {Number} amt Number of elements that should be remove from the array, + * starting at *idx* + * @param {Array} objects An array of zero or more objects that should be + * inserted into the array at *idx* + * + * @return {Array} The changed array. + */ replace: function(array, idx, amt, objects) { if (array.replace) { return array.replace(idx, amt, objects); @@ -1853,6 +2016,29 @@ var utils = Ember.EnumerableUtils = { } }, + /** + * Calculates the intersection of two arrays. This method returns a new array + * filled with the records that the two passed arrays share with each other. + * If there is no intersection, an empty array will be returned. + * + * ```javascript + * var array1 = [1, 2, 3, 4, 5]; + * var array2 = [1, 3, 5, 6, 7]; + * + * Ember.EnumerableUtils.intersection(array1, array2); // [1, 3, 5] + * + * var array1 = [1, 2, 3]; + * var array2 = [4, 5, 6]; + * + * Ember.EnumerableUtils.intersection(array1, array2); // [] + * ``` + * + * @method intersection + * @param {Array} array1 The first array + * @param {Array} array2 The second array + * + * @return {Array} The intersection of the two passed arrays. + */ intersection: function(array1, array2) { var intersection = []; @@ -1986,7 +2172,7 @@ var normalizeTuple = Ember.normalizeTuple = function(target, path) { } // must return some kind of path to be valid else other things will break. - if (!path || path.length===0) throw new Ember.Error('Invalid Path'); + if (!path || path.length===0) throw new Ember.Error('Path cannot be empty'); return [ target, path ]; }; @@ -2505,7 +2691,7 @@ ObserverSet.prototype.clear = function() { (function() { -var metaFor = Ember.meta, +var META_KEY = Ember.META_KEY, guidFor = Ember.guidFor, tryFinally = Ember.tryFinally, sendEvent = Ember.sendEvent, @@ -2536,10 +2722,10 @@ var metaFor = Ember.meta, @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]; + var m = obj[META_KEY], + watching = (m && m.watching[keyName] > 0) || keyName === 'length', + proto = m && m.proto, + desc = m && m.descs[keyName]; if (!watching) { return; } if (proto === obj) { return; } @@ -2566,10 +2752,10 @@ Ember.propertyWillChange = propertyWillChange; @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]; + var m = obj[META_KEY], + watching = (m && m.watching[keyName] > 0) || keyName === 'length', + proto = m && m.proto, + desc = m && m.descs[keyName]; if (proto === obj) { return; } @@ -2642,7 +2828,7 @@ function chainsWillChange(obj, keyName, m) { } function chainsDidChange(obj, keyName, m, suppressEvents) { - if (!(m.hasOwnProperty('chainWatchers') && + if (!(m && m.hasOwnProperty('chainWatchers') && m.chainWatchers[keyName])) { return; } @@ -2758,16 +2944,6 @@ var META_KEY = Ember.META_KEY, property is not defined but the object implements the `setUnknownProperty` method then that will be invoked as well. - If you plan to run on IE8 and older browsers then you should use this - method anytime you want to set a property on an object that you don't - know for sure is private. (Properties beginning with an underscore '_' - are considered private.) - - On all newer browsers, you only need to use this method to set - properties if the property might not be defined on the object and you want - to respect the `setUnknownProperty` handler. Otherwise you can ignore this - method. - @method set @for Ember @param {Object} obj The object to modify. @@ -3523,7 +3699,8 @@ Ember.defineProperty = function(obj, keyName, desc, data, meta) { } else { obj[keyName] = undefined; // make enumerable } - } else { + + } else { descs[keyName] = undefined; // shadow descriptor in proto if (desc == null) { value = data; @@ -3645,11 +3822,11 @@ var metaFor = Ember.meta, // utils.js MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, o_defineProperty = Ember.platform.defineProperty; -Ember.watchKey = function(obj, keyName) { +Ember.watchKey = function(obj, keyName, meta) { // can't watch length on Array - it is special... if (keyName === 'length' && typeOf(obj) === 'array') { return; } - var m = metaFor(obj), watching = m.watching; + var m = meta || metaFor(obj), watching = m.watching; // activate watching first time if (!watching[keyName]) { @@ -3674,8 +3851,8 @@ Ember.watchKey = function(obj, keyName) { }; -Ember.unwatchKey = function(obj, keyName) { - var m = metaFor(obj), watching = m.watching; +Ember.unwatchKey = function(obj, keyName, meta) { + var m = meta || metaFor(obj), watching = m.watching; if (watching[keyName] === 1) { watching[keyName] = 0; @@ -3718,7 +3895,8 @@ var metaFor = Ember.meta, // utils.js warn = Ember.warn, watchKey = Ember.watchKey, unwatchKey = Ember.unwatchKey, - FIRST_KEY = /^([^\.\*]+)/; + FIRST_KEY = /^([^\.\*]+)/, + META_KEY = Ember.META_KEY; function firstKey(path) { return path.match(FIRST_KEY)[0]; @@ -3752,24 +3930,24 @@ function addChainWatcher(obj, keyName, node) { if (!nodes[keyName]) { nodes[keyName] = []; } nodes[keyName].push(node); - watchKey(obj, keyName); + watchKey(obj, keyName, m); } 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 + var m = obj[META_KEY]; + if (m && !m.hasOwnProperty('chainWatchers')) { return; } // nothing to do - var nodes = m.chainWatchers; + var nodes = m && m.chainWatchers; - if (nodes[keyName]) { + if (nodes && nodes[keyName]) { nodes = nodes[keyName]; for (var i = 0, l = nodes.length; i < l; i++) { if (nodes[i] === node) { nodes.splice(i, 1); } } } - unwatchKey(obj, keyName); + unwatchKey(obj, keyName, m); }; // A ChainNode watches a single key on an object. If you provide a starting @@ -3809,14 +3987,14 @@ var ChainNodePrototype = ChainNode.prototype; function lazyGet(obj, key) { if (!obj) return undefined; - var meta = metaFor(obj, false); + var meta = obj[META_KEY]; // check if object meant only to be a prototype - if (meta.proto === obj) return undefined; + if (meta && meta.proto === obj) return undefined; if (key === "@each") return get(obj, key); // if a CP only return cached value - var desc = meta.descs[key]; + var desc = meta && meta.descs[key]; if (desc && desc._cacheable) { if (key in meta.cache) { return meta.cache[key]; @@ -4028,12 +4206,14 @@ ChainNodePrototype.didChange = function(events) { }; Ember.finishChains = function(obj) { - var m = metaFor(obj, false), chains = m.chains; + // We only create meta if we really have to + var m = obj[META_KEY], chains = m && m.chains; if (chains) { if (chains.value() !== obj) { - m.chains = chains = chains.copy(obj); + metaFor(obj).chains = chains = chains.copy(obj); + } else { + chains.didChange(null); } - chains.didChange(null); } }; @@ -4042,6 +4222,50 @@ Ember.finishChains = function(obj) { (function() { +/** + @module ember-metal + */ + +var forEach = Ember.EnumerableUtils.forEach, +BRACE_EXPANSION = /^((?:[^\.]*\.)*)\{(.*)\}$/; + +/** + Expands `pattern`, invoking `callback` for each expansion. + + The only pattern supported is brace-expansion, anything else will be passed + once to `callback` directly. Brace expansion can only appear at the end of a + pattern, for example as the last item in a chain. + + Example + ```js + function echo(arg){ console.log(arg); } + + Ember.expandProperties('foo.bar', echo); //=> 'foo.bar' + Ember.expandProperties('{foo,bar}', echo); //=> 'foo', 'bar' + Ember.expandProperties('foo.{bar,baz}', echo); //=> 'foo.bar', 'foo.baz' + Ember.expandProperties('{foo,bar}.baz', echo); //=> '{foo,bar}.baz' + ``` + + @method + @private + @param {string} pattern The property pattern to expand. + @param {function} callback The callback to invoke. It is invoked once per + expansion, and is passed the expansion. + */ +Ember.expandProperties = function (pattern, callback) { + var match, prefix, list; + + if (match = BRACE_EXPANSION.exec(pattern)) { + prefix = match[1]; + list = match[2]; + + forEach(list.split(','), function (suffix) { + callback(prefix + suffix); + }); + } else { + callback(pattern); + } +}; })(); @@ -4055,8 +4279,8 @@ var metaFor = Ember.meta, // utils.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; +function chainsFor(obj, meta) { + var m = meta || metaFor(obj), ret = m.chains; if (!ret) { ret = m.chains = new ChainNode(null, null, obj); } else if (ret.value() !== obj) { @@ -4065,26 +4289,26 @@ function chainsFor(obj) { return ret; } -Ember.watchPath = function(obj, keyPath) { +Ember.watchPath = function(obj, keyPath, meta) { // can't watch length on Array - it is special... if (keyPath === 'length' && typeOf(obj) === 'array') { return; } - var m = metaFor(obj), watching = m.watching; + var m = meta || metaFor(obj), watching = m.watching; if (!watching[keyPath]) { // activate watching first time watching[keyPath] = 1; - chainsFor(obj).add(keyPath); + chainsFor(obj, m).add(keyPath); } else { watching[keyPath] = (watching[keyPath] || 0) + 1; } }; -Ember.unwatchPath = function(obj, keyPath) { - var m = metaFor(obj), watching = m.watching; +Ember.unwatchPath = function(obj, keyPath, meta) { + var m = meta || metaFor(obj), watching = m.watching; if (watching[keyPath] === 1) { watching[keyPath] = 0; - chainsFor(obj).remove(keyPath); + chainsFor(obj, m).remove(keyPath); } else if (watching[keyPath] > 1) { watching[keyPath]--; } @@ -4128,14 +4352,14 @@ function isKeyName(path) { @param obj @param {String} keyName */ -Ember.watch = function(obj, _keyPath) { +Ember.watch = function(obj, _keyPath, m) { // can't watch length on Array - it is special... if (_keyPath === 'length' && typeOf(obj) === 'array') { return; } if (isKeyName(_keyPath)) { - watchKey(obj, _keyPath); + watchKey(obj, _keyPath, m); } else { - watchPath(obj, _keyPath); + watchPath(obj, _keyPath, m); } }; @@ -4146,14 +4370,14 @@ Ember.isWatching = function isWatching(obj, key) { Ember.watch.flushPending = Ember.flushPendingChains; -Ember.unwatch = function(obj, _keyPath) { +Ember.unwatch = function(obj, _keyPath, m) { // can't watch length on Array - it is special... if (_keyPath === 'length' && typeOf(obj) === 'array') { return; } if (isKeyName(_keyPath)) { - unwatchKey(obj, _keyPath); + unwatchKey(obj, _keyPath, m); } else { - unwatchPath(obj, _keyPath); + unwatchPath(obj, _keyPath, m); } }; @@ -4168,7 +4392,7 @@ Ember.unwatch = function(obj, _keyPath) { @param obj */ Ember.rewatch = function(obj) { - var m = metaFor(obj, false), chains = m.chains; + var m = obj[META_KEY], chains = m && m.chains; // make sure the object has its own guid. if (GUID_KEY in obj && !obj.hasOwnProperty(GUID_KEY)) { @@ -4245,6 +4469,7 @@ var get = Ember.get, watch = Ember.watch, unwatch = Ember.unwatch; +var expandProperties = Ember.expandProperties; // .......................................................... // DEPENDENT KEYS @@ -4294,7 +4519,7 @@ function addDependentKeys(desc, obj, keyName, meta) { // Increment the number of times depKey depends on keyName. keys[keyName] = (keys[keyName] || 0) + 1; // Watch the depKey - watch(obj, depKey); + watch(obj, depKey, meta); } } @@ -4313,7 +4538,7 @@ function removeDependentKeys(desc, obj, keyName, meta) { // Increment the number of times depKey depends on keyName. keys[keyName] = (keys[keyName] || 0) - 1; // Watch the depKey - unwatch(obj, depKey); + unwatch(obj, depKey, meta); } } @@ -4404,9 +4629,11 @@ function removeDependentKeys(desc, obj, keyName, meta) { */ function ComputedProperty(func, opts) { this.func = func; + + this._dependentKeys = opts && opts.dependentKeys; + this._cacheable = (opts && opts.cacheable !== undefined) ? opts.cacheable : true; - this._dependentKeys = opts && opts.dependentKeys; this._readOnly = opts && (opts.readOnly !== undefined || !!opts.readOnly); } @@ -4415,6 +4642,7 @@ ComputedProperty.prototype = new Ember.Descriptor(); var ComputedPropertyPrototype = ComputedProperty.prototype; + /** Properties are cacheable by default. Computed property will automatically cache the return value of your function until one of the dependent keys changes. @@ -4509,11 +4737,19 @@ ComputedPropertyPrototype.readOnly = function(readOnly) { ComputedPropertyPrototype.property = function() { var args; + var addArg = function (property) { + args.push(property); + }; + + args = []; + for (var i = 0, l = arguments.length; i < l; i++) { + expandProperties(arguments[i], addArg); + } + - args = a_slice.call(arguments); + this._dependentKeys = args; - this._dependentKeys = args; return this; }; @@ -4640,7 +4876,7 @@ ComputedPropertyPrototype.set = function(obj, keyName, value) { funcArgLength, cachedValue, ret; if (this._readOnly) { - throw new Ember.Error('Cannot Set: ' + keyName + ' on: ' + obj.toString() ); + throw new Ember.Error('Cannot Set: ' + keyName + ' on: ' + Ember.inspect(obj)); } this._suspended = obj; @@ -4756,7 +4992,8 @@ Ember.computed = function(func) { @return {Object} the cached value */ Ember.cacheFor = function cacheFor(obj, key) { - var cache = metaFor(obj, false).cache; + var meta = obj[META_KEY], + cache = meta && meta.cache; if (cache && key in cache) { return cache[key]; @@ -4771,26 +5008,33 @@ function getProperties(self, propertyNames) { return ret; } -function registerComputed(name, macro) { - Ember.computed[name] = function(dependentKey) { - var args = a_slice.call(arguments); - return Ember.computed(dependentKey, function() { - return macro.apply(this, args); - }); +var registerComputed, registerComputedWithProperties; + + + + registerComputed = function (name, macro) { + Ember.computed[name] = function(dependentKey) { + var args = a_slice.call(arguments); + return Ember.computed(dependentKey, function() { + return macro.apply(this, args); + }); + }; }; -} -function registerComputedWithProperties(name, macro) { - Ember.computed[name] = function() { - var properties = a_slice.call(arguments); + registerComputedWithProperties = function(name, macro) { + Ember.computed[name] = function() { + var properties = a_slice.call(arguments); - var computed = Ember.computed(function() { - return macro.apply(this, [getProperties(this, properties)]); - }); + var computed = Ember.computed(function() { + return macro.apply(this, [getProperties(this, properties)]); + }); - return computed.property.apply(computed, properties); + return computed.property.apply(computed, properties); + }; }; -} + + + /** A computed property that returns true if the value of the dependent @@ -5321,7 +5565,6 @@ Ember.computed.oneWay = function(dependentKey) { }); }; - /** A computed property that acts like a standard getter and setter, but returns the value at the provided `defaultPath` if the @@ -5998,7 +6241,7 @@ define("backburner", return true; } } - } else if (window.toString.call(timer) === "[object Array]"){ // we're cancelling a throttle or debounce + } else if (Object.prototype.toString.call(timer) === "[object Array]"){ // we're cancelling a throttle or debounce return this._cancelItem(findThrottler, throttlers, timer) || this._cancelItem(findDebouncee, debouncees, timer); } else { @@ -6112,6 +6355,7 @@ define("backburner", __exports__.Backburner = Backburner; }); + })(); @@ -6135,7 +6379,8 @@ var Backburner = requireModule('backburner').Backburner, onBegin: onBegin, onEnd: onEnd }), - slice = [].slice; + slice = [].slice, + concat = [].concat; // .......................................................... // Ember.run - this is ideally the only public API the dev sees @@ -6221,7 +6466,7 @@ Ember.run = function(target, method) { @return {Object} Return value from invoking the passed function. Please note, when called within an existing loop, no return value is possible. */ -Ember.run.join = function(target, method) { +Ember.run.join = function(target, method /* args */) { if (!Ember.run.currentRunLoop) { return Ember.run.apply(Ember.run, arguments); } @@ -6231,6 +6476,53 @@ Ember.run.join = function(target, method) { Ember.run.schedule.apply(Ember.run, args); }; +/** + Provides a useful utility for when integrating with non-Ember libraries + that provide asynchronous callbacks. + + Ember utilizes a run-loop to batch and coalesce changes. This works by + marking the start and end of Ember-related Javascript execution. + + When using events such as a View's click handler, Ember wraps the event + handler in a run-loop, but when integrating with non-Ember libraries this + can be tedious. + + For example, the following is rather verbose but is the correct way to combine + third-party events and Ember code. + + ```javascript + var that = this; + jQuery(window).on('resize', function(){ + Ember.run(function(){ + that.handleResize(); + }); + }); + ``` + + To reduce the boilerplate, the following can be used to construct a + run-loop-wrapped callback handler. + + ```javascript + jQuery(window).on('resize', Ember.run.bind(this, this.triggerResize)); + ``` + + @method bind + @namespace Ember.run + @param {Object} [target] target of method to call + @param {Function|String} method Method to invoke. + May be a function or a string. If you pass a string + then it will be looked up on the passed target. + @param {Object} [args*] Any additional arguments you wish to pass to the method. + @return {Object} return value from invoking the passed function. Please note, + when called within an existing loop, no return value is possible. +*/ +Ember.run.bind = function(target, method /* args*/) { + var args = arguments; + return function() { + return Ember.run.join.apply(Ember.run, args); + }; +}; + Ember.run.backburner = backburner; var run = Ember.run; @@ -6400,7 +6692,7 @@ Ember.run.later = function(target, method) { 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 + @return {Object} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.once = function(target, method) { checkAutoRun(); @@ -6451,7 +6743,7 @@ Ember.run.once = function(target, method) { 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 + @return {Object} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.scheduleOnce = function(queue, target, method) { checkAutoRun(); @@ -6514,7 +6806,7 @@ Ember.run.scheduleOnce = function(queue, target, method) { 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 + @return {Object} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.next = function() { var args = slice.call(arguments); @@ -6524,7 +6816,8 @@ Ember.run.next = function() { /** Cancels a scheduled item. Must be a value returned by `Ember.run.later()`, - `Ember.run.once()`, or `Ember.run.next()`. + `Ember.run.once()`, `Ember.run.next()`, `Ember.run.debounce()`, or + `Ember.run.throttle()`. ```javascript var runNext = Ember.run.next(myContext, function() { @@ -6541,11 +6834,29 @@ Ember.run.next = function() { // will not be executed }); Ember.run.cancel(runOnce); + + var throttle = Ember.run.throttle(myContext, function() { + // will not be executed + }, 1); + Ember.run.cancel(throttle); + + var debounce = Ember.run.debounce(myContext, function() { + // will not be executed + }, 1); + Ember.run.cancel(debounce); + + var debounceImmediate = Ember.run.debounce(myContext, function() { + // will be executed since we passed in true (immediate) + }, 100, true); + // the 100ms delay until this method can be called again will be cancelled + Ember.run.cancel(debounceImmediate); + ``` + ``` ``` @method cancel @param {Object} timer Timer object to cancel - @return {void} + @return {Boolean} true if cancelled or false/undefined if it wasn't found */ Ember.run.cancel = function(timer) { return backburner.cancel(timer); @@ -6577,6 +6888,34 @@ Ember.run.cancel = function(timer) { // console logs 'debounce ran.' one time. ``` + Immediate allows you to run the function immediately, but debounce + other calls for this function until the wait time has elapsed. If + `debounce` is called again before the specified time has elapsed, + the timer is reset and the entire period msut pass again before + the method can be called again. + + ```javascript + var myFunc = function() { console.log(this.name + ' ran.'); }; + var myContext = {name: 'debounce'}; + + Ember.run.debounce(myContext, myFunc, 150, true); + + // console logs 'debounce ran.' one time immediately. + // 100ms passes + + Ember.run.debounce(myContext, myFunc, 150, true); + + // 150ms passes and nothing else is logged to the console and + // the debouncee is no longer being watched + + Ember.run.debounce(myContext, myFunc, 150, true); + + // console logs 'debounce ran.' one time immediately. + // 150ms passes and nothing else is logged tot he console and + // the debouncee is no longer being watched + + ``` + @method debounce @param {Object} [target] target of method to invoke @param {Function|String} method The method to invoke. @@ -6585,7 +6924,7 @@ Ember.run.cancel = function(timer) { @param {Object} [args*] Optional arguments to pass to the timeout. @param {Number} wait Number of milliseconds to wait. @param {Boolean} immediate Trigger the function on the leading instead of the trailing edge of the wait interval. - @return {void} + @return {Array} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.debounce = function() { return backburner.debounce.apply(backburner, arguments); @@ -6622,7 +6961,7 @@ Ember.run.debounce = function() { then it will be looked up on the passed target. @param {Object} [args*] Optional arguments to pass to the timeout. @param {Number} spacing Number of milliseconds to space out requests. - @return {void} + @return {Array} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.throttle = function() { return backburner.throttle.apply(backburner, arguments); @@ -7130,11 +7469,14 @@ var Mixin, REQUIRED, Alias, a_slice = [].slice, o_create = Ember.create, defineProperty = Ember.defineProperty, - guidFor = Ember.guidFor; + guidFor = Ember.guidFor, + metaFor = Ember.meta, + META_KEY = Ember.META_KEY; +var expandProperties = Ember.expandProperties; function mixinsMeta(obj) { - var m = Ember.meta(obj, true), ret = m.mixins; + var m = metaFor(obj, true), ret = m.mixins; if (!ret) { ret = m.mixins = {}; } else if (!m.hasOwnProperty('mixins')) { @@ -7319,7 +7661,7 @@ function mergeMixins(mixins, m, descs, values, base, keys) { if (props === CONTINUE) { continue; } if (props) { - meta = Ember.meta(base); + meta = metaFor(base); if (base.willMergeMixin) { base.willMergeMixin(props); } concats = concatenatedMixinProperties('concatenatedProperties', props, values, base); mergings = concatenatedMixinProperties('mergedProperties', props, values, base); @@ -7377,7 +7719,7 @@ function connectBindings(obj, m) { } function finishPartial(obj, m) { - connectBindings(obj, m || Ember.meta(obj)); + connectBindings(obj, m || metaFor(obj)); return obj; } @@ -7424,7 +7766,7 @@ function replaceObserversAndListeners(obj, key, observerOrListener) { } function applyMixin(obj, mixins, partial) { - var descs = {}, values = {}, m = Ember.meta(obj), + var descs = {}, values = {}, m = metaFor(obj), key, value, desc, keys = []; // Go through all mixins and hashes passed in, and: @@ -7506,7 +7848,7 @@ Ember.mixin = function(obj) { Note that mixins extend a constructor's prototype so arrays and object literals defined as properties will be shared amongst objects that implement the mixin. - If you want to define an property in a mixin that is not shared, you can define + If you want to define a property in a mixin that is not shared, you can define it either as a computed property or have it be created on initialization of the object. ```javascript @@ -7636,7 +7978,8 @@ function _detect(curMixin, targetMixin, seen) { MixinPrototype.detect = function(obj) { if (!obj) { return false; } if (obj instanceof Mixin) { return _detect(obj, this, {}); } - var mixins = Ember.meta(obj, false).mixins; + var m = obj[META_KEY], + mixins = m && m.mixins; if (mixins) { return !!mixins[guidFor(this)]; } @@ -7675,7 +8018,8 @@ MixinPrototype.keys = function() { // returns the mixins currently applied to the specified object // TODO: Make Ember.mixin Mixin.mixins = function(obj) { - var mixins = Ember.meta(obj, false).mixins, ret = []; + var m = obj[META_KEY], + mixins = m && m.mixins, ret = []; if (!mixins) { return ret; } @@ -7707,35 +8051,6 @@ Alias = function(methodName) { }; Alias.prototype = new Ember.Descriptor(); -/** - Makes a property or method available via an additional name. - - ```javascript - App.PaintSample = Ember.Object.extend({ - color: 'red', - colour: Ember.alias('color'), - name: function() { - return "Zed"; - }, - moniker: Ember.alias("name") - }); - - var paintSample = App.PaintSample.create() - paintSample.get('colour'); // 'red' - paintSample.moniker(); // 'Zed' - ``` - - @method alias - @for Ember - @param {String} methodName name of the method or property to alias - @return {Ember.Descriptor} - @deprecated Use `Ember.aliasMethod` or `Ember.computed.alias` instead -*/ -Ember.alias = function(methodName) { - Ember.deprecate("Ember.alias is deprecated. Please use Ember.aliasMethod or Ember.computed.alias instead."); - return new Alias(methodName); -}; - /** Makes a method available via an additional name. @@ -7790,16 +8105,21 @@ Ember.observer = function() { var func = a_slice.call(arguments, -1)[0]; var paths; - - paths = a_slice.call(arguments, 0, -1); + var addWatchedProperty = function (path) { paths.push(path); }; + var _paths = a_slice.call(arguments, 0, -1); - if (typeof func !== "function") { - // revert to old, soft-deprecated argument ordering + if (typeof func !== "function") { + // revert to old, soft-deprecated argument ordering - func = arguments[0]; - paths = a_slice.call(arguments, 1); - } - + func = arguments[0]; + _paths = a_slice.call(arguments, 1); + } + + paths = []; + + for (var i=0; i<_paths.length; ++i) { + expandProperties(_paths[i], addWatchedProperty); + } if (typeof func !== "function") { throw new Ember.Error("Ember.observer called without a function"); @@ -7888,16 +8208,22 @@ Ember.beforeObserver = function() { var func = a_slice.call(arguments, -1)[0]; var paths; - - paths = a_slice.call(arguments, 0, -1); + var addWatchedProperty = function(path) { paths.push(path); }; - if (typeof func !== "function") { - // revert to old, soft-deprecated argument ordering + var _paths = a_slice.call(arguments, 0, -1); - func = arguments[0]; - paths = a_slice.call(arguments, 1); - } - + if (typeof func !== "function") { + // revert to old, soft-deprecated argument ordering + + func = arguments[0]; + _paths = a_slice.call(arguments, 1); + } + + paths = []; + + for (var i=0; i<_paths.length; ++i) { + expandProperties(_paths[i], addWatchedProperty); + } if (typeof func !== "function") { throw new Ember.Error("Ember.beforeObserver called without a function"); @@ -9571,7 +9897,7 @@ define("rsvp/promise/all", ``` @method all - @for RSVP.Promise + @for Ember.RSVP.Promise @param {Array} entries array of promises @param {String} label optional string for labeling the promise. Useful for tooling. @@ -10135,6 +10461,7 @@ Ember.MODEL_FACTORY_INJECTIONS = false || !!Ember.ENV.MODEL_FACTORY_INJECTIONS; define("container", [], function() { + "use strict"; // A safe and simple inheriting object. function InheritingDict(parent) { @@ -10256,7 +10583,8 @@ define("container", this.registry = new InheritingDict(parent && parent.registry); this.cache = new InheritingDict(parent && parent.cache); - this.factoryCache = new InheritingDict(parent && parent.cache); + this.factoryCache = new InheritingDict(parent && parent.factoryCache); + this.resolveCache = new InheritingDict(parent && parent.resolveCache); this.typeInjections = new InheritingDict(parent && parent.typeInjections); this.injections = {}; @@ -10377,9 +10705,7 @@ define("container", @param {Object} options */ register: function(fullName, factory, options) { - if (fullName.indexOf(':') === -1) { - throw new TypeError("malformed fullName, expected: `type:name` got: " + fullName + ""); - } + validateFullName(fullName); if (factory === undefined) { throw new TypeError('Attempting to register an unknown factory: `' + fullName + '`'); @@ -10412,11 +10738,14 @@ define("container", @param {String} fullName */ unregister: function(fullName) { + validateFullName(fullName); + var normalizedName = this.normalize(fullName); this.registry.remove(normalizedName); this.cache.remove(normalizedName); this.factoryCache.remove(normalizedName); + this.resolveCache.remove(normalizedName); this._options.remove(normalizedName); }, @@ -10453,7 +10782,18 @@ define("container", @return {Function} fullName's factory */ resolve: function(fullName) { - return this.resolver(fullName) || this.registry.get(fullName); + validateFullName(fullName); + + var normalizedName = this.normalize(fullName); + var cached = this.resolveCache.get(normalizedName); + + if (cached) { return cached; } + + var resolved = this.resolver(normalizedName) || this.registry.get(normalizedName); + + this.resolveCache.set(normalizedName, resolved); + + return resolved; }, /** @@ -10534,23 +10874,8 @@ define("container", @return {any} */ lookup: function(fullName, options) { - fullName = this.normalize(fullName); - - options = options || {}; - - if (this.cache.has(fullName) && options.singleton !== false) { - return this.cache.get(fullName); - } - - var value = instantiate(this, fullName); - - if (value === undefined) { return; } - - if (isSingleton(this, fullName) && options.singleton !== false) { - this.cache.set(fullName, value); - } - - return value; + validateFullName(fullName); + return lookup(this, this.normalize(fullName), options); }, /** @@ -10561,7 +10886,8 @@ define("container", @return {any} */ lookupFactory: function(fullName) { - return factoryFor(this, fullName); + validateFullName(fullName); + return factoryFor(this, this.normalize(fullName)); }, /** @@ -10573,11 +10899,8 @@ define("container", @return {Boolean} */ has: function(fullName) { - if (this.cache.has(fullName)) { - return true; - } - - return !!this.resolve(fullName); + validateFullName(fullName); + return has(this, this.normalize(fullName)); }, /** @@ -10658,6 +10981,7 @@ define("container", @param {String} fullName */ typeInjection: function(type, property, fullName) { + validateFullName(fullName); if (this.parent) { illegalChildOperation('typeInjection'); } addTypeInjection(this.typeInjections, type, property, fullName); @@ -10707,14 +11031,20 @@ define("container", @param {String} property @param {String} injectionName */ - injection: function(factoryName, property, injectionName) { + injection: function(fullName, property, injectionName) { if (this.parent) { illegalChildOperation('injection'); } - if (factoryName.indexOf(':') === -1) { - return this.typeInjection(factoryName, property, injectionName); + validateFullName(injectionName); + var normalizedInjectionName = this.normalize(injectionName); + + if (fullName.indexOf(':') === -1) { + return this.typeInjection(fullName, property, normalizedInjectionName); } - addInjection(this.injections, factoryName, property, injectionName); + validateFullName(fullName); + var normalizedName = this.normalize(fullName); + + addInjection(this.injections, normalizedName, property, normalizedInjectionName); }, @@ -10750,7 +11080,7 @@ define("container", factoryTypeInjection: function(type, property, fullName) { if (this.parent) { illegalChildOperation('factoryTypeInjection'); } - addTypeInjection(this.factoryTypeInjections, type, property, fullName); + addTypeInjection(this.factoryTypeInjections, type, property, this.normalize(fullName)); }, /** @@ -10802,14 +11132,21 @@ define("container", @param {String} property @param {String} injectionName */ - factoryInjection: function(factoryName, property, injectionName) { + factoryInjection: function(fullName, property, injectionName) { if (this.parent) { illegalChildOperation('injection'); } - if (factoryName.indexOf(':') === -1) { - return this.factoryTypeInjection(factoryName, property, injectionName); + var normalizedName = this.normalize(fullName); + var normalizedInjectionName = this.normalize(injectionName); + + validateFullName(injectionName); + + if (fullName.indexOf(':') === -1) { + return this.factoryTypeInjection(normalizedName, property, normalizedInjectionName); } - addInjection(this.factoryInjections, factoryName, property, injectionName); + validateFullName(fullName); + + addInjection(this.factoryInjections, normalizedName, property, normalizedInjectionName); }, /** @@ -10819,7 +11156,6 @@ define("container", @method destroy */ destroy: function() { - for (var i=0, l=this.children.length; i` and the following code: ```javascript - anUndorderedListView = Ember.CollectionView.create({ + anUnorderedListView = Ember.CollectionView.create({ tagName: 'ul', content: ['A','B','C'], itemViewClass: Ember.View.extend({ @@ -24942,7 +25392,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; }) }); - anUndorderedListView.appendTo('body'); + anUnorderedListView.appendTo('body'); ``` Will result in the following HTML structure @@ -25291,6 +25741,70 @@ Ember.CollectionView.CONTAINER_MAP = { +(function() { +/** + The ComponentTemplateDeprecation mixin is used to provide a useful + deprecation warning when using either `template` or `templateName` with + a component. The `template` and `templateName` properties specified at + extend time are moved to `layout` and `layoutName` respectively. + + `Ember.ComponentTemplateDeprecation` is used internally by Ember in + `Ember.Component`. + + @class ComponentTemplateDeprecation + @namespace Ember +*/ +Ember.ComponentTemplateDeprecation = Ember.Mixin.create({ + /** + @private + + Moves `templateName` to `layoutName` and `template` to `layout` at extend + time if a layout is not also specified. + + Note that this currently modifies the mixin themselves, which is technically + dubious but is practically of little consequence. This may change in the + future. + + @method willMergeMixin + */ + willMergeMixin: function(props) { + // must call _super here to ensure that the ActionHandler + // mixin is setup properly (moves actions -> _actions) + // + // Calling super is only OK here since we KNOW that + // there is another Mixin loaded first. + this._super.apply(this, arguments); + + var deprecatedProperty, replacementProperty, + layoutSpecified = (props.layoutName || props.layout); + + if (props.templateName && !layoutSpecified) { + deprecatedProperty = 'templateName'; + replacementProperty = 'layoutName'; + + props.layoutName = props.templateName; + delete props['templateName']; + } + + if (props.template && !layoutSpecified) { + deprecatedProperty = 'template'; + replacementProperty = 'layout'; + + props.layout = props.template; + delete props['template']; + } + + if (deprecatedProperty) { + Ember.deprecate('Do not specify ' + deprecatedProperty + ' on a Component, use ' + replacementProperty + ' instead.', false); + } + } +}); + + +})(); + + + (function() { var get = Ember.get, set = Ember.set, isNone = Ember.isNone, a_slice = Array.prototype.slice; @@ -25315,11 +25829,11 @@ var get = Ember.get, set = Ember.set, isNone = Ember.isNone, `{{my-foo}}` in other templates, which will make an instance of the isolated component. - ```html + ```handlebars {{app-profile person=currentUser}} ``` - ```html + ```handlebars

{{person.title}}

@@ -25334,7 +25848,7 @@ var get = Ember.get, set = Ember.set, isNone = Ember.isNone, ```handlebars {{#app-profile person=currentUser}}

Admin mode

- {{! Executed in the controllers context. }} + {{! Executed in the controller's context. }} {{/app-profile}} ``` @@ -25366,7 +25880,7 @@ var get = Ember.get, set = Ember.set, isNone = Ember.isNone, And then use it in the component's template: - ```html + ```handlebars

{{person.title}}

@@ -25386,18 +25900,56 @@ var get = Ember.get, set = Ember.set, isNone = Ember.isNone, @namespace Ember @extends Ember.View */ -Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { +Ember.Component = Ember.View.extend(Ember.TargetActionSupport, Ember.ComponentTemplateDeprecation, { init: function() { this._super(); set(this, 'context', this); set(this, 'controller', this); }, - defaultLayout: function(options){ - options.data = {view: options._context}; - Ember.Handlebars.helpers['yield'].apply(this, [options]); + defaultLayout: function(context, options){ + Ember.Handlebars.helpers['yield'].call(context, options); }, + /** + A components template property is set by passing a block + during its invocation. It is executed within the parent context. + + Example: + + ```handlebars + {{#my-component}} + // something that is run in the context + // of the parent context + {{/my-component}} + ``` + + Specifying a template directly to a component is deprecated without + also specifying the layout property. + + @deprecated + @property template + */ + template: Ember.computed(function(key, value) { + if (value !== undefined) { return value; } + + var templateName = get(this, 'templateName'), + template = this.templateForName(templateName, 'template'); + + Ember.assert("You specified the templateName " + templateName + " for " + this + ", but it did not exist.", !templateName || template); + + return template || get(this, 'defaultTemplate'); + }).property('templateName'), + + /** + Specifying a components `templateName` is deprecated without also + providing the `layout` or `layoutName` properties. + + @deprecated + @property templateName + */ + templateName: null, + // during render, isolate keywords cloneKeywords: function() { return { @@ -25452,9 +26004,9 @@ Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { App.PlayButtonComponent = Ember.Component.extend({ click: function(){ if (this.get('isPlaying')) { - this.triggerAction('play'); + this.sendAction('play'); } else { - this.triggerAction('stop'); + this.sendAction('stop'); } } }); @@ -25655,12 +26207,12 @@ define("metamorph", })(), // Feature-detect the W3C range API, the extended check is for IE9 which only partially supports ranges - supportsRange = (!disableRange) && document && ('createRange' in document) && (typeof Range !== 'undefined') && Range.prototype.createContextualFragment, + supportsRange = (!disableRange) && typeof document !== 'undefined' && ('createRange' in document) && (typeof Range !== 'undefined') && Range.prototype.createContextualFragment, // Internet Explorer prior to 9 does not allow setting innerHTML if the first element // is a "zero-scope" element. This problem can be worked around by making // the first node an invisible text node. We, like Modernizr, use ­ - needsShy = document && (function() { + needsShy = typeof document !== 'undefined' && (function() { var testEl = document.createElement('div'); testEl.innerHTML = "
"; testEl.firstChild.innerHTML = ""; @@ -27423,6 +27975,8 @@ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ preserveContext = get(this, 'preserveContext'), context = get(this, 'previousContext'); + var _contextController = get(this, '_contextController'); + var inverseTemplate = get(this, 'inverseTemplate'), displayTemplate = get(this, 'displayTemplate'); @@ -27442,6 +27996,10 @@ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ // Otherwise, determine if this is a block bind or not. // If so, pass the specified object to the template if (displayTemplate) { + if (_contextController) { + set(_contextController, 'content', result); + result = _contextController; + } set(this, '_context', result); } else { // This is not a bind block, just push the result of the @@ -27543,6 +28101,14 @@ function bind(property, options, preserveContext, shouldDisplay, valueNormalizer templateData: options.data }); + if (options.hash.controller) { + bindView.set('_contextController', this.container.lookupFactory('controller:'+options.hash.controller).create({ + container: currentContext.container, + parentController: currentContext, + target: currentContext + })); + } + view.appendChild(bindView); observer = function() { @@ -27617,6 +28183,17 @@ function simpleBind(currentContext, property, options) { } } +function shouldDisplayIfHelperContent(result) { + var truthy = result && get(result, 'isTruthy'); + if (typeof truthy === 'boolean') { return truthy; } + + if (Ember.isArray(result)) { + return get(result, 'length') !== 0; + } else { + return !!result; + } +} + /** '_triageMustache' is used internally select between a binding, helper, or component for the given context. Until this point, it would be hard to determine if the @@ -27721,18 +28298,43 @@ EmberHandlebars.registerHelper('bind', function bindHelper(property, options) { */ EmberHandlebars.registerHelper('boundIf', function boundIfHelper(property, fn) { var context = (fn.contexts && fn.contexts.length) ? fn.contexts[0] : this; - var func = function(result) { - var truthy = result && get(result, 'isTruthy'); - if (typeof truthy === 'boolean') { return truthy; } - if (Ember.isArray(result)) { - return get(result, 'length') !== 0; - } else { - return !!result; - } - }; + return bind.call(context, property, fn, true, shouldDisplayIfHelperContent, shouldDisplayIfHelperContent, ['isTruthy', 'length']); +}); - return bind.call(context, property, fn, true, func, func, ['isTruthy', 'length']); + +/** + @private + + Use the `unboundIf` helper to create a conditional that evaluates once. + + ```handlebars + {{#unboundIf "content.shouldDisplayTitle"}} + {{content.title}} + {{/unboundIf}} + ``` + + @method unboundIf + @for Ember.Handlebars.helpers + @param {String} property Property to bind + @param {Function} fn Context to provide for rendering + @return {String} HTML string +*/ +EmberHandlebars.registerHelper('unboundIf', function unboundIfHelper(property, fn) { + var context = (fn.contexts && fn.contexts.length) ? fn.contexts[0] : this, + data = fn.data, + template = fn.fn, + inverse = fn.inverse, + normalized, propertyValue, result; + + normalized = normalizePath(context, property, data); + propertyValue = handlebarsGet(context, property, fn); + + if (!shouldDisplayIfHelperContent(propertyValue)) { + template = inverse; + } + + template(context, { data: data }); }); /** @@ -27786,6 +28388,23 @@ EmberHandlebars.registerHelper('boundIf', function boundIfHelper(property, fn) { Without the `as` operator, it would be impossible to reference `user.name` in the example above. + ### `controller` option + + Adding `controller='something'` instructs the `{{with}}` helper to create and use an instance of + the specified controller with the new context as its content. + + This is very similar to using an `itemController` option with the `{{each}}` helper. + + ```handlebars + {{#with users.posts controller='userBlogPosts'}} + {{!- The current context is wrapped in our controller instance }} + {{/with}} + ``` + + In the above example, the template provided to the `{{with}}` block is now wrapped in the + `userBlogPost` controller, which provides a very elegant way to decorate the context with custom + functions/properties. + @method with @for Ember.Handlebars.helpers @param {Function} context @@ -27835,6 +28454,7 @@ EmberHandlebars.registerHelper('with', function withHelper(context, options) { /** See [boundIf](/api/classes/Ember.Handlebars.helpers.html#method_boundIf) + and [unboundIf](/api/classes/Ember.Handlebars.helpers.html#method_unboundIf) @method if @for Ember.Handlebars.helpers @@ -27845,8 +28465,11 @@ EmberHandlebars.registerHelper('with', function withHelper(context, options) { EmberHandlebars.registerHelper('if', function ifHelper(context, options) { Ember.assert("You must pass exactly one argument to the if helper", arguments.length === 2); Ember.assert("You must pass a block to the if helper", options.fn && options.fn !== Handlebars.VM.noop); - - return helpers.boundIf.call(options.contexts[0], context, options); + if (options.data.isUnbound) { + return helpers.unboundIf.call(options.contexts[0], context, options); + } else { + return helpers.boundIf.call(options.contexts[0], context, options); + } }); /** @@ -27865,7 +28488,11 @@ EmberHandlebars.registerHelper('unless', function unlessHelper(context, options) options.fn = inverse; options.inverse = fn; - return helpers.boundIf.call(options.contexts[0], context, options); + if (options.data.isUnbound) { + return helpers.unboundIf.call(options.contexts[0], context, options); + } else { + return helpers.boundIf.call(options.contexts[0], context, options); + } }); /** @@ -28641,7 +29268,7 @@ var get = Ember.get, handlebarsGet = Ember.Handlebars.get, fmt = Ember.String.fm ``` - ### Blockless Use + ### Blockless use in a collection If you provide an `itemViewClass` option that has its own `template` you can omit the block. @@ -28738,11 +29365,20 @@ Ember.Handlebars.registerHelper('collection', function collectionHelper(path, op var inverse = options.inverse; var view = options.data.view; + + var controller, container; // If passed a path string, convert that into an object. // Otherwise, just default to the standard class. var collectionClass; - collectionClass = path ? handlebarsGet(this, path, options) : Ember.CollectionView; - Ember.assert(fmt("%@ #collection: Could not find collection class %@", [data.view, path]), !!collectionClass); + if (path) { + controller = data.keywords.controller; + container = controller && controller.container; + collectionClass = handlebarsGet(this, path, options) || container.lookupFactory('view:' + path); + Ember.assert(fmt("%@ #collection: Could not find collection class %@", [data.view, path]), !!collectionClass); + } + else { + collectionClass = Ember.CollectionView; + } var hash = options.hash, itemHash = {}, match; @@ -28751,15 +29387,15 @@ Ember.Handlebars.registerHelper('collection', function collectionHelper(path, op itemViewClass; if (hash.itemView) { - var controller = data.keywords.controller; + controller = data.keywords.controller; Ember.assert('You specified an itemView, but the current context has no ' + 'container to look the itemView up in. This probably means ' + 'that you created a view manually, instead of through the ' + 'container. Instead, use container.lookup("view:viewName"), ' + 'which will properly instantiate your view.', controller && controller.container); - var container = controller.container; - itemViewClass = container.resolve('view:' + hash.itemView); + container = controller.container; + itemViewClass = container.lookupFactory('view:' + hash.itemView); Ember.assert('You specified the itemView ' + hash.itemView + ", but it was " + "not found at " + container.describe("view:" + hash.itemView) + " (and it was not registered in the container)", !!itemViewClass); @@ -28957,6 +29593,7 @@ Ember.Handlebars.registerHelper('debugger', function debuggerHelper(options) { */ var get = Ember.get, set = Ember.set; +var fmt = Ember.String.fmt; Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { init: function() { @@ -28965,6 +29602,7 @@ Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { if (itemController) { var controller = get(this, 'controller.container').lookupFactory('controller:array').create({ + _isVirtual: true, parentController: get(this, 'controller'), itemController: itemController, target: get(this, 'controller'), @@ -28989,8 +29627,13 @@ Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { }, _assertArrayLike: function(content) { - Ember.assert("The value that #each loops over must be an Array. You passed " + content.constructor + ", but it should have been an ArrayController", !Ember.ControllerMixin.detect(content) || (content && content.isGenerated) || content instanceof Ember.ArrayController); - Ember.assert("The value that #each loops over must be an Array. You passed " + ((Ember.ControllerMixin.detect(content) && content.get('model') !== undefined) ? ("" + content.get('model') + " (wrapped in " + content + ")") : ("" + content)), Ember.Array.detect(content)); + Ember.assert(fmt("The value that #each loops over must be an Array. You " + + "passed %@, but it should have been an ArrayController", + [content.constructor]), + !Ember.ControllerMixin.detect(content) || + (content && content.isGenerated) || + content instanceof Ember.ArrayController); + Ember.assert(fmt("The value that #each loops over must be an Array. You passed %@", [(Ember.ControllerMixin.detect(content) && content.get('model') !== undefined) ? fmt("'%@' (wrapped in %@)", [content.get('model'), content]) : content]), Ember.Array.detect(content)); }, disableContentObservers: function(callback) { @@ -29949,7 +30592,7 @@ Ember.TextField = Ember.Component.extend(Ember.TextSupport, { classNames: ['ember-text-field'], tagName: "input", - attributeBindings: ['type', 'value', 'size', 'pattern', 'name'], + attributeBindings: ['type', 'value', 'size', 'pattern', 'name', 'min', 'max'], /** The `value` attribute of the input element. As the user inputs text, this @@ -29980,141 +30623,31 @@ Ember.TextField = Ember.Component.extend(Ember.TextSupport, { size: null, /** - The `pattern` the pattern attribute of input element. + The `pattern` attribute of input element. @property pattern @type String @default null */ - pattern: null -}); + pattern: null, -})(); + /** + The `min` attribute of input element used with `type="number"` or `type="range"`. - - -(function() { -/* -@module ember -@submodule ember-handlebars -*/ - -var get = Ember.get, set = Ember.set; - -/* - @class Button - @namespace Ember - @extends Ember.View - @uses Ember.TargetActionSupport - @deprecated -*/ -Ember.Button = Ember.View.extend(Ember.TargetActionSupport, { - classNames: ['ember-button'], - classNameBindings: ['isActive'], - - tagName: 'button', - - propagateEvents: false, - - attributeBindings: ['type', 'disabled', 'href', 'tabindex'], - - /* - @private - - Overrides `TargetActionSupport`'s `targetObject` computed - property to use Handlebars-specific path resolution. - - @property targetObject + @property min + @type String + @default null */ - targetObject: Ember.computed(function() { - var target = get(this, 'target'), - root = get(this, 'context'), - data = get(this, 'templateData'); + min: null, - if (typeof target !== 'string') { return target; } + /** + The `max` attribute of input element used with `type="number"` or `type="range"`. - return Ember.Handlebars.get(root, target, { data: data }); - }).property('target'), - - // Defaults to 'button' if tagName is 'input' or 'button' - type: Ember.computed(function(key) { - var tagName = this.tagName; - if (tagName === 'input' || tagName === 'button') { return 'button'; } - }), - - disabled: false, - - // Allow 'a' tags to act like buttons - href: Ember.computed(function() { - return this.tagName === 'a' ? '#' : null; - }), - - mouseDown: function() { - if (!get(this, 'disabled')) { - set(this, 'isActive', true); - this._mouseDown = true; - this._mouseEntered = true; - } - return get(this, 'propagateEvents'); - }, - - mouseLeave: function() { - if (this._mouseDown) { - set(this, 'isActive', false); - this._mouseEntered = false; - } - }, - - mouseEnter: function() { - if (this._mouseDown) { - set(this, 'isActive', true); - this._mouseEntered = true; - } - }, - - mouseUp: function(event) { - if (get(this, 'isActive')) { - // Actually invoke the button's target and action. - // This method comes from the Ember.TargetActionSupport mixin. - this.triggerAction(); - set(this, 'isActive', false); - } - - this._mouseDown = false; - this._mouseEntered = false; - return get(this, 'propagateEvents'); - }, - - keyDown: function(event) { - // Handle space or enter - if (event.keyCode === 13 || event.keyCode === 32) { - this.mouseDown(); - } - }, - - keyUp: function(event) { - // Handle space or enter - if (event.keyCode === 13 || event.keyCode === 32) { - this.mouseUp(); - } - }, - - // TODO: Handle proper touch behavior. Including should make inactive when - // finger moves more than 20x outside of the edge of the button (vs mouse - // which goes inactive as soon as mouse goes out of edges.) - - touchStart: function(touch) { - return this.mouseDown(touch); - }, - - touchEnd: function(touch) { - return this.mouseUp(touch); - }, - - init: function() { - Ember.deprecate("Ember.Button is deprecated and will be removed from future releases. Consider using the `{{action}}` helper."); - this._super(); - } + @property max + @type String + @default null + */ + max: null }); })(); @@ -30506,15 +31039,13 @@ Ember.Select = Ember.View.extend({ defaultTemplate: Ember.Handlebars.template(function anonymous(Handlebars,depth0,helpers,partials,data) { this.compilerInfo = [4,'>= 1.0.0']; helpers = this.merge(helpers, Ember.Handlebars.helpers); data = data || {}; - var buffer = '', stack1, hashTypes, hashContexts, escapeExpression=this.escapeExpression, self=this; + var buffer = '', stack1, escapeExpression=this.escapeExpression, self=this; function program1(depth0,data) { - var buffer = '', stack1, hashTypes, hashContexts; + var buffer = '', stack1; data.buffer.push(""); return buffer; @@ -30522,50 +31053,38 @@ function program1(depth0,data) { function program3(depth0,data) { - var stack1, hashTypes, hashContexts; - hashTypes = {}; - hashContexts = {}; - stack1 = helpers.each.call(depth0, "view.groupedContent", {hash:{},inverse:self.noop,fn:self.program(4, program4, data),contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}); + var stack1; + stack1 = helpers.each.call(depth0, "view.groupedContent", {hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(4, program4, data),contexts:[depth0],types:["ID"],data:data}); if(stack1 || stack1 === 0) { data.buffer.push(stack1); } else { data.buffer.push(''); } } function program4(depth0,data) { - var hashContexts, hashTypes; - hashContexts = {'content': depth0,'label': depth0}; - hashTypes = {'content': "ID",'label': "ID"}; + data.buffer.push(escapeExpression(helpers.view.call(depth0, "view.groupView", {hash:{ 'content': ("content"), 'label': ("label") - },contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}))); + },hashTypes:{'content': "ID",'label': "ID"},hashContexts:{'content': depth0,'label': depth0},contexts:[depth0],types:["ID"],data:data}))); } function program6(depth0,data) { - var stack1, hashTypes, hashContexts; - hashTypes = {}; - hashContexts = {}; - stack1 = helpers.each.call(depth0, "view.content", {hash:{},inverse:self.noop,fn:self.program(7, program7, data),contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}); + var stack1; + stack1 = helpers.each.call(depth0, "view.content", {hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(7, program7, data),contexts:[depth0],types:["ID"],data:data}); if(stack1 || stack1 === 0) { data.buffer.push(stack1); } else { data.buffer.push(''); } } function program7(depth0,data) { - var hashContexts, hashTypes; - hashContexts = {'content': depth0}; - hashTypes = {'content': "ID"}; + data.buffer.push(escapeExpression(helpers.view.call(depth0, "view.optionView", {hash:{ 'content': ("") - },contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}))); + },hashTypes:{'content': "ID"},hashContexts:{'content': depth0},contexts:[depth0],types:["ID"],data:data}))); } - hashTypes = {}; - hashContexts = {}; - stack1 = helpers['if'].call(depth0, "view.prompt", {hash:{},inverse:self.noop,fn:self.program(1, program1, data),contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}); + stack1 = helpers['if'].call(depth0, "view.prompt", {hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(1, program1, data),contexts:[depth0],types:["ID"],data:data}); if(stack1 || stack1 === 0) { data.buffer.push(stack1); } - hashTypes = {}; - hashContexts = {}; - stack1 = helpers['if'].call(depth0, "view.optionGroupPath", {hash:{},inverse:self.program(6, program6, data),fn:self.program(3, program3, data),contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}); + stack1 = helpers['if'].call(depth0, "view.optionGroupPath", {hash:{},hashTypes:{},hashContexts:{},inverse:self.program(6, program6, data),fn:self.program(3, program3, data),contexts:[depth0],types:["ID"],data:data}); if(stack1 || stack1 === 0) { data.buffer.push(stack1); } return buffer; @@ -31004,6 +31523,7 @@ Ember.Handlebars.registerHelper('input', function(options) { delete hash.on; if (inputType === 'checkbox') { + Ember.assert("{{input type='checkbox'}} does not support setting `value=someBooleanValue`; you must use `checked=someBooleanValue` instead.", options.hashTypes.value !== 'ID'); return Ember.Handlebars.helpers.view.call(this, Ember.Checkbox, options); } else { if (inputType) { hash.type = inputType; } @@ -31307,9 +31827,9 @@ Ember.runLoadHooks('Ember.Handlebars', Ember.Handlebars); })(); (function() { -define("route-recognizer", - [], - function() { +define("route-recognizer", + ["exports"], + function(__exports__) { "use strict"; var specials = [ '/', '.', '*', '+', '?', '|', @@ -31338,11 +31858,11 @@ define("route-recognizer", function StaticSegment(string) { this.string = string; } StaticSegment.prototype = { eachChar: function(callback) { - var string = this.string, char; + var string = this.string, ch; for (var i=0, l=string.length; i 0) { - currentResult.queryParams = activeQueryParams; - } - result.push(currentResult); + + result.push({ handler: handler.handler, params: params, isDynamic: !!names.length }); } return result; } function addSegment(currentState, segment) { - segment.eachChar(function(char) { + segment.eachChar(function(ch) { var state; - currentState = currentState.put(char); + currentState = currentState.put(ch); }); return currentState; @@ -31639,9 +32164,6 @@ define("route-recognizer", } var handler = { handler: route.handler, names: names }; - if(route.queryParams) { - handler.queryParams = route.queryParams; - } handlers.push(handler); } @@ -31702,24 +32224,26 @@ define("route-recognizer", }, generateQueryString: function(params, handlers) { - var pairs = [], allowedParams = []; - for(var i=0; i < handlers.length; i++) { - var currentParamList = handlers[i].queryParams; - if(currentParamList) { - allowedParams.push.apply(allowedParams, currentParamList); - } - } + var pairs = []; for(var key in params) { if (params.hasOwnProperty(key)) { - if(allowedParams.indexOf(key) === -1) { - throw 'Query param "' + key + '" is not specified as a valid param for this route'; - } var value = params[key]; - var pair = encodeURIComponent(key); - if(value !== true) { - pair += "=" + encodeURIComponent(value); + if (value === false || value == null) { + continue; + } + var pair = key; + if (Array.isArray(value)) { + for (var i = 0, l = value.length; i < l; i++) { + var arrayPair = key + '[]' + '=' + encodeURIComponent(value[i]); + pairs.push(arrayPair); + } + } + else if (value !== true) { + pair += "=" + encodeURIComponent(value); + pairs.push(pair); + } else { + pairs.push(pair); } - pairs.push(pair); } } @@ -31733,15 +32257,36 @@ define("route-recognizer", for(var i=0; i < pairs.length; i++) { var pair = pairs[i].split('='), key = decodeURIComponent(pair[0]), - value = pair[1] ? decodeURIComponent(pair[1]) : true; - queryParams[key] = value; + keyLength = key.length, + isArray = false, + value; + if (pair.length === 1) { + value = true; + } else { + //Handle arrays + if (keyLength > 2 && key.slice(keyLength -2) === '[]') { + isArray = true; + key = key.slice(0, keyLength - 2); + if(!queryParams[key]) { + queryParams[key] = []; + } + } + value = pair[1] ? decodeURIComponent(pair[1]) : ''; + } + if (isArray) { + queryParams[key].push(value); + } else { + queryParams[key] = value; + } + } return queryParams; }, recognize: function(path) { var states = [ this.rootState ], - pathLen, i, l, queryStart, queryParams = {}; + pathLen, i, l, queryStart, queryParams = {}, + isSlashDropped = false; queryStart = path.indexOf('?'); if (queryStart !== -1) { @@ -31757,6 +32302,7 @@ define("route-recognizer", pathLen = path.length; if (pathLen > 1 && path.charAt(pathLen - 1) === "/") { path = path.substr(0, pathLen - 1); + isSlashDropped = true; } for (i=0, l=path.length; i 0) { - var err = 'You supplied the params '; - err += missingParams.map(function(param) { - return '"' + param + "=" + queryParams[param] + '"'; - }).join(' and '); - - err += ' which are not valid for the "' + handlerName + '" handler or its parents'; - - throw new Error(err); + for (var i = 0, len = state.handlerInfos.length; i < len; ++i) { + var handlerInfo = state.handlerInfos[i]; + var handlerParams = handlerInfo.params || + serialize(handlerInfo.handler, handlerInfo.context, handlerInfo.names); + merge(params, handlerParams); } + params.queryParams = queryParams; return this.recognizer.generate(handlerName, params); }, isActive: function(handlerName) { + var partitionedArgs = extractQueryParams(slice.call(arguments, 1)), contexts = partitionedArgs[0], queryParams = partitionedArgs[1], - activeQueryParams = {}, - effectiveQueryParams = {}; + activeQueryParams = this.state.queryParams; - var targetHandlerInfos = this.targetHandlerInfos, - found = false, names, object, handlerInfo, handlerObj; + var targetHandlerInfos = this.state.handlerInfos, + found = false, names, object, handlerInfo, handlerObj, i, len; - if (!targetHandlerInfos) { return false; } + if (!targetHandlerInfos.length) { return false; } - var recogHandlers = this.recognizer.handlersFor(targetHandlerInfos[targetHandlerInfos.length - 1].name); - for (var i=targetHandlerInfos.length-1; i>=0; i--) { - handlerInfo = targetHandlerInfos[i]; - if (handlerInfo.name === handlerName) { found = true; } + var targetHandler = targetHandlerInfos[targetHandlerInfos.length - 1].name; + var recogHandlers = this.recognizer.handlersFor(targetHandler); - if (found) { - var recogHandler = recogHandlers[i]; - - merge(activeQueryParams, handlerInfo.queryParams); - if (queryParams !== false) { - merge(effectiveQueryParams, handlerInfo.queryParams); - mergeSomeKeys(effectiveQueryParams, queryParams, recogHandler.queryParams); - } - - if (handlerInfo.isDynamic && contexts.length > 0) { - object = contexts.pop(); - - if (isParam(object)) { - var name = recogHandler.names[0]; - if (!this.currentParams || "" + object !== this.currentParams[name]) { return false; } - } else if (handlerInfo.context !== object) { - return false; - } - } - } + var index = 0; + for (len = recogHandlers.length; index < len; ++index) { + handlerInfo = targetHandlerInfos[index]; + if (handlerInfo.name === handlerName) { break; } } + if (index === recogHandlers.length) { + // The provided route name isn't even in the route hierarchy. + return false; + } - return contexts.length === 0 && found && queryParamsEqual(activeQueryParams, effectiveQueryParams); + var state = new TransitionState(); + state.handlerInfos = targetHandlerInfos.slice(0, index + 1); + recogHandlers = recogHandlers.slice(0, index + 1); + + var intent = new NamedTransitionIntent({ + name: targetHandler, + contexts: contexts + }); + + var newState = intent.applyToHandlers(state, recogHandlers, this.getHandler, targetHandler, true, true); + + return handlerInfosEqual(newState.handlerInfos, state.handlerInfos) && + !getChangelist(activeQueryParams, queryParams); }, trigger: function(name) { @@ -32331,6 +32983,24 @@ define("router", trigger(this, this.currentHandlerInfos, false, args); }, + /** + @private + + Pluggable hook for possibly running route hooks + in a try-catch escaping manner. + + @param {Function} callback the callback that will + be asynchronously called + + @return {Promise} a promise that fulfills with the + value returned from the callback + */ + async: function(callback) { + return new Promise(function(resolve) { + resolve(callback()); + }); + }, + /** Hook point for logging transition status updates. @@ -32339,299 +33009,6 @@ define("router", log: null }; - /** - @private - - Used internally for both URL and named transition to determine - a shared pivot parent route and other data necessary to perform - a transition. - */ - function getMatchPoint(router, handlers, objects, inputParams, queryParams) { - - var matchPoint = handlers.length, - providedModels = {}, i, - currentHandlerInfos = router.currentHandlerInfos || [], - params = {}, - oldParams = router.currentParams || {}, - activeTransition = router.activeTransition, - handlerParams = {}, - obj; - - objects = slice.call(objects); - merge(params, inputParams); - - for (i = handlers.length - 1; i >= 0; i--) { - var handlerObj = handlers[i], - handlerName = handlerObj.handler, - oldHandlerInfo = currentHandlerInfos[i], - hasChanged = false; - - // Check if handler names have changed. - if (!oldHandlerInfo || oldHandlerInfo.name !== handlerObj.handler) { hasChanged = true; } - - if (handlerObj.isDynamic) { - // URL transition. - - if (obj = getMatchPointObject(objects, handlerName, activeTransition, true, params)) { - hasChanged = true; - providedModels[handlerName] = obj; - } else { - handlerParams[handlerName] = {}; - for (var prop in handlerObj.params) { - if (!handlerObj.params.hasOwnProperty(prop)) { continue; } - var newParam = handlerObj.params[prop]; - if (oldParams[prop] !== newParam) { hasChanged = true; } - handlerParams[handlerName][prop] = params[prop] = newParam; - } - } - } else if (handlerObj.hasOwnProperty('names')) { - // Named transition. - - if (objects.length) { hasChanged = true; } - - if (obj = getMatchPointObject(objects, handlerName, activeTransition, handlerObj.names[0], params)) { - providedModels[handlerName] = obj; - } else { - var names = handlerObj.names; - handlerParams[handlerName] = {}; - for (var j = 0, len = names.length; j < len; ++j) { - var name = names[j]; - handlerParams[handlerName][name] = params[name] = params[name] || oldParams[name]; - } - } - } - - // If there is an old handler, see if query params are the same. If there isn't an old handler, - // hasChanged will already be true here - if(oldHandlerInfo && !queryParamsEqual(oldHandlerInfo.queryParams, handlerObj.queryParams)) { - hasChanged = true; - } - - if (hasChanged) { matchPoint = i; } - } - - if (objects.length > 0) { - throw new Error("More context objects were passed than there are dynamic segments for the route: " + handlers[handlers.length - 1].handler); - } - - var pivotHandlerInfo = currentHandlerInfos[matchPoint - 1], - pivotHandler = pivotHandlerInfo && pivotHandlerInfo.handler; - - return { matchPoint: matchPoint, providedModels: providedModels, params: params, handlerParams: handlerParams, pivotHandler: pivotHandler }; - } - - function getMatchPointObject(objects, handlerName, activeTransition, paramName, params) { - - if (objects.length && paramName) { - - var object = objects.pop(); - - // If provided object is string or number, treat as param. - if (isParam(object)) { - params[paramName] = object.toString(); - } else { - return object; - } - } else if (activeTransition) { - // Use model from previous transition attempt, preferably the resolved one. - return activeTransition.resolvedModels[handlerName] || - (paramName && activeTransition.providedModels[handlerName]); - } - } - - function isParam(object) { - return (typeof object === "string" || object instanceof String || typeof object === "number" || object instanceof Number); - } - - - - /** - @private - - This method takes a handler name and returns a list of query params - that are valid to pass to the handler or its parents - - @param {Router} router - @param {String} handlerName - @return {Array[String]} a list of query parameters - */ - function queryParamsForHandler(router, handlerName) { - var handlers = router.recognizer.handlersFor(handlerName), - queryParams = []; - - for (var i = 0; i < handlers.length; i++) { - queryParams.push.apply(queryParams, handlers[i].queryParams || []); - } - - return queryParams; - } - /** - @private - - This method takes a handler name and a list of contexts and returns - a serialized parameter hash suitable to pass to `recognizer.generate()`. - - @param {Router} router - @param {String} handlerName - @param {Array[Object]} objects - @return {Object} a serialized parameter hash - */ - function paramsForHandler(router, handlerName, objects, queryParams) { - - var handlers = router.recognizer.handlersFor(handlerName), - params = {}, - handlerInfos = generateHandlerInfosWithQueryParams(router, handlers, queryParams), - matchPoint = getMatchPoint(router, handlerInfos, objects).matchPoint, - mergedQueryParams = {}, - object, handlerObj, handler, names, i; - - params.queryParams = {}; - - for (i=0; i= matchPoint) { - object = objects.shift(); - // Otherwise use existing context - } else { - object = handler.context; - } - - // Serialize to generate params - merge(params, serialize(handler, object, names)); - } - if (queryParams !== false) { - mergeSomeKeys(params.queryParams, router.currentQueryParams, handlerObj.queryParams); - mergeSomeKeys(params.queryParams, queryParams, handlerObj.queryParams); - } - } - - if (queryParamsEqual(params.queryParams, {})) { delete params.queryParams; } - return params; - } - - function merge(hash, other) { - for (var prop in other) { - if (other.hasOwnProperty(prop)) { hash[prop] = other[prop]; } - } - } - - function mergeSomeKeys(hash, other, keys) { - if (!other || !keys) { return; } - for(var i = 0; i < keys.length; i++) { - var key = keys[i], value; - if(other.hasOwnProperty(key)) { - value = other[key]; - if(value === null || value === false || typeof value === "undefined") { - delete hash[key]; - } else { - hash[key] = other[key]; - } - } - } - } - - /** - @private - */ - - function generateHandlerInfosWithQueryParams(router, handlers, queryParams) { - var handlerInfos = []; - - for (var i = 0; i < handlers.length; i++) { - var handler = handlers[i], - handlerInfo = { handler: handler.handler, names: handler.names, context: handler.context, isDynamic: handler.isDynamic }, - activeQueryParams = {}; - - if (queryParams !== false) { - mergeSomeKeys(activeQueryParams, router.currentQueryParams, handler.queryParams); - mergeSomeKeys(activeQueryParams, queryParams, handler.queryParams); - } - - if (handler.queryParams && handler.queryParams.length > 0) { - handlerInfo.queryParams = activeQueryParams; - } - - handlerInfos.push(handlerInfo); - } - - return handlerInfos; - } - - /** - @private - */ - function createQueryParamTransition(router, queryParams, isIntermediate) { - var currentHandlers = router.currentHandlerInfos, - currentHandler = currentHandlers[currentHandlers.length - 1], - name = currentHandler.name; - - log(router, "Attempting query param transition"); - - return createNamedTransition(router, [name, queryParams], isIntermediate); - } - - /** - @private - */ - function createNamedTransition(router, args, isIntermediate) { - var partitionedArgs = extractQueryParams(args), - pureArgs = partitionedArgs[0], - queryParams = partitionedArgs[1], - handlers = router.recognizer.handlersFor(pureArgs[0]), - handlerInfos = generateHandlerInfosWithQueryParams(router, handlers, queryParams); - - - log(router, "Attempting transition to " + pureArgs[0]); - - return performTransition(router, - handlerInfos, - slice.call(pureArgs, 1), - router.currentParams, - queryParams, - null, - isIntermediate); - } - - /** - @private - */ - function createURLTransition(router, url, isIntermediate) { - var results = router.recognizer.recognize(url), - currentHandlerInfos = router.currentHandlerInfos, - queryParams = {}, - i, len; - - log(router, "Attempting URL transition to " + url); - - if (results) { - // Make sure this route is actually accessible by URL. - for (i = 0, len = results.length; i < len; ++i) { - - if (router.getHandler(results[i].handler).inaccessibleByURL) { - results = null; - break; - } - } - } - - if (!results) { - return errorTransition(router, new Router.UnrecognizedURLError(url)); - } - - for(i = 0, len = results.length; i < len; i++) { - merge(queryParams, results[i].queryParams); - } - - return performTransition(router, results, [], {}, queryParams, null, isIntermediate); - } - - /** @private @@ -32670,104 +33047,70 @@ define("router", 3. Triggers the `enter` callback on `about` 4. Triggers the `setup` callback on `about` - @param {Transition} transition - @param {Array[HandlerInfo]} handlerInfos + @param {Router} transition + @param {TransitionState} newState */ - function setupContexts(transition, handlerInfos) { - var router = transition.router, - partition = partitionHandlers(router.currentHandlerInfos || [], handlerInfos); + function setupContexts(router, newState, transition) { + var partition = partitionHandlers(router.state, newState); - router.targetHandlerInfos = handlerInfos; - - eachHandler(partition.exited, function(handlerInfo) { + forEach(partition.exited, function(handlerInfo) { var handler = handlerInfo.handler; delete handler.context; if (handler.exit) { handler.exit(); } }); - var currentHandlerInfos = partition.unchanged.slice(); - router.currentHandlerInfos = currentHandlerInfos; + var oldState = router.oldState = router.state; + router.state = newState; + var currentHandlerInfos = router.currentHandlerInfos = partition.unchanged.slice(); - eachHandler(partition.updatedContext, function(handlerInfo) { - handlerEnteredOrUpdated(transition, currentHandlerInfos, handlerInfo, false); - }); + try { + forEach(partition.updatedContext, function(handlerInfo) { + return handlerEnteredOrUpdated(currentHandlerInfos, handlerInfo, false, transition); + }); - eachHandler(partition.entered, function(handlerInfo) { - handlerEnteredOrUpdated(transition, currentHandlerInfos, handlerInfo, true); - }); + forEach(partition.entered, function(handlerInfo) { + return handlerEnteredOrUpdated(currentHandlerInfos, handlerInfo, true, transition); + }); + } catch(e) { + router.state = oldState; + router.currentHandlerInfos = oldState.handlerInfos; + throw e; + } + + router.state.queryParams = finalizeQueryParamChange(router, currentHandlerInfos, newState.queryParams); } + /** @private Helper method used by setupContexts. Handles errors or redirects that may happen in enter/setup. */ - function handlerEnteredOrUpdated(transition, currentHandlerInfos, handlerInfo, enter) { + function handlerEnteredOrUpdated(currentHandlerInfos, handlerInfo, enter, transition) { + var handler = handlerInfo.handler, context = handlerInfo.context; - try { - if (enter && handler.enter) { handler.enter(); } - checkAbort(transition); + if (enter && handler.enter) { handler.enter(transition); } + if (transition && transition.isAborted) { + throw new TransitionAborted(); + } - setContext(handler, context); - setQueryParams(handler, handlerInfo.queryParams); + handler.context = context; + if (handler.contextDidChange) { handler.contextDidChange(); } - if (handler.setup) { handler.setup(context, handlerInfo.queryParams); } - checkAbort(transition); - } catch(e) { - if (!(e instanceof Router.TransitionAborted)) { - // Trigger the `error` event starting from this failed handler. - transition.trigger(true, 'error', e, transition, handler); - } - - // Propagate the error so that the transition promise will reject. - throw e; + if (handler.setup) { handler.setup(context, transition); } + if (transition && transition.isAborted) { + throw new TransitionAborted(); } currentHandlerInfos.push(handlerInfo); - } - - /** - @private - - Iterates over an array of `HandlerInfo`s, passing the handler - and context into the callback. - - @param {Array[HandlerInfo]} handlerInfos - @param {Function(Object, Object)} callback - */ - function eachHandler(handlerInfos, callback) { - for (var i=0, l=handlerInfos.length; i= 0; --i) { + var handlerInfo = handlerInfos[i]; + merge(params, handlerInfo.params); + if (handlerInfo.handler.inaccessibleByURL) { + urlMethod = null; + } + } + + if (urlMethod) { + params.queryParams = state.queryParams; + var url = router.recognizer.generate(handlerName, params); + + if (urlMethod === 'replaceQuery') { + if (url !== inputUrl) { + router.replaceURL(url); + } + } else if (urlMethod === 'replace') { + router.replaceURL(url); + } else { + router.updateURL(url); + } + } + } + + /** + @private + + Updates the URL (if necessary) and calls `setupContexts` + to update the router's array of `currentHandlerInfos`. + */ + function finalizeTransition(transition, newState) { + + try { + log(transition.router, transition.sequence, "Resolved all models on destination route; finalizing transition."); + + var router = transition.router, + handlerInfos = newState.handlerInfos, + seq = transition.sequence; + + // Run all the necessary enter/setup/exit hooks + setupContexts(router, newState, transition); + + // Check if a redirect occurred in enter/setup + if (transition.isAborted) { + // TODO: cleaner way? distinguish b/w targetHandlerInfos? + router.state.handlerInfos = router.currentHandlerInfos; + return reject(logAbort(transition)); + } + + updateURL(transition, newState, transition.intent.url); + + transition.isActive = false; + router.activeTransition = null; + + trigger(router, router.currentHandlerInfos, true, ['didTransition']); + + if (router.didTransition) { + router.didTransition(router.currentHandlerInfos); + } + + log(router, transition.sequence, "TRANSITION COMPLETE."); + + // Resolve with the final handler. + return handlerInfos[handlerInfos.length - 1].handler; + } catch(e) { + if (!(e instanceof TransitionAborted)) { + //var erroneousHandler = handlerInfos.pop(); + var infos = transition.state.handlerInfos; + transition.trigger(true, 'error', e, transition, infos[infos.length-1]); + transition.abort(); + } + + throw e; + } + } + + /** + @private + + Begins and returns a Transition based on the provided + arguments. Accepts arguments in the form of both URL + transitions and named transitions. + + @param {Router} router + @param {Array[Object]} args arguments passed to transitionTo, + replaceWith, or handleURL + */ + function doTransition(router, args, isIntermediate) { + // Normalize blank transitions to root URL transitions. + var name = args[0] || '/'; + + var lastArg = args[args.length-1]; + var queryParams = {}; + if (lastArg && lastArg.hasOwnProperty('queryParams')) { + queryParams = pop.call(args).queryParams; + } + + var intent; + if (args.length === 0) { + + log(router, "Updating query params"); + + // A query param update is really just a transition + // into the route you're already on. + var handlerInfos = router.state.handlerInfos; + intent = new NamedTransitionIntent({ + name: handlerInfos[handlerInfos.length - 1].name, + contexts: [], + queryParams: queryParams + }); + + } else if (name.charAt(0) === '/') { + + log(router, "Attempting URL transition to " + name); + intent = new URLTransitionIntent({ url: name }); + + } else { + + log(router, "Attempting transition to " + name); + intent = new NamedTransitionIntent({ + name: args[0], + contexts: slice.call(args, 1), + queryParams: queryParams + }); + } + + return router.transitionByIntent(intent, isIntermediate); + } + + function handlerInfosEqual(handlerInfos, otherHandlerInfos) { + if (handlerInfos.length !== otherHandlerInfos.length) { + return false; + } + + for (var i = 0, len = handlerInfos.length; i < len; ++i) { + if (handlerInfos[i] !== otherHandlerInfos[i]) { + return false; + } + } + return true; + } + + function finalizeQueryParamChange(router, resolvedHandlers, newQueryParams) { + // We fire a finalizeQueryParamChange event which + // gives the new route hierarchy a chance to tell + // us which query params it's consuming and what + // their final values are. If a query param is + // no longer consumed in the final route hierarchy, + // its serialized segment will be removed + // from the URL. + var finalQueryParamsArray = []; + trigger(router, resolvedHandlers, true, ['finalizeQueryParamChange', newQueryParams, finalQueryParamsArray]); + + var finalQueryParams = {}; + for (var i = 0, len = finalQueryParamsArray.length; i < len; ++i) { + var qp = finalQueryParamsArray[i]; + finalQueryParams[qp.key] = qp.value; + } + return finalQueryParams; + } + + __exports__.Router = Router; + }); +define("router/transition-intent", + ["./utils","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var merge = __dependency1__.merge; + + function TransitionIntent(props) { + if (props) { + merge(this, props); + } + this.data = this.data || {}; + } + + TransitionIntent.prototype.applyToState = function(oldState) { + // Default TransitionIntent is a no-op. + return oldState; + }; + + __exports__.TransitionIntent = TransitionIntent; + }); +define("router/transition-intent/named-transition-intent", + ["../transition-intent","../transition-state","../handler-info","../utils","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + var TransitionIntent = __dependency1__.TransitionIntent; + var TransitionState = __dependency2__.TransitionState; + var UnresolvedHandlerInfoByParam = __dependency3__.UnresolvedHandlerInfoByParam; + var UnresolvedHandlerInfoByObject = __dependency3__.UnresolvedHandlerInfoByObject; + var isParam = __dependency4__.isParam; + var forEach = __dependency4__.forEach; + var extractQueryParams = __dependency4__.extractQueryParams; + var oCreate = __dependency4__.oCreate; + var merge = __dependency4__.merge; + + function NamedTransitionIntent(props) { + TransitionIntent.call(this, props); + } + + NamedTransitionIntent.prototype = oCreate(TransitionIntent.prototype); + NamedTransitionIntent.prototype.applyToState = function(oldState, recognizer, getHandler, isIntermediate) { + + var partitionedArgs = extractQueryParams([this.name].concat(this.contexts)), + pureArgs = partitionedArgs[0], + queryParams = partitionedArgs[1], + handlers = recognizer.handlersFor(pureArgs[0]); + + var targetRouteName = handlers[handlers.length-1].handler; + + return this.applyToHandlers(oldState, handlers, getHandler, targetRouteName, isIntermediate); + }; + + NamedTransitionIntent.prototype.applyToHandlers = function(oldState, handlers, getHandler, targetRouteName, isIntermediate, checkingIfActive) { + + var i; + var newState = new TransitionState(); + var objects = this.contexts.slice(0); + + var invalidateIndex = handlers.length; + var nonDynamicIndexes = []; + + // Pivot handlers are provided for refresh transitions + if (this.pivotHandler) { + for (i = 0; i < handlers.length; ++i) { + if (getHandler(handlers[i].handler) === this.pivotHandler) { + invalidateIndex = i; + break; + } + } + } + + var pivotHandlerFound = !this.pivotHandler; + + for (i = handlers.length - 1; i >= 0; --i) { + var result = handlers[i]; + var name = result.handler; + var handler = getHandler(name); + + var oldHandlerInfo = oldState.handlerInfos[i]; + var newHandlerInfo = null; + + if (result.names.length > 0) { + if (i >= invalidateIndex) { + newHandlerInfo = this.createParamHandlerInfo(name, handler, result.names, objects, oldHandlerInfo); + } else { + newHandlerInfo = this.getHandlerInfoForDynamicSegment(name, handler, result.names, objects, oldHandlerInfo, targetRouteName); + } + } else { + // This route has no dynamic segment. + // Therefore treat as a param-based handlerInfo + // with empty params. This will cause the `model` + // hook to be called with empty params, which is desirable. + newHandlerInfo = this.createParamHandlerInfo(name, handler, result.names, objects, oldHandlerInfo); + nonDynamicIndexes.unshift(i); + } + + if (checkingIfActive) { + // If we're performing an isActive check, we want to + // serialize URL params with the provided context, but + // ignore mismatches between old and new context. + newHandlerInfo = newHandlerInfo.becomeResolved(null, newHandlerInfo.context); + var oldContext = oldHandlerInfo && oldHandlerInfo.context; + if (result.names.length > 0 && newHandlerInfo.context === oldContext) { + // If contexts match in isActive test, assume params also match. + // This allows for flexibility in not requiring that every last + // handler provide a `serialize` method + newHandlerInfo.params = oldHandlerInfo && oldHandlerInfo.params; + } + newHandlerInfo.context = oldContext; + } + + var handlerToUse = oldHandlerInfo; + if (i >= invalidateIndex || newHandlerInfo.shouldSupercede(oldHandlerInfo)) { + invalidateIndex = Math.min(i, invalidateIndex); + handlerToUse = newHandlerInfo; + } + + if (isIntermediate && !checkingIfActive) { + handlerToUse = handlerToUse.becomeResolved(null, handlerToUse.context); + } + + newState.handlerInfos.unshift(handlerToUse); + } + + if (objects.length > 0) { + throw new Error("More context objects were passed than there are dynamic segments for the route: " + targetRouteName); + } + + if (!isIntermediate) { + this.invalidateNonDynamicHandlers(newState.handlerInfos, nonDynamicIndexes, invalidateIndex); + } + + merge(newState.queryParams, oldState.queryParams); + merge(newState.queryParams, this.queryParams || {}); + + return newState; + }; + + NamedTransitionIntent.prototype.invalidateNonDynamicHandlers = function(handlerInfos, indexes, invalidateIndex) { + forEach(indexes, function(i) { + if (i >= invalidateIndex) { + var handlerInfo = handlerInfos[i]; + handlerInfos[i] = new UnresolvedHandlerInfoByParam({ + name: handlerInfo.name, + handler: handlerInfo.handler, + params: {} + }); + } + }); + }; + + NamedTransitionIntent.prototype.getHandlerInfoForDynamicSegment = function(name, handler, names, objects, oldHandlerInfo, targetRouteName) { + + var numNames = names.length; + var objectToUse; + if (objects.length > 0) { + + // Use the objects provided for this transition. + objectToUse = objects[objects.length - 1]; + if (isParam(objectToUse)) { + return this.createParamHandlerInfo(name, handler, names, objects, oldHandlerInfo); + } else { + objects.pop(); + } + } else if (oldHandlerInfo && oldHandlerInfo.name === name) { + // Reuse the matching oldHandlerInfo + return oldHandlerInfo; + } else { + // Ideally we should throw this error to provide maximal + // information to the user that not enough context objects + // were provided, but this proves too cumbersome in Ember + // in cases where inner template helpers are evaluated + // before parent helpers un-render, in which cases this + // error somewhat prematurely fires. + //throw new Error("Not enough context objects were provided to complete a transition to " + targetRouteName + ". Specifically, the " + name + " route needs an object that can be serialized into its dynamic URL segments [" + names.join(', ') + "]"); + return oldHandlerInfo; + } + + return new UnresolvedHandlerInfoByObject({ + name: name, + handler: handler, + context: objectToUse, + names: names + }); + }; + + NamedTransitionIntent.prototype.createParamHandlerInfo = function(name, handler, names, objects, oldHandlerInfo) { + var params = {}; + + // Soak up all the provided string/numbers + var numNames = names.length; + while (numNames--) { + + // Only use old params if the names match with the new handler + var oldParams = (oldHandlerInfo && name === oldHandlerInfo.name && oldHandlerInfo.params) || {}; + + var peek = objects[objects.length - 1]; + var paramName = names[numNames]; + if (isParam(peek)) { + params[paramName] = "" + objects.pop(); + } else { + // If we're here, this means only some of the params + // were string/number params, so try and use a param + // value from a previous handler. + if (oldParams.hasOwnProperty(paramName)) { + params[paramName] = oldParams[paramName]; + } else { + throw new Error("You didn't provide enough string/numeric parameters to satisfy all of the dynamic segments for route " + name); + } + } + } + + return new UnresolvedHandlerInfoByParam({ + name: name, + handler: handler, + params: params + }); + }; + + __exports__.NamedTransitionIntent = NamedTransitionIntent; + }); +define("router/transition-intent/url-transition-intent", + ["../transition-intent","../transition-state","../handler-info","../utils","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + var TransitionIntent = __dependency1__.TransitionIntent; + var TransitionState = __dependency2__.TransitionState; + var UnresolvedHandlerInfoByParam = __dependency3__.UnresolvedHandlerInfoByParam; + var oCreate = __dependency4__.oCreate; + var merge = __dependency4__.merge; + + function URLTransitionIntent(props) { + TransitionIntent.call(this, props); + } + + URLTransitionIntent.prototype = oCreate(TransitionIntent.prototype); + URLTransitionIntent.prototype.applyToState = function(oldState, recognizer, getHandler) { + var newState = new TransitionState(); + + var results = recognizer.recognize(this.url), + queryParams = {}, + i, len; + + if (!results) { + throw new UnrecognizedURLError(this.url); + } + + var statesDiffer = false; + + for (i = 0, len = results.length; i < len; ++i) { + var result = results[i]; + var name = result.handler; + var handler = getHandler(name); + + if (handler.inaccessibleByURL) { + throw new UnrecognizedURLError(this.url); + } + + var newHandlerInfo = new UnresolvedHandlerInfoByParam({ + name: name, + handler: handler, + params: result.params + }); + + var oldHandlerInfo = oldState.handlerInfos[i]; + if (statesDiffer || newHandlerInfo.shouldSupercede(oldHandlerInfo)) { + statesDiffer = true; + newState.handlerInfos[i] = newHandlerInfo; + } else { + newState.handlerInfos[i] = oldHandlerInfo; + } + } + + merge(newState.queryParams, results.queryParams); + + return newState; + }; + + /** + Promise reject reasons passed to promise rejection + handlers for failed transitions. + */ + function UnrecognizedURLError(message) { + this.message = (message || "UnrecognizedURLError"); + this.name = "UnrecognizedURLError"; + } + + __exports__.URLTransitionIntent = URLTransitionIntent; + }); +define("router/transition-state", + ["./handler-info","./utils","rsvp","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var ResolvedHandlerInfo = __dependency1__.ResolvedHandlerInfo; + var forEach = __dependency2__.forEach; + var resolve = __dependency3__.resolve; + + function TransitionState(other) { + this.handlerInfos = []; + this.queryParams = {}; + this.params = {}; + } + + TransitionState.prototype = { + handlerInfos: null, + queryParams: null, + params: null, + + resolve: function(async, shouldContinue, payload) { + + // First, calculate params for this state. This is useful + // information to provide to the various route hooks. + var params = this.params; + forEach(this.handlerInfos, function(handlerInfo) { + params[handlerInfo.name] = handlerInfo.params || {}; + }); + + payload = payload || {}; + payload.resolveIndex = 0; + + var currentState = this; + var wasAborted = false; + + // The prelude RSVP.resolve() asyncs us into the promise land. + return resolve().then(resolveOneHandlerInfo)['catch'](handleError); + + function innerShouldContinue() { + return resolve(shouldContinue())['catch'](function(reason) { + // We distinguish between errors that occurred + // during resolution (e.g. beforeModel/model/afterModel), + // and aborts due to a rejecting promise from shouldContinue(). + wasAborted = true; + throw reason; + }); + } + + function handleError(error) { + // This is the only possible + // reject value of TransitionState#resolve + throw { + error: error, + handlerWithError: currentState.handlerInfos[payload.resolveIndex].handler, + wasAborted: wasAborted, + state: currentState + }; + } + + function proceed(resolvedHandlerInfo) { + // Swap the previously unresolved handlerInfo with + // the resolved handlerInfo + currentState.handlerInfos[payload.resolveIndex++] = resolvedHandlerInfo; + + // Call the redirect hook. The reason we call it here + // vs. afterModel is so that redirects into child + // routes don't re-run the model hooks for this + // already-resolved route. + var handler = resolvedHandlerInfo.handler; + if (handler && handler.redirect) { + handler.redirect(resolvedHandlerInfo.context, payload); + } + + // Proceed after ensuring that the redirect hook + // didn't abort this transition by transitioning elsewhere. + return innerShouldContinue().then(resolveOneHandlerInfo); + } + + function resolveOneHandlerInfo() { + if (payload.resolveIndex === currentState.handlerInfos.length) { + // This is is the only possible + // fulfill value of TransitionState#resolve + return { + error: null, + state: currentState + }; + } + + var handlerInfo = currentState.handlerInfos[payload.resolveIndex]; + + return handlerInfo.resolve(async, innerShouldContinue, payload) + .then(proceed); + } + } + }; + + __exports__.TransitionState = TransitionState; + }); +define("router/transition", + ["rsvp","./handler-info","./utils","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var reject = __dependency1__.reject; + var resolve = __dependency1__.resolve; + var ResolvedHandlerInfo = __dependency2__.ResolvedHandlerInfo; + var trigger = __dependency3__.trigger; + var slice = __dependency3__.slice; + var log = __dependency3__.log; + + /** + @private + + A Transition is a thennable (a promise-like object) that represents + an attempt to transition to another route. It can be aborted, either + explicitly via `abort` or by attempting another transition while a + previous one is still underway. An aborted transition can also + be `retry()`d later. + */ + function Transition(router, intent, state, error) { + var transition = this; + this.state = state || router.state; + this.intent = intent; + this.router = router; + this.data = this.intent && this.intent.data || {}; + this.resolvedModels = {}; + this.queryParams = {}; + + if (error) { + this.promise = reject(error); + return; + } + + if (state) { + this.params = state.params; + this.queryParams = state.queryParams; + + var len = state.handlerInfos.length; + if (len) { + this.targetName = state.handlerInfos[state.handlerInfos.length-1].name; + } + + for (var i = 0; i < len; ++i) { + var handlerInfo = state.handlerInfos[i]; + if (!(handlerInfo instanceof ResolvedHandlerInfo)) { + break; + } + this.pivotHandler = handlerInfo.handler; + } + + this.sequence = Transition.currentSequence++; + this.promise = state.resolve(router.async, checkForAbort, this)['catch'](function(result) { + if (result.wasAborted) { + throw logAbort(transition); + } else { + transition.trigger('error', result.error, transition, result.handlerWithError); + transition.abort(); + throw result.error; + } + }); + } else { + this.promise = resolve(this.state); + this.params = {}; + } + + function checkForAbort() { + if (transition.isAborted) { + return reject(); + } + } + } + + Transition.currentSequence = 0; + + Transition.prototype = { + targetName: null, + urlMethod: 'update', + intent: null, + params: null, + pivotHandler: null, + resolveIndex: 0, + handlerInfos: null, + resolvedModels: null, + isActive: true, + state: null, + + /** + @public + + The Transition's internal promise. Calling `.then` on this property + is that same as calling `.then` on the Transition object itself, but + this property is exposed for when you want to pass around a + Transition's promise, but not the Transition object itself, since + Transition object can be externally `abort`ed, while the promise + cannot. + */ + promise: null, + + /** + @public + + Custom state can be stored on a Transition's `data` object. + This can be useful for decorating a Transition within an earlier + hook and shared with a later hook. Properties set on `data` will + be copied to new transitions generated by calling `retry` on this + transition. + */ + data: null, + + /** + @public + + A standard promise hook that resolves if the transition + succeeds and rejects if it fails/redirects/aborts. + + Forwards to the internal `promise` property which you can + use in situations where you want to pass around a thennable, + but not the Transition itself. + + @param {Function} success + @param {Function} failure + */ + then: function(success, failure) { + return this.promise.then(success, failure); + }, + + /** + @public + + Aborts the Transition. Note you can also implicitly abort a transition + by initiating another transition while a previous one is underway. + */ + abort: function() { + if (this.isAborted) { return this; } + log(this.router, this.sequence, this.targetName + ": transition was aborted"); + this.isAborted = true; + this.isActive = false; + this.router.activeTransition = null; + return this; + }, + + /** + @public + + Retries a previously-aborted transition (making sure to abort the + transition if it's still active). Returns a new transition that + represents the new attempt to transition. + */ + retry: function() { + // TODO: add tests for merged state retry()s + this.abort(); + return this.router.transitionByIntent(this.intent, false); + }, + + /** + @public + + Sets the URL-changing method to be employed at the end of a + successful transition. By default, a new Transition will just + use `updateURL`, but passing 'replace' to this method will + cause the URL to update using 'replaceWith' instead. Omitting + a parameter will disable the URL change, allowing for transitions + that don't update the URL at completion (this is also used for + handleURL, since the URL has already changed before the + transition took place). + + @param {String} method the type of URL-changing method to use + at the end of a transition. Accepted values are 'replace', + falsy values, or any other non-falsy value (which is + interpreted as an updateURL transition). + + @return {Transition} this transition + */ + method: function(method) { + this.urlMethod = method; + return this; + }, + + /** + @public + + Fires an event on the current list of resolved/resolving + handlers within this transition. Useful for firing events + on route hierarchies that haven't fully been entered yet. + + Note: This method is also aliased as `send` + + @param {Boolean} ignoreFailure the name of the event to fire + @param {String} name the name of the event to fire + */ + trigger: function (ignoreFailure) { + var args = slice.call(arguments); + if (typeof ignoreFailure === 'boolean') { + args.shift(); + } else { + // Throw errors on unhandled trigger events by default + ignoreFailure = false; + } + trigger(this.router, this.state.handlerInfos.slice(0, this.resolveIndex + 1), ignoreFailure, args); + }, + + /** + @public + + Transitions are aborted and their promises rejected + when redirects occur; this method returns a promise + that will follow any redirects that occur and fulfill + with the value fulfilled by any redirecting transitions + that occur. + + @return {Promise} a promise that fulfills with the same + value that the final redirecting transition fulfills with + */ + followRedirects: function() { + var router = this.router; + return this.promise['catch'](function(reason) { + if (router.activeTransition) { + return router.activeTransition.followRedirects(); + } + throw reason; + }); + }, + + toString: function() { + return "Transition (sequence " + this.sequence + ")"; + }, + + /** + @private + */ + log: function(message) { + log(this.router, this.sequence, message); + } + }; + + // Alias 'trigger' as 'send' + Transition.prototype.send = Transition.prototype.trigger; + + /** + @private + + Logs and returns a TransitionAborted error. + */ + function logAbort(transition) { + log(transition.router, transition.sequence, "detected abort."); + return new TransitionAborted(); + } + + function TransitionAborted(message) { + this.message = (message || "TransitionAborted"); + this.name = "TransitionAborted"; + } + + __exports__.Transition = Transition; + __exports__.logAbort = logAbort; + __exports__.TransitionAborted = TransitionAborted; + }); +define("router/utils", + ["exports"], + function(__exports__) { + "use strict"; + var slice = Array.prototype.slice; + + function merge(hash, other) { + for (var prop in other) { + if (other.hasOwnProperty(prop)) { hash[prop] = other[prop]; } + } + } + + var oCreate = Object.create || function(proto) { + function F() {} + F.prototype = proto; + return new F(); + }; + + /** + @private + + Extracts query params from the end of an array + **/ + function extractQueryParams(array) { + var len = (array && array.length), head, queryParams; + + if(len && len > 0 && array[len - 1] && array[len - 1].hasOwnProperty('queryParams')) { + queryParams = array[len - 1].queryParams; + head = slice.call(array, 0, len - 1); + return [head, queryParams]; + } else { + return [array, null]; + } + } + + /** + @private + */ + function log(router, sequence, msg) { + if (!router.log) { return; } + + if (arguments.length === 3) { + router.log("Transition #" + sequence + ": " + msg); + } else { + msg = sequence; + router.log(msg); + } + } + + function bind(fn, context) { + var boundArgs = arguments; + return function(value) { + var args = slice.call(boundArgs, 2); + args.push(value); + return fn.apply(context, args); + }; + } + + function isParam(object) { + return (typeof object === "string" || object instanceof String || typeof object === "number" || object instanceof Number); + } + + + function forEach(array, callback) { + for (var i=0, l=array.length; i 0 && array[len - 1] && array[len - 1].hasOwnProperty('queryParams')) { - queryParams = array[len - 1].queryParams; - head = slice.call(array, 0, len - 1); - return [head, queryParams]; - } else { - return [array, null]; - } - } - - function performIntermediateTransition(router, recogHandlers, matchPointResults) { - - var handlerInfos = generateHandlerInfos(router, recogHandlers); - for (var i = 0; i < handlerInfos.length; ++i) { - var handlerInfo = handlerInfos[i]; - handlerInfo.context = matchPointResults.providedModels[handlerInfo.name]; - } - - var stubbedTransition = { - router: router, - isAborted: false + function getChangelist(oldObject, newObject) { + var key; + var results = { + all: {}, + changed: {}, + removed: {} }; - setupContexts(stubbedTransition, handlerInfos); - } + merge(results.all, newObject); - /** - @private + var didChange = false; - Creates, begins, and returns a Transition. - */ - function performTransition(router, recogHandlers, providedModelsArray, params, queryParams, data, isIntermediate) { - - var matchPointResults = getMatchPoint(router, recogHandlers, providedModelsArray, params, queryParams), - targetName = recogHandlers[recogHandlers.length - 1].handler, - wasTransitioning = false, - currentHandlerInfos = router.currentHandlerInfos; - - if (isIntermediate) { - return performIntermediateTransition(router, recogHandlers, matchPointResults); - } - - // Check if there's already a transition underway. - if (router.activeTransition) { - if (transitionsIdentical(router.activeTransition, targetName, providedModelsArray, queryParams)) { - return router.activeTransition; - } - router.activeTransition.abort(); - wasTransitioning = true; - } - - var deferred = RSVP.defer(), - transition = new Transition(router, deferred.promise); - - transition.targetName = targetName; - transition.providedModels = matchPointResults.providedModels; - transition.providedModelsArray = providedModelsArray; - transition.params = matchPointResults.params; - transition.data = data || {}; - transition.queryParams = queryParams; - transition.pivotHandler = matchPointResults.pivotHandler; - router.activeTransition = transition; - - var handlerInfos = generateHandlerInfos(router, recogHandlers); - transition.handlerInfos = handlerInfos; - - // Fire 'willTransition' event on current handlers, but don't fire it - // if a transition was already underway. - if (!wasTransitioning) { - trigger(router, currentHandlerInfos, true, ['willTransition', transition]); - } - - log(router, transition.sequence, "Beginning validation for transition to " + transition.targetName); - validateEntry(transition, matchPointResults.matchPoint, matchPointResults.handlerParams) - .then(transitionSuccess, transitionFailure); - - return transition; - - function transitionSuccess() { - checkAbort(transition); - - try { - finalizeTransition(transition, handlerInfos); - - // currentHandlerInfos was updated in finalizeTransition - trigger(router, router.currentHandlerInfos, true, ['didTransition']); - - if (router.didTransition) { - router.didTransition(handlerInfos); + // Calculate removals + for (key in oldObject) { + if (oldObject.hasOwnProperty(key)) { + if (!newObject.hasOwnProperty(key)) { + didChange = true; + results.removed[key] = oldObject[key]; } - - log(router, transition.sequence, "TRANSITION COMPLETE."); - - // Resolve with the final handler. - transition.isActive = false; - deferred.resolve(handlerInfos[handlerInfos.length - 1].handler); - } catch(e) { - deferred.reject(e); - } - - // Don't nullify if another transition is underway (meaning - // there was a transition initiated with enter/setup). - if (!transition.isAborted) { - router.activeTransition = null; } } - function transitionFailure(reason) { - deferred.reject(reason); - } - } - - /** - @private - - Accepts handlers in Recognizer format, either returned from - recognize() or handlersFor(), and returns unified - `HandlerInfo`s. - */ - function generateHandlerInfos(router, recogHandlers) { - var handlerInfos = []; - for (var i = 0, len = recogHandlers.length; i < len; ++i) { - var handlerObj = recogHandlers[i], - isDynamic = handlerObj.isDynamic || (handlerObj.names && handlerObj.names.length); - - var handlerInfo = { - isDynamic: !!isDynamic, - name: handlerObj.handler, - handler: router.getHandler(handlerObj.handler) - }; - if(handlerObj.queryParams) { - handlerInfo.queryParams = handlerObj.queryParams; - } - handlerInfos.push(handlerInfo); - } - return handlerInfos; - } - - /** - @private - */ - function transitionsIdentical(oldTransition, targetName, providedModelsArray, queryParams) { - - if (oldTransition.targetName !== targetName) { return false; } - - var oldModels = oldTransition.providedModelsArray; - if (oldModels.length !== providedModelsArray.length) { return false; } - - for (var i = 0, len = oldModels.length; i < len; ++i) { - if (oldModels[i] !== providedModelsArray[i]) { return false; } - } - - if(!queryParamsEqual(oldTransition.queryParams, queryParams)) { - return false; - } - - return true; - } - - /** - @private - - Updates the URL (if necessary) and calls `setupContexts` - to update the router's array of `currentHandlerInfos`. - */ - function finalizeTransition(transition, handlerInfos) { - - log(transition.router, transition.sequence, "Validation succeeded, finalizing transition;"); - - var router = transition.router, - seq = transition.sequence, - handlerName = handlerInfos[handlerInfos.length - 1].name, - urlMethod = transition.urlMethod, - i; - - // Collect params for URL. - var objects = [], providedModels = transition.providedModelsArray.slice(); - for (i = handlerInfos.length - 1; i>=0; --i) { - var handlerInfo = handlerInfos[i]; - if (handlerInfo.isDynamic) { - var providedModel = providedModels.pop(); - objects.unshift(isParam(providedModel) ? providedModel.toString() : handlerInfo.context); - } - - if (handlerInfo.handler.inaccessibleByURL) { - urlMethod = null; + // Calculate changes + for (key in newObject) { + if (newObject.hasOwnProperty(key)) { + if (oldObject[key] !== newObject[key]) { + results.changed[key] = newObject[key]; + didChange = true; + } } } - var newQueryParams = {}; - for (i = handlerInfos.length - 1; i>=0; --i) { - merge(newQueryParams, handlerInfos[i].queryParams); - } - router.currentQueryParams = newQueryParams; - - - var params = paramsForHandler(router, handlerName, objects, transition.queryParams); - - router.currentParams = params; - - if (urlMethod) { - var url = router.recognizer.generate(handlerName, params); - - if (urlMethod === 'replace') { - router.replaceURL(url); - } else { - // Assume everything else is just a URL update for now. - router.updateURL(url); - } - } - - setupContexts(transition, handlerInfos); + return didChange && results; } - /** - @private - - Internal function used to construct the chain of promises used - to validate a transition. Wraps calls to `beforeModel`, `model`, - and `afterModel` in promises, and checks for redirects/aborts - between each. - */ - function validateEntry(transition, matchPoint, handlerParams) { - - var handlerInfos = transition.handlerInfos, - index = transition.resolveIndex; - - if (index === handlerInfos.length) { - // No more contexts to resolve. - return RSVP.resolve(transition.resolvedModels); - } - - var router = transition.router, - handlerInfo = handlerInfos[index], - handler = handlerInfo.handler, - handlerName = handlerInfo.name, - seq = transition.sequence; - - if (index < matchPoint) { - log(router, seq, handlerName + ": using context from already-active handler"); - - // We're before the match point, so don't run any hooks, - // just use the already resolved context from the handler. - transition.resolvedModels[handlerInfo.name] = - transition.providedModels[handlerInfo.name] || - handlerInfo.handler.context; - return proceed(); - } - - transition.trigger(true, 'willResolveModel', transition, handler); - - return RSVP.resolve().then(handleAbort) - .then(beforeModel) - .then(handleAbort) - .then(model) - .then(handleAbort) - .then(afterModel) - .then(handleAbort) - .then(null, handleError) - .then(proceed); - - function handleAbort(result) { - if (transition.isAborted) { - log(transition.router, transition.sequence, "detected abort."); - return RSVP.reject(new Router.TransitionAborted()); - } - - return result; - } - - function handleError(reason) { - if (reason instanceof Router.TransitionAborted || transition.isAborted) { - // if the transition was aborted and *no additional* error was thrown, - // reject with the Router.TransitionAborted instance - return RSVP.reject(reason); - } - - // otherwise, we're here because of a different error - transition.abort(); - - log(router, seq, handlerName + ": handling error: " + reason); - - // An error was thrown / promise rejected, so fire an - // `error` event from this handler info up to root. - transition.trigger(true, 'error', reason, transition, handlerInfo.handler); - - // Propagate the original error. - return RSVP.reject(reason); - } - - function beforeModel() { - - log(router, seq, handlerName + ": calling beforeModel hook"); - - var args; - - if (handlerInfo.queryParams) { - args = [handlerInfo.queryParams, transition]; - } else { - args = [transition]; - } - - var p = handler.beforeModel && handler.beforeModel.apply(handler, args); - return (p instanceof Transition) ? null : p; - } - - function model() { - log(router, seq, handlerName + ": resolving model"); - var p = getModel(handlerInfo, transition, handlerParams[handlerName], index >= matchPoint); - return (p instanceof Transition) ? null : p; - } - - function afterModel(context) { - - log(router, seq, handlerName + ": calling afterModel hook"); - - // Pass the context and resolved parent contexts to afterModel, but we don't - // want to use the value returned from `afterModel` in any way, but rather - // always resolve with the original `context` object. - - transition.resolvedModels[handlerInfo.name] = context; - - var args; - - if (handlerInfo.queryParams) { - args = [context, handlerInfo.queryParams, transition]; - } else { - args = [context, transition]; - } - - var p = handler.afterModel && handler.afterModel.apply(handler, args); - return (p instanceof Transition) ? null : p; - } - - function proceed() { - log(router, seq, handlerName + ": validation succeeded, proceeding"); - - handlerInfo.context = transition.resolvedModels[handlerInfo.name]; - transition.resolveIndex++; - return validateEntry(transition, matchPoint, handlerParams); - } - } - - /** - @private - - Throws a TransitionAborted if the provided transition has been aborted. - */ - function checkAbort(transition) { - if (transition.isAborted) { - log(transition.router, transition.sequence, "detected abort."); - throw new Router.TransitionAborted(); - } - } - - /** - @private - - Encapsulates the logic for whether to call `model` on a route, - or use one of the models provided to `transitionTo`. - */ - function getModel(handlerInfo, transition, handlerParams, needsUpdate) { - var handler = handlerInfo.handler, - handlerName = handlerInfo.name, args; - - if (!needsUpdate && handler.hasOwnProperty('context')) { - return handler.context; - } - - if (transition.providedModels.hasOwnProperty(handlerName)) { - var providedModel = transition.providedModels[handlerName]; - return typeof providedModel === 'function' ? providedModel() : providedModel; - } - - if (handlerInfo.queryParams) { - args = [handlerParams || {}, handlerInfo.queryParams, transition]; - } else { - args = [handlerParams || {}, transition, handlerInfo.queryParams]; - } - - return handler.model && handler.model.apply(handler, args); - } - - /** - @private - */ - function log(router, sequence, msg) { - - if (!router.log) { return; } - - if (arguments.length === 3) { - router.log("Transition #" + sequence + ": " + msg); - } else { - msg = sequence; - router.log(msg); - } - } - - /** - @private - - Begins and returns a Transition based on the provided - arguments. Accepts arguments in the form of both URL - transitions and named transitions. - - @param {Router} router - @param {Array[Object]} args arguments passed to transitionTo, - replaceWith, or handleURL - */ - function doTransition(router, args, isIntermediate) { - // Normalize blank transitions to root URL transitions. - var name = args[0] || '/'; - - if(args.length === 1 && args[0].hasOwnProperty('queryParams')) { - return createQueryParamTransition(router, args[0], isIntermediate); - } else if (name.charAt(0) === '/') { - return createURLTransition(router, name, isIntermediate); - } else { - return createNamedTransition(router, slice.call(args), isIntermediate); - } - } - - /** - @private - - Serializes a handler using its custom `serialize` method or - by a default that looks up the expected property name from - the dynamic segment. - - @param {Object} handler a router handler - @param {Object} model the model to be serialized for this handler - @param {Array[Object]} names the names array attached to an - handler object returned from router.recognizer.handlersFor() - */ - function serialize(handler, model, names) { - - var object = {}; - if (isParam(model)) { - object[names[0]] = model; - return object; - } - - // Use custom serialize if it exists. - if (handler.serialize) { - return handler.serialize(model, names); - } - - if (names.length !== 1) { return; } - - var name = names[0]; - - if (/_id$/.test(name)) { - object[name] = model.id; - } else { - object[name] = model; - } - return object; - } + __exports__.trigger = trigger; + __exports__.log = log; + __exports__.oCreate = oCreate; + __exports__.merge = merge; + __exports__.extractQueryParams = extractQueryParams; + __exports__.bind = bind; + __exports__.isParam = isParam; + __exports__.forEach = forEach; + __exports__.slice = slice; + __exports__.serialize = serialize; + __exports__.getChangelist = getChangelist; }); +define("router", + ["./router/router","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Router = __dependency1__.Router; + __exports__.Router = Router; + }); })(); @@ -33423,7 +34265,7 @@ DSL.prototype = { for (var i=0, l=dslMatches.length; i 1) { + urlKeyName = parts[1]; + } else { + // TODO: use _queryParamScope here? + if (controllerName !== 'application') { + urlKeyName = controllerName + '[' + propName + ']'; + } else { + urlKeyName = propName; + } + } + + var controllerFullname = controllerName + ':' + propName; + + result.queryParams[controllerFullname] = urlKeyName; + result.translations[parts[0]] = controllerFullname; + }); + } +} + /** Helper function for iterating root-ward, starting from (but not including) the provided `originRoute`. @@ -33893,7 +34852,7 @@ Ember.Router = Ember.Object.extend(Ember.Evented, { @private */ function forEachRouteAbove(originRoute, transition, callback) { - var handlerInfos = transition.handlerInfos, + var handlerInfos = transition.state.handlerInfos, originRouteFound = false; for (var i = handlerInfos.length - 1; i >= 0; --i) { @@ -34113,11 +35072,22 @@ Ember.Router.reopenClass({ } return path.join("."); + }, + + _translateQueryParams: function(queryParams, translations, routeName) { + for (var name in queryParams) { + if (!queryParams.hasOwnProperty(name)) { continue; } + + if (name in translations) { + queryParams[translations[name]] = queryParams[name]; + delete queryParams[name]; + } else { + Ember.assert(fmt("You supplied an unknown query param controller property '%@' for route '%@'. Only the following query param properties can be set for this route: %@", [name, routeName, Ember.keys(translations)]), name in queryParams); + } + } } }); -Router.Transition.prototype.send = Router.Transition.prototype.trigger; - })(); @@ -34155,7 +35125,7 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { @method exit */ exit: function() { - this.deactivate(); + this.deactivate(); this.teardownViews(); }, @@ -34168,6 +35138,74 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { this.activate(); }, + /** + The name of the view to use by default when rendering this routes template. + + When rendering a template, the route will, by default, determine the + template and view to use from the name of the route itself. If you need to + define a specific view, set this property. + + This is useful when multiple routes would benefit from using the same view + because it doesn't require a custom `renderTemplate` method. For example, + the following routes will all render using the `App.PostsListView` view: + + ```js + var PostsList = Ember.Route.extend({ + viewName: 'postsList', + }); + + App.PostsIndexRoute = PostsList.extend(); + App.PostsArchivedRoute = PostsList.extend(); + ``` + + @property viewName + @type String + @default null + */ + viewName: null, + + /** + The name of the template to use by default when rendering this routes + template. + + This is similar with `viewName`, but is useful when you just want a custom + template without a view. + + ```js + var PostsList = Ember.Route.extend({ + templateName: 'posts/list' + }); + + App.PostsIndexRoute = PostsList.extend(); + App.PostsArchivedRoute = PostsList.extend(); + ``` + + @property templateName + @type String + @default null + */ + templateName: null, + + /** + The name of the controller to associate with this route. + + By default, Ember will lookup a route's controller that matches the name + of the route (i.e. `App.PostController` for `App.PostRoute`). However, + if you would like to define a specific controller to use, you can do so + using this property. + + This is useful in many ways, as the controller specified will be: + + * passed to the `setupController` method. + * used as the controller for the view being rendered by the route. + * returned from a call to `controllerFor` for the route. + + @property controllerName + @type String + @default null + */ + controllerName: null, + /** The collection of functions, keyed by name, available on this route as action targets. @@ -34477,6 +35515,8 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { @param {String} name the name of the route @param {...Object} models the model(s) to be used while transitioning to the route. + @return {Transition} the transition object associated with this + attempted transition */ transitionTo: function(name, context) { var router = this.router; @@ -34484,7 +35524,7 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { }, /** - Perform a synchronous transition into another route with out attempting + Perform a synchronous transition into another route without attempting to resolve promises, update the URL, or abort any currently active asynchronous transitions (i.e. regular transitions caused by `transitionTo` or URL changes). @@ -34503,6 +35543,30 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { router.intermediateTransitionTo.apply(router, arguments); }, + /** + Refresh the model on this route and any child routes, firing the + `beforeModel`, `model`, and `afterModel` hooks in a similar fashion + to how routes are entered when transitioning in from other route. + The current route params (e.g. `article_id`) will be passed in + to the respective model hooks, and if a different model is returned, + `setupController` and associated route hooks will re-fire as well. + + An example usage of this method is re-querying the server for the + latest information using the same parameters as when the route + was first entered. + + Note that this will cause `model` hooks to fire even on routes + that were provided a model object when the route was initially + entered. + + @method refresh + @return {Transition} the transition object associated with this + attempted transition + */ + refresh: function() { + return this.router.router.refresh(this).method('replace'); + }, + /** Transition into another route while replacing the current URL, if possible. This will replace the current history entry instead of adding a new one. @@ -34530,6 +35594,8 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { @param {String} name the name of the route @param {...Object} models the model(s) to be used while transitioning to the route. + @return {Transition} the transition object associated with this + attempted transition */ replaceWith: function() { var router = this.router; @@ -34580,7 +35646,7 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { @private @method setup */ - setup: function(context, queryParams) { + setup: function(context, transition) { var controllerName = this.controllerName || this.routeName, controller = this.controllerFor(controllerName, true); if (!controller) { @@ -34591,40 +35657,25 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { // referenced in action handlers this.controller = controller; - var args = [controller, context]; - if (this.setupControllers) { Ember.deprecate("Ember.Route.setupControllers is deprecated. Please use Ember.Route.setupController(controller, model) instead."); this.setupControllers(controller, context); } else { - this.setupController.apply(this, args); + + + this.setupController(controller, context); + } if (this.renderTemplates) { Ember.deprecate("Ember.Route.renderTemplates is deprecated. Please use Ember.Route.renderTemplate(controller, model) instead."); this.renderTemplates(context); } else { - this.renderTemplate.apply(this, args); + this.renderTemplate(controller, context); } }, - /** - A hook you can implement to optionally redirect to another route. - - If you call `this.transitionTo` from inside of this hook, this route - will not be entered in favor of the other hook. - - Note that this hook is called by the default implementation of - `afterModel`, so if you override `afterModel`, you must either - explicitly call `redirect` or just put your redirecting - `this.transitionTo()` call within `afterModel`. - - @method redirect - @param {Object} model the model for this route - */ - redirect: Ember.K, - /** This hook is the first of the route entry validation hooks called when an attempt is made to transition into a route @@ -34686,8 +35737,6 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { // error so that it'd be handled by the `error` // hook, you would have to either return Ember.RSVP.reject(e); - // or - throw e; }); } } @@ -34736,10 +35785,32 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { resolves. Otherwise, non-promise return values are not utilized in any way. */ - afterModel: function(resolvedModel, transition, queryParams) { - this.redirect(resolvedModel, transition); - }, + afterModel: Ember.K, + /** + A hook you can implement to optionally redirect to another route. + + If you call `this.transitionTo` from inside of this hook, this route + will not be entered in favor of the other hook. + + `redirect` and `afterModel` behave very similarly and are + called almost at the same time, but they have an important + distinction in the case that, from one of these hooks, a + redirect into a child route of this route occurs: redirects + from `afterModel` essentially invalidate the current attempt + to enter this route, and will result in this route's `beforeModel`, + `model`, and `afterModel` hooks being fired again within + the new, redirecting transition. Redirects that occur within + the `redirect` hook, on the other hand, will _not_ cause + these hooks to be fired again the second time around; in + other words, by the time the `redirect` hook has been called, + both the resolved model and attempted entry into this route + are considered to be fully validated. + + @method redirect + @param {Object} model the model for this route + */ + redirect: Ember.K, /** Called when the context is changed by router.js. @@ -34805,6 +35876,8 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { var match, name, sawParams, value; for (var prop in params) { + if (prop === 'queryParams') { continue; } + if (match = prop.match(/^(.*)_id$/)) { name = match[1]; value = params[prop]; @@ -34818,6 +35891,17 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { return this.findModel(name, value); }, + /** + @private + + Router.js hook. + */ + deserialize: function(params, transition) { + + return this.model(params, transition); + + }, + /** @method findModel @@ -34856,6 +35940,8 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { " did not exist and you did not override your route's `model` " + "hook.", modelClass); + if (!modelClass) { return; } + return modelClass.find(value); } }; @@ -34960,7 +36046,7 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { @param {Controller} controller instance @param {Object} model */ - setupController: function(controller, context) { + setupController: function(controller, context, transition) { if (controller && (context !== undefined)) { set(controller, 'model', context); } @@ -35255,7 +36341,7 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { options.outlet = options.outlet || 'main'; var parentView = this.router._lookupActiveView(options.parentView); - parentView.disconnectOutlet(options.outlet); + if (parentView) { parentView.disconnectOutlet(options.outlet); } }, willDestroy: function() { @@ -35283,8 +36369,10 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { } }); + + function parentRoute(route) { - var handlerInfos = route.router.router.targetHandlerInfos; + var handlerInfos = route.router.router.state.handlerInfos; if (!handlerInfos) { return; } @@ -35330,7 +36418,11 @@ function normalizeOptions(route, name, template, options) { } if (typeof controller === 'string') { - controller = route.container.lookup('controller:' + controller); + var controllerName = controller; + controller = route.container.lookup('controller:' + controllerName); + if (!controller) { + throw new Ember.Error("You passed `controller: '" + controllerName + "'` into the `render` method, but no such controller could be found."); + } } options.controller = controller; @@ -35456,9 +36548,16 @@ Ember.onLoad('Ember.Handlebars', function() { */ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; + +var slice = Array.prototype.slice; Ember.onLoad('Ember.Handlebars', function(Handlebars) { + var QueryParams = Ember.Object.extend({ + values: null + }); + var resolveParams = Ember.Router.resolveParams, + translateQueryParams = Ember.Router._translateQueryParams, resolvePaths = Ember.Router.resolvePaths, isSimpleClick = Ember.ViewUtils.isSimpleClick; @@ -35634,8 +36733,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { // Map desired event name to invoke function var eventName = get(this, 'eventName'), i; this.on(eventName, this, this._invoke); - - }, + }, /** This method is invoked by observers installed during `init` that fire @@ -35676,6 +36774,22 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { normalizedPath = Ember.Handlebars.normalizePath(helperParameters.context, path, helperParameters.options.data); this.registerObserver(normalizedPath.root, normalizedPath.path, this, this._paramsChanged); } + + var queryParamsObject = this.queryParamsObject; + if (queryParamsObject) { + var values = queryParamsObject.values; + + // Install observers for all of the hash options + // provided in the (query-params) subexpression. + for (var k in values) { + if (!values.hasOwnProperty(k)) { continue; } + + if (queryParamsObject.types[k] === 'ID') { + normalizedPath = Ember.Handlebars.normalizePath(helperParameters.context, values[k], helperParameters.options.data); + this.registerObserver(normalizedPath.root, normalizedPath.path, this, this._paramsChanged); + } + } + } }, afterRender: function(){ @@ -35683,16 +36797,6 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { this._setupPathObservers(); }, - /** - This method is invoked by observers installed during `init` that fire - whenever the query params change - @private - */ - _queryParamsChanged: function (object, path) { - this.notifyPropertyChange('queryParams'); - }, - - /** 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}} @@ -35741,7 +36845,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { router.isActive.apply(router, [currentWithIndex].concat(contexts)); if (isActive) { return get(this, 'activeClass'); } - }).property('resolvedParams', 'routeArgs', 'router.url'), + }).property('resolvedParams', 'routeArgs'), /** Accessed as a classname binding to apply the `LinkView`'s `loadingClass` @@ -35799,7 +36903,19 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { }, /** - Computed property that returns the resolved parameters. + Computed property that returns an array of the + resolved parameters passed to the `link-to` helper, + e.g.: + + ```hbs + {{link-to a b '123' c}} + ``` + + will generate a `resolvedParams` of: + + ```js + [aObject, bObject, '123', cObject] + ``` @private @property @@ -35811,10 +36927,16 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { types = options.types, data = options.data; - + if (parameters.params.length === 0) { + var appController = this.container.lookup('controller:application'); + return [get(appController, 'currentRouteName')]; + } else { + return resolveParams(parameters.context, parameters.params, { types: types, data: data }); + } + // Original implementation if query params not enabled return resolveParams(parameters.context, parameters.params, { types: types, data: data }); - }).property(), + }).property('router.url'), /** Computed property that returns the current route name and @@ -35844,36 +36966,53 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { return resolvedParams; - }).property('resolvedParams', 'queryParams', 'router.url'), + }).property('resolvedParams', 'queryParams'), + queryParamsObject: null, + queryParams: Ember.computed(function computeLinkViewQueryParams() { - _potentialQueryParams: Ember.computed(function () { - var namedRoute = get(this, 'resolvedParams')[0]; - if (!namedRoute) { return null; } - var router = get(this, 'router'); + var queryParamsObject = get(this, 'queryParamsObject'), + suppliedParams = {}; - namedRoute = fullRouteName(router, namedRoute); + if (queryParamsObject) { + Ember.merge(suppliedParams, queryParamsObject.values); + } - return router.router.queryParamsForHandler(namedRoute); - }).property('resolvedParams'), + var resolvedParams = get(this, 'resolvedParams'), + router = get(this, 'router'), + routeName = resolvedParams[0], + paramsForRoute = router._queryParamNamesFor(routeName), + queryParams = paramsForRoute.queryParams, + translations = paramsForRoute.translations, + paramsForRecognizer = {}; - queryParams: Ember.computed(function () { - var self = this, - queryParams = null, - allowedQueryParams = get(this, '_potentialQueryParams'); + // Normalize supplied params into their long-form name + // e.g. 'foo' -> 'controllername:foo' + translateQueryParams(suppliedParams, translations, routeName); - if (!allowedQueryParams) { return null; } - allowedQueryParams.forEach(function (param) { - var value = get(self, param); - if (typeof value !== 'undefined') { - queryParams = queryParams || {}; - queryParams[param] = value; + var helperParameters = this.parameters; + router._queryParamOverrides(paramsForRecognizer, queryParams, function(name, resultsName) { + if (!(name in suppliedParams)) { return; } + + var parts = name.split(':'); + + var type = queryParamsObject.types[parts[1]]; + + var value; + if (type === 'ID') { + var normalizedPath = Ember.Handlebars.normalizePath(helperParameters.context, suppliedParams[name], helperParameters.options.data); + value = Ember.Handlebars.get(normalizedPath.root, normalizedPath.path, helperParameters.options); + } else { + value = suppliedParams[name]; } + + delete suppliedParams[name]; + + paramsForRecognizer[resultsName] = value; }); - - return queryParams; - }).property('_potentialQueryParams.[]'), + return paramsForRecognizer; + }).property('resolvedParams.[]'), /** Sets the element's `href` attribute to the url for @@ -36169,10 +37308,14 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { @see {Ember.LinkView} */ Ember.Handlebars.registerHelper('link-to', function linkToHelper(name) { - var options = [].slice.call(arguments, -1)[0], - params = [].slice.call(arguments, 0, -1), + var options = slice.call(arguments, -1)[0], + params = slice.call(arguments, 0, -1), hash = options.hash; + if (params[params.length - 1] instanceof QueryParams) { + hash.queryParamsObject = params.pop(); + } + hash.disabledBinding = hash.disabledWhen; if (!options.fn) { @@ -36200,6 +37343,8 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { return Ember.Handlebars.helpers.view.call(this, LinkView, options); }); + + /** See [link-to](/api/classes/Ember.Handlebars.helpers.html#method_link-to) @@ -36378,7 +37523,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { ```handelbars

My great app

- {{render navigation}} + {{render "navigation"}} ``` ```html @@ -36423,7 +37568,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { */ Ember.Handlebars.registerHelper('render', function renderHelper(name, contextString, options) { var length = arguments.length; - Ember.assert("You must pass a template to render", length >= 2); + var contextProvided = length === 3, container, router, controller, view, context, lookupOptions; @@ -36442,6 +37587,8 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { throw Ember.Error("You must pass a templateName to render"); } + Ember.deprecate("Using a quoteless parameter with {{render}} is deprecated. Please update to quoted usage '{{render \"" + name + "\"}}.", options.types[0] !== 'ID'); + // # legacy namespace name = name.replace(/\//g, '.'); // \ legacy slash as namespace support @@ -36491,7 +37638,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { options.hash.viewName = Ember.String.camelize(name); var templateName = 'template:' + name; - Ember.assert("You used `{{render '" + name + "'}}`, but '" + name + "' can not be found as either a template or a view.", container.has("view:" + name) || container.has(templateName)); + Ember.assert("You used `{{render '" + name + "'}}`, but '" + name + "' can not be found as either a template or a view.", container.has("view:" + name) || container.has(templateName) || options.fn); options.hash.template = container.lookup(templateName); options.hash.controller = controller; @@ -36591,6 +37738,10 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { target = target.root; } + if (options.boundProperty) { + Ember.deprecate("Using a quoteless parameter with {{action}} is deprecated. Please update to quoted usage '{{action \"" + actionName + "\"}}.", false); + } + Ember.run(function runRegisteredAction() { if (target.send) { target.send.apply(target, args(options.parameters, actionName)); @@ -36809,6 +37960,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { action.target = { root: root, target: target, options: options }; action.bubbles = hash.bubbles; action.preventDefault = hash.preventDefault; + action.boundProperty = options.types[0] === "ID"; var actionId = ActionHelper.registerAction(actionName, action, hash.allowedKeys); return new SafeString('data-ember-action="' + actionId + '"'); @@ -36832,7 +37984,10 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { @submodule ember-routing */ -var get = Ember.get, set = Ember.set; +var get = Ember.get, set = Ember.set, + map = Ember.EnumerableUtils.map; + +var queuedQueryParamChanges = {}; Ember.ControllerMixin.reopen({ /** @@ -36890,7 +38045,7 @@ Ember.ControllerMixin.reopen({ /** Transition into another route while replacing the current URL, if possible. - This will replace the current history entry instead of adding a new one. + This will replace the current history entry instead of adding a new one. Beside that, it is identical to `transitionToRoute` in all other respects. ```javascript @@ -36941,6 +38096,7 @@ Ember.ControllerMixin.reopen({ } }); + })(); @@ -37083,6 +38239,7 @@ Ember.View.reopen({ @method _finishDisconnections */ _finishDisconnections: function() { + if (this.isDestroyed) return; // _outlets will be gone anyway var outlets = get(this, '_outlets'); var pendingDisconnections = this._pendingDisconnections; this._pendingDisconnections = null; @@ -37253,10 +38410,32 @@ Ember.Location = { container directly. */ registerImplementation: function(name, implementation) { + Ember.deprecate('Using the Ember.Location.registerImplementation is no longer supported. Register your custom location implementation with the container instead.', false); + this.implementations[name] = implementation; }, - implementations: {} + implementations: {}, + + /** + Returns the current `location.hash` by parsing location.href since browsers + inconsistently URL-decode `location.hash`. + + https://bugzilla.mozilla.org/show_bug.cgi?id=483304 + + @private + @method getHash + */ + getHash: function () { + var href = window.location.href, + hashIndex = href.indexOf('#'); + + if (hashIndex === -1) { + return ''; + } else { + return href.substr(hashIndex); + } + } }; })(); @@ -37282,6 +38461,7 @@ var get = Ember.get, set = Ember.set; @extends Ember.Object */ Ember.NoneLocation = Ember.Object.extend({ + implementation: 'none', path: '', /** @@ -37352,8 +38532,6 @@ Ember.NoneLocation = Ember.Object.extend({ } }); -Ember.Location.registerImplementation('none', Ember.NoneLocation); - })(); @@ -37364,7 +38542,8 @@ Ember.Location.registerImplementation('none', Ember.NoneLocation); @submodule ember-routing */ -var get = Ember.get, set = Ember.set; +var get = Ember.get, set = Ember.set, + getHash = Ember.Location.getHash; /** `Ember.HashLocation` implements the location API using the browser's @@ -37376,6 +38555,7 @@ var get = Ember.get, set = Ember.set; @extends Ember.Object */ Ember.HashLocation = Ember.Object.extend({ + implementation: 'hash', init: function() { set(this, 'location', get(this, 'location') || window.location); @@ -37388,8 +38568,7 @@ Ember.HashLocation = Ember.Object.extend({ @method getURL */ getURL: function() { - // Default implementation without feature flag enabled - return get(this, 'location').hash.substr(1); + return getHash().substr(1); }, /** @@ -37416,6 +38595,7 @@ Ember.HashLocation = Ember.Object.extend({ */ replaceURL: function(path) { get(this, 'location').replace('#' + path); + set(this, 'lastSetURL', path); }, /** @@ -37433,7 +38613,7 @@ Ember.HashLocation = Ember.Object.extend({ Ember.$(window).on('hashchange.ember-location-'+guid, function() { Ember.run(function() { - var path = location.hash.substr(1); + var path = self.getURL(); if (get(self, 'lastSetURL') === path) { return; } set(self, 'lastSetURL', null); @@ -37471,8 +38651,6 @@ Ember.HashLocation = Ember.Object.extend({ } }); -Ember.Location.registerImplementation('hash', Ember.HashLocation); - })(); @@ -37496,9 +38674,11 @@ var supportsHistoryState = window.history && 'state' in window.history; @extends Ember.Object */ Ember.HistoryLocation = Ember.Object.extend({ + implementation: 'history', init: function() { set(this, 'location', get(this, 'location') || window.location); + set(this, 'baseURL', Ember.$('base').attr('href') || ''); }, /** @@ -37530,10 +38710,12 @@ Ember.HistoryLocation = Ember.Object.extend({ getURL: function() { var rootURL = get(this, 'rootURL'), location = get(this, 'location'), - path = location.pathname; + path = location.pathname, + baseURL = get(this, 'baseURL'); rootURL = rootURL.replace(/\/$/, ''); - var url = path.replace(rootURL, ''); + baseURL = baseURL.replace(/\/$/, ''); + var url = path.replace(baseURL, '').replace(rootURL, ''); return url; @@ -37658,13 +38840,17 @@ Ember.HistoryLocation = Ember.Object.extend({ @return formatted url {String} */ formatURL: function(url) { - var rootURL = get(this, 'rootURL'); + var rootURL = get(this, 'rootURL'), + baseURL = get(this, 'baseURL'); if (url !== '') { rootURL = rootURL.replace(/\/$/, ''); + baseURL = baseURL.replace(/\/$/, ''); + } else if(baseURL.match(/^\//) && rootURL.match(/^\//)) { + baseURL = baseURL.replace(/\/$/, ''); } - return rootURL + url; + return baseURL + rootURL + url; }, /** @@ -37680,8 +38866,6 @@ Ember.HistoryLocation = Ember.Object.extend({ } }); -Ember.Location.registerImplementation('history', Ember.HistoryLocation); - })(); @@ -38536,9 +39720,15 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin ```javascript App.inject(, , ) - App.inject('model:user', 'email', 'model:email') - App.inject('model', 'source', 'source:main') + App.inject('controller:application', 'email', 'model:email') + App.inject('controller', 'source', 'source:main') ``` + Please note that injections on models are currently disabled. + This was done because ember-data was not ready for fully a container aware ecosystem. + + You can enable injections on models by setting `Ember.MODEL_FACTORY_INJECTIONS` flag to `true` + If model factory injections are enabled, models should not be + accessed globally (only through `container.lookupFactory('model:modelName'))`); @method inject @param factoryNameOrType {String} @@ -38787,6 +39977,8 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin willDestroy: function() { Ember.BOOTED = false; + // Ensure deactivation of routes before objects are destroyed + this.__container__.lookup('router:main').reset(); this.__container__.destroy(); }, @@ -38868,6 +40060,10 @@ Ember.Application.reopenClass({ container.register('router:main', Ember.Router); container.injection('router:main', 'namespace', 'application:main'); + container.register('location:hash', Ember.HashLocation); + container.register('location:history', Ember.HistoryLocation); + container.register('location:none', Ember.NoneLocation); + container.injection('controller', 'target', 'router:main'); container.injection('controller', 'namespace', 'application:main'); @@ -39671,7 +40867,7 @@ Ember.Test = { Example: - ``` + ```javascript Ember.Test.unregisterHelper('wait'); ``` @@ -39691,7 +40887,8 @@ Ember.Test = { The callback will receive the current application as an argument. Example: - ``` + + ```javascript Ember.Test.onInjectHelpers(function() { Ember.$(document).ajaxStart(function() { Test.pendingAjaxRequests++; @@ -39734,7 +40931,7 @@ Ember.Test = { Example: - ``` + ```javascript Ember.Test.adapter = MyCustomAdapter.create() ``` @@ -39768,18 +40965,21 @@ Ember.Test = { transition or an IndexDB transaction. For example: + ```javascript Ember.Test.registerWaiter(function() { - return myPendingTransactions() == 0; + return myPendingTransactions() == 0; }); ``` The `context` argument allows you to optionally specify the `this` with which your callback will be invoked. For example: + ```javascript Ember.Test.registerWaiter(MyDB, MyDB.hasPendingTransactions); ``` + @public @method registerWaiter @param {Object} context (optional) @@ -40040,7 +41240,7 @@ function isolate(fn, val) { // Reset lastPromise for nested helpers Ember.Test.lastPromise = null; - value = fn.call(null, val); + value = fn(val); lastPromise = Ember.Test.lastPromise; @@ -40267,10 +41467,17 @@ function currentURL(app){ } function visit(app, url) { - Ember.run(app, 'advanceReadiness'); + var router = app.__container__.lookup('router:main'); + router.location.setURL(url); + + if (app._readinessDeferrals > 0) { + router['initialURL'] = url; + Ember.run(app, 'advanceReadiness'); + delete router['initialURL']; + } else { + Ember.run(app, app.handleURL, url); + } - app.__container__.lookup('router:main').location.setURL(url); - Ember.run(app, app.handleURL, url); return wait(app); } @@ -40369,7 +41576,7 @@ function wait(app, value) { // Every 10ms, poll for the async thing to have finished var watcher = setInterval(function() { // 1. If the router is loading, keep polling - var routerIsLoading = app.__container__.lookup('router:main').router.isLoading; + var routerIsLoading = !!app.__container__.lookup('router:main').router.activeTransition; if (routerIsLoading) { return; } // 2. If there are pending Ajax requests, keep polling @@ -40425,7 +41632,7 @@ asyncHelper('visit', visit); * * ```javascript * click('.some-jQuery-selector').then(function() { -* // assert something +* // assert something * }); * ``` * @@ -40480,7 +41687,7 @@ asyncHelper('fillIn', fillIn); * Example: * * ```javascript -* var $el = find('.my-selector); +* var $el = find('.my-selector'); * ``` * * @method find diff --git a/assets/scripts/vendor/ember.prod.js b/assets/scripts/vendor/ember.prod.js index b90d83d2..e1e9ce0c 100644 --- a/assets/scripts/vendor/ember.prod.js +++ b/assets/scripts/vendor/ember.prod.js @@ -5,7 +5,7 @@ * Portions Copyright 2008-2011 Apple Inc. All rights reserved. * @license Licensed under MIT license * See https://raw.github.com/emberjs/ember.js/master/LICENSE - * @version 1.3.2 + * @version 1.4.0 */ @@ -88,7 +88,7 @@ var define, requireModule, require, requirejs; @class Ember @static - @version 1.3.2 + @version 1.4.0 */ if ('undefined' === typeof Ember) { @@ -115,10 +115,10 @@ Ember.toString = function() { return "Ember"; }; /** @property VERSION @type String - @default '1.3.2' + @default '1.4.0' @static */ -Ember.VERSION = '1.3.2'; +Ember.VERSION = '1.4.0'; /** Standard environmental variables. You can define these in a global `EmberENV` @@ -593,17 +593,19 @@ var arrayIndexOf = isNativeFunc(Array.prototype.indexOf) ? Array.prototype.index return -1; }; -/** - Array polyfills to support ES5 features in older browsers. - @namespace Ember - @property ArrayPolyfills -*/ -Ember.ArrayPolyfills = { - map: arrayMap, - forEach: arrayForEach, - indexOf: arrayIndexOf -}; + /** + Array polyfills to support ES5 features in older browsers. + + @namespace Ember + @property ArrayPolyfills + */ + Ember.ArrayPolyfills = { + map: arrayMap, + forEach: arrayForEach, + indexOf: arrayIndexOf + }; + if (Ember.SHIM_ES5) { if (!Array.prototype.map) { @@ -831,7 +833,7 @@ Ember.guidFor = function guidFor(obj) { // META // -var META_DESC = { +var META_DESC = Ember.META_DESC = { writable: true, configurable: false, enumerable: false, @@ -871,7 +873,8 @@ Meta.prototype = { bindings: null, chains: null, chainWatchers: null, - values: null + values: null, + proto: null }; if (isDefinePropertySimulated) { @@ -1377,6 +1380,41 @@ Ember.typeOf = function(item) { return ret; }; +/** + Convenience method to inspect an object. This method will attempt to + convert the object into a useful string description. + + It is a pretty simple implementation. If you want something more robust, + use something like JSDump: https://github.com/NV/jsDump + + @method inspect + @for Ember + @param {Object} obj The object you want to inspect. + @return {String} A description of the object +*/ +Ember.inspect = function(obj) { + var type = Ember.typeOf(obj); + if (type === 'array') { + return '[' + obj + ']'; + } + if (type !== 'object') { + return obj + ''; + } + + var v, ret = []; + for(var key in obj) { + if (obj.hasOwnProperty(key)) { + v = obj[key]; + if (v === 'toString') { continue; } // ignore useless items + if (Ember.typeOf(v) === 'function') { v = "function() { ... }"; } + ret.push(key + ": " + v); + } + } + return "{" + ret.join(", ") + "}"; +}; + + + })(); @@ -1591,36 +1629,130 @@ Ember.subscribe = Ember.Instrumentation.subscribe; (function() { -var map, forEach, indexOf, splice; +var map, forEach, indexOf, splice, filter; map = Array.prototype.map || Ember.ArrayPolyfills.map; forEach = Array.prototype.forEach || Ember.ArrayPolyfills.forEach; indexOf = Array.prototype.indexOf || Ember.ArrayPolyfills.indexOf; +filter = Array.prototype.filter || Ember.ArrayPolyfills.filter; splice = Array.prototype.splice; +/** + * Defines some convenience methods for working with Enumerables. + * `Ember.EnumerableUtils` uses `Ember.ArrayPolyfills` when necessary. + * + * @class EnumerableUtils + * @namespace Ember + * @static + * */ var utils = Ember.EnumerableUtils = { + /** + * Calls the map function on the passed object with a specified callback. This + * uses `Ember.ArrayPolyfill`'s-map method when necessary. + * + * @method map + * @param {Object} obj The object that should be mapped + * @param {Function} callback The callback to execute + * @param {Object} thisArg Value to use as this when executing *callback* + * + * @return {Array} An array of mapped values. + */ map: function(obj, callback, thisArg) { return obj.map ? obj.map.call(obj, callback, thisArg) : map.call(obj, callback, thisArg); }, + /** + * Calls the forEach function on the passed object with a specified callback. This + * uses `Ember.ArrayPolyfill`'s-forEach method when necessary. + * + * @method forEach + * @param {Object} obj The object to call forEach on + * @param {Function} callback The callback to execute + * @param {Object} thisArg Value to use as this when executing *callback* + * + */ forEach: function(obj, callback, thisArg) { return obj.forEach ? obj.forEach.call(obj, callback, thisArg) : forEach.call(obj, callback, thisArg); }, + /** + * Calls the filter function on the passed object with a specified callback. This + * uses `Ember.ArrayPolyfill`'s-filter method when necessary. + * + * @method filter + * @param {Object} obj The object to call filter on + * @param {Function} callback The callback to execute + * @param {Object} thisArg Value to use as this when executing *callback* + * + * @return {Array} An array containing the filtered values + */ + filter: function(obj, callback, thisArg) { + return obj.filter ? obj.filter.call(obj, callback, thisArg) : filter.call(obj, callback, thisArg); + }, + + /** + * Calls the indexOf function on the passed object with a specified callback. This + * uses `Ember.ArrayPolyfill`'s-indexOf method when necessary. + * + * @method indexOf + * @param {Object} obj The object to call indexOn on + * @param {Function} callback The callback to execute + * @param {Object} index The index to start searching from + * + */ indexOf: function(obj, element, index) { return obj.indexOf ? obj.indexOf.call(obj, element, index) : indexOf.call(obj, element, index); }, + /** + * Returns an array of indexes of the first occurrences of the passed elements + * on the passed object. + * + * ```javascript + * var array = [1, 2, 3, 4, 5]; + * Ember.EnumerableUtils.indexesOf(array, [2, 5]); // [1, 4] + * + * var fubar = "Fubarr"; + * Ember.EnumerableUtils.indexesOf(fubar, ['b', 'r']); // [2, 4] + * ``` + * + * @method indexesOf + * @param {Object} obj The object to check for element indexes + * @param {Array} elements The elements to search for on *obj* + * + * @return {Array} An array of indexes. + * + */ indexesOf: function(obj, elements) { return elements === undefined ? [] : utils.map(elements, function(item) { return utils.indexOf(obj, item); }); }, + /** + * Adds an object to an array. If the array already includes the object this + * method has no effect. + * + * @method addObject + * @param {Array} array The array the passed item should be added to + * @param {Object} item The item to add to the passed array + * + * @return 'undefined' + */ addObject: function(array, item) { var index = utils.indexOf(array, item); if (index === -1) { array.push(item); } }, + /** + * Removes an object from an array. If the array does not contain the passed + * object this method has no effect. + * + * @method removeObject + * @param {Array} array The array to remove the item from. + * @param {Object} item The item to remove from the passed array. + * + * @return 'undefined' + */ removeObject: function(array, item) { var index = utils.indexOf(array, item); if (index !== -1) { array.splice(index, 1); } @@ -1646,6 +1778,31 @@ var utils = Ember.EnumerableUtils = { return ret; }, + /** + * Replaces objects in an array with the passed objects. + * + * ```javascript + * var array = [1,2,3]; + * Ember.EnumerableUtils.replace(array, 1, 2, [4, 5]); // [1, 4, 5] + * + * var array = [1,2,3]; + * Ember.EnumerableUtils.replace(array, 1, 1, [4, 5]); // [1, 4, 5, 3] + * + * var array = [1,2,3]; + * Ember.EnumerableUtils.replace(array, 10, 1, [4, 5]); // [1, 2, 3, 4, 5] + * ``` + * + * @method replace + * @param {Array} array The array the objects should be inserted into. + * @param {Number} idx Starting index in the array to replace. If *idx* >= + * length, then append to the end of the array. + * @param {Number} amt Number of elements that should be remove from the array, + * starting at *idx* + * @param {Array} objects An array of zero or more objects that should be + * inserted into the array at *idx* + * + * @return {Array} The changed array. + */ replace: function(array, idx, amt, objects) { if (array.replace) { return array.replace(idx, amt, objects); @@ -1654,6 +1811,29 @@ var utils = Ember.EnumerableUtils = { } }, + /** + * Calculates the intersection of two arrays. This method returns a new array + * filled with the records that the two passed arrays share with each other. + * If there is no intersection, an empty array will be returned. + * + * ```javascript + * var array1 = [1, 2, 3, 4, 5]; + * var array2 = [1, 3, 5, 6, 7]; + * + * Ember.EnumerableUtils.intersection(array1, array2); // [1, 3, 5] + * + * var array1 = [1, 2, 3]; + * var array2 = [4, 5, 6]; + * + * Ember.EnumerableUtils.intersection(array1, array2); // [] + * ``` + * + * @method intersection + * @param {Array} array1 The first array + * @param {Array} array2 The second array + * + * @return {Array} The intersection of the two passed arrays. + */ intersection: function(array1, array2) { var intersection = []; @@ -1785,7 +1965,7 @@ var normalizeTuple = Ember.normalizeTuple = function(target, path) { } // must return some kind of path to be valid else other things will break. - if (!path || path.length===0) throw new Ember.Error('Invalid Path'); + if (!path || path.length===0) throw new Ember.Error('Path cannot be empty'); return [ target, path ]; }; @@ -2302,7 +2482,7 @@ ObserverSet.prototype.clear = function() { (function() { -var metaFor = Ember.meta, +var META_KEY = Ember.META_KEY, guidFor = Ember.guidFor, tryFinally = Ember.tryFinally, sendEvent = Ember.sendEvent, @@ -2333,10 +2513,10 @@ var metaFor = Ember.meta, @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]; + var m = obj[META_KEY], + watching = (m && m.watching[keyName] > 0) || keyName === 'length', + proto = m && m.proto, + desc = m && m.descs[keyName]; if (!watching) { return; } if (proto === obj) { return; } @@ -2363,10 +2543,10 @@ Ember.propertyWillChange = propertyWillChange; @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]; + var m = obj[META_KEY], + watching = (m && m.watching[keyName] > 0) || keyName === 'length', + proto = m && m.proto, + desc = m && m.descs[keyName]; if (proto === obj) { return; } @@ -2439,7 +2619,7 @@ function chainsWillChange(obj, keyName, m) { } function chainsDidChange(obj, keyName, m, suppressEvents) { - if (!(m.hasOwnProperty('chainWatchers') && + if (!(m && m.hasOwnProperty('chainWatchers') && m.chainWatchers[keyName])) { return; } @@ -2555,16 +2735,6 @@ var META_KEY = Ember.META_KEY, property is not defined but the object implements the `setUnknownProperty` method then that will be invoked as well. - If you plan to run on IE8 and older browsers then you should use this - method anytime you want to set a property on an object that you don't - know for sure is private. (Properties beginning with an underscore '_' - are considered private.) - - On all newer browsers, you only need to use this method to set - properties if the property might not be defined on the object and you want - to respect the `setUnknownProperty` handler. Otherwise you can ignore this - method. - @method set @for Ember @param {Object} obj The object to modify. @@ -3315,7 +3485,8 @@ Ember.defineProperty = function(obj, keyName, desc, data, meta) { } else { obj[keyName] = undefined; // make enumerable } - } else { + + } else { descs[keyName] = undefined; // shadow descriptor in proto if (desc == null) { value = data; @@ -3437,11 +3608,11 @@ var metaFor = Ember.meta, // utils.js MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, o_defineProperty = Ember.platform.defineProperty; -Ember.watchKey = function(obj, keyName) { +Ember.watchKey = function(obj, keyName, meta) { // can't watch length on Array - it is special... if (keyName === 'length' && typeOf(obj) === 'array') { return; } - var m = metaFor(obj), watching = m.watching; + var m = meta || metaFor(obj), watching = m.watching; // activate watching first time if (!watching[keyName]) { @@ -3466,8 +3637,8 @@ Ember.watchKey = function(obj, keyName) { }; -Ember.unwatchKey = function(obj, keyName) { - var m = metaFor(obj), watching = m.watching; +Ember.unwatchKey = function(obj, keyName, meta) { + var m = meta || metaFor(obj), watching = m.watching; if (watching[keyName] === 1) { watching[keyName] = 0; @@ -3510,7 +3681,8 @@ var metaFor = Ember.meta, // utils.js warn = Ember.warn, watchKey = Ember.watchKey, unwatchKey = Ember.unwatchKey, - FIRST_KEY = /^([^\.\*]+)/; + FIRST_KEY = /^([^\.\*]+)/, + META_KEY = Ember.META_KEY; function firstKey(path) { return path.match(FIRST_KEY)[0]; @@ -3544,24 +3716,24 @@ function addChainWatcher(obj, keyName, node) { if (!nodes[keyName]) { nodes[keyName] = []; } nodes[keyName].push(node); - watchKey(obj, keyName); + watchKey(obj, keyName, m); } 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 + var m = obj[META_KEY]; + if (m && !m.hasOwnProperty('chainWatchers')) { return; } // nothing to do - var nodes = m.chainWatchers; + var nodes = m && m.chainWatchers; - if (nodes[keyName]) { + if (nodes && nodes[keyName]) { nodes = nodes[keyName]; for (var i = 0, l = nodes.length; i < l; i++) { if (nodes[i] === node) { nodes.splice(i, 1); } } } - unwatchKey(obj, keyName); + unwatchKey(obj, keyName, m); }; // A ChainNode watches a single key on an object. If you provide a starting @@ -3601,14 +3773,14 @@ var ChainNodePrototype = ChainNode.prototype; function lazyGet(obj, key) { if (!obj) return undefined; - var meta = metaFor(obj, false); + var meta = obj[META_KEY]; // check if object meant only to be a prototype - if (meta.proto === obj) return undefined; + if (meta && meta.proto === obj) return undefined; if (key === "@each") return get(obj, key); // if a CP only return cached value - var desc = meta.descs[key]; + var desc = meta && meta.descs[key]; if (desc && desc._cacheable) { if (key in meta.cache) { return meta.cache[key]; @@ -3820,12 +3992,14 @@ ChainNodePrototype.didChange = function(events) { }; Ember.finishChains = function(obj) { - var m = metaFor(obj, false), chains = m.chains; + // We only create meta if we really have to + var m = obj[META_KEY], chains = m && m.chains; if (chains) { if (chains.value() !== obj) { - m.chains = chains = chains.copy(obj); + metaFor(obj).chains = chains = chains.copy(obj); + } else { + chains.didChange(null); } - chains.didChange(null); } }; @@ -3834,6 +4008,50 @@ Ember.finishChains = function(obj) { (function() { +/** + @module ember-metal + */ + +var forEach = Ember.EnumerableUtils.forEach, +BRACE_EXPANSION = /^((?:[^\.]*\.)*)\{(.*)\}$/; + +/** + Expands `pattern`, invoking `callback` for each expansion. + + The only pattern supported is brace-expansion, anything else will be passed + once to `callback` directly. Brace expansion can only appear at the end of a + pattern, for example as the last item in a chain. + + Example + ```js + function echo(arg){ console.log(arg); } + + Ember.expandProperties('foo.bar', echo); //=> 'foo.bar' + Ember.expandProperties('{foo,bar}', echo); //=> 'foo', 'bar' + Ember.expandProperties('foo.{bar,baz}', echo); //=> 'foo.bar', 'foo.baz' + Ember.expandProperties('{foo,bar}.baz', echo); //=> '{foo,bar}.baz' + ``` + + @method + @private + @param {string} pattern The property pattern to expand. + @param {function} callback The callback to invoke. It is invoked once per + expansion, and is passed the expansion. + */ +Ember.expandProperties = function (pattern, callback) { + var match, prefix, list; + + if (match = BRACE_EXPANSION.exec(pattern)) { + prefix = match[1]; + list = match[2]; + + forEach(list.split(','), function (suffix) { + callback(prefix + suffix); + }); + } else { + callback(pattern); + } +}; })(); @@ -3847,8 +4065,8 @@ var metaFor = Ember.meta, // utils.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; +function chainsFor(obj, meta) { + var m = meta || metaFor(obj), ret = m.chains; if (!ret) { ret = m.chains = new ChainNode(null, null, obj); } else if (ret.value() !== obj) { @@ -3857,26 +4075,26 @@ function chainsFor(obj) { return ret; } -Ember.watchPath = function(obj, keyPath) { +Ember.watchPath = function(obj, keyPath, meta) { // can't watch length on Array - it is special... if (keyPath === 'length' && typeOf(obj) === 'array') { return; } - var m = metaFor(obj), watching = m.watching; + var m = meta || metaFor(obj), watching = m.watching; if (!watching[keyPath]) { // activate watching first time watching[keyPath] = 1; - chainsFor(obj).add(keyPath); + chainsFor(obj, m).add(keyPath); } else { watching[keyPath] = (watching[keyPath] || 0) + 1; } }; -Ember.unwatchPath = function(obj, keyPath) { - var m = metaFor(obj), watching = m.watching; +Ember.unwatchPath = function(obj, keyPath, meta) { + var m = meta || metaFor(obj), watching = m.watching; if (watching[keyPath] === 1) { watching[keyPath] = 0; - chainsFor(obj).remove(keyPath); + chainsFor(obj, m).remove(keyPath); } else if (watching[keyPath] > 1) { watching[keyPath]--; } @@ -3920,14 +4138,14 @@ function isKeyName(path) { @param obj @param {String} keyName */ -Ember.watch = function(obj, _keyPath) { +Ember.watch = function(obj, _keyPath, m) { // can't watch length on Array - it is special... if (_keyPath === 'length' && typeOf(obj) === 'array') { return; } if (isKeyName(_keyPath)) { - watchKey(obj, _keyPath); + watchKey(obj, _keyPath, m); } else { - watchPath(obj, _keyPath); + watchPath(obj, _keyPath, m); } }; @@ -3938,14 +4156,14 @@ Ember.isWatching = function isWatching(obj, key) { Ember.watch.flushPending = Ember.flushPendingChains; -Ember.unwatch = function(obj, _keyPath) { +Ember.unwatch = function(obj, _keyPath, m) { // can't watch length on Array - it is special... if (_keyPath === 'length' && typeOf(obj) === 'array') { return; } if (isKeyName(_keyPath)) { - unwatchKey(obj, _keyPath); + unwatchKey(obj, _keyPath, m); } else { - unwatchPath(obj, _keyPath); + unwatchPath(obj, _keyPath, m); } }; @@ -3960,7 +4178,7 @@ Ember.unwatch = function(obj, _keyPath) { @param obj */ Ember.rewatch = function(obj) { - var m = metaFor(obj, false), chains = m.chains; + var m = obj[META_KEY], chains = m && m.chains; // make sure the object has its own guid. if (GUID_KEY in obj && !obj.hasOwnProperty(GUID_KEY)) { @@ -4036,6 +4254,7 @@ var get = Ember.get, watch = Ember.watch, unwatch = Ember.unwatch; +var expandProperties = Ember.expandProperties; // .......................................................... // DEPENDENT KEYS @@ -4085,7 +4304,7 @@ function addDependentKeys(desc, obj, keyName, meta) { // Increment the number of times depKey depends on keyName. keys[keyName] = (keys[keyName] || 0) + 1; // Watch the depKey - watch(obj, depKey); + watch(obj, depKey, meta); } } @@ -4104,7 +4323,7 @@ function removeDependentKeys(desc, obj, keyName, meta) { // Increment the number of times depKey depends on keyName. keys[keyName] = (keys[keyName] || 0) - 1; // Watch the depKey - unwatch(obj, depKey); + unwatch(obj, depKey, meta); } } @@ -4195,9 +4414,11 @@ function removeDependentKeys(desc, obj, keyName, meta) { */ function ComputedProperty(func, opts) { this.func = func; + + this._dependentKeys = opts && opts.dependentKeys; + this._cacheable = (opts && opts.cacheable !== undefined) ? opts.cacheable : true; - this._dependentKeys = opts && opts.dependentKeys; this._readOnly = opts && (opts.readOnly !== undefined || !!opts.readOnly); } @@ -4206,6 +4427,7 @@ ComputedProperty.prototype = new Ember.Descriptor(); var ComputedPropertyPrototype = ComputedProperty.prototype; + /** Properties are cacheable by default. Computed property will automatically cache the return value of your function until one of the dependent keys changes. @@ -4300,11 +4522,19 @@ ComputedPropertyPrototype.readOnly = function(readOnly) { ComputedPropertyPrototype.property = function() { var args; + var addArg = function (property) { + args.push(property); + }; + + args = []; + for (var i = 0, l = arguments.length; i < l; i++) { + expandProperties(arguments[i], addArg); + } + - args = a_slice.call(arguments); + this._dependentKeys = args; - this._dependentKeys = args; return this; }; @@ -4431,7 +4661,7 @@ ComputedPropertyPrototype.set = function(obj, keyName, value) { funcArgLength, cachedValue, ret; if (this._readOnly) { - throw new Ember.Error('Cannot Set: ' + keyName + ' on: ' + obj.toString() ); + throw new Ember.Error('Cannot Set: ' + keyName + ' on: ' + Ember.inspect(obj)); } this._suspended = obj; @@ -4547,7 +4777,8 @@ Ember.computed = function(func) { @return {Object} the cached value */ Ember.cacheFor = function cacheFor(obj, key) { - var cache = metaFor(obj, false).cache; + var meta = obj[META_KEY], + cache = meta && meta.cache; if (cache && key in cache) { return cache[key]; @@ -4562,26 +4793,33 @@ function getProperties(self, propertyNames) { return ret; } -function registerComputed(name, macro) { - Ember.computed[name] = function(dependentKey) { - var args = a_slice.call(arguments); - return Ember.computed(dependentKey, function() { - return macro.apply(this, args); - }); +var registerComputed, registerComputedWithProperties; + + + + registerComputed = function (name, macro) { + Ember.computed[name] = function(dependentKey) { + var args = a_slice.call(arguments); + return Ember.computed(dependentKey, function() { + return macro.apply(this, args); + }); + }; }; -} -function registerComputedWithProperties(name, macro) { - Ember.computed[name] = function() { - var properties = a_slice.call(arguments); + registerComputedWithProperties = function(name, macro) { + Ember.computed[name] = function() { + var properties = a_slice.call(arguments); - var computed = Ember.computed(function() { - return macro.apply(this, [getProperties(this, properties)]); - }); + var computed = Ember.computed(function() { + return macro.apply(this, [getProperties(this, properties)]); + }); - return computed.property.apply(computed, properties); + return computed.property.apply(computed, properties); + }; }; -} + + + /** A computed property that returns true if the value of the dependent @@ -5112,7 +5350,6 @@ Ember.computed.oneWay = function(dependentKey) { }); }; - /** A computed property that acts like a standard getter and setter, but returns the value at the provided `defaultPath` if the @@ -5789,7 +6026,7 @@ define("backburner", return true; } } - } else if (window.toString.call(timer) === "[object Array]"){ // we're cancelling a throttle or debounce + } else if (Object.prototype.toString.call(timer) === "[object Array]"){ // we're cancelling a throttle or debounce return this._cancelItem(findThrottler, throttlers, timer) || this._cancelItem(findDebouncee, debouncees, timer); } else { @@ -5903,6 +6140,7 @@ define("backburner", __exports__.Backburner = Backburner; }); + })(); @@ -5926,7 +6164,8 @@ var Backburner = requireModule('backburner').Backburner, onBegin: onBegin, onEnd: onEnd }), - slice = [].slice; + slice = [].slice, + concat = [].concat; // .......................................................... // Ember.run - this is ideally the only public API the dev sees @@ -6012,7 +6251,7 @@ Ember.run = function(target, method) { @return {Object} Return value from invoking the passed function. Please note, when called within an existing loop, no return value is possible. */ -Ember.run.join = function(target, method) { +Ember.run.join = function(target, method /* args */) { if (!Ember.run.currentRunLoop) { return Ember.run.apply(Ember.run, arguments); } @@ -6022,6 +6261,53 @@ Ember.run.join = function(target, method) { Ember.run.schedule.apply(Ember.run, args); }; +/** + Provides a useful utility for when integrating with non-Ember libraries + that provide asynchronous callbacks. + + Ember utilizes a run-loop to batch and coalesce changes. This works by + marking the start and end of Ember-related Javascript execution. + + When using events such as a View's click handler, Ember wraps the event + handler in a run-loop, but when integrating with non-Ember libraries this + can be tedious. + + For example, the following is rather verbose but is the correct way to combine + third-party events and Ember code. + + ```javascript + var that = this; + jQuery(window).on('resize', function(){ + Ember.run(function(){ + that.handleResize(); + }); + }); + ``` + + To reduce the boilerplate, the following can be used to construct a + run-loop-wrapped callback handler. + + ```javascript + jQuery(window).on('resize', Ember.run.bind(this, this.triggerResize)); + ``` + + @method bind + @namespace Ember.run + @param {Object} [target] target of method to call + @param {Function|String} method Method to invoke. + May be a function or a string. If you pass a string + then it will be looked up on the passed target. + @param {Object} [args*] Any additional arguments you wish to pass to the method. + @return {Object} return value from invoking the passed function. Please note, + when called within an existing loop, no return value is possible. +*/ +Ember.run.bind = function(target, method /* args*/) { + var args = arguments; + return function() { + return Ember.run.join.apply(Ember.run, args); + }; +}; + Ember.run.backburner = backburner; var run = Ember.run; @@ -6191,7 +6477,7 @@ Ember.run.later = function(target, method) { 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 + @return {Object} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.once = function(target, method) { checkAutoRun(); @@ -6242,7 +6528,7 @@ Ember.run.once = function(target, method) { 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 + @return {Object} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.scheduleOnce = function(queue, target, method) { checkAutoRun(); @@ -6305,7 +6591,7 @@ Ember.run.scheduleOnce = function(queue, target, method) { 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 + @return {Object} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.next = function() { var args = slice.call(arguments); @@ -6315,7 +6601,8 @@ Ember.run.next = function() { /** Cancels a scheduled item. Must be a value returned by `Ember.run.later()`, - `Ember.run.once()`, or `Ember.run.next()`. + `Ember.run.once()`, `Ember.run.next()`, `Ember.run.debounce()`, or + `Ember.run.throttle()`. ```javascript var runNext = Ember.run.next(myContext, function() { @@ -6332,11 +6619,29 @@ Ember.run.next = function() { // will not be executed }); Ember.run.cancel(runOnce); + + var throttle = Ember.run.throttle(myContext, function() { + // will not be executed + }, 1); + Ember.run.cancel(throttle); + + var debounce = Ember.run.debounce(myContext, function() { + // will not be executed + }, 1); + Ember.run.cancel(debounce); + + var debounceImmediate = Ember.run.debounce(myContext, function() { + // will be executed since we passed in true (immediate) + }, 100, true); + // the 100ms delay until this method can be called again will be cancelled + Ember.run.cancel(debounceImmediate); + ``` + ``` ``` @method cancel @param {Object} timer Timer object to cancel - @return {void} + @return {Boolean} true if cancelled or false/undefined if it wasn't found */ Ember.run.cancel = function(timer) { return backburner.cancel(timer); @@ -6368,6 +6673,34 @@ Ember.run.cancel = function(timer) { // console logs 'debounce ran.' one time. ``` + Immediate allows you to run the function immediately, but debounce + other calls for this function until the wait time has elapsed. If + `debounce` is called again before the specified time has elapsed, + the timer is reset and the entire period msut pass again before + the method can be called again. + + ```javascript + var myFunc = function() { console.log(this.name + ' ran.'); }; + var myContext = {name: 'debounce'}; + + Ember.run.debounce(myContext, myFunc, 150, true); + + // console logs 'debounce ran.' one time immediately. + // 100ms passes + + Ember.run.debounce(myContext, myFunc, 150, true); + + // 150ms passes and nothing else is logged to the console and + // the debouncee is no longer being watched + + Ember.run.debounce(myContext, myFunc, 150, true); + + // console logs 'debounce ran.' one time immediately. + // 150ms passes and nothing else is logged tot he console and + // the debouncee is no longer being watched + + ``` + @method debounce @param {Object} [target] target of method to invoke @param {Function|String} method The method to invoke. @@ -6376,7 +6709,7 @@ Ember.run.cancel = function(timer) { @param {Object} [args*] Optional arguments to pass to the timeout. @param {Number} wait Number of milliseconds to wait. @param {Boolean} immediate Trigger the function on the leading instead of the trailing edge of the wait interval. - @return {void} + @return {Array} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.debounce = function() { return backburner.debounce.apply(backburner, arguments); @@ -6413,7 +6746,7 @@ Ember.run.debounce = function() { then it will be looked up on the passed target. @param {Object} [args*] Optional arguments to pass to the timeout. @param {Number} spacing Number of milliseconds to space out requests. - @return {void} + @return {Array} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.throttle = function() { return backburner.throttle.apply(backburner, arguments); @@ -6918,11 +7251,14 @@ var Mixin, REQUIRED, Alias, a_slice = [].slice, o_create = Ember.create, defineProperty = Ember.defineProperty, - guidFor = Ember.guidFor; + guidFor = Ember.guidFor, + metaFor = Ember.meta, + META_KEY = Ember.META_KEY; +var expandProperties = Ember.expandProperties; function mixinsMeta(obj) { - var m = Ember.meta(obj, true), ret = m.mixins; + var m = metaFor(obj, true), ret = m.mixins; if (!ret) { ret = m.mixins = {}; } else if (!m.hasOwnProperty('mixins')) { @@ -7105,7 +7441,7 @@ function mergeMixins(mixins, m, descs, values, base, keys) { if (props === CONTINUE) { continue; } if (props) { - meta = Ember.meta(base); + meta = metaFor(base); if (base.willMergeMixin) { base.willMergeMixin(props); } concats = concatenatedMixinProperties('concatenatedProperties', props, values, base); mergings = concatenatedMixinProperties('mergedProperties', props, values, base); @@ -7163,7 +7499,7 @@ function connectBindings(obj, m) { } function finishPartial(obj, m) { - connectBindings(obj, m || Ember.meta(obj)); + connectBindings(obj, m || metaFor(obj)); return obj; } @@ -7210,7 +7546,7 @@ function replaceObserversAndListeners(obj, key, observerOrListener) { } function applyMixin(obj, mixins, partial) { - var descs = {}, values = {}, m = Ember.meta(obj), + var descs = {}, values = {}, m = metaFor(obj), key, value, desc, keys = []; // Go through all mixins and hashes passed in, and: @@ -7292,7 +7628,7 @@ Ember.mixin = function(obj) { Note that mixins extend a constructor's prototype so arrays and object literals defined as properties will be shared amongst objects that implement the mixin. - If you want to define an property in a mixin that is not shared, you can define + If you want to define a property in a mixin that is not shared, you can define it either as a computed property or have it be created on initialization of the object. ```javascript @@ -7420,7 +7756,8 @@ function _detect(curMixin, targetMixin, seen) { MixinPrototype.detect = function(obj) { if (!obj) { return false; } if (obj instanceof Mixin) { return _detect(obj, this, {}); } - var mixins = Ember.meta(obj, false).mixins; + var m = obj[META_KEY], + mixins = m && m.mixins; if (mixins) { return !!mixins[guidFor(this)]; } @@ -7459,7 +7796,8 @@ MixinPrototype.keys = function() { // returns the mixins currently applied to the specified object // TODO: Make Ember.mixin Mixin.mixins = function(obj) { - var mixins = Ember.meta(obj, false).mixins, ret = []; + var m = obj[META_KEY], + mixins = m && m.mixins, ret = []; if (!mixins) { return ret; } @@ -7491,34 +7829,6 @@ Alias = function(methodName) { }; Alias.prototype = new Ember.Descriptor(); -/** - Makes a property or method available via an additional name. - - ```javascript - App.PaintSample = Ember.Object.extend({ - color: 'red', - colour: Ember.alias('color'), - name: function() { - return "Zed"; - }, - moniker: Ember.alias("name") - }); - - var paintSample = App.PaintSample.create() - paintSample.get('colour'); // 'red' - paintSample.moniker(); // 'Zed' - ``` - - @method alias - @for Ember - @param {String} methodName name of the method or property to alias - @return {Ember.Descriptor} - @deprecated Use `Ember.aliasMethod` or `Ember.computed.alias` instead -*/ -Ember.alias = function(methodName) { - return new Alias(methodName); -}; - /** Makes a method available via an additional name. @@ -7573,16 +7883,21 @@ Ember.observer = function() { var func = a_slice.call(arguments, -1)[0]; var paths; - - paths = a_slice.call(arguments, 0, -1); + var addWatchedProperty = function (path) { paths.push(path); }; + var _paths = a_slice.call(arguments, 0, -1); - if (typeof func !== "function") { - // revert to old, soft-deprecated argument ordering + if (typeof func !== "function") { + // revert to old, soft-deprecated argument ordering - func = arguments[0]; - paths = a_slice.call(arguments, 1); - } - + func = arguments[0]; + _paths = a_slice.call(arguments, 1); + } + + paths = []; + + for (var i=0; i<_paths.length; ++i) { + expandProperties(_paths[i], addWatchedProperty); + } if (typeof func !== "function") { throw new Ember.Error("Ember.observer called without a function"); @@ -7670,16 +7985,22 @@ Ember.beforeObserver = function() { var func = a_slice.call(arguments, -1)[0]; var paths; - - paths = a_slice.call(arguments, 0, -1); + var addWatchedProperty = function(path) { paths.push(path); }; - if (typeof func !== "function") { - // revert to old, soft-deprecated argument ordering + var _paths = a_slice.call(arguments, 0, -1); - func = arguments[0]; - paths = a_slice.call(arguments, 1); - } - + if (typeof func !== "function") { + // revert to old, soft-deprecated argument ordering + + func = arguments[0]; + _paths = a_slice.call(arguments, 1); + } + + paths = []; + + for (var i=0; i<_paths.length; ++i) { + expandProperties(_paths[i], addWatchedProperty); + } if (typeof func !== "function") { throw new Ember.Error("Ember.beforeObserver called without a function"); @@ -9353,7 +9674,7 @@ define("rsvp/promise/all", ``` @method all - @for RSVP.Promise + @for Ember.RSVP.Promise @param {Array} entries array of promises @param {String} label optional string for labeling the promise. Useful for tooling. @@ -9917,6 +10238,7 @@ Ember.MODEL_FACTORY_INJECTIONS = false || !!Ember.ENV.MODEL_FACTORY_INJECTIONS; define("container", [], function() { + "use strict"; // A safe and simple inheriting object. function InheritingDict(parent) { @@ -10038,7 +10360,8 @@ define("container", this.registry = new InheritingDict(parent && parent.registry); this.cache = new InheritingDict(parent && parent.cache); - this.factoryCache = new InheritingDict(parent && parent.cache); + this.factoryCache = new InheritingDict(parent && parent.factoryCache); + this.resolveCache = new InheritingDict(parent && parent.resolveCache); this.typeInjections = new InheritingDict(parent && parent.typeInjections); this.injections = {}; @@ -10159,9 +10482,7 @@ define("container", @param {Object} options */ register: function(fullName, factory, options) { - if (fullName.indexOf(':') === -1) { - throw new TypeError("malformed fullName, expected: `type:name` got: " + fullName + ""); - } + validateFullName(fullName); if (factory === undefined) { throw new TypeError('Attempting to register an unknown factory: `' + fullName + '`'); @@ -10194,11 +10515,14 @@ define("container", @param {String} fullName */ unregister: function(fullName) { + validateFullName(fullName); + var normalizedName = this.normalize(fullName); this.registry.remove(normalizedName); this.cache.remove(normalizedName); this.factoryCache.remove(normalizedName); + this.resolveCache.remove(normalizedName); this._options.remove(normalizedName); }, @@ -10235,7 +10559,18 @@ define("container", @return {Function} fullName's factory */ resolve: function(fullName) { - return this.resolver(fullName) || this.registry.get(fullName); + validateFullName(fullName); + + var normalizedName = this.normalize(fullName); + var cached = this.resolveCache.get(normalizedName); + + if (cached) { return cached; } + + var resolved = this.resolver(normalizedName) || this.registry.get(normalizedName); + + this.resolveCache.set(normalizedName, resolved); + + return resolved; }, /** @@ -10316,23 +10651,8 @@ define("container", @return {any} */ lookup: function(fullName, options) { - fullName = this.normalize(fullName); - - options = options || {}; - - if (this.cache.has(fullName) && options.singleton !== false) { - return this.cache.get(fullName); - } - - var value = instantiate(this, fullName); - - if (value === undefined) { return; } - - if (isSingleton(this, fullName) && options.singleton !== false) { - this.cache.set(fullName, value); - } - - return value; + validateFullName(fullName); + return lookup(this, this.normalize(fullName), options); }, /** @@ -10343,7 +10663,8 @@ define("container", @return {any} */ lookupFactory: function(fullName) { - return factoryFor(this, fullName); + validateFullName(fullName); + return factoryFor(this, this.normalize(fullName)); }, /** @@ -10355,11 +10676,8 @@ define("container", @return {Boolean} */ has: function(fullName) { - if (this.cache.has(fullName)) { - return true; - } - - return !!this.resolve(fullName); + validateFullName(fullName); + return has(this, this.normalize(fullName)); }, /** @@ -10440,6 +10758,7 @@ define("container", @param {String} fullName */ typeInjection: function(type, property, fullName) { + validateFullName(fullName); if (this.parent) { illegalChildOperation('typeInjection'); } addTypeInjection(this.typeInjections, type, property, fullName); @@ -10489,14 +10808,20 @@ define("container", @param {String} property @param {String} injectionName */ - injection: function(factoryName, property, injectionName) { + injection: function(fullName, property, injectionName) { if (this.parent) { illegalChildOperation('injection'); } - if (factoryName.indexOf(':') === -1) { - return this.typeInjection(factoryName, property, injectionName); + validateFullName(injectionName); + var normalizedInjectionName = this.normalize(injectionName); + + if (fullName.indexOf(':') === -1) { + return this.typeInjection(fullName, property, normalizedInjectionName); } - addInjection(this.injections, factoryName, property, injectionName); + validateFullName(fullName); + var normalizedName = this.normalize(fullName); + + addInjection(this.injections, normalizedName, property, normalizedInjectionName); }, @@ -10532,7 +10857,7 @@ define("container", factoryTypeInjection: function(type, property, fullName) { if (this.parent) { illegalChildOperation('factoryTypeInjection'); } - addTypeInjection(this.factoryTypeInjections, type, property, fullName); + addTypeInjection(this.factoryTypeInjections, type, property, this.normalize(fullName)); }, /** @@ -10584,14 +10909,21 @@ define("container", @param {String} property @param {String} injectionName */ - factoryInjection: function(factoryName, property, injectionName) { + factoryInjection: function(fullName, property, injectionName) { if (this.parent) { illegalChildOperation('injection'); } - if (factoryName.indexOf(':') === -1) { - return this.factoryTypeInjection(factoryName, property, injectionName); + var normalizedName = this.normalize(fullName); + var normalizedInjectionName = this.normalize(injectionName); + + validateFullName(injectionName); + + if (fullName.indexOf(':') === -1) { + return this.factoryTypeInjection(normalizedName, property, normalizedInjectionName); } - addInjection(this.factoryInjections, factoryName, property, injectionName); + validateFullName(fullName); + + addInjection(this.factoryInjections, normalizedName, property, normalizedInjectionName); }, /** @@ -10601,7 +10933,6 @@ define("container", @method destroy */ destroy: function() { - for (var i=0, l=this.children.length; i` and the following code: ```javascript - anUndorderedListView = Ember.CollectionView.create({ + anUnorderedListView = Ember.CollectionView.create({ tagName: 'ul', content: ['A','B','C'], itemViewClass: Ember.View.extend({ @@ -24663,7 +25106,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; }) }); - anUndorderedListView.appendTo('body'); + anUnorderedListView.appendTo('body'); ``` Will result in the following HTML structure @@ -25010,6 +25453,69 @@ Ember.CollectionView.CONTAINER_MAP = { +(function() { +/** + The ComponentTemplateDeprecation mixin is used to provide a useful + deprecation warning when using either `template` or `templateName` with + a component. The `template` and `templateName` properties specified at + extend time are moved to `layout` and `layoutName` respectively. + + `Ember.ComponentTemplateDeprecation` is used internally by Ember in + `Ember.Component`. + + @class ComponentTemplateDeprecation + @namespace Ember +*/ +Ember.ComponentTemplateDeprecation = Ember.Mixin.create({ + /** + @private + + Moves `templateName` to `layoutName` and `template` to `layout` at extend + time if a layout is not also specified. + + Note that this currently modifies the mixin themselves, which is technically + dubious but is practically of little consequence. This may change in the + future. + + @method willMergeMixin + */ + willMergeMixin: function(props) { + // must call _super here to ensure that the ActionHandler + // mixin is setup properly (moves actions -> _actions) + // + // Calling super is only OK here since we KNOW that + // there is another Mixin loaded first. + this._super.apply(this, arguments); + + var deprecatedProperty, replacementProperty, + layoutSpecified = (props.layoutName || props.layout); + + if (props.templateName && !layoutSpecified) { + deprecatedProperty = 'templateName'; + replacementProperty = 'layoutName'; + + props.layoutName = props.templateName; + delete props['templateName']; + } + + if (props.template && !layoutSpecified) { + deprecatedProperty = 'template'; + replacementProperty = 'layout'; + + props.layout = props.template; + delete props['template']; + } + + if (deprecatedProperty) { + } + } +}); + + +})(); + + + (function() { var get = Ember.get, set = Ember.set, isNone = Ember.isNone, a_slice = Array.prototype.slice; @@ -25034,11 +25540,11 @@ var get = Ember.get, set = Ember.set, isNone = Ember.isNone, `{{my-foo}}` in other templates, which will make an instance of the isolated component. - ```html + ```handlebars {{app-profile person=currentUser}} ``` - ```html + ```handlebars

{{person.title}}

@@ -25053,7 +25559,7 @@ var get = Ember.get, set = Ember.set, isNone = Ember.isNone, ```handlebars {{#app-profile person=currentUser}}

Admin mode

- {{! Executed in the controllers context. }} + {{! Executed in the controller's context. }} {{/app-profile}} ``` @@ -25085,7 +25591,7 @@ var get = Ember.get, set = Ember.set, isNone = Ember.isNone, And then use it in the component's template: - ```html + ```handlebars

{{person.title}}

@@ -25105,18 +25611,55 @@ var get = Ember.get, set = Ember.set, isNone = Ember.isNone, @namespace Ember @extends Ember.View */ -Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { +Ember.Component = Ember.View.extend(Ember.TargetActionSupport, Ember.ComponentTemplateDeprecation, { init: function() { this._super(); set(this, 'context', this); set(this, 'controller', this); }, - defaultLayout: function(options){ - options.data = {view: options._context}; - Ember.Handlebars.helpers['yield'].apply(this, [options]); + defaultLayout: function(context, options){ + Ember.Handlebars.helpers['yield'].call(context, options); }, + /** + A components template property is set by passing a block + during its invocation. It is executed within the parent context. + + Example: + + ```handlebars + {{#my-component}} + // something that is run in the context + // of the parent context + {{/my-component}} + ``` + + Specifying a template directly to a component is deprecated without + also specifying the layout property. + + @deprecated + @property template + */ + template: Ember.computed(function(key, value) { + if (value !== undefined) { return value; } + + var templateName = get(this, 'templateName'), + template = this.templateForName(templateName, 'template'); + + + return template || get(this, 'defaultTemplate'); + }).property('templateName'), + + /** + Specifying a components `templateName` is deprecated without also + providing the `layout` or `layoutName` properties. + + @deprecated + @property templateName + */ + templateName: null, + // during render, isolate keywords cloneKeywords: function() { return { @@ -25170,9 +25713,9 @@ Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { App.PlayButtonComponent = Ember.Component.extend({ click: function(){ if (this.get('isPlaying')) { - this.triggerAction('play'); + this.sendAction('play'); } else { - this.triggerAction('stop'); + this.sendAction('stop'); } } }); @@ -25366,12 +25909,12 @@ define("metamorph", })(), // Feature-detect the W3C range API, the extended check is for IE9 which only partially supports ranges - supportsRange = (!disableRange) && document && ('createRange' in document) && (typeof Range !== 'undefined') && Range.prototype.createContextualFragment, + supportsRange = (!disableRange) && typeof document !== 'undefined' && ('createRange' in document) && (typeof Range !== 'undefined') && Range.prototype.createContextualFragment, // Internet Explorer prior to 9 does not allow setting innerHTML if the first element // is a "zero-scope" element. This problem can be worked around by making // the first node an invisible text node. We, like Modernizr, use ­ - needsShy = document && (function() { + needsShy = typeof document !== 'undefined' && (function() { var testEl = document.createElement('div'); testEl.innerHTML = "
"; testEl.firstChild.innerHTML = ""; @@ -27118,6 +27661,8 @@ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ preserveContext = get(this, 'preserveContext'), context = get(this, 'previousContext'); + var _contextController = get(this, '_contextController'); + var inverseTemplate = get(this, 'inverseTemplate'), displayTemplate = get(this, 'displayTemplate'); @@ -27137,6 +27682,10 @@ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ // Otherwise, determine if this is a block bind or not. // If so, pass the specified object to the template if (displayTemplate) { + if (_contextController) { + set(_contextController, 'content', result); + result = _contextController; + } set(this, '_context', result); } else { // This is not a bind block, just push the result of the @@ -27238,6 +27787,14 @@ function bind(property, options, preserveContext, shouldDisplay, valueNormalizer templateData: options.data }); + if (options.hash.controller) { + bindView.set('_contextController', this.container.lookupFactory('controller:'+options.hash.controller).create({ + container: currentContext.container, + parentController: currentContext, + target: currentContext + })); + } + view.appendChild(bindView); observer = function() { @@ -27312,6 +27869,17 @@ function simpleBind(currentContext, property, options) { } } +function shouldDisplayIfHelperContent(result) { + var truthy = result && get(result, 'isTruthy'); + if (typeof truthy === 'boolean') { return truthy; } + + if (Ember.isArray(result)) { + return get(result, 'length') !== 0; + } else { + return !!result; + } +} + /** '_triageMustache' is used internally select between a binding, helper, or component for the given context. Until this point, it would be hard to determine if the @@ -27413,18 +27981,43 @@ EmberHandlebars.registerHelper('bind', function bindHelper(property, options) { */ EmberHandlebars.registerHelper('boundIf', function boundIfHelper(property, fn) { var context = (fn.contexts && fn.contexts.length) ? fn.contexts[0] : this; - var func = function(result) { - var truthy = result && get(result, 'isTruthy'); - if (typeof truthy === 'boolean') { return truthy; } - if (Ember.isArray(result)) { - return get(result, 'length') !== 0; - } else { - return !!result; - } - }; + return bind.call(context, property, fn, true, shouldDisplayIfHelperContent, shouldDisplayIfHelperContent, ['isTruthy', 'length']); +}); - return bind.call(context, property, fn, true, func, func, ['isTruthy', 'length']); + +/** + @private + + Use the `unboundIf` helper to create a conditional that evaluates once. + + ```handlebars + {{#unboundIf "content.shouldDisplayTitle"}} + {{content.title}} + {{/unboundIf}} + ``` + + @method unboundIf + @for Ember.Handlebars.helpers + @param {String} property Property to bind + @param {Function} fn Context to provide for rendering + @return {String} HTML string +*/ +EmberHandlebars.registerHelper('unboundIf', function unboundIfHelper(property, fn) { + var context = (fn.contexts && fn.contexts.length) ? fn.contexts[0] : this, + data = fn.data, + template = fn.fn, + inverse = fn.inverse, + normalized, propertyValue, result; + + normalized = normalizePath(context, property, data); + propertyValue = handlebarsGet(context, property, fn); + + if (!shouldDisplayIfHelperContent(propertyValue)) { + template = inverse; + } + + template(context, { data: data }); }); /** @@ -27478,6 +28071,23 @@ EmberHandlebars.registerHelper('boundIf', function boundIfHelper(property, fn) { Without the `as` operator, it would be impossible to reference `user.name` in the example above. + ### `controller` option + + Adding `controller='something'` instructs the `{{with}}` helper to create and use an instance of + the specified controller with the new context as its content. + + This is very similar to using an `itemController` option with the `{{each}}` helper. + + ```handlebars + {{#with users.posts controller='userBlogPosts'}} + {{!- The current context is wrapped in our controller instance }} + {{/with}} + ``` + + In the above example, the template provided to the `{{with}}` block is now wrapped in the + `userBlogPost` controller, which provides a very elegant way to decorate the context with custom + functions/properties. + @method with @for Ember.Handlebars.helpers @param {Function} context @@ -27523,6 +28133,7 @@ EmberHandlebars.registerHelper('with', function withHelper(context, options) { /** See [boundIf](/api/classes/Ember.Handlebars.helpers.html#method_boundIf) + and [unboundIf](/api/classes/Ember.Handlebars.helpers.html#method_unboundIf) @method if @for Ember.Handlebars.helpers @@ -27531,8 +28142,11 @@ EmberHandlebars.registerHelper('with', function withHelper(context, options) { @return {String} HTML string */ EmberHandlebars.registerHelper('if', function ifHelper(context, options) { - - return helpers.boundIf.call(options.contexts[0], context, options); + if (options.data.isUnbound) { + return helpers.unboundIf.call(options.contexts[0], context, options); + } else { + return helpers.boundIf.call(options.contexts[0], context, options); + } }); /** @@ -27549,7 +28163,11 @@ EmberHandlebars.registerHelper('unless', function unlessHelper(context, options) options.fn = inverse; options.inverse = fn; - return helpers.boundIf.call(options.contexts[0], context, options); + if (options.data.isUnbound) { + return helpers.unboundIf.call(options.contexts[0], context, options); + } else { + return helpers.boundIf.call(options.contexts[0], context, options); + } }); /** @@ -28311,7 +28929,7 @@ var get = Ember.get, handlebarsGet = Ember.Handlebars.get, fmt = Ember.String.fm ``` - ### Blockless Use + ### Blockless use in a collection If you provide an `itemViewClass` option that has its own `template` you can omit the block. @@ -28405,11 +29023,20 @@ Ember.Handlebars.registerHelper('collection', function collectionHelper(path, op var inverse = options.inverse; var view = options.data.view; + + var controller, container; // If passed a path string, convert that into an object. // Otherwise, just default to the standard class. var collectionClass; - collectionClass = path ? handlebarsGet(this, path, options) : Ember.CollectionView; - + if (path) { + controller = data.keywords.controller; + container = controller && controller.container; + collectionClass = handlebarsGet(this, path, options) || container.lookupFactory('view:' + path); + } + else { + collectionClass = Ember.CollectionView; + } + var hash = options.hash, itemHash = {}, match; // Extract item view class if provided else default to the standard class @@ -28417,9 +29044,9 @@ Ember.Handlebars.registerHelper('collection', function collectionHelper(path, op itemViewClass; if (hash.itemView) { - var controller = data.keywords.controller; - var container = controller.container; - itemViewClass = container.resolve('view:' + hash.itemView); + controller = data.keywords.controller; + container = controller.container; + itemViewClass = container.lookupFactory('view:' + hash.itemView); } else if (hash.itemViewClass) { itemViewClass = handlebarsGet(collectionPrototype, hash.itemViewClass, options); } else { @@ -28613,6 +29240,7 @@ Ember.Handlebars.registerHelper('debugger', function debuggerHelper(options) { */ var get = Ember.get, set = Ember.set; +var fmt = Ember.String.fmt; Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { init: function() { @@ -28621,6 +29249,7 @@ Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { if (itemController) { var controller = get(this, 'controller.container').lookupFactory('controller:array').create({ + _isVirtual: true, parentController: get(this, 'controller'), itemController: itemController, target: get(this, 'controller'), @@ -29599,7 +30228,7 @@ Ember.TextField = Ember.Component.extend(Ember.TextSupport, { classNames: ['ember-text-field'], tagName: "input", - attributeBindings: ['type', 'value', 'size', 'pattern', 'name'], + attributeBindings: ['type', 'value', 'size', 'pattern', 'name', 'min', 'max'], /** The `value` attribute of the input element. As the user inputs text, this @@ -29630,140 +30259,31 @@ Ember.TextField = Ember.Component.extend(Ember.TextSupport, { size: null, /** - The `pattern` the pattern attribute of input element. + The `pattern` attribute of input element. @property pattern @type String @default null */ - pattern: null -}); + pattern: null, -})(); + /** + The `min` attribute of input element used with `type="number"` or `type="range"`. - - -(function() { -/* -@module ember -@submodule ember-handlebars -*/ - -var get = Ember.get, set = Ember.set; - -/* - @class Button - @namespace Ember - @extends Ember.View - @uses Ember.TargetActionSupport - @deprecated -*/ -Ember.Button = Ember.View.extend(Ember.TargetActionSupport, { - classNames: ['ember-button'], - classNameBindings: ['isActive'], - - tagName: 'button', - - propagateEvents: false, - - attributeBindings: ['type', 'disabled', 'href', 'tabindex'], - - /* - @private - - Overrides `TargetActionSupport`'s `targetObject` computed - property to use Handlebars-specific path resolution. - - @property targetObject + @property min + @type String + @default null */ - targetObject: Ember.computed(function() { - var target = get(this, 'target'), - root = get(this, 'context'), - data = get(this, 'templateData'); + min: null, - if (typeof target !== 'string') { return target; } + /** + The `max` attribute of input element used with `type="number"` or `type="range"`. - return Ember.Handlebars.get(root, target, { data: data }); - }).property('target'), - - // Defaults to 'button' if tagName is 'input' or 'button' - type: Ember.computed(function(key) { - var tagName = this.tagName; - if (tagName === 'input' || tagName === 'button') { return 'button'; } - }), - - disabled: false, - - // Allow 'a' tags to act like buttons - href: Ember.computed(function() { - return this.tagName === 'a' ? '#' : null; - }), - - mouseDown: function() { - if (!get(this, 'disabled')) { - set(this, 'isActive', true); - this._mouseDown = true; - this._mouseEntered = true; - } - return get(this, 'propagateEvents'); - }, - - mouseLeave: function() { - if (this._mouseDown) { - set(this, 'isActive', false); - this._mouseEntered = false; - } - }, - - mouseEnter: function() { - if (this._mouseDown) { - set(this, 'isActive', true); - this._mouseEntered = true; - } - }, - - mouseUp: function(event) { - if (get(this, 'isActive')) { - // Actually invoke the button's target and action. - // This method comes from the Ember.TargetActionSupport mixin. - this.triggerAction(); - set(this, 'isActive', false); - } - - this._mouseDown = false; - this._mouseEntered = false; - return get(this, 'propagateEvents'); - }, - - keyDown: function(event) { - // Handle space or enter - if (event.keyCode === 13 || event.keyCode === 32) { - this.mouseDown(); - } - }, - - keyUp: function(event) { - // Handle space or enter - if (event.keyCode === 13 || event.keyCode === 32) { - this.mouseUp(); - } - }, - - // TODO: Handle proper touch behavior. Including should make inactive when - // finger moves more than 20x outside of the edge of the button (vs mouse - // which goes inactive as soon as mouse goes out of edges.) - - touchStart: function(touch) { - return this.mouseDown(touch); - }, - - touchEnd: function(touch) { - return this.mouseUp(touch); - }, - - init: function() { - this._super(); - } + @property max + @type String + @default null + */ + max: null }); })(); @@ -30155,15 +30675,13 @@ Ember.Select = Ember.View.extend({ defaultTemplate: Ember.Handlebars.template(function anonymous(Handlebars,depth0,helpers,partials,data) { this.compilerInfo = [4,'>= 1.0.0']; helpers = this.merge(helpers, Ember.Handlebars.helpers); data = data || {}; - var buffer = '', stack1, hashTypes, hashContexts, escapeExpression=this.escapeExpression, self=this; + var buffer = '', stack1, escapeExpression=this.escapeExpression, self=this; function program1(depth0,data) { - var buffer = '', stack1, hashTypes, hashContexts; + var buffer = '', stack1; data.buffer.push(""); return buffer; @@ -30171,50 +30689,38 @@ function program1(depth0,data) { function program3(depth0,data) { - var stack1, hashTypes, hashContexts; - hashTypes = {}; - hashContexts = {}; - stack1 = helpers.each.call(depth0, "view.groupedContent", {hash:{},inverse:self.noop,fn:self.program(4, program4, data),contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}); + var stack1; + stack1 = helpers.each.call(depth0, "view.groupedContent", {hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(4, program4, data),contexts:[depth0],types:["ID"],data:data}); if(stack1 || stack1 === 0) { data.buffer.push(stack1); } else { data.buffer.push(''); } } function program4(depth0,data) { - var hashContexts, hashTypes; - hashContexts = {'content': depth0,'label': depth0}; - hashTypes = {'content': "ID",'label': "ID"}; + data.buffer.push(escapeExpression(helpers.view.call(depth0, "view.groupView", {hash:{ 'content': ("content"), 'label': ("label") - },contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}))); + },hashTypes:{'content': "ID",'label': "ID"},hashContexts:{'content': depth0,'label': depth0},contexts:[depth0],types:["ID"],data:data}))); } function program6(depth0,data) { - var stack1, hashTypes, hashContexts; - hashTypes = {}; - hashContexts = {}; - stack1 = helpers.each.call(depth0, "view.content", {hash:{},inverse:self.noop,fn:self.program(7, program7, data),contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}); + var stack1; + stack1 = helpers.each.call(depth0, "view.content", {hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(7, program7, data),contexts:[depth0],types:["ID"],data:data}); if(stack1 || stack1 === 0) { data.buffer.push(stack1); } else { data.buffer.push(''); } } function program7(depth0,data) { - var hashContexts, hashTypes; - hashContexts = {'content': depth0}; - hashTypes = {'content': "ID"}; + data.buffer.push(escapeExpression(helpers.view.call(depth0, "view.optionView", {hash:{ 'content': ("") - },contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}))); + },hashTypes:{'content': "ID"},hashContexts:{'content': depth0},contexts:[depth0],types:["ID"],data:data}))); } - hashTypes = {}; - hashContexts = {}; - stack1 = helpers['if'].call(depth0, "view.prompt", {hash:{},inverse:self.noop,fn:self.program(1, program1, data),contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}); + stack1 = helpers['if'].call(depth0, "view.prompt", {hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(1, program1, data),contexts:[depth0],types:["ID"],data:data}); if(stack1 || stack1 === 0) { data.buffer.push(stack1); } - hashTypes = {}; - hashContexts = {}; - stack1 = helpers['if'].call(depth0, "view.optionGroupPath", {hash:{},inverse:self.program(6, program6, data),fn:self.program(3, program3, data),contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}); + stack1 = helpers['if'].call(depth0, "view.optionGroupPath", {hash:{},hashTypes:{},hashContexts:{},inverse:self.program(6, program6, data),fn:self.program(3, program3, data),contexts:[depth0],types:["ID"],data:data}); if(stack1 || stack1 === 0) { data.buffer.push(stack1); } return buffer; @@ -30652,7 +31158,7 @@ Ember.Handlebars.registerHelper('input', function(options) { delete hash.on; if (inputType === 'checkbox') { - return Ember.Handlebars.helpers.view.call(this, Ember.Checkbox, options); + return Ember.Handlebars.helpers.view.call(this, Ember.Checkbox, options); } else { if (inputType) { hash.type = inputType; } hash.onEvent = onEvent || 'enter'; @@ -30954,9 +31460,9 @@ Ember.runLoadHooks('Ember.Handlebars', Ember.Handlebars); })(); (function() { -define("route-recognizer", - [], - function() { +define("route-recognizer", + ["exports"], + function(__exports__) { "use strict"; var specials = [ '/', '.', '*', '+', '?', '|', @@ -30985,11 +31491,11 @@ define("route-recognizer", function StaticSegment(string) { this.string = string; } StaticSegment.prototype = { eachChar: function(callback) { - var string = this.string, char; + var string = this.string, ch; for (var i=0, l=string.length; i 0) { - currentResult.queryParams = activeQueryParams; - } - result.push(currentResult); + + result.push({ handler: handler.handler, params: params, isDynamic: !!names.length }); } return result; } function addSegment(currentState, segment) { - segment.eachChar(function(char) { + segment.eachChar(function(ch) { var state; - currentState = currentState.put(char); + currentState = currentState.put(ch); }); return currentState; @@ -31286,9 +31797,6 @@ define("route-recognizer", } var handler = { handler: route.handler, names: names }; - if(route.queryParams) { - handler.queryParams = route.queryParams; - } handlers.push(handler); } @@ -31349,24 +31857,26 @@ define("route-recognizer", }, generateQueryString: function(params, handlers) { - var pairs = [], allowedParams = []; - for(var i=0; i < handlers.length; i++) { - var currentParamList = handlers[i].queryParams; - if(currentParamList) { - allowedParams.push.apply(allowedParams, currentParamList); - } - } + var pairs = []; for(var key in params) { if (params.hasOwnProperty(key)) { - if(allowedParams.indexOf(key) === -1) { - throw 'Query param "' + key + '" is not specified as a valid param for this route'; - } var value = params[key]; - var pair = encodeURIComponent(key); - if(value !== true) { - pair += "=" + encodeURIComponent(value); + if (value === false || value == null) { + continue; + } + var pair = key; + if (Array.isArray(value)) { + for (var i = 0, l = value.length; i < l; i++) { + var arrayPair = key + '[]' + '=' + encodeURIComponent(value[i]); + pairs.push(arrayPair); + } + } + else if (value !== true) { + pair += "=" + encodeURIComponent(value); + pairs.push(pair); + } else { + pairs.push(pair); } - pairs.push(pair); } } @@ -31380,15 +31890,36 @@ define("route-recognizer", for(var i=0; i < pairs.length; i++) { var pair = pairs[i].split('='), key = decodeURIComponent(pair[0]), - value = pair[1] ? decodeURIComponent(pair[1]) : true; - queryParams[key] = value; + keyLength = key.length, + isArray = false, + value; + if (pair.length === 1) { + value = true; + } else { + //Handle arrays + if (keyLength > 2 && key.slice(keyLength -2) === '[]') { + isArray = true; + key = key.slice(0, keyLength - 2); + if(!queryParams[key]) { + queryParams[key] = []; + } + } + value = pair[1] ? decodeURIComponent(pair[1]) : ''; + } + if (isArray) { + queryParams[key].push(value); + } else { + queryParams[key] = value; + } + } return queryParams; }, recognize: function(path) { var states = [ this.rootState ], - pathLen, i, l, queryStart, queryParams = {}; + pathLen, i, l, queryStart, queryParams = {}, + isSlashDropped = false; queryStart = path.indexOf('?'); if (queryStart !== -1) { @@ -31404,6 +31935,7 @@ define("route-recognizer", pathLen = path.length; if (pathLen > 1 && path.charAt(pathLen - 1) === "/") { path = path.substr(0, pathLen - 1); + isSlashDropped = true; } for (i=0, l=path.length; i 0) { - var err = 'You supplied the params '; - err += missingParams.map(function(param) { - return '"' + param + "=" + queryParams[param] + '"'; - }).join(' and '); - - err += ' which are not valid for the "' + handlerName + '" handler or its parents'; - - throw new Error(err); + for (var i = 0, len = state.handlerInfos.length; i < len; ++i) { + var handlerInfo = state.handlerInfos[i]; + var handlerParams = handlerInfo.params || + serialize(handlerInfo.handler, handlerInfo.context, handlerInfo.names); + merge(params, handlerParams); } + params.queryParams = queryParams; return this.recognizer.generate(handlerName, params); }, isActive: function(handlerName) { + var partitionedArgs = extractQueryParams(slice.call(arguments, 1)), contexts = partitionedArgs[0], queryParams = partitionedArgs[1], - activeQueryParams = {}, - effectiveQueryParams = {}; + activeQueryParams = this.state.queryParams; - var targetHandlerInfos = this.targetHandlerInfos, - found = false, names, object, handlerInfo, handlerObj; + var targetHandlerInfos = this.state.handlerInfos, + found = false, names, object, handlerInfo, handlerObj, i, len; - if (!targetHandlerInfos) { return false; } + if (!targetHandlerInfos.length) { return false; } - var recogHandlers = this.recognizer.handlersFor(targetHandlerInfos[targetHandlerInfos.length - 1].name); - for (var i=targetHandlerInfos.length-1; i>=0; i--) { - handlerInfo = targetHandlerInfos[i]; - if (handlerInfo.name === handlerName) { found = true; } + var targetHandler = targetHandlerInfos[targetHandlerInfos.length - 1].name; + var recogHandlers = this.recognizer.handlersFor(targetHandler); - if (found) { - var recogHandler = recogHandlers[i]; - - merge(activeQueryParams, handlerInfo.queryParams); - if (queryParams !== false) { - merge(effectiveQueryParams, handlerInfo.queryParams); - mergeSomeKeys(effectiveQueryParams, queryParams, recogHandler.queryParams); - } - - if (handlerInfo.isDynamic && contexts.length > 0) { - object = contexts.pop(); - - if (isParam(object)) { - var name = recogHandler.names[0]; - if (!this.currentParams || "" + object !== this.currentParams[name]) { return false; } - } else if (handlerInfo.context !== object) { - return false; - } - } - } + var index = 0; + for (len = recogHandlers.length; index < len; ++index) { + handlerInfo = targetHandlerInfos[index]; + if (handlerInfo.name === handlerName) { break; } } + if (index === recogHandlers.length) { + // The provided route name isn't even in the route hierarchy. + return false; + } - return contexts.length === 0 && found && queryParamsEqual(activeQueryParams, effectiveQueryParams); + var state = new TransitionState(); + state.handlerInfos = targetHandlerInfos.slice(0, index + 1); + recogHandlers = recogHandlers.slice(0, index + 1); + + var intent = new NamedTransitionIntent({ + name: targetHandler, + contexts: contexts + }); + + var newState = intent.applyToHandlers(state, recogHandlers, this.getHandler, targetHandler, true, true); + + return handlerInfosEqual(newState.handlerInfos, state.handlerInfos) && + !getChangelist(activeQueryParams, queryParams); }, trigger: function(name) { @@ -31978,6 +32616,24 @@ define("router", trigger(this, this.currentHandlerInfos, false, args); }, + /** + @private + + Pluggable hook for possibly running route hooks + in a try-catch escaping manner. + + @param {Function} callback the callback that will + be asynchronously called + + @return {Promise} a promise that fulfills with the + value returned from the callback + */ + async: function(callback) { + return new Promise(function(resolve) { + resolve(callback()); + }); + }, + /** Hook point for logging transition status updates. @@ -31986,299 +32642,6 @@ define("router", log: null }; - /** - @private - - Used internally for both URL and named transition to determine - a shared pivot parent route and other data necessary to perform - a transition. - */ - function getMatchPoint(router, handlers, objects, inputParams, queryParams) { - - var matchPoint = handlers.length, - providedModels = {}, i, - currentHandlerInfos = router.currentHandlerInfos || [], - params = {}, - oldParams = router.currentParams || {}, - activeTransition = router.activeTransition, - handlerParams = {}, - obj; - - objects = slice.call(objects); - merge(params, inputParams); - - for (i = handlers.length - 1; i >= 0; i--) { - var handlerObj = handlers[i], - handlerName = handlerObj.handler, - oldHandlerInfo = currentHandlerInfos[i], - hasChanged = false; - - // Check if handler names have changed. - if (!oldHandlerInfo || oldHandlerInfo.name !== handlerObj.handler) { hasChanged = true; } - - if (handlerObj.isDynamic) { - // URL transition. - - if (obj = getMatchPointObject(objects, handlerName, activeTransition, true, params)) { - hasChanged = true; - providedModels[handlerName] = obj; - } else { - handlerParams[handlerName] = {}; - for (var prop in handlerObj.params) { - if (!handlerObj.params.hasOwnProperty(prop)) { continue; } - var newParam = handlerObj.params[prop]; - if (oldParams[prop] !== newParam) { hasChanged = true; } - handlerParams[handlerName][prop] = params[prop] = newParam; - } - } - } else if (handlerObj.hasOwnProperty('names')) { - // Named transition. - - if (objects.length) { hasChanged = true; } - - if (obj = getMatchPointObject(objects, handlerName, activeTransition, handlerObj.names[0], params)) { - providedModels[handlerName] = obj; - } else { - var names = handlerObj.names; - handlerParams[handlerName] = {}; - for (var j = 0, len = names.length; j < len; ++j) { - var name = names[j]; - handlerParams[handlerName][name] = params[name] = params[name] || oldParams[name]; - } - } - } - - // If there is an old handler, see if query params are the same. If there isn't an old handler, - // hasChanged will already be true here - if(oldHandlerInfo && !queryParamsEqual(oldHandlerInfo.queryParams, handlerObj.queryParams)) { - hasChanged = true; - } - - if (hasChanged) { matchPoint = i; } - } - - if (objects.length > 0) { - throw new Error("More context objects were passed than there are dynamic segments for the route: " + handlers[handlers.length - 1].handler); - } - - var pivotHandlerInfo = currentHandlerInfos[matchPoint - 1], - pivotHandler = pivotHandlerInfo && pivotHandlerInfo.handler; - - return { matchPoint: matchPoint, providedModels: providedModels, params: params, handlerParams: handlerParams, pivotHandler: pivotHandler }; - } - - function getMatchPointObject(objects, handlerName, activeTransition, paramName, params) { - - if (objects.length && paramName) { - - var object = objects.pop(); - - // If provided object is string or number, treat as param. - if (isParam(object)) { - params[paramName] = object.toString(); - } else { - return object; - } - } else if (activeTransition) { - // Use model from previous transition attempt, preferably the resolved one. - return activeTransition.resolvedModels[handlerName] || - (paramName && activeTransition.providedModels[handlerName]); - } - } - - function isParam(object) { - return (typeof object === "string" || object instanceof String || typeof object === "number" || object instanceof Number); - } - - - - /** - @private - - This method takes a handler name and returns a list of query params - that are valid to pass to the handler or its parents - - @param {Router} router - @param {String} handlerName - @return {Array[String]} a list of query parameters - */ - function queryParamsForHandler(router, handlerName) { - var handlers = router.recognizer.handlersFor(handlerName), - queryParams = []; - - for (var i = 0; i < handlers.length; i++) { - queryParams.push.apply(queryParams, handlers[i].queryParams || []); - } - - return queryParams; - } - /** - @private - - This method takes a handler name and a list of contexts and returns - a serialized parameter hash suitable to pass to `recognizer.generate()`. - - @param {Router} router - @param {String} handlerName - @param {Array[Object]} objects - @return {Object} a serialized parameter hash - */ - function paramsForHandler(router, handlerName, objects, queryParams) { - - var handlers = router.recognizer.handlersFor(handlerName), - params = {}, - handlerInfos = generateHandlerInfosWithQueryParams(router, handlers, queryParams), - matchPoint = getMatchPoint(router, handlerInfos, objects).matchPoint, - mergedQueryParams = {}, - object, handlerObj, handler, names, i; - - params.queryParams = {}; - - for (i=0; i= matchPoint) { - object = objects.shift(); - // Otherwise use existing context - } else { - object = handler.context; - } - - // Serialize to generate params - merge(params, serialize(handler, object, names)); - } - if (queryParams !== false) { - mergeSomeKeys(params.queryParams, router.currentQueryParams, handlerObj.queryParams); - mergeSomeKeys(params.queryParams, queryParams, handlerObj.queryParams); - } - } - - if (queryParamsEqual(params.queryParams, {})) { delete params.queryParams; } - return params; - } - - function merge(hash, other) { - for (var prop in other) { - if (other.hasOwnProperty(prop)) { hash[prop] = other[prop]; } - } - } - - function mergeSomeKeys(hash, other, keys) { - if (!other || !keys) { return; } - for(var i = 0; i < keys.length; i++) { - var key = keys[i], value; - if(other.hasOwnProperty(key)) { - value = other[key]; - if(value === null || value === false || typeof value === "undefined") { - delete hash[key]; - } else { - hash[key] = other[key]; - } - } - } - } - - /** - @private - */ - - function generateHandlerInfosWithQueryParams(router, handlers, queryParams) { - var handlerInfos = []; - - for (var i = 0; i < handlers.length; i++) { - var handler = handlers[i], - handlerInfo = { handler: handler.handler, names: handler.names, context: handler.context, isDynamic: handler.isDynamic }, - activeQueryParams = {}; - - if (queryParams !== false) { - mergeSomeKeys(activeQueryParams, router.currentQueryParams, handler.queryParams); - mergeSomeKeys(activeQueryParams, queryParams, handler.queryParams); - } - - if (handler.queryParams && handler.queryParams.length > 0) { - handlerInfo.queryParams = activeQueryParams; - } - - handlerInfos.push(handlerInfo); - } - - return handlerInfos; - } - - /** - @private - */ - function createQueryParamTransition(router, queryParams, isIntermediate) { - var currentHandlers = router.currentHandlerInfos, - currentHandler = currentHandlers[currentHandlers.length - 1], - name = currentHandler.name; - - log(router, "Attempting query param transition"); - - return createNamedTransition(router, [name, queryParams], isIntermediate); - } - - /** - @private - */ - function createNamedTransition(router, args, isIntermediate) { - var partitionedArgs = extractQueryParams(args), - pureArgs = partitionedArgs[0], - queryParams = partitionedArgs[1], - handlers = router.recognizer.handlersFor(pureArgs[0]), - handlerInfos = generateHandlerInfosWithQueryParams(router, handlers, queryParams); - - - log(router, "Attempting transition to " + pureArgs[0]); - - return performTransition(router, - handlerInfos, - slice.call(pureArgs, 1), - router.currentParams, - queryParams, - null, - isIntermediate); - } - - /** - @private - */ - function createURLTransition(router, url, isIntermediate) { - var results = router.recognizer.recognize(url), - currentHandlerInfos = router.currentHandlerInfos, - queryParams = {}, - i, len; - - log(router, "Attempting URL transition to " + url); - - if (results) { - // Make sure this route is actually accessible by URL. - for (i = 0, len = results.length; i < len; ++i) { - - if (router.getHandler(results[i].handler).inaccessibleByURL) { - results = null; - break; - } - } - } - - if (!results) { - return errorTransition(router, new Router.UnrecognizedURLError(url)); - } - - for(i = 0, len = results.length; i < len; i++) { - merge(queryParams, results[i].queryParams); - } - - return performTransition(router, results, [], {}, queryParams, null, isIntermediate); - } - - /** @private @@ -32317,104 +32680,70 @@ define("router", 3. Triggers the `enter` callback on `about` 4. Triggers the `setup` callback on `about` - @param {Transition} transition - @param {Array[HandlerInfo]} handlerInfos + @param {Router} transition + @param {TransitionState} newState */ - function setupContexts(transition, handlerInfos) { - var router = transition.router, - partition = partitionHandlers(router.currentHandlerInfos || [], handlerInfos); + function setupContexts(router, newState, transition) { + var partition = partitionHandlers(router.state, newState); - router.targetHandlerInfos = handlerInfos; - - eachHandler(partition.exited, function(handlerInfo) { + forEach(partition.exited, function(handlerInfo) { var handler = handlerInfo.handler; delete handler.context; if (handler.exit) { handler.exit(); } }); - var currentHandlerInfos = partition.unchanged.slice(); - router.currentHandlerInfos = currentHandlerInfos; + var oldState = router.oldState = router.state; + router.state = newState; + var currentHandlerInfos = router.currentHandlerInfos = partition.unchanged.slice(); - eachHandler(partition.updatedContext, function(handlerInfo) { - handlerEnteredOrUpdated(transition, currentHandlerInfos, handlerInfo, false); - }); + try { + forEach(partition.updatedContext, function(handlerInfo) { + return handlerEnteredOrUpdated(currentHandlerInfos, handlerInfo, false, transition); + }); - eachHandler(partition.entered, function(handlerInfo) { - handlerEnteredOrUpdated(transition, currentHandlerInfos, handlerInfo, true); - }); + forEach(partition.entered, function(handlerInfo) { + return handlerEnteredOrUpdated(currentHandlerInfos, handlerInfo, true, transition); + }); + } catch(e) { + router.state = oldState; + router.currentHandlerInfos = oldState.handlerInfos; + throw e; + } + + router.state.queryParams = finalizeQueryParamChange(router, currentHandlerInfos, newState.queryParams); } + /** @private Helper method used by setupContexts. Handles errors or redirects that may happen in enter/setup. */ - function handlerEnteredOrUpdated(transition, currentHandlerInfos, handlerInfo, enter) { + function handlerEnteredOrUpdated(currentHandlerInfos, handlerInfo, enter, transition) { + var handler = handlerInfo.handler, context = handlerInfo.context; - try { - if (enter && handler.enter) { handler.enter(); } - checkAbort(transition); + if (enter && handler.enter) { handler.enter(transition); } + if (transition && transition.isAborted) { + throw new TransitionAborted(); + } - setContext(handler, context); - setQueryParams(handler, handlerInfo.queryParams); + handler.context = context; + if (handler.contextDidChange) { handler.contextDidChange(); } - if (handler.setup) { handler.setup(context, handlerInfo.queryParams); } - checkAbort(transition); - } catch(e) { - if (!(e instanceof Router.TransitionAborted)) { - // Trigger the `error` event starting from this failed handler. - transition.trigger(true, 'error', e, transition, handler); - } - - // Propagate the error so that the transition promise will reject. - throw e; + if (handler.setup) { handler.setup(context, transition); } + if (transition && transition.isAborted) { + throw new TransitionAborted(); } currentHandlerInfos.push(handlerInfo); - } - - /** - @private - - Iterates over an array of `HandlerInfo`s, passing the handler - and context into the callback. - - @param {Array[HandlerInfo]} handlerInfos - @param {Function(Object, Object)} callback - */ - function eachHandler(handlerInfos, callback) { - for (var i=0, l=handlerInfos.length; i= 0; --i) { + var handlerInfo = handlerInfos[i]; + merge(params, handlerInfo.params); + if (handlerInfo.handler.inaccessibleByURL) { + urlMethod = null; + } + } + + if (urlMethod) { + params.queryParams = state.queryParams; + var url = router.recognizer.generate(handlerName, params); + + if (urlMethod === 'replaceQuery') { + if (url !== inputUrl) { + router.replaceURL(url); + } + } else if (urlMethod === 'replace') { + router.replaceURL(url); + } else { + router.updateURL(url); + } + } + } + + /** + @private + + Updates the URL (if necessary) and calls `setupContexts` + to update the router's array of `currentHandlerInfos`. + */ + function finalizeTransition(transition, newState) { + + try { + log(transition.router, transition.sequence, "Resolved all models on destination route; finalizing transition."); + + var router = transition.router, + handlerInfos = newState.handlerInfos, + seq = transition.sequence; + + // Run all the necessary enter/setup/exit hooks + setupContexts(router, newState, transition); + + // Check if a redirect occurred in enter/setup + if (transition.isAborted) { + // TODO: cleaner way? distinguish b/w targetHandlerInfos? + router.state.handlerInfos = router.currentHandlerInfos; + return reject(logAbort(transition)); + } + + updateURL(transition, newState, transition.intent.url); + + transition.isActive = false; + router.activeTransition = null; + + trigger(router, router.currentHandlerInfos, true, ['didTransition']); + + if (router.didTransition) { + router.didTransition(router.currentHandlerInfos); + } + + log(router, transition.sequence, "TRANSITION COMPLETE."); + + // Resolve with the final handler. + return handlerInfos[handlerInfos.length - 1].handler; + } catch(e) { + if (!(e instanceof TransitionAborted)) { + //var erroneousHandler = handlerInfos.pop(); + var infos = transition.state.handlerInfos; + transition.trigger(true, 'error', e, transition, infos[infos.length-1]); + transition.abort(); + } + + throw e; + } + } + + /** + @private + + Begins and returns a Transition based on the provided + arguments. Accepts arguments in the form of both URL + transitions and named transitions. + + @param {Router} router + @param {Array[Object]} args arguments passed to transitionTo, + replaceWith, or handleURL + */ + function doTransition(router, args, isIntermediate) { + // Normalize blank transitions to root URL transitions. + var name = args[0] || '/'; + + var lastArg = args[args.length-1]; + var queryParams = {}; + if (lastArg && lastArg.hasOwnProperty('queryParams')) { + queryParams = pop.call(args).queryParams; + } + + var intent; + if (args.length === 0) { + + log(router, "Updating query params"); + + // A query param update is really just a transition + // into the route you're already on. + var handlerInfos = router.state.handlerInfos; + intent = new NamedTransitionIntent({ + name: handlerInfos[handlerInfos.length - 1].name, + contexts: [], + queryParams: queryParams + }); + + } else if (name.charAt(0) === '/') { + + log(router, "Attempting URL transition to " + name); + intent = new URLTransitionIntent({ url: name }); + + } else { + + log(router, "Attempting transition to " + name); + intent = new NamedTransitionIntent({ + name: args[0], + contexts: slice.call(args, 1), + queryParams: queryParams + }); + } + + return router.transitionByIntent(intent, isIntermediate); + } + + function handlerInfosEqual(handlerInfos, otherHandlerInfos) { + if (handlerInfos.length !== otherHandlerInfos.length) { + return false; + } + + for (var i = 0, len = handlerInfos.length; i < len; ++i) { + if (handlerInfos[i] !== otherHandlerInfos[i]) { + return false; + } + } + return true; + } + + function finalizeQueryParamChange(router, resolvedHandlers, newQueryParams) { + // We fire a finalizeQueryParamChange event which + // gives the new route hierarchy a chance to tell + // us which query params it's consuming and what + // their final values are. If a query param is + // no longer consumed in the final route hierarchy, + // its serialized segment will be removed + // from the URL. + var finalQueryParamsArray = []; + trigger(router, resolvedHandlers, true, ['finalizeQueryParamChange', newQueryParams, finalQueryParamsArray]); + + var finalQueryParams = {}; + for (var i = 0, len = finalQueryParamsArray.length; i < len; ++i) { + var qp = finalQueryParamsArray[i]; + finalQueryParams[qp.key] = qp.value; + } + return finalQueryParams; + } + + __exports__.Router = Router; + }); +define("router/transition-intent", + ["./utils","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var merge = __dependency1__.merge; + + function TransitionIntent(props) { + if (props) { + merge(this, props); + } + this.data = this.data || {}; + } + + TransitionIntent.prototype.applyToState = function(oldState) { + // Default TransitionIntent is a no-op. + return oldState; + }; + + __exports__.TransitionIntent = TransitionIntent; + }); +define("router/transition-intent/named-transition-intent", + ["../transition-intent","../transition-state","../handler-info","../utils","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + var TransitionIntent = __dependency1__.TransitionIntent; + var TransitionState = __dependency2__.TransitionState; + var UnresolvedHandlerInfoByParam = __dependency3__.UnresolvedHandlerInfoByParam; + var UnresolvedHandlerInfoByObject = __dependency3__.UnresolvedHandlerInfoByObject; + var isParam = __dependency4__.isParam; + var forEach = __dependency4__.forEach; + var extractQueryParams = __dependency4__.extractQueryParams; + var oCreate = __dependency4__.oCreate; + var merge = __dependency4__.merge; + + function NamedTransitionIntent(props) { + TransitionIntent.call(this, props); + } + + NamedTransitionIntent.prototype = oCreate(TransitionIntent.prototype); + NamedTransitionIntent.prototype.applyToState = function(oldState, recognizer, getHandler, isIntermediate) { + + var partitionedArgs = extractQueryParams([this.name].concat(this.contexts)), + pureArgs = partitionedArgs[0], + queryParams = partitionedArgs[1], + handlers = recognizer.handlersFor(pureArgs[0]); + + var targetRouteName = handlers[handlers.length-1].handler; + + return this.applyToHandlers(oldState, handlers, getHandler, targetRouteName, isIntermediate); + }; + + NamedTransitionIntent.prototype.applyToHandlers = function(oldState, handlers, getHandler, targetRouteName, isIntermediate, checkingIfActive) { + + var i; + var newState = new TransitionState(); + var objects = this.contexts.slice(0); + + var invalidateIndex = handlers.length; + var nonDynamicIndexes = []; + + // Pivot handlers are provided for refresh transitions + if (this.pivotHandler) { + for (i = 0; i < handlers.length; ++i) { + if (getHandler(handlers[i].handler) === this.pivotHandler) { + invalidateIndex = i; + break; + } + } + } + + var pivotHandlerFound = !this.pivotHandler; + + for (i = handlers.length - 1; i >= 0; --i) { + var result = handlers[i]; + var name = result.handler; + var handler = getHandler(name); + + var oldHandlerInfo = oldState.handlerInfos[i]; + var newHandlerInfo = null; + + if (result.names.length > 0) { + if (i >= invalidateIndex) { + newHandlerInfo = this.createParamHandlerInfo(name, handler, result.names, objects, oldHandlerInfo); + } else { + newHandlerInfo = this.getHandlerInfoForDynamicSegment(name, handler, result.names, objects, oldHandlerInfo, targetRouteName); + } + } else { + // This route has no dynamic segment. + // Therefore treat as a param-based handlerInfo + // with empty params. This will cause the `model` + // hook to be called with empty params, which is desirable. + newHandlerInfo = this.createParamHandlerInfo(name, handler, result.names, objects, oldHandlerInfo); + nonDynamicIndexes.unshift(i); + } + + if (checkingIfActive) { + // If we're performing an isActive check, we want to + // serialize URL params with the provided context, but + // ignore mismatches between old and new context. + newHandlerInfo = newHandlerInfo.becomeResolved(null, newHandlerInfo.context); + var oldContext = oldHandlerInfo && oldHandlerInfo.context; + if (result.names.length > 0 && newHandlerInfo.context === oldContext) { + // If contexts match in isActive test, assume params also match. + // This allows for flexibility in not requiring that every last + // handler provide a `serialize` method + newHandlerInfo.params = oldHandlerInfo && oldHandlerInfo.params; + } + newHandlerInfo.context = oldContext; + } + + var handlerToUse = oldHandlerInfo; + if (i >= invalidateIndex || newHandlerInfo.shouldSupercede(oldHandlerInfo)) { + invalidateIndex = Math.min(i, invalidateIndex); + handlerToUse = newHandlerInfo; + } + + if (isIntermediate && !checkingIfActive) { + handlerToUse = handlerToUse.becomeResolved(null, handlerToUse.context); + } + + newState.handlerInfos.unshift(handlerToUse); + } + + if (objects.length > 0) { + throw new Error("More context objects were passed than there are dynamic segments for the route: " + targetRouteName); + } + + if (!isIntermediate) { + this.invalidateNonDynamicHandlers(newState.handlerInfos, nonDynamicIndexes, invalidateIndex); + } + + merge(newState.queryParams, oldState.queryParams); + merge(newState.queryParams, this.queryParams || {}); + + return newState; + }; + + NamedTransitionIntent.prototype.invalidateNonDynamicHandlers = function(handlerInfos, indexes, invalidateIndex) { + forEach(indexes, function(i) { + if (i >= invalidateIndex) { + var handlerInfo = handlerInfos[i]; + handlerInfos[i] = new UnresolvedHandlerInfoByParam({ + name: handlerInfo.name, + handler: handlerInfo.handler, + params: {} + }); + } + }); + }; + + NamedTransitionIntent.prototype.getHandlerInfoForDynamicSegment = function(name, handler, names, objects, oldHandlerInfo, targetRouteName) { + + var numNames = names.length; + var objectToUse; + if (objects.length > 0) { + + // Use the objects provided for this transition. + objectToUse = objects[objects.length - 1]; + if (isParam(objectToUse)) { + return this.createParamHandlerInfo(name, handler, names, objects, oldHandlerInfo); + } else { + objects.pop(); + } + } else if (oldHandlerInfo && oldHandlerInfo.name === name) { + // Reuse the matching oldHandlerInfo + return oldHandlerInfo; + } else { + // Ideally we should throw this error to provide maximal + // information to the user that not enough context objects + // were provided, but this proves too cumbersome in Ember + // in cases where inner template helpers are evaluated + // before parent helpers un-render, in which cases this + // error somewhat prematurely fires. + //throw new Error("Not enough context objects were provided to complete a transition to " + targetRouteName + ". Specifically, the " + name + " route needs an object that can be serialized into its dynamic URL segments [" + names.join(', ') + "]"); + return oldHandlerInfo; + } + + return new UnresolvedHandlerInfoByObject({ + name: name, + handler: handler, + context: objectToUse, + names: names + }); + }; + + NamedTransitionIntent.prototype.createParamHandlerInfo = function(name, handler, names, objects, oldHandlerInfo) { + var params = {}; + + // Soak up all the provided string/numbers + var numNames = names.length; + while (numNames--) { + + // Only use old params if the names match with the new handler + var oldParams = (oldHandlerInfo && name === oldHandlerInfo.name && oldHandlerInfo.params) || {}; + + var peek = objects[objects.length - 1]; + var paramName = names[numNames]; + if (isParam(peek)) { + params[paramName] = "" + objects.pop(); + } else { + // If we're here, this means only some of the params + // were string/number params, so try and use a param + // value from a previous handler. + if (oldParams.hasOwnProperty(paramName)) { + params[paramName] = oldParams[paramName]; + } else { + throw new Error("You didn't provide enough string/numeric parameters to satisfy all of the dynamic segments for route " + name); + } + } + } + + return new UnresolvedHandlerInfoByParam({ + name: name, + handler: handler, + params: params + }); + }; + + __exports__.NamedTransitionIntent = NamedTransitionIntent; + }); +define("router/transition-intent/url-transition-intent", + ["../transition-intent","../transition-state","../handler-info","../utils","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + var TransitionIntent = __dependency1__.TransitionIntent; + var TransitionState = __dependency2__.TransitionState; + var UnresolvedHandlerInfoByParam = __dependency3__.UnresolvedHandlerInfoByParam; + var oCreate = __dependency4__.oCreate; + var merge = __dependency4__.merge; + + function URLTransitionIntent(props) { + TransitionIntent.call(this, props); + } + + URLTransitionIntent.prototype = oCreate(TransitionIntent.prototype); + URLTransitionIntent.prototype.applyToState = function(oldState, recognizer, getHandler) { + var newState = new TransitionState(); + + var results = recognizer.recognize(this.url), + queryParams = {}, + i, len; + + if (!results) { + throw new UnrecognizedURLError(this.url); + } + + var statesDiffer = false; + + for (i = 0, len = results.length; i < len; ++i) { + var result = results[i]; + var name = result.handler; + var handler = getHandler(name); + + if (handler.inaccessibleByURL) { + throw new UnrecognizedURLError(this.url); + } + + var newHandlerInfo = new UnresolvedHandlerInfoByParam({ + name: name, + handler: handler, + params: result.params + }); + + var oldHandlerInfo = oldState.handlerInfos[i]; + if (statesDiffer || newHandlerInfo.shouldSupercede(oldHandlerInfo)) { + statesDiffer = true; + newState.handlerInfos[i] = newHandlerInfo; + } else { + newState.handlerInfos[i] = oldHandlerInfo; + } + } + + merge(newState.queryParams, results.queryParams); + + return newState; + }; + + /** + Promise reject reasons passed to promise rejection + handlers for failed transitions. + */ + function UnrecognizedURLError(message) { + this.message = (message || "UnrecognizedURLError"); + this.name = "UnrecognizedURLError"; + } + + __exports__.URLTransitionIntent = URLTransitionIntent; + }); +define("router/transition-state", + ["./handler-info","./utils","rsvp","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var ResolvedHandlerInfo = __dependency1__.ResolvedHandlerInfo; + var forEach = __dependency2__.forEach; + var resolve = __dependency3__.resolve; + + function TransitionState(other) { + this.handlerInfos = []; + this.queryParams = {}; + this.params = {}; + } + + TransitionState.prototype = { + handlerInfos: null, + queryParams: null, + params: null, + + resolve: function(async, shouldContinue, payload) { + + // First, calculate params for this state. This is useful + // information to provide to the various route hooks. + var params = this.params; + forEach(this.handlerInfos, function(handlerInfo) { + params[handlerInfo.name] = handlerInfo.params || {}; + }); + + payload = payload || {}; + payload.resolveIndex = 0; + + var currentState = this; + var wasAborted = false; + + // The prelude RSVP.resolve() asyncs us into the promise land. + return resolve().then(resolveOneHandlerInfo)['catch'](handleError); + + function innerShouldContinue() { + return resolve(shouldContinue())['catch'](function(reason) { + // We distinguish between errors that occurred + // during resolution (e.g. beforeModel/model/afterModel), + // and aborts due to a rejecting promise from shouldContinue(). + wasAborted = true; + throw reason; + }); + } + + function handleError(error) { + // This is the only possible + // reject value of TransitionState#resolve + throw { + error: error, + handlerWithError: currentState.handlerInfos[payload.resolveIndex].handler, + wasAborted: wasAborted, + state: currentState + }; + } + + function proceed(resolvedHandlerInfo) { + // Swap the previously unresolved handlerInfo with + // the resolved handlerInfo + currentState.handlerInfos[payload.resolveIndex++] = resolvedHandlerInfo; + + // Call the redirect hook. The reason we call it here + // vs. afterModel is so that redirects into child + // routes don't re-run the model hooks for this + // already-resolved route. + var handler = resolvedHandlerInfo.handler; + if (handler && handler.redirect) { + handler.redirect(resolvedHandlerInfo.context, payload); + } + + // Proceed after ensuring that the redirect hook + // didn't abort this transition by transitioning elsewhere. + return innerShouldContinue().then(resolveOneHandlerInfo); + } + + function resolveOneHandlerInfo() { + if (payload.resolveIndex === currentState.handlerInfos.length) { + // This is is the only possible + // fulfill value of TransitionState#resolve + return { + error: null, + state: currentState + }; + } + + var handlerInfo = currentState.handlerInfos[payload.resolveIndex]; + + return handlerInfo.resolve(async, innerShouldContinue, payload) + .then(proceed); + } + } + }; + + __exports__.TransitionState = TransitionState; + }); +define("router/transition", + ["rsvp","./handler-info","./utils","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var reject = __dependency1__.reject; + var resolve = __dependency1__.resolve; + var ResolvedHandlerInfo = __dependency2__.ResolvedHandlerInfo; + var trigger = __dependency3__.trigger; + var slice = __dependency3__.slice; + var log = __dependency3__.log; + + /** + @private + + A Transition is a thennable (a promise-like object) that represents + an attempt to transition to another route. It can be aborted, either + explicitly via `abort` or by attempting another transition while a + previous one is still underway. An aborted transition can also + be `retry()`d later. + */ + function Transition(router, intent, state, error) { + var transition = this; + this.state = state || router.state; + this.intent = intent; + this.router = router; + this.data = this.intent && this.intent.data || {}; + this.resolvedModels = {}; + this.queryParams = {}; + + if (error) { + this.promise = reject(error); + return; + } + + if (state) { + this.params = state.params; + this.queryParams = state.queryParams; + + var len = state.handlerInfos.length; + if (len) { + this.targetName = state.handlerInfos[state.handlerInfos.length-1].name; + } + + for (var i = 0; i < len; ++i) { + var handlerInfo = state.handlerInfos[i]; + if (!(handlerInfo instanceof ResolvedHandlerInfo)) { + break; + } + this.pivotHandler = handlerInfo.handler; + } + + this.sequence = Transition.currentSequence++; + this.promise = state.resolve(router.async, checkForAbort, this)['catch'](function(result) { + if (result.wasAborted) { + throw logAbort(transition); + } else { + transition.trigger('error', result.error, transition, result.handlerWithError); + transition.abort(); + throw result.error; + } + }); + } else { + this.promise = resolve(this.state); + this.params = {}; + } + + function checkForAbort() { + if (transition.isAborted) { + return reject(); + } + } + } + + Transition.currentSequence = 0; + + Transition.prototype = { + targetName: null, + urlMethod: 'update', + intent: null, + params: null, + pivotHandler: null, + resolveIndex: 0, + handlerInfos: null, + resolvedModels: null, + isActive: true, + state: null, + + /** + @public + + The Transition's internal promise. Calling `.then` on this property + is that same as calling `.then` on the Transition object itself, but + this property is exposed for when you want to pass around a + Transition's promise, but not the Transition object itself, since + Transition object can be externally `abort`ed, while the promise + cannot. + */ + promise: null, + + /** + @public + + Custom state can be stored on a Transition's `data` object. + This can be useful for decorating a Transition within an earlier + hook and shared with a later hook. Properties set on `data` will + be copied to new transitions generated by calling `retry` on this + transition. + */ + data: null, + + /** + @public + + A standard promise hook that resolves if the transition + succeeds and rejects if it fails/redirects/aborts. + + Forwards to the internal `promise` property which you can + use in situations where you want to pass around a thennable, + but not the Transition itself. + + @param {Function} success + @param {Function} failure + */ + then: function(success, failure) { + return this.promise.then(success, failure); + }, + + /** + @public + + Aborts the Transition. Note you can also implicitly abort a transition + by initiating another transition while a previous one is underway. + */ + abort: function() { + if (this.isAborted) { return this; } + log(this.router, this.sequence, this.targetName + ": transition was aborted"); + this.isAborted = true; + this.isActive = false; + this.router.activeTransition = null; + return this; + }, + + /** + @public + + Retries a previously-aborted transition (making sure to abort the + transition if it's still active). Returns a new transition that + represents the new attempt to transition. + */ + retry: function() { + // TODO: add tests for merged state retry()s + this.abort(); + return this.router.transitionByIntent(this.intent, false); + }, + + /** + @public + + Sets the URL-changing method to be employed at the end of a + successful transition. By default, a new Transition will just + use `updateURL`, but passing 'replace' to this method will + cause the URL to update using 'replaceWith' instead. Omitting + a parameter will disable the URL change, allowing for transitions + that don't update the URL at completion (this is also used for + handleURL, since the URL has already changed before the + transition took place). + + @param {String} method the type of URL-changing method to use + at the end of a transition. Accepted values are 'replace', + falsy values, or any other non-falsy value (which is + interpreted as an updateURL transition). + + @return {Transition} this transition + */ + method: function(method) { + this.urlMethod = method; + return this; + }, + + /** + @public + + Fires an event on the current list of resolved/resolving + handlers within this transition. Useful for firing events + on route hierarchies that haven't fully been entered yet. + + Note: This method is also aliased as `send` + + @param {Boolean} ignoreFailure the name of the event to fire + @param {String} name the name of the event to fire + */ + trigger: function (ignoreFailure) { + var args = slice.call(arguments); + if (typeof ignoreFailure === 'boolean') { + args.shift(); + } else { + // Throw errors on unhandled trigger events by default + ignoreFailure = false; + } + trigger(this.router, this.state.handlerInfos.slice(0, this.resolveIndex + 1), ignoreFailure, args); + }, + + /** + @public + + Transitions are aborted and their promises rejected + when redirects occur; this method returns a promise + that will follow any redirects that occur and fulfill + with the value fulfilled by any redirecting transitions + that occur. + + @return {Promise} a promise that fulfills with the same + value that the final redirecting transition fulfills with + */ + followRedirects: function() { + var router = this.router; + return this.promise['catch'](function(reason) { + if (router.activeTransition) { + return router.activeTransition.followRedirects(); + } + throw reason; + }); + }, + + toString: function() { + return "Transition (sequence " + this.sequence + ")"; + }, + + /** + @private + */ + log: function(message) { + log(this.router, this.sequence, message); + } + }; + + // Alias 'trigger' as 'send' + Transition.prototype.send = Transition.prototype.trigger; + + /** + @private + + Logs and returns a TransitionAborted error. + */ + function logAbort(transition) { + log(transition.router, transition.sequence, "detected abort."); + return new TransitionAborted(); + } + + function TransitionAborted(message) { + this.message = (message || "TransitionAborted"); + this.name = "TransitionAborted"; + } + + __exports__.Transition = Transition; + __exports__.logAbort = logAbort; + __exports__.TransitionAborted = TransitionAborted; + }); +define("router/utils", + ["exports"], + function(__exports__) { + "use strict"; + var slice = Array.prototype.slice; + + function merge(hash, other) { + for (var prop in other) { + if (other.hasOwnProperty(prop)) { hash[prop] = other[prop]; } + } + } + + var oCreate = Object.create || function(proto) { + function F() {} + F.prototype = proto; + return new F(); + }; + + /** + @private + + Extracts query params from the end of an array + **/ + function extractQueryParams(array) { + var len = (array && array.length), head, queryParams; + + if(len && len > 0 && array[len - 1] && array[len - 1].hasOwnProperty('queryParams')) { + queryParams = array[len - 1].queryParams; + head = slice.call(array, 0, len - 1); + return [head, queryParams]; + } else { + return [array, null]; + } + } + + /** + @private + */ + function log(router, sequence, msg) { + if (!router.log) { return; } + + if (arguments.length === 3) { + router.log("Transition #" + sequence + ": " + msg); + } else { + msg = sequence; + router.log(msg); + } + } + + function bind(fn, context) { + var boundArgs = arguments; + return function(value) { + var args = slice.call(boundArgs, 2); + args.push(value); + return fn.apply(context, args); + }; + } + + function isParam(object) { + return (typeof object === "string" || object instanceof String || typeof object === "number" || object instanceof Number); + } + + + function forEach(array, callback) { + for (var i=0, l=array.length; i 0 && array[len - 1] && array[len - 1].hasOwnProperty('queryParams')) { - queryParams = array[len - 1].queryParams; - head = slice.call(array, 0, len - 1); - return [head, queryParams]; - } else { - return [array, null]; - } - } - - function performIntermediateTransition(router, recogHandlers, matchPointResults) { - - var handlerInfos = generateHandlerInfos(router, recogHandlers); - for (var i = 0; i < handlerInfos.length; ++i) { - var handlerInfo = handlerInfos[i]; - handlerInfo.context = matchPointResults.providedModels[handlerInfo.name]; - } - - var stubbedTransition = { - router: router, - isAborted: false + function getChangelist(oldObject, newObject) { + var key; + var results = { + all: {}, + changed: {}, + removed: {} }; - setupContexts(stubbedTransition, handlerInfos); - } + merge(results.all, newObject); - /** - @private + var didChange = false; - Creates, begins, and returns a Transition. - */ - function performTransition(router, recogHandlers, providedModelsArray, params, queryParams, data, isIntermediate) { - - var matchPointResults = getMatchPoint(router, recogHandlers, providedModelsArray, params, queryParams), - targetName = recogHandlers[recogHandlers.length - 1].handler, - wasTransitioning = false, - currentHandlerInfos = router.currentHandlerInfos; - - if (isIntermediate) { - return performIntermediateTransition(router, recogHandlers, matchPointResults); - } - - // Check if there's already a transition underway. - if (router.activeTransition) { - if (transitionsIdentical(router.activeTransition, targetName, providedModelsArray, queryParams)) { - return router.activeTransition; - } - router.activeTransition.abort(); - wasTransitioning = true; - } - - var deferred = RSVP.defer(), - transition = new Transition(router, deferred.promise); - - transition.targetName = targetName; - transition.providedModels = matchPointResults.providedModels; - transition.providedModelsArray = providedModelsArray; - transition.params = matchPointResults.params; - transition.data = data || {}; - transition.queryParams = queryParams; - transition.pivotHandler = matchPointResults.pivotHandler; - router.activeTransition = transition; - - var handlerInfos = generateHandlerInfos(router, recogHandlers); - transition.handlerInfos = handlerInfos; - - // Fire 'willTransition' event on current handlers, but don't fire it - // if a transition was already underway. - if (!wasTransitioning) { - trigger(router, currentHandlerInfos, true, ['willTransition', transition]); - } - - log(router, transition.sequence, "Beginning validation for transition to " + transition.targetName); - validateEntry(transition, matchPointResults.matchPoint, matchPointResults.handlerParams) - .then(transitionSuccess, transitionFailure); - - return transition; - - function transitionSuccess() { - checkAbort(transition); - - try { - finalizeTransition(transition, handlerInfos); - - // currentHandlerInfos was updated in finalizeTransition - trigger(router, router.currentHandlerInfos, true, ['didTransition']); - - if (router.didTransition) { - router.didTransition(handlerInfos); + // Calculate removals + for (key in oldObject) { + if (oldObject.hasOwnProperty(key)) { + if (!newObject.hasOwnProperty(key)) { + didChange = true; + results.removed[key] = oldObject[key]; } - - log(router, transition.sequence, "TRANSITION COMPLETE."); - - // Resolve with the final handler. - transition.isActive = false; - deferred.resolve(handlerInfos[handlerInfos.length - 1].handler); - } catch(e) { - deferred.reject(e); - } - - // Don't nullify if another transition is underway (meaning - // there was a transition initiated with enter/setup). - if (!transition.isAborted) { - router.activeTransition = null; } } - function transitionFailure(reason) { - deferred.reject(reason); - } - } - - /** - @private - - Accepts handlers in Recognizer format, either returned from - recognize() or handlersFor(), and returns unified - `HandlerInfo`s. - */ - function generateHandlerInfos(router, recogHandlers) { - var handlerInfos = []; - for (var i = 0, len = recogHandlers.length; i < len; ++i) { - var handlerObj = recogHandlers[i], - isDynamic = handlerObj.isDynamic || (handlerObj.names && handlerObj.names.length); - - var handlerInfo = { - isDynamic: !!isDynamic, - name: handlerObj.handler, - handler: router.getHandler(handlerObj.handler) - }; - if(handlerObj.queryParams) { - handlerInfo.queryParams = handlerObj.queryParams; - } - handlerInfos.push(handlerInfo); - } - return handlerInfos; - } - - /** - @private - */ - function transitionsIdentical(oldTransition, targetName, providedModelsArray, queryParams) { - - if (oldTransition.targetName !== targetName) { return false; } - - var oldModels = oldTransition.providedModelsArray; - if (oldModels.length !== providedModelsArray.length) { return false; } - - for (var i = 0, len = oldModels.length; i < len; ++i) { - if (oldModels[i] !== providedModelsArray[i]) { return false; } - } - - if(!queryParamsEqual(oldTransition.queryParams, queryParams)) { - return false; - } - - return true; - } - - /** - @private - - Updates the URL (if necessary) and calls `setupContexts` - to update the router's array of `currentHandlerInfos`. - */ - function finalizeTransition(transition, handlerInfos) { - - log(transition.router, transition.sequence, "Validation succeeded, finalizing transition;"); - - var router = transition.router, - seq = transition.sequence, - handlerName = handlerInfos[handlerInfos.length - 1].name, - urlMethod = transition.urlMethod, - i; - - // Collect params for URL. - var objects = [], providedModels = transition.providedModelsArray.slice(); - for (i = handlerInfos.length - 1; i>=0; --i) { - var handlerInfo = handlerInfos[i]; - if (handlerInfo.isDynamic) { - var providedModel = providedModels.pop(); - objects.unshift(isParam(providedModel) ? providedModel.toString() : handlerInfo.context); - } - - if (handlerInfo.handler.inaccessibleByURL) { - urlMethod = null; + // Calculate changes + for (key in newObject) { + if (newObject.hasOwnProperty(key)) { + if (oldObject[key] !== newObject[key]) { + results.changed[key] = newObject[key]; + didChange = true; + } } } - var newQueryParams = {}; - for (i = handlerInfos.length - 1; i>=0; --i) { - merge(newQueryParams, handlerInfos[i].queryParams); - } - router.currentQueryParams = newQueryParams; - - - var params = paramsForHandler(router, handlerName, objects, transition.queryParams); - - router.currentParams = params; - - if (urlMethod) { - var url = router.recognizer.generate(handlerName, params); - - if (urlMethod === 'replace') { - router.replaceURL(url); - } else { - // Assume everything else is just a URL update for now. - router.updateURL(url); - } - } - - setupContexts(transition, handlerInfos); + return didChange && results; } - /** - @private - - Internal function used to construct the chain of promises used - to validate a transition. Wraps calls to `beforeModel`, `model`, - and `afterModel` in promises, and checks for redirects/aborts - between each. - */ - function validateEntry(transition, matchPoint, handlerParams) { - - var handlerInfos = transition.handlerInfos, - index = transition.resolveIndex; - - if (index === handlerInfos.length) { - // No more contexts to resolve. - return RSVP.resolve(transition.resolvedModels); - } - - var router = transition.router, - handlerInfo = handlerInfos[index], - handler = handlerInfo.handler, - handlerName = handlerInfo.name, - seq = transition.sequence; - - if (index < matchPoint) { - log(router, seq, handlerName + ": using context from already-active handler"); - - // We're before the match point, so don't run any hooks, - // just use the already resolved context from the handler. - transition.resolvedModels[handlerInfo.name] = - transition.providedModels[handlerInfo.name] || - handlerInfo.handler.context; - return proceed(); - } - - transition.trigger(true, 'willResolveModel', transition, handler); - - return RSVP.resolve().then(handleAbort) - .then(beforeModel) - .then(handleAbort) - .then(model) - .then(handleAbort) - .then(afterModel) - .then(handleAbort) - .then(null, handleError) - .then(proceed); - - function handleAbort(result) { - if (transition.isAborted) { - log(transition.router, transition.sequence, "detected abort."); - return RSVP.reject(new Router.TransitionAborted()); - } - - return result; - } - - function handleError(reason) { - if (reason instanceof Router.TransitionAborted || transition.isAborted) { - // if the transition was aborted and *no additional* error was thrown, - // reject with the Router.TransitionAborted instance - return RSVP.reject(reason); - } - - // otherwise, we're here because of a different error - transition.abort(); - - log(router, seq, handlerName + ": handling error: " + reason); - - // An error was thrown / promise rejected, so fire an - // `error` event from this handler info up to root. - transition.trigger(true, 'error', reason, transition, handlerInfo.handler); - - // Propagate the original error. - return RSVP.reject(reason); - } - - function beforeModel() { - - log(router, seq, handlerName + ": calling beforeModel hook"); - - var args; - - if (handlerInfo.queryParams) { - args = [handlerInfo.queryParams, transition]; - } else { - args = [transition]; - } - - var p = handler.beforeModel && handler.beforeModel.apply(handler, args); - return (p instanceof Transition) ? null : p; - } - - function model() { - log(router, seq, handlerName + ": resolving model"); - var p = getModel(handlerInfo, transition, handlerParams[handlerName], index >= matchPoint); - return (p instanceof Transition) ? null : p; - } - - function afterModel(context) { - - log(router, seq, handlerName + ": calling afterModel hook"); - - // Pass the context and resolved parent contexts to afterModel, but we don't - // want to use the value returned from `afterModel` in any way, but rather - // always resolve with the original `context` object. - - transition.resolvedModels[handlerInfo.name] = context; - - var args; - - if (handlerInfo.queryParams) { - args = [context, handlerInfo.queryParams, transition]; - } else { - args = [context, transition]; - } - - var p = handler.afterModel && handler.afterModel.apply(handler, args); - return (p instanceof Transition) ? null : p; - } - - function proceed() { - log(router, seq, handlerName + ": validation succeeded, proceeding"); - - handlerInfo.context = transition.resolvedModels[handlerInfo.name]; - transition.resolveIndex++; - return validateEntry(transition, matchPoint, handlerParams); - } - } - - /** - @private - - Throws a TransitionAborted if the provided transition has been aborted. - */ - function checkAbort(transition) { - if (transition.isAborted) { - log(transition.router, transition.sequence, "detected abort."); - throw new Router.TransitionAborted(); - } - } - - /** - @private - - Encapsulates the logic for whether to call `model` on a route, - or use one of the models provided to `transitionTo`. - */ - function getModel(handlerInfo, transition, handlerParams, needsUpdate) { - var handler = handlerInfo.handler, - handlerName = handlerInfo.name, args; - - if (!needsUpdate && handler.hasOwnProperty('context')) { - return handler.context; - } - - if (transition.providedModels.hasOwnProperty(handlerName)) { - var providedModel = transition.providedModels[handlerName]; - return typeof providedModel === 'function' ? providedModel() : providedModel; - } - - if (handlerInfo.queryParams) { - args = [handlerParams || {}, handlerInfo.queryParams, transition]; - } else { - args = [handlerParams || {}, transition, handlerInfo.queryParams]; - } - - return handler.model && handler.model.apply(handler, args); - } - - /** - @private - */ - function log(router, sequence, msg) { - - if (!router.log) { return; } - - if (arguments.length === 3) { - router.log("Transition #" + sequence + ": " + msg); - } else { - msg = sequence; - router.log(msg); - } - } - - /** - @private - - Begins and returns a Transition based on the provided - arguments. Accepts arguments in the form of both URL - transitions and named transitions. - - @param {Router} router - @param {Array[Object]} args arguments passed to transitionTo, - replaceWith, or handleURL - */ - function doTransition(router, args, isIntermediate) { - // Normalize blank transitions to root URL transitions. - var name = args[0] || '/'; - - if(args.length === 1 && args[0].hasOwnProperty('queryParams')) { - return createQueryParamTransition(router, args[0], isIntermediate); - } else if (name.charAt(0) === '/') { - return createURLTransition(router, name, isIntermediate); - } else { - return createNamedTransition(router, slice.call(args), isIntermediate); - } - } - - /** - @private - - Serializes a handler using its custom `serialize` method or - by a default that looks up the expected property name from - the dynamic segment. - - @param {Object} handler a router handler - @param {Object} model the model to be serialized for this handler - @param {Array[Object]} names the names array attached to an - handler object returned from router.recognizer.handlersFor() - */ - function serialize(handler, model, names) { - - var object = {}; - if (isParam(model)) { - object[names[0]] = model; - return object; - } - - // Use custom serialize if it exists. - if (handler.serialize) { - return handler.serialize(model, names); - } - - if (names.length !== 1) { return; } - - var name = names[0]; - - if (/_id$/.test(name)) { - object[name] = model.id; - } else { - object[name] = model; - } - return object; - } + __exports__.trigger = trigger; + __exports__.log = log; + __exports__.oCreate = oCreate; + __exports__.merge = merge; + __exports__.extractQueryParams = extractQueryParams; + __exports__.bind = bind; + __exports__.isParam = isParam; + __exports__.forEach = forEach; + __exports__.slice = slice; + __exports__.serialize = serialize; + __exports__.getChangelist = getChangelist; }); +define("router", + ["./router/router","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Router = __dependency1__.Router; + __exports__.Router = Router; + }); })(); @@ -33070,7 +33898,7 @@ DSL.prototype = { for (var i=0, l=dslMatches.length; i 1) { + urlKeyName = parts[1]; + } else { + // TODO: use _queryParamScope here? + if (controllerName !== 'application') { + urlKeyName = controllerName + '[' + propName + ']'; + } else { + urlKeyName = propName; + } + } + + var controllerFullname = controllerName + ':' + propName; + + result.queryParams[controllerFullname] = urlKeyName; + result.translations[parts[0]] = controllerFullname; + }); + } +} + /** Helper function for iterating root-ward, starting from (but not including) the provided `originRoute`. @@ -33535,7 +34479,7 @@ Ember.Router = Ember.Object.extend(Ember.Evented, { @private */ function forEachRouteAbove(originRoute, transition, callback) { - var handlerInfos = transition.handlerInfos, + var handlerInfos = transition.state.handlerInfos, originRouteFound = false; for (var i = handlerInfos.length - 1; i >= 0; --i) { @@ -33755,11 +34699,21 @@ Ember.Router.reopenClass({ } return path.join("."); + }, + + _translateQueryParams: function(queryParams, translations, routeName) { + for (var name in queryParams) { + if (!queryParams.hasOwnProperty(name)) { continue; } + + if (name in translations) { + queryParams[translations[name]] = queryParams[name]; + delete queryParams[name]; + } else { + } + } } }); -Router.Transition.prototype.send = Router.Transition.prototype.trigger; - })(); @@ -33797,7 +34751,7 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { @method exit */ exit: function() { - this.deactivate(); + this.deactivate(); this.teardownViews(); }, @@ -33810,6 +34764,74 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { this.activate(); }, + /** + The name of the view to use by default when rendering this routes template. + + When rendering a template, the route will, by default, determine the + template and view to use from the name of the route itself. If you need to + define a specific view, set this property. + + This is useful when multiple routes would benefit from using the same view + because it doesn't require a custom `renderTemplate` method. For example, + the following routes will all render using the `App.PostsListView` view: + + ```js + var PostsList = Ember.Route.extend({ + viewName: 'postsList', + }); + + App.PostsIndexRoute = PostsList.extend(); + App.PostsArchivedRoute = PostsList.extend(); + ``` + + @property viewName + @type String + @default null + */ + viewName: null, + + /** + The name of the template to use by default when rendering this routes + template. + + This is similar with `viewName`, but is useful when you just want a custom + template without a view. + + ```js + var PostsList = Ember.Route.extend({ + templateName: 'posts/list' + }); + + App.PostsIndexRoute = PostsList.extend(); + App.PostsArchivedRoute = PostsList.extend(); + ``` + + @property templateName + @type String + @default null + */ + templateName: null, + + /** + The name of the controller to associate with this route. + + By default, Ember will lookup a route's controller that matches the name + of the route (i.e. `App.PostController` for `App.PostRoute`). However, + if you would like to define a specific controller to use, you can do so + using this property. + + This is useful in many ways, as the controller specified will be: + + * passed to the `setupController` method. + * used as the controller for the view being rendered by the route. + * returned from a call to `controllerFor` for the route. + + @property controllerName + @type String + @default null + */ + controllerName: null, + /** The collection of functions, keyed by name, available on this route as action targets. @@ -34119,6 +35141,8 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { @param {String} name the name of the route @param {...Object} models the model(s) to be used while transitioning to the route. + @return {Transition} the transition object associated with this + attempted transition */ transitionTo: function(name, context) { var router = this.router; @@ -34126,7 +35150,7 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { }, /** - Perform a synchronous transition into another route with out attempting + Perform a synchronous transition into another route without attempting to resolve promises, update the URL, or abort any currently active asynchronous transitions (i.e. regular transitions caused by `transitionTo` or URL changes). @@ -34145,6 +35169,30 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { router.intermediateTransitionTo.apply(router, arguments); }, + /** + Refresh the model on this route and any child routes, firing the + `beforeModel`, `model`, and `afterModel` hooks in a similar fashion + to how routes are entered when transitioning in from other route. + The current route params (e.g. `article_id`) will be passed in + to the respective model hooks, and if a different model is returned, + `setupController` and associated route hooks will re-fire as well. + + An example usage of this method is re-querying the server for the + latest information using the same parameters as when the route + was first entered. + + Note that this will cause `model` hooks to fire even on routes + that were provided a model object when the route was initially + entered. + + @method refresh + @return {Transition} the transition object associated with this + attempted transition + */ + refresh: function() { + return this.router.router.refresh(this).method('replace'); + }, + /** Transition into another route while replacing the current URL, if possible. This will replace the current history entry instead of adding a new one. @@ -34172,6 +35220,8 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { @param {String} name the name of the route @param {...Object} models the model(s) to be used while transitioning to the route. + @return {Transition} the transition object associated with this + attempted transition */ replaceWith: function() { var router = this.router; @@ -34222,7 +35272,7 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { @private @method setup */ - setup: function(context, queryParams) { + setup: function(context, transition) { var controllerName = this.controllerName || this.routeName, controller = this.controllerFor(controllerName, true); if (!controller) { @@ -34233,38 +35283,23 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { // referenced in action handlers this.controller = controller; - var args = [controller, context]; - if (this.setupControllers) { this.setupControllers(controller, context); } else { - this.setupController.apply(this, args); + + + this.setupController(controller, context); + } if (this.renderTemplates) { this.renderTemplates(context); } else { - this.renderTemplate.apply(this, args); + this.renderTemplate(controller, context); } }, - /** - A hook you can implement to optionally redirect to another route. - - If you call `this.transitionTo` from inside of this hook, this route - will not be entered in favor of the other hook. - - Note that this hook is called by the default implementation of - `afterModel`, so if you override `afterModel`, you must either - explicitly call `redirect` or just put your redirecting - `this.transitionTo()` call within `afterModel`. - - @method redirect - @param {Object} model the model for this route - */ - redirect: Ember.K, - /** This hook is the first of the route entry validation hooks called when an attempt is made to transition into a route @@ -34326,8 +35361,6 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { // error so that it'd be handled by the `error` // hook, you would have to either return Ember.RSVP.reject(e); - // or - throw e; }); } } @@ -34376,10 +35409,32 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { resolves. Otherwise, non-promise return values are not utilized in any way. */ - afterModel: function(resolvedModel, transition, queryParams) { - this.redirect(resolvedModel, transition); - }, + afterModel: Ember.K, + /** + A hook you can implement to optionally redirect to another route. + + If you call `this.transitionTo` from inside of this hook, this route + will not be entered in favor of the other hook. + + `redirect` and `afterModel` behave very similarly and are + called almost at the same time, but they have an important + distinction in the case that, from one of these hooks, a + redirect into a child route of this route occurs: redirects + from `afterModel` essentially invalidate the current attempt + to enter this route, and will result in this route's `beforeModel`, + `model`, and `afterModel` hooks being fired again within + the new, redirecting transition. Redirects that occur within + the `redirect` hook, on the other hand, will _not_ cause + these hooks to be fired again the second time around; in + other words, by the time the `redirect` hook has been called, + both the resolved model and attempted entry into this route + are considered to be fully validated. + + @method redirect + @param {Object} model the model for this route + */ + redirect: Ember.K, /** Called when the context is changed by router.js. @@ -34445,6 +35500,8 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { var match, name, sawParams, value; for (var prop in params) { + if (prop === 'queryParams') { continue; } + if (match = prop.match(/^(.*)_id$/)) { name = match[1]; value = params[prop]; @@ -34458,6 +35515,17 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { return this.findModel(name, value); }, + /** + @private + + Router.js hook. + */ + deserialize: function(params, transition) { + + return this.model(params, transition); + + }, + /** @method findModel @@ -34492,6 +35560,8 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { var modelClass = container.lookupFactory('model:' + name); + if (!modelClass) { return; } + return modelClass.find(value); } }; @@ -34596,7 +35666,7 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { @param {Controller} controller instance @param {Object} model */ - setupController: function(controller, context) { + setupController: function(controller, context, transition) { if (controller && (context !== undefined)) { set(controller, 'model', context); } @@ -34883,7 +35953,7 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { options.outlet = options.outlet || 'main'; var parentView = this.router._lookupActiveView(options.parentView); - parentView.disconnectOutlet(options.outlet); + if (parentView) { parentView.disconnectOutlet(options.outlet); } }, willDestroy: function() { @@ -34911,8 +35981,10 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, { } }); + + function parentRoute(route) { - var handlerInfos = route.router.router.targetHandlerInfos; + var handlerInfos = route.router.router.state.handlerInfos; if (!handlerInfos) { return; } @@ -34957,7 +36029,11 @@ function normalizeOptions(route, name, template, options) { } if (typeof controller === 'string') { - controller = route.container.lookup('controller:' + controller); + var controllerName = controller; + controller = route.container.lookup('controller:' + controllerName); + if (!controller) { + throw new Ember.Error("You passed `controller: '" + controllerName + "'` into the `render` method, but no such controller could be found."); + } } options.controller = controller; @@ -35081,9 +36157,16 @@ Ember.onLoad('Ember.Handlebars', function() { */ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; + +var slice = Array.prototype.slice; Ember.onLoad('Ember.Handlebars', function(Handlebars) { + var QueryParams = Ember.Object.extend({ + values: null + }); + var resolveParams = Ember.Router.resolveParams, + translateQueryParams = Ember.Router._translateQueryParams, resolvePaths = Ember.Router.resolvePaths, isSimpleClick = Ember.ViewUtils.isSimpleClick; @@ -35255,8 +36338,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { // Map desired event name to invoke function var eventName = get(this, 'eventName'), i; this.on(eventName, this, this._invoke); - - }, + }, /** This method is invoked by observers installed during `init` that fire @@ -35297,6 +36379,22 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { normalizedPath = Ember.Handlebars.normalizePath(helperParameters.context, path, helperParameters.options.data); this.registerObserver(normalizedPath.root, normalizedPath.path, this, this._paramsChanged); } + + var queryParamsObject = this.queryParamsObject; + if (queryParamsObject) { + var values = queryParamsObject.values; + + // Install observers for all of the hash options + // provided in the (query-params) subexpression. + for (var k in values) { + if (!values.hasOwnProperty(k)) { continue; } + + if (queryParamsObject.types[k] === 'ID') { + normalizedPath = Ember.Handlebars.normalizePath(helperParameters.context, values[k], helperParameters.options.data); + this.registerObserver(normalizedPath.root, normalizedPath.path, this, this._paramsChanged); + } + } + } }, afterRender: function(){ @@ -35304,16 +36402,6 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { this._setupPathObservers(); }, - /** - This method is invoked by observers installed during `init` that fire - whenever the query params change - @private - */ - _queryParamsChanged: function (object, path) { - this.notifyPropertyChange('queryParams'); - }, - - /** 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}} @@ -35362,7 +36450,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { router.isActive.apply(router, [currentWithIndex].concat(contexts)); if (isActive) { return get(this, 'activeClass'); } - }).property('resolvedParams', 'routeArgs', 'router.url'), + }).property('resolvedParams', 'routeArgs'), /** Accessed as a classname binding to apply the `LinkView`'s `loadingClass` @@ -35420,7 +36508,19 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { }, /** - Computed property that returns the resolved parameters. + Computed property that returns an array of the + resolved parameters passed to the `link-to` helper, + e.g.: + + ```hbs + {{link-to a b '123' c}} + ``` + + will generate a `resolvedParams` of: + + ```js + [aObject, bObject, '123', cObject] + ``` @private @property @@ -35432,10 +36532,16 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { types = options.types, data = options.data; - + if (parameters.params.length === 0) { + var appController = this.container.lookup('controller:application'); + return [get(appController, 'currentRouteName')]; + } else { + return resolveParams(parameters.context, parameters.params, { types: types, data: data }); + } + // Original implementation if query params not enabled return resolveParams(parameters.context, parameters.params, { types: types, data: data }); - }).property(), + }).property('router.url'), /** Computed property that returns the current route name and @@ -35465,36 +36571,53 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { return resolvedParams; - }).property('resolvedParams', 'queryParams', 'router.url'), + }).property('resolvedParams', 'queryParams'), + queryParamsObject: null, + queryParams: Ember.computed(function computeLinkViewQueryParams() { - _potentialQueryParams: Ember.computed(function () { - var namedRoute = get(this, 'resolvedParams')[0]; - if (!namedRoute) { return null; } - var router = get(this, 'router'); + var queryParamsObject = get(this, 'queryParamsObject'), + suppliedParams = {}; - namedRoute = fullRouteName(router, namedRoute); + if (queryParamsObject) { + Ember.merge(suppliedParams, queryParamsObject.values); + } - return router.router.queryParamsForHandler(namedRoute); - }).property('resolvedParams'), + var resolvedParams = get(this, 'resolvedParams'), + router = get(this, 'router'), + routeName = resolvedParams[0], + paramsForRoute = router._queryParamNamesFor(routeName), + queryParams = paramsForRoute.queryParams, + translations = paramsForRoute.translations, + paramsForRecognizer = {}; - queryParams: Ember.computed(function () { - var self = this, - queryParams = null, - allowedQueryParams = get(this, '_potentialQueryParams'); + // Normalize supplied params into their long-form name + // e.g. 'foo' -> 'controllername:foo' + translateQueryParams(suppliedParams, translations, routeName); - if (!allowedQueryParams) { return null; } - allowedQueryParams.forEach(function (param) { - var value = get(self, param); - if (typeof value !== 'undefined') { - queryParams = queryParams || {}; - queryParams[param] = value; + var helperParameters = this.parameters; + router._queryParamOverrides(paramsForRecognizer, queryParams, function(name, resultsName) { + if (!(name in suppliedParams)) { return; } + + var parts = name.split(':'); + + var type = queryParamsObject.types[parts[1]]; + + var value; + if (type === 'ID') { + var normalizedPath = Ember.Handlebars.normalizePath(helperParameters.context, suppliedParams[name], helperParameters.options.data); + value = Ember.Handlebars.get(normalizedPath.root, normalizedPath.path, helperParameters.options); + } else { + value = suppliedParams[name]; } + + delete suppliedParams[name]; + + paramsForRecognizer[resultsName] = value; }); - - return queryParams; - }).property('_potentialQueryParams.[]'), + return paramsForRecognizer; + }).property('resolvedParams.[]'), /** Sets the element's `href` attribute to the url for @@ -35790,10 +36913,14 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { @see {Ember.LinkView} */ Ember.Handlebars.registerHelper('link-to', function linkToHelper(name) { - var options = [].slice.call(arguments, -1)[0], - params = [].slice.call(arguments, 0, -1), + var options = slice.call(arguments, -1)[0], + params = slice.call(arguments, 0, -1), hash = options.hash; + if (params[params.length - 1] instanceof QueryParams) { + hash.queryParamsObject = params.pop(); + } + hash.disabledBinding = hash.disabledWhen; if (!options.fn) { @@ -35821,6 +36948,8 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { return Ember.Handlebars.helpers.view.call(this, LinkView, options); }); + + /** See [link-to](/api/classes/Ember.Handlebars.helpers.html#method_link-to) @@ -35996,7 +37125,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { ```handelbars

My great app

- {{render navigation}} + {{render "navigation"}} ``` ```html @@ -36041,7 +37170,8 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { */ Ember.Handlebars.registerHelper('render', function renderHelper(name, contextString, options) { var length = arguments.length; - var contextProvided = length === 3, + + var contextProvided = length === 3, container, router, controller, view, context, lookupOptions; container = (options || contextString).data.keywords.controller.container; @@ -36058,6 +37188,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { throw Ember.Error("You must pass a templateName to render"); } + // # legacy namespace name = name.replace(/\//g, '.'); // \ legacy slash as namespace support @@ -36205,6 +37336,9 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { target = target.root; } + if (options.boundProperty) { + } + Ember.run(function runRegisteredAction() { if (target.send) { target.send.apply(target, args(options.parameters, actionName)); @@ -36422,6 +37556,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { action.target = { root: root, target: target, options: options }; action.bubbles = hash.bubbles; action.preventDefault = hash.preventDefault; + action.boundProperty = options.types[0] === "ID"; var actionId = ActionHelper.registerAction(actionName, action, hash.allowedKeys); return new SafeString('data-ember-action="' + actionId + '"'); @@ -36445,7 +37580,10 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { @submodule ember-routing */ -var get = Ember.get, set = Ember.set; +var get = Ember.get, set = Ember.set, + map = Ember.EnumerableUtils.map; + +var queuedQueryParamChanges = {}; Ember.ControllerMixin.reopen({ /** @@ -36502,7 +37640,7 @@ Ember.ControllerMixin.reopen({ /** Transition into another route while replacing the current URL, if possible. - This will replace the current history entry instead of adding a new one. + This will replace the current history entry instead of adding a new one. Beside that, it is identical to `transitionToRoute` in all other respects. ```javascript @@ -36552,6 +37690,7 @@ Ember.ControllerMixin.reopen({ } }); + })(); @@ -36694,6 +37833,7 @@ Ember.View.reopen({ @method _finishDisconnections */ _finishDisconnections: function() { + if (this.isDestroyed) return; // _outlets will be gone anyway var outlets = get(this, '_outlets'); var pendingDisconnections = this._pendingDisconnections; this._pendingDisconnections = null; @@ -36862,10 +38002,31 @@ Ember.Location = { container directly. */ registerImplementation: function(name, implementation) { + this.implementations[name] = implementation; }, - implementations: {} + implementations: {}, + + /** + Returns the current `location.hash` by parsing location.href since browsers + inconsistently URL-decode `location.hash`. + + https://bugzilla.mozilla.org/show_bug.cgi?id=483304 + + @private + @method getHash + */ + getHash: function () { + var href = window.location.href, + hashIndex = href.indexOf('#'); + + if (hashIndex === -1) { + return ''; + } else { + return href.substr(hashIndex); + } + } }; })(); @@ -36891,6 +38052,7 @@ var get = Ember.get, set = Ember.set; @extends Ember.Object */ Ember.NoneLocation = Ember.Object.extend({ + implementation: 'none', path: '', /** @@ -36961,8 +38123,6 @@ Ember.NoneLocation = Ember.Object.extend({ } }); -Ember.Location.registerImplementation('none', Ember.NoneLocation); - })(); @@ -36973,7 +38133,8 @@ Ember.Location.registerImplementation('none', Ember.NoneLocation); @submodule ember-routing */ -var get = Ember.get, set = Ember.set; +var get = Ember.get, set = Ember.set, + getHash = Ember.Location.getHash; /** `Ember.HashLocation` implements the location API using the browser's @@ -36985,6 +38146,7 @@ var get = Ember.get, set = Ember.set; @extends Ember.Object */ Ember.HashLocation = Ember.Object.extend({ + implementation: 'hash', init: function() { set(this, 'location', get(this, 'location') || window.location); @@ -36997,8 +38159,7 @@ Ember.HashLocation = Ember.Object.extend({ @method getURL */ getURL: function() { - // Default implementation without feature flag enabled - return get(this, 'location').hash.substr(1); + return getHash().substr(1); }, /** @@ -37025,6 +38186,7 @@ Ember.HashLocation = Ember.Object.extend({ */ replaceURL: function(path) { get(this, 'location').replace('#' + path); + set(this, 'lastSetURL', path); }, /** @@ -37042,7 +38204,7 @@ Ember.HashLocation = Ember.Object.extend({ Ember.$(window).on('hashchange.ember-location-'+guid, function() { Ember.run(function() { - var path = location.hash.substr(1); + var path = self.getURL(); if (get(self, 'lastSetURL') === path) { return; } set(self, 'lastSetURL', null); @@ -37080,8 +38242,6 @@ Ember.HashLocation = Ember.Object.extend({ } }); -Ember.Location.registerImplementation('hash', Ember.HashLocation); - })(); @@ -37105,9 +38265,11 @@ var supportsHistoryState = window.history && 'state' in window.history; @extends Ember.Object */ Ember.HistoryLocation = Ember.Object.extend({ + implementation: 'history', init: function() { set(this, 'location', get(this, 'location') || window.location); + set(this, 'baseURL', Ember.$('base').attr('href') || ''); }, /** @@ -37139,10 +38301,12 @@ Ember.HistoryLocation = Ember.Object.extend({ getURL: function() { var rootURL = get(this, 'rootURL'), location = get(this, 'location'), - path = location.pathname; + path = location.pathname, + baseURL = get(this, 'baseURL'); rootURL = rootURL.replace(/\/$/, ''); - var url = path.replace(rootURL, ''); + baseURL = baseURL.replace(/\/$/, ''); + var url = path.replace(baseURL, '').replace(rootURL, ''); return url; @@ -37267,13 +38431,17 @@ Ember.HistoryLocation = Ember.Object.extend({ @return formatted url {String} */ formatURL: function(url) { - var rootURL = get(this, 'rootURL'); + var rootURL = get(this, 'rootURL'), + baseURL = get(this, 'baseURL'); if (url !== '') { rootURL = rootURL.replace(/\/$/, ''); + baseURL = baseURL.replace(/\/$/, ''); + } else if(baseURL.match(/^\//) && rootURL.match(/^\//)) { + baseURL = baseURL.replace(/\/$/, ''); } - return rootURL + url; + return baseURL + rootURL + url; }, /** @@ -37289,8 +38457,6 @@ Ember.HistoryLocation = Ember.Object.extend({ } }); -Ember.Location.registerImplementation('history', Ember.HistoryLocation); - })(); @@ -38133,9 +39299,15 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin ```javascript App.inject(, , ) - App.inject('model:user', 'email', 'model:email') - App.inject('model', 'source', 'source:main') + App.inject('controller:application', 'email', 'model:email') + App.inject('controller', 'source', 'source:main') ``` + Please note that injections on models are currently disabled. + This was done because ember-data was not ready for fully a container aware ecosystem. + + You can enable injections on models by setting `Ember.MODEL_FACTORY_INJECTIONS` flag to `true` + If model factory injections are enabled, models should not be + accessed globally (only through `container.lookupFactory('model:modelName'))`); @method inject @param factoryNameOrType {String} @@ -38382,6 +39554,8 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin willDestroy: function() { Ember.BOOTED = false; + // Ensure deactivation of routes before objects are destroyed + this.__container__.lookup('router:main').reset(); this.__container__.destroy(); }, @@ -38460,6 +39634,10 @@ Ember.Application.reopenClass({ container.register('router:main', Ember.Router); container.injection('router:main', 'namespace', 'application:main'); + container.register('location:hash', Ember.HashLocation); + container.register('location:history', Ember.HistoryLocation); + container.register('location:none', Ember.NoneLocation); + container.injection('controller', 'target', 'router:main'); container.injection('controller', 'namespace', 'application:main');