dist/ember.prod.js in ember-source-1.3.2 vs dist/ember.prod.js in ember-source-1.4.0.beta.1

- old
+ new

@@ -3,11 +3,11 @@ * @copyright Copyright 2011-2014 Tilde Inc. and contributors * Portions Copyright 2006-2011 Strobe Inc. * 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-beta.1 */ (function() { var define, requireModule, require, requirejs; @@ -86,11 +86,11 @@ The core Runtime framework is based on the jQuery API with a number of performance optimizations. @class Ember @static - @version 1.3.2 + @version 1.4.0-beta.1+canary.64fee6ed */ if ('undefined' === typeof Ember) { // Create core object. Make it act like an instance of Ember.Namespace so that // objects assigned to it are given a sane string representation. @@ -113,14 +113,14 @@ /** @property VERSION @type String - @default '1.3.2' + @default '1.4.0-beta.1+canary.64fee6ed' @static */ -Ember.VERSION = '1.3.2'; +Ember.VERSION = '1.4.0-beta.1+canary.64fee6ed'; /** Standard environmental variables. You can define these in a global `EmberENV` variable before loading Ember to control various configuration settings. @@ -591,22 +591,24 @@ if (this[i] === obj) { return i; } } 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) { Array.prototype.map = arrayMap; } @@ -869,11 +871,12 @@ source: null, mixins: null, bindings: null, chains: null, chainWatchers: null, - values: null + values: null, + proto: null }; if (isDefinePropertySimulated) { // on platforms that don't support enumerable false // make meta fail jQuery.isPlainObject() to hide from @@ -1375,10 +1378,45 @@ } 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(", ") + "}"; +}; + + + })(); (function() { @@ -1589,14 +1627,15 @@ })(); (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; var utils = Ember.EnumerableUtils = { map: function(obj, callback, thisArg) { return obj.map ? obj.map.call(obj, callback, thisArg) : map.call(obj, callback, thisArg); @@ -1604,10 +1643,14 @@ forEach: function(obj, callback, thisArg) { return obj.forEach ? obj.forEach.call(obj, callback, thisArg) : forEach.call(obj, callback, thisArg); }, + filter: function(obj, callback, thisArg) { + return obj.filter ? obj.filter.call(obj, callback, thisArg) : filter.call(obj, callback, thisArg); + }, + indexOf: function(obj, element, index) { return obj.indexOf ? obj.indexOf.call(obj, element, index) : indexOf.call(obj, element, index); }, indexesOf: function(obj, elements) { @@ -1783,11 +1826,11 @@ target = get(target, key); path = path.slice(key.length+1); } // must return some kind of path to be valid else other things will break. - if (!path || path.length===0) throw new Ember.Error('Invalid Path'); + if (!path || path.length===0) throw new Ember.Error('Path cannot be empty'); return [ target, path ]; }; var getPath = Ember._getPath = function(root, path) { @@ -3313,10 +3356,16 @@ value: undefined // make enumerable }); } else { obj[keyName] = undefined; // make enumerable } + + + if (desc.func && desc._dependentCPs) { + addImplicitCPs(obj, desc._dependentCPs, meta); + } + } else { descs[keyName] = undefined; // shadow descriptor in proto if (desc == null) { value = data; @@ -3349,10 +3398,26 @@ return this; }; + var addImplicitCPs = function defineImplicitCPs(obj, implicitCPs, meta) { + var cp, key, length = implicitCPs.length; + + for (var i=0; i<length; ++i) { + cp = implicitCPs[i]; + key = cp.implicitCPKey; + + Ember.defineProperty(obj, key, cp, undefined, meta); + + if (cp._dependentCPs) { + addImplicitCPs(obj, cp._dependentCPs, meta); + } + } + }; + + })(); (function() { @@ -3833,10 +3898,56 @@ (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); + } + }; + + })(); (function() { @@ -4035,10 +4146,17 @@ META_KEY = Ember.META_KEY, watch = Ember.watch, unwatch = Ember.unwatch; + + + + + var expandProperties = Ember.expandProperties; + + // .......................................................... // DEPENDENT KEYS // // data structure: @@ -4193,21 +4311,31 @@ @extends Ember.Descriptor @constructor */ function ComputedProperty(func, opts) { this.func = func; - + + setDependentKeys(this, 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); } Ember.ComputedProperty = ComputedProperty; ComputedProperty.prototype = new Ember.Descriptor(); var ComputedPropertyPrototype = ComputedProperty.prototype; + + ComputedPropertyPrototype.toString = function() { + if (this.implicitCPKey) { + return this.implicitCPKey; + } + return Ember.Descriptor.prototype.toString.apply(this, arguments); + }; + + /** Properties are cacheable by default. Computed property will automatically cache the return value of your function until one of the dependent keys changes. Call `volatile()` to set it into non-cached mode. When in this mode @@ -4299,14 +4427,22 @@ */ ComputedPropertyPrototype.property = function() { var args; - args = a_slice.call(arguments); - + var addArg = function (property) { + args.push(property); + }; - this._dependentKeys = args; + args = []; + for (var i = 0, l = arguments.length; i < l; i++) { + expandProperties(arguments[i], addArg); + } + + + setDependentKeys(this, args); + return this; }; /** In some cases, you may want to annotate computed properties with additional @@ -4429,11 +4565,11 @@ hadCachedValue = false, cache = meta.cache, 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; try { @@ -4560,31 +4696,91 @@ ret[propertyNames[i]] = get(self, propertyNames[i]); } return ret; } -function registerComputed(name, macro) { - Ember.computed[name] = function(dependentKey) { - var args = a_slice.call(arguments); - return Ember.computed(dependentKey, function() { - return macro.apply(this, args); - }); +var registerComputed, registerComputedWithProperties; + + + var guidFor = Ember.guidFor, + map = Ember.EnumerableUtils.map, + filter = Ember.EnumerableUtils.filter, + typeOf = Ember.typeOf; + + var implicitKey = function (cp) { + return [guidFor(cp)].concat(cp._dependentKeys).join('_'); }; -} -function registerComputedWithProperties(name, macro) { - Ember.computed[name] = function() { - var properties = a_slice.call(arguments); + var normalizeDependentKey = function (key) { + if (key instanceof Ember.ComputedProperty) { + return implicitKey(key); + } else if (typeof key === 'string' || key instanceof String || typeof key === 'object' || typeof key === 'number') { + return key; + } else { + } + }; - var computed = Ember.computed(function() { - return macro.apply(this, [getProperties(this, properties)]); + var normalizeDependentKeys = function (keys) { + return map(keys, function (key) { + return normalizeDependentKey(key); }); + }; - return computed.property.apply(computed, properties); + var selectDependentCPs = function (keys) { + return filter(keys, function (key) { + return key instanceof Ember.ComputedProperty; + }); }; -} + var setDependentKeys = function(cp, dependentKeys) { + if (dependentKeys) { + cp._dependentKeys = normalizeDependentKeys(dependentKeys); + cp._dependentCPs = selectDependentCPs(dependentKeys); + cp.implicitCPKey = implicitKey(cp); + } else { + cp._dependentKeys = cp._dependentCPs = []; + delete cp.implicitCPKey; + } + }; + // expose `normalizeDependentKey[s]` so user CP macros can easily support + // composition + Ember.computed.normalizeDependentKey = normalizeDependentKey; + Ember.computed.normalizeDependentKeys = normalizeDependentKeys; + + registerComputed = function (name, macro) { + Ember.computed[name] = function(dependentKey) { + var args = normalizeDependentKeys(a_slice.call(arguments)); + return Ember.computed(dependentKey, function() { + return macro.apply(this, args); + }); + }; + }; + + + + registerComputedWithProperties = function(name, macro) { + Ember.computed[name] = function() { + var args = a_slice.call(arguments); + var properties = normalizeDependentKeys(args); + + var computed = Ember.computed(function() { + return macro.apply(this, [getProperties(this, properties)]); + }); + + return computed.property.apply(computed, args); + }; + }; + + + + Ember.computed.literal = function (value) { + return Ember.computed(function () { + return value; + }); + }; + + /** A computed property that returns true if the value of the dependent property is null, an empty string, empty array, or empty function. Note: When using `Ember.computed.empty` to watch an array make sure to @@ -5110,11 +5306,55 @@ return Ember.computed(dependentKey, function() { return get(this, dependentKey); }); }; +if (Ember.FEATURES.isEnabled('computed-read-only')) { +/** + Where `computed.oneWay` provides oneWay bindings, `computed.readOnly` provides + a readOnly one way binding. Very often when using `computed.oneWay` one does + not also want changes to propogate back up, as they will replace the value. + This prevents the reverse flow, and also throws an exception when it occurs. + + Example + + ```javascript + User = Ember.Object.extend({ + firstName: null, + lastName: null, + nickName: Ember.computed.readOnly('firstName') + }); + + user = User.create({ + firstName: 'Teddy', + lastName: 'Zeenny' + }); + + user.get('nickName'); + # 'Teddy' + + user.set('nickName', 'TeddyBear'); + # throws Exception + # throw new Ember.Error('Cannot Set: nickName on: <User:ember27288>' );` + + user.get('firstName'); + # 'Teddy' + ``` + + @method computed.readOnly + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which creates a + one way computed property to the original value for property. +*/ +Ember.computed.readOnly = function(dependentKey) { + return Ember.computed(dependentKey, function() { + return get(this, dependentKey); + }).readOnly(); +}; +} /** A computed property that acts like a standard getter and setter, but returns the value at the provided `defaultPath` if the property itself has not been set to a value @@ -5924,11 +6164,12 @@ }, defaultQueue: 'actions', onBegin: onBegin, onEnd: onEnd }), - slice = [].slice; + slice = [].slice, + concat = [].concat; // .......................................................... // Ember.run - this is ideally the only public API the dev sees // @@ -6010,20 +6251,69 @@ then it will be looked up on the passed target. @param {Object} [args*] Any additional arguments you wish to pass to the method. @return {Object} Return value from invoking the passed function. Please note, when called within an existing loop, no return value is possible. */ -Ember.run.join = function(target, method) { +Ember.run.join = function(target, method /* args */) { if (!Ember.run.currentRunLoop) { return Ember.run.apply(Ember.run, arguments); } var args = slice.call(arguments); args.unshift('actions'); 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; Ember.run.currentRunLoop = null; @@ -6189,11 +6479,11 @@ @param {Object} [target] The target of the method to invoke. @param {Function|String} method The method to invoke. If you pass a string it will be resolved on the target at the time the method is invoked. @param {Object} [args*] Optional arguments to pass to the timeout. - @return {Object} timer + @return {Object} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.once = function(target, method) { checkAutoRun(); var args = slice.call(arguments); args.unshift('actions'); @@ -6240,11 +6530,11 @@ @param {Object} [target] The target of the method to invoke. @param {Function|String} method The method to invoke. If you pass a string it will be resolved on the target at the time the method is invoked. @param {Object} [args*] Optional arguments to pass to the timeout. - @return {Object} timer + @return {Object} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.scheduleOnce = function(queue, target, method) { checkAutoRun(); return backburner.scheduleOnce.apply(backburner, arguments); }; @@ -6303,21 +6593,22 @@ @param {Object} [target] target of method to invoke @param {Function|String} method The method to invoke. If you pass a string it will be resolved on the target at the time the method is invoked. @param {Object} [args*] Optional arguments to pass to the timeout. - @return {Object} timer + @return {Object} Timer information for use in cancelling, see `Ember.run.cancel`. */ Ember.run.next = function() { var args = slice.call(arguments); args.push(1); return backburner.later.apply(backburner, args); }; /** 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() { // will not be executed }); @@ -6330,15 +6621,33 @@ var runOnce = Ember.run.once(myContext, 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); }; @@ -6366,19 +6675,47 @@ // 150ms passes // myFunc is invoked with context myContext // 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. 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*] 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); }; @@ -6411,11 +6748,11 @@ @param {Function|String} method The 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*] 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); }; @@ -6919,10 +7256,13 @@ o_create = Ember.create, defineProperty = Ember.defineProperty, guidFor = Ember.guidFor; + var expandProperties = Ember.expandProperties; + + function mixinsMeta(obj) { var m = Ember.meta(obj, true), ret = m.mixins; if (!ret) { ret = m.mixins = {}; } else if (!m.hasOwnProperty('mixins')) { @@ -7490,38 +7830,10 @@ this.methodName = 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. ```javascript App.Person = Ember.Object.extend({ name: function() { @@ -7572,20 +7884,26 @@ 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 func = arguments[0]; - paths = a_slice.call(arguments, 1); + _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"); } func.__ember_observes__ = paths; @@ -7669,20 +7987,27 @@ 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); }; + var _paths = a_slice.call(arguments, 0, -1); + if (typeof func !== "function") { // revert to old, soft-deprecated argument ordering func = arguments[0]; - paths = a_slice.call(arguments, 1); + _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"); } func.__ember_observesBefore__ = paths; @@ -9915,10 +10240,11 @@ Ember.MODEL_FACTORY_INJECTIONS = false || !!Ember.ENV.MODEL_FACTORY_INJECTIONS; define("container", [], function() { + "use strict"; // A safe and simple inheriting object. function InheritingDict(parent) { this.parent = parent; this.dict = {}; @@ -10036,11 +10362,12 @@ this.resolver = parent && parent.resolver || function() {}; 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 = {}; this.factoryTypeInjections = new InheritingDict(parent && parent.factoryTypeInjections); this.factoryInjections = {}; @@ -10157,13 +10484,11 @@ @param {String} fullName @param {Function} factory @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 + '`'); } @@ -10192,15 +10517,18 @@ @method unregister @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); }, /** Given a fullName return the corresponding factory. @@ -10233,11 +10561,22 @@ @method resolve @param {String} fullName @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; }, /** A hook that can be used to describe how the resolver will attempt to find the factory. @@ -10314,38 +10653,24 @@ @param {String} fullName @param {Object} options @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); }, /** Given a fullName return the corresponding factory. @method lookupFactory @param {String} fullName @return {any} */ lookupFactory: function(fullName) { - return factoryFor(this, fullName); + validateFullName(fullName); + return factoryFor(this, this.normalize(fullName)); }, /** Given a fullName check if the container is aware of its factory or singleton instance. @@ -10353,15 +10678,12 @@ @method has @param {String} fullName @return {Boolean} */ has: function(fullName) { - if (this.cache.has(fullName)) { - return true; - } - - return !!this.resolve(fullName); + validateFullName(fullName); + return has(this, this.normalize(fullName)); }, /** Allow registering options for all factories of a type. @@ -10438,10 +10760,11 @@ @param {String} type @param {String} property @param {String} fullName */ typeInjection: function(type, property, fullName) { + validateFullName(fullName); if (this.parent) { illegalChildOperation('typeInjection'); } addTypeInjection(this.typeInjections, type, property, fullName); }, @@ -10487,18 +10810,24 @@ @method injection @param {String} factoryName @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); }, /** Used only via `factoryInjection`. @@ -10530,11 +10859,11 @@ @param {String} fullName */ factoryTypeInjection: function(type, property, fullName) { if (this.parent) { illegalChildOperation('factoryTypeInjection'); } - addTypeInjection(this.factoryTypeInjections, type, property, fullName); + addTypeInjection(this.factoryTypeInjections, type, property, this.normalize(fullName)); }, /** Defines factory injection rules. @@ -10582,28 +10911,34 @@ @method factoryInjection @param {String} factoryName @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); }, /** A depth first traversal, destroying the container, its descendant containers and all their managed objects. @method destroy */ destroy: function() { - for (var i=0, l=this.children.length; i<l; i++) { this.children[i].destroy(); } this.children = []; @@ -10625,10 +10960,36 @@ } resetCache(this); } }; + function has(container, fullName){ + if (container.cache.has(fullName)) { + return true; + } + + return !!container.resolve(fullName); + } + + function lookup(container, fullName, options) { + options = options || {}; + + if (container.cache.has(fullName) && options.singleton !== false) { + return container.cache.get(fullName); + } + + var value = instantiate(container, fullName); + + if (value === undefined) { return; } + + if (isSingleton(container, fullName) && options.singleton !== false) { + container.cache.set(fullName, value); + } + + return value; + } + function illegalChildOperation(operation) { throw new Error(operation + " is not currently supported on child containers"); } function isSingleton(container, fullName) { @@ -10640,18 +11001,18 @@ function buildInjections(container, injections) { var hash = {}; if (!injections) { return hash; } - var injection, lookup; + var injection, injectable; for (var i=0, l=injections.length; i<l; i++) { injection = injections[i]; - lookup = container.lookup(injection.fullName); + injectable = lookup(container, injection.fullName); - if (lookup !== undefined) { - hash[injection.property] = lookup; + if (injectable !== undefined) { + hash[injection.property] = injectable; } else { throw new Error('Attempting to inject an unknown injection: `' + injection.fullName + '`'); } } @@ -10672,11 +11033,11 @@ return options[optionName]; } } function factoryFor(container, fullName) { - var name = container.normalize(fullName); + var name = fullName; var factory = container.resolve(name); var injectedFactory; var cache = container.factoryCache; var type = fullName.split(":")[0]; @@ -10782,10 +11143,17 @@ property: property, fullName: fullName }); } + var VALID_FULL_NAME_REGEXP = /^[^:]+.+:[^:]+$/; + function validateFullName(fullName) { + if (!VALID_FULL_NAME_REGEXP.test(fullName)) { + throw new TypeError('Invalid Fullname, expected: `type:name` got: ' + fullName); + } + } + function addInjection(rules, factoryName, property, injectionName) { var injections = rules[factoryName] = rules[factoryName] || []; injections.push({ property: property, fullName: injectionName }); } @@ -10976,43 +11344,10 @@ if (Ember.Copyable && Ember.Copyable.detect(obj)) return obj.copy(deep); return _copy(obj, deep, deep ? [] : null, deep ? [] : null); }; /** - 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(", ") + "}"; -}; - -/** Compares two objects, returning true if they are logically equal. This is a deeper comparison than a simple triple equal. For sets it will compare the internal objects. For any other object that implements `isEqual()` it will respect that method. @@ -11112,10 +11447,14 @@ var STRING_DASHERIZE_CACHE = {}; var STRING_DECAMELIZE_REGEXP = (/([a-z\d])([A-Z])/g); var STRING_CAMELIZE_REGEXP = (/(\-|_|\.|\s)+(.)?/g); var STRING_UNDERSCORE_REGEXP_1 = (/([a-z\d])([A-Z]+)/g); var STRING_UNDERSCORE_REGEXP_2 = (/\-|\s+/g); +var STRING_PARAMETERIZE_REGEXP_1 = (/[_|\/|\s]+/g); +var STRING_PARAMETERIZE_REGEXP_2 = (/[^a-z0-9\-]+/gi); +var STRING_PARAMETERIZE_REGEXP_3 = (/[\-]+/g); +var STRING_PARAMETERIZE_REGEXP_4 = (/^-+|-+$/g); /** Defines the hash of localized strings for the current language. Used by the `Ember.String.loc()` helper. To localize, add string values to this hash. @@ -11350,12 +11689,60 @@ capitalize: function(str) { return str.charAt(0).toUpperCase() + str.substr(1); } }; +if (Ember.FEATURES.isEnabled("string-humanize")) { + /** + Returns the Humanized form of a string + Replaces underscores with spaces, and capitializes first character + of string. Also strips "_id" suffixes. + ```javascript + 'first_name'.humanize() // 'First name' + 'user_id'.humanize() // 'User' + ``` + + @method humanize + @param {String} str The string to humanize. + @return {String} The humanized string. + */ + + Ember.String.humanize = function(str) { + return str.replace(/_id$/, ''). + replace(/_/g, ' '). + replace(/^\w/g, function(s){ + return s.toUpperCase(); + }); + }; +} + +if (Ember.FEATURES.isEnabled("string-parameterize")) { + /** + Transforms a string so that it may be used as part of a 'pretty' / SEO friendly URL. + + ```javascript + 'My favorite items.'.parameterize(); // 'my-favorite-items' + 'action_name'.parameterize(); // 'action-name' + '100 ways Ember.js is better than Angular.'.parameterize(); // '100-ways-emberjs-is-better-than-angular' + ``` + + @method parameterize + @param {String} str The string to parameterize. + @return {String} the parameterized string. + */ + Ember.String.parameterize = function(str) { + return str.replace(STRING_PARAMETERIZE_REGEXP_1, '-') // replace underscores, slashes and spaces with separator + .replace(STRING_PARAMETERIZE_REGEXP_2, '') // remove non-alphanumeric characters except the separator + .replace(STRING_PARAMETERIZE_REGEXP_3, '-') // replace multiple occurring separators + .replace(STRING_PARAMETERIZE_REGEXP_4, '') // trim leading and trailing separators + .toLowerCase(); + }; +} + + })(); (function() { @@ -11374,11 +11761,18 @@ dasherize = Ember.String.dasherize, underscore = Ember.String.underscore, capitalize = Ember.String.capitalize, classify = Ember.String.classify; +if (Ember.FEATURES.isEnabled("string-humanize")) { + var humanize = Ember.String.humanize; +} +if (Ember.FEATURES.isEnabled("string-parameterize")) { + var parameterize = Ember.String.parameterize; +} + if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { /** See [Ember.String.fmt](/api/classes/Ember.String.html#method_fmt). @@ -11467,11 +11861,34 @@ */ String.prototype.capitalize = function() { return capitalize(this); }; - + if (Ember.FEATURES.isEnabled("string-humanize")) { + /** + See [Ember.String.humanize](/api/classes/Ember.String.html#method_humanize). + + @method humanize + @for String + */ + String.prototype.humanize = function() { + return humanize(this); + }; + } + + if (Ember.FEATURES.isEnabled("string-parameterize")) { + /** + See [Ember.String.parameterize](/api/classes/Ember.String.html#method_parameterize). + + @method parameterize + @for String + */ + String.prototype.parameterize = function() { + return parameterize(this); + }; + } + } })(); @@ -13829,11 +14246,11 @@ if (typeof callback !== "function") { throw new TypeError(); } var ret = initialValue; this.forEach(function(item, i) { - ret = callback.call(null, ret, item, i, this, reducerProperty); + ret = callback(ret, item, i, this, reducerProperty); }, this); return ret; }, @@ -13852,11 +14269,11 @@ if (arguments.length>1) args = a_slice.call(arguments, 1); this.forEach(function(x, idx) { var method = x && x[methodName]; if ('function' === typeof method) { - ret[idx] = args ? method.apply(x, args) : method.call(x); + ret[idx] = args ? method.apply(x, args) : x[methodName](); } }, this); return ret; }, @@ -14047,12 +14464,10 @@ implementing an ordered enumerable (such as an array), also pass the start and end values where the content changed so that it can be used to notify range observers. @method enumerableContentDidChange - @param {Number} [start] optional start offset for the content change. - For unordered enumerables, you should always pass -1. @param {Ember.Enumerable|Number} removing An enumerable of the objects to be removed or the number of items to be removed. @param {Ember.Enumerable|Number} adding An enumerable of the objects to be added or the number of items to be added. @chainable @@ -14258,11 +14673,11 @@ arr.slice(1, 100); // ['green', 'blue'] ``` @method slice @param {Integer} beginIndex (Optional) index to begin slicing from. - @param {Integer} endIndex (Optional) index to end the slice at. + @param {Integer} endIndex (Optional) index to end the slice at (but not included). @return {Array} New array with specified slice */ slice: function(beginIndex, endIndex) { var ret = Ember.A(); var length = get(this, 'length') ; @@ -14409,11 +14824,11 @@ /** Becomes true whenever the array currently has observers watching changes on the array. - @property Boolean + @property {Boolean} hasArrayObservers */ hasArrayObservers: Ember.computed(function() { return Ember.hasListeners(this, '@array:change') || Ember.hasListeners(this, '@array:before'); }), @@ -14559,10 +14974,13 @@ eachPropertyPattern = /^(.*)\.@each\.(.*)/, doubleEachPropertyPattern = /(.*\.@each){2,}/, arrayBracketPattern = /\.\[\]$/; + var expandProperties = Ember.expandProperties; + + function get(obj, key) { if (key === '@this') { return obj; } @@ -15051,10 +15469,11 @@ addItems.call(this, dependentArray, callbacks, cp, propertyName, meta); } }, this); }; + this.func = function (propertyName) { recompute.call(this, propertyName); return cp._instanceMeta(this, propertyName).getValue(); @@ -15137,14 +15556,17 @@ throw new Ember.Error("Nested @each properties not supported: " + dependentKey); } else if (match = eachPropertyPattern.exec(dependentKey)) { dependentArrayKey = match[1]; - itemPropertyKey = match[2]; - cp.itemPropertyKey(dependentArrayKey, itemPropertyKey); - - propertyArgs.add(dependentArrayKey); + var itemPropertyKeyPattern = match[2], + addItemPropertyKey = function (itemPropertyKey) { + cp.itemPropertyKey(dependentArrayKey, itemPropertyKey); + }; + + expandProperties(itemPropertyKeyPattern, addItemPropertyKey); + propertyArgs.add(dependentArrayKey); } else { propertyArgs.add(dependentKey); } }); @@ -15250,11 +15672,11 @@ Example ```javascript Ember.computed.max = function (dependentKey) { - return Ember.reduceComputed.call(null, dependentKey, { + return Ember.reduceComputed(dependentKey, { initialValue: -Infinity, addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { return Math.max(accumulatedValue, item); }, @@ -15560,10 +15982,34 @@ forEach = Ember.EnumerableUtils.forEach, map = Ember.EnumerableUtils.map, SearchProxy; /** + A computed property that returns the sum of the value + in the dependent array. + + @method computed.sum + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computes the sum of all values in the dependentKey's array +*/ + +Ember.computed.sum = function(dependentKey){ + return Ember.reduceComputed(dependentKey, { + initialValue: 0, + + addedItem: function(accumulatedValue, item, changeMeta, instanceMeta){ + return accumulatedValue + item; + }, + + removedItem: function(accumulatedValue, item, changeMeta, instanceMeta){ + return accumulatedValue - item; + } + }); +}; + +/** A computed property that calculates the maximum value in the dependent array. This will return `-Infinity` when the dependent array is empty. ```javascript @@ -15592,11 +16038,11 @@ @for Ember @param {String} dependentKey @return {Ember.ComputedProperty} computes the largest value in the dependentKey's array */ Ember.computed.max = function (dependentKey) { - return Ember.reduceComputed.call(null, dependentKey, { + return Ember.reduceComputed(dependentKey, { initialValue: -Infinity, addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { return Math.max(accumulatedValue, item); }, @@ -15640,11 +16086,11 @@ @for Ember @param {String} dependentKey @return {Ember.ComputedProperty} computes the smallest value in the dependentKey's array */ Ember.computed.min = function (dependentKey) { - return Ember.reduceComputed.call(null, dependentKey, { + return Ember.reduceComputed(dependentKey, { initialValue: Infinity, addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { return Math.min(accumulatedValue, item); }, @@ -15925,11 +16371,11 @@ */ Ember.computed.union = Ember.computed.uniq; /** A computed property which returns a new array with all the duplicated - elements from two or more dependeny arrays. + elements from two or more dependent arrays. Example ```javascript var obj = Ember.Object.createWithMixins({ @@ -16031,11 +16477,11 @@ */ Ember.computed.setDiff = function (setAProperty, setBProperty) { if (arguments.length !== 2) { throw new Ember.Error("setDiff requires exactly two dependent arrays."); } - return Ember.arrayComputed.call(null, setAProperty, setBProperty, { + return Ember.arrayComputed(setAProperty, setBProperty, { addedItem: function (array, item, changeMeta, instanceMeta) { var setA = get(this, setAProperty), setB = get(this, setBProperty); if (changeMeta.arrayChanged === setA) { @@ -16231,11 +16677,11 @@ instanceMeta.binarySearch = binarySearch; }; } - return Ember.arrayComputed.call(null, itemsKey, { + return Ember.arrayComputed(itemsKey, { initialize: initFn, addedItem: function (array, item, changeMeta, instanceMeta) { var index = instanceMeta.binarySearch(array, item); array.insertAt(index, item); @@ -16294,10 +16740,13 @@ */ var a_slice = Array.prototype.slice; + var expandProperties = Ember.expandProperties; + + if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Function) { /** The `property` extension of Javascript's Function prototype is available when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is @@ -16390,13 +16839,19 @@ @method observes @for Function */ Function.prototype.observes = function() { - this.__ember_observes__ = a_slice.call(arguments); - + var addWatchedProperty = function (obs) { watched.push(obs); }; + var watched = []; + for (var i=0; i<arguments.length; ++i) { + expandProperties(arguments[i], addWatchedProperty); + } + + this.__ember_observes__ = watched; + return this; }; /** The `observesImmediately` extension of Javascript's Function prototype is @@ -16454,13 +16909,19 @@ @method observesBefore @for Function */ Function.prototype.observesBefore = function() { - this.__ember_observesBefore__ = a_slice.call(arguments); - + var addWatchedProperty = function (obs) { watched.push(obs); }; + var watched = []; + for (var i=0; i<arguments.length; ++i) { + expandProperties(arguments[i], addWatchedProperty); + } + + this.__ember_observesBefore__ = watched; + return this; }; /** The `on` extension of Javascript's Function prototype is available @@ -17657,10 +18118,11 @@ */ willMergeMixin: function(props) { var hashName; if (!props._actions) { + if (typeOf(props.actions) === 'object') { hashName = 'actions'; } else if (typeOf(props.events) === 'object') { hashName = 'events'; } @@ -17701,28 +18163,27 @@ (function() { var set = Ember.set, get = Ember.get, - resolve = Ember.RSVP.resolve, - rethrow = Ember.RSVP.rethrow, not = Ember.computed.not, or = Ember.computed.or; /** @module ember @submodule ember-runtime */ -function observePromise(proxy, promise) { - promise.then(function(value) { +function tap(proxy, promise) { + return promise.then(function(value) { set(proxy, 'isFulfilled', true); set(proxy, 'content', value); + return value; }, function(reason) { set(proxy, 'isRejected', true); set(proxy, 'reason', reason); - // don't re-throw, as we are merely observing + throw reason; }, "Ember: PromiseProxy"); } /** A low level mixin making ObjectProxy, ObjectController or ArrayController's promise aware. @@ -17847,13 +18308,11 @@ @property promise */ promise: Ember.computed(function(key, promise) { if (arguments.length === 2) { - promise = resolve(promise); - observePromise(this, promise); - return promise.then(); // fork the promise. + return tap(this, promise); } else { throw new Ember.Error("PromiseProxy's promise must be set"); } }), @@ -20334,11 +20793,11 @@ /** @module ember @submodule ember-views */ -var jQuery = this.jQuery || (Ember.imports && Ember.imports.jQuery); +var jQuery = (this && this.jQuery) || (Ember.imports && Ember.imports.jQuery); if (!jQuery && typeof require === 'function') { jQuery = require('jquery'); } @@ -20384,21 +20843,21 @@ // 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 &shy; -var needsShy = this.document && (function() { +var needsShy = typeof document !== 'undefined' && (function() { var testEl = document.createElement('div'); testEl.innerHTML = "<div></div>"; testEl.firstChild.innerHTML = "<script></script>"; return testEl.firstChild.innerHTML === ''; })(); // IE 8 (and likely earlier) likes to move whitespace preceeding // a script tag to appear after it. This means that we can // accidentally remove whitespace when updating a morph. -var movesWhitespace = this.document && (function() { +var movesWhitespace = typeof document !== 'undefined' && (function() { var testEl = document.createElement('div'); testEl.innerHTML = "Test: <script type='text/x-placeholder'></script>Value"; return testEl.childNodes[0].nodeValue === 'Test:' && testEl.childNodes[2].nodeValue === ' Value'; })(); @@ -23593,10 +24052,12 @@ * preRender: when a view is first instantiated, and after its element was destroyed, it is in the preRender state * inBuffer: once a view has been rendered, but before it has been inserted into the DOM, it is in the inBuffer state + * hasElement: the DOM representation of the view is created, + and is ready to be inserted * inDOM: once a view has been inserted into the DOM it is in the inDOM state. A view spends the vast majority of its existence in this state. * destroyed: once a view has been destroyed (using the destroy method), it is in this state. No further actions can be invoked @@ -24092,11 +24553,21 @@ invokeObserver: function(target, observer) { observer.call(target); } }); +})(); + + +(function() { +/** +@module ember +@submodule ember-views +*/ + +var hasElement = Ember.View.states.hasElement; var inDOM = Ember.View.states.inDOM = Ember.create(hasElement); Ember.merge(inDOM, { enter: function(view) { // Register the view for event handling. This hash is used by @@ -24653,19 +25124,19 @@ in the item views receiving an appropriately matched `tagName` property. Given an empty `<body>` and the following code: ```javascript - anUndorderedListView = Ember.CollectionView.create({ + anUnorderedListView = Ember.CollectionView.create({ tagName: 'ul', content: ['A','B','C'], itemViewClass: Ember.View.extend({ template: Ember.Handlebars.compile("the letter: {{view.content}}") }) }); - anUndorderedListView.appendTo('body'); + anUnorderedListView.appendTo('body'); ``` Will result in the following HTML structure ```html @@ -25009,10 +25480,73 @@ })(); (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; /** @@ -25103,11 +25637,11 @@ @class Component @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); }, @@ -25115,10 +25649,48 @@ defaultLayout: function(options){ options.data = {view: options._context}; Ember.Handlebars.helpers['yield'].apply(this, [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 { view: this, controller: this @@ -25364,16 +25936,16 @@ return false; } })(), // 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 &shy; - needsShy = document && (function() { + needsShy = typeof document !== 'undefined' && (function() { var testEl = document.createElement('div'); testEl.innerHTML = "<div></div>"; testEl.firstChild.innerHTML = "<script></script>"; return testEl.firstChild.innerHTML === ''; })(), @@ -26187,51 +26759,38 @@ var handlebarsGet = Ember.Handlebars.get = function(root, path, options) { var data = options && options.data, normalizedPath = normalizePath(root, path, data), value; - + if (Ember.FEATURES.isEnabled("ember-handlebars-caps-lookup")) { + + // If the path starts with a capital letter, look it up on Ember.lookup, + // which defaults to the `window` object in browsers. + if (Ember.isGlobalPath(path)) { + value = Ember.get(Ember.lookup, path); + } else { + + // In cases where the path begins with a keyword, change the + // root to the value represented by that keyword, and ensure + // the path is relative to it. + value = Ember.get(normalizedPath.root, normalizedPath.path); + } + + } else { root = normalizedPath.root; path = normalizedPath.path; value = Ember.get(root, path); if (value === undefined && root !== Ember.lookup && Ember.isGlobalPath(path)) { value = Ember.get(Ember.lookup, path); } - + } return value; }; -/** - This method uses `Ember.Handlebars.get` to lookup a value, then ensures - that the value is escaped properly. - - If `unescaped` is a truthy value then the escaping will not be performed. - - @method getEscaped - @for Ember.Handlebars - @param {Object} root The object to look up the property on - @param {String} path The path to be lookedup - @param {Object} options The template's option hash -*/ -Ember.Handlebars.getEscaped = function(root, path, options) { - var result = handlebarsGet(root, path, options); - - if (result === null || result === undefined) { - result = ""; - } else if (!(result instanceof Handlebars.SafeString)) { - result = String(result); - } - if (!options.hash.unescaped){ - result = Handlebars.Utils.escapeExpression(result); - } - - return result; -}; - Ember.Handlebars.resolveParams = function(context, params, options) { var resolvedParams = [], types = options.types, param, type; for (var i=0, l=params.length; i<l; i++) { param = params[i]; @@ -27116,10 +27675,16 @@ var shouldDisplay = get(this, 'shouldDisplayFunc'), preserveContext = get(this, 'preserveContext'), context = get(this, 'previousContext'); + var _contextController; + + + _contextController = get(this, '_contextController'); + + var inverseTemplate = get(this, 'inverseTemplate'), displayTemplate = get(this, 'displayTemplate'); var result = this.normalizedValue(); this._lastNormalizedValue = result; @@ -27135,10 +27700,16 @@ set(this, '_context', context); } else { // 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 // expression to the render context and return. if (result === null || result === undefined) { @@ -27178,11 +27749,10 @@ @submodule ember-handlebars */ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.normalizePath; -var handlebarsGetEscaped = Ember.Handlebars.getEscaped; var forEach = Ember.ArrayPolyfills.forEach; var o_create = Ember.create; var EmberHandlebars = Ember.Handlebars, helpers = EmberHandlebars.helpers; @@ -27236,10 +27806,20 @@ previousContext: currentContext, isEscaped: !options.hash.unescaped, 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() { Ember.run.scheduleOnce('render', bindView, 'rerenderIfNeeded'); }; @@ -27258,11 +27838,11 @@ } } } else { // The object is not observable, so just render it out and // be done with it. - data.buffer.push(handlebarsGetEscaped(currentContext, property, options)); + data.buffer.push(handlebarsGet(currentContext, property, options)); } } EmberHandlebars.bind = bind; @@ -27279,13 +27859,13 @@ if (data.insideGroup) { observer = function() { Ember.run.once(view, 'rerender'); }; - output = handlebarsGetEscaped(currentContext, property, options); - - data.buffer.push(output); + var result = handlebarsGet(currentContext, property, options); + if (result === null || result === undefined) { result = ""; } + data.buffer.push(result); } else { var bindView = new Ember._SimpleHandlebarsView( property, currentContext, !options.hash.unescaped, options.data ); @@ -27305,15 +27885,26 @@ view.registerObserver(normalized.root, normalized.path, observer); } } else { // The object is not observable, so just render it out and // be done with it. - output = handlebarsGetEscaped(currentContext, property, options); - data.buffer.push(output); + output = handlebarsGet(currentContext, property, options); + data.buffer.push((output === null || typeof output === 'undefined') ? '' : output); } } +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 mustache is a property reference or a regular helper reference. This triage helper resolves that. @@ -27411,22 +28002,47 @@ @param {Function} fn Context to provide for rendering @return {String} HTML string */ 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 }); }); /** Use the `{{with}}` helper when you want to scope context. Take the following code as an example: @@ -27521,20 +28137,24 @@ }); /** 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 @param {Function} context @param {Hash} 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); + } }); /** @method unless @for Ember.Handlebars.helpers @@ -27547,11 +28167,15 @@ var fn = options.fn, inverse = options.inverse; 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); + } }); /** `bind-attr` allows you to create a binding between DOM element attributes and Ember objects. For example: @@ -28309,11 +28933,11 @@ <div class="ember-view">Hi Mary</div> <div class="ember-view">Hi Sara</div> </div> ``` - ### Blockless Use + ### Blockless use in a collection If you provide an `itemViewClass` option that has its own `template` you can omit the block. The following template: @@ -28403,25 +29027,34 @@ var fn = options.fn; var data = options.data; 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 var collectionPrototype = collectionClass.proto(), 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 { itemViewClass = collectionPrototype.itemViewClass; } @@ -28611,10 +29244,11 @@ @module ember @submodule ember-handlebars */ var get = Ember.get, set = Ember.set; +var fmt = Ember.String.fmt; Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { init: function() { var itemController = get(this, 'itemController'); var binding; @@ -29597,11 +30231,11 @@ */ 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 property is updated live. @@ -29628,144 +30262,35 @@ @default null */ 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 }); })(); @@ -30153,70 +30678,56 @@ tagName: 'select', classNames: ['ember-select'], 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("<option value=\"\">"); - hashTypes = {}; - hashContexts = {}; - stack1 = helpers._triageMustache.call(depth0, "view.prompt", {hash:{},contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}); + stack1 = helpers._triageMustache.call(depth0, "view.prompt", {hash:{},hashTypes:{},hashContexts:{},contexts:[depth0],types:["ID"],data:data}); if(stack1 || stack1 === 0) { data.buffer.push(stack1); } data.buffer.push("</option>"); return buffer; } 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; }), attributeBindings: ['multiple', 'disabled', 'tabindex', 'name'], @@ -30952,13 +31463,13 @@ Ember.runLoadHooks('Ember.Handlebars', Ember.Handlebars); })(); (function() { -define("route-recognizer", - [], - function() { +define("route-recognizer", + ["exports"], + function(__exports__) { "use strict"; var specials = [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\' ]; @@ -30983,15 +31494,15 @@ // * `repeat`: true if the character specification can repeat 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<l; i++) { - char = string.charAt(i); - callback({ validChars: char }); + ch = string.charAt(i); + callback({ validChars: ch }); } }, regex: function() { return this.string.replace(escapeRegex, '\\$1'); @@ -31127,12 +31638,12 @@ // Return the new state return state; }, // Find a list of child states matching the next character - match: function(char) { - // DEBUG "Processing `" + char + "`:" + match: function(ch) { + // DEBUG "Processing `" + ch + "`:" var nextStates = this.nextStates, child, charSpec, chars; // DEBUG " " + debugState(this) var returned = []; @@ -31141,13 +31652,13 @@ child = nextStates[i]; charSpec = child.charSpec; if (typeof (chars = charSpec.validChars) !== 'undefined') { - if (chars.indexOf(char) !== -1) { returned.push(child); } + if (chars.indexOf(ch) !== -1) { returned.push(child); } } else if (typeof (chars = charSpec.invalidChars) !== 'undefined') { - if (chars.indexOf(char) === -1) { returned.push(child); } + if (chars.indexOf(ch) === -1) { returned.push(child); } } } return returned; } @@ -31192,57 +31703,62 @@ return 0; }); } - function recognizeChar(states, char) { + function recognizeChar(states, ch) { var nextStates = []; for (var i=0, l=states.length; i<l; i++) { var state = states[i]; - nextStates = nextStates.concat(state.match(char)); + nextStates = nextStates.concat(state.match(ch)); } return nextStates; } + var oCreate = Object.create || function(proto) { + function F() {} + F.prototype = proto; + return new F(); + }; + + function RecognizeResults(queryParams) { + this.queryParams = queryParams || {}; + } + RecognizeResults.prototype = oCreate({ + splice: Array.prototype.splice, + slice: Array.prototype.slice, + push: Array.prototype.push, + length: 0, + queryParams: null + }); + function findHandler(state, path, queryParams) { var handlers = state.handlers, regex = state.regex; var captures = path.match(regex), currentCapture = 1; - var result = []; + var result = new RecognizeResults(queryParams); for (var i=0, l=handlers.length; i<l; i++) { - var handler = handlers[i], names = handler.names, params = {}, - watchedQueryParams = handler.queryParams || [], - activeQueryParams = {}, - j, m; + var handler = handlers[i], names = handler.names, params = {}; - for (j=0, m=names.length; j<m; j++) { + for (var j=0, m=names.length; j<m; j++) { params[names[j]] = captures[currentCapture++]; } - for (j=0, m=watchedQueryParams.length; j < m; j++) { - var key = watchedQueryParams[j]; - if(queryParams[key]){ - activeQueryParams[key] = queryParams[key]; - } - } - var currentResult = { handler: handler.handler, params: params, isDynamic: !!names.length }; - if(watchedQueryParams && watchedQueryParams.length > 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; } @@ -31284,13 +31800,10 @@ currentState = addSegment(currentState, segment); regex += segment.regex(); } var handler = { handler: route.handler, names: names }; - if(route.queryParams) { - handler.queryParams = route.queryParams; - } handlers.push(handler); } if (isEmpty) { currentState = currentState.put({ validChars: "/" }); @@ -31347,28 +31860,30 @@ return output; }, 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) { + 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); } } if (pairs.length === 0) { return ''; } @@ -31378,12 +31893,32 @@ parseQueryString: function(queryString) { var pairs = queryString.split("&"), queryParams = {}; 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) { @@ -31426,10 +31961,12 @@ return findHandler(state, path, queryParams); } } }; + __exports__["default"] = RouteRecognizer; + function Target(path, matcher, delegate) { this.path = path; this.matcher = matcher; this.delegate = delegate; } @@ -31447,40 +31984,24 @@ if (callback) { if (callback.length === 0) { throw new Error("You must have an argument in the function passed to `to`"); } this.matcher.addChild(this.path, target, callback, this.delegate); } return this; - }, - - withQueryParams: function() { - if (arguments.length === 0) { throw new Error("you must provide arguments to the withQueryParams method"); } - for (var i = 0; i < arguments.length; i++) { - if (typeof arguments[i] !== "string") { - throw new Error('you should call withQueryParams with a list of strings, e.g. withQueryParams("foo", "bar")'); - } - } - var queryParams = [].slice.call(arguments); - this.matcher.addQueryParams(this.path, queryParams); } }; function Matcher(target) { this.routes = {}; this.children = {}; - this.queryParams = {}; this.target = target; } Matcher.prototype = { add: function(path, handler) { this.routes[path] = handler; }, - addQueryParams: function(path, params) { - this.queryParams[path] = params; - }, - addChild: function(path, target, callback, delegate) { var matcher = new Matcher(target); this.children[path] = matcher; var match = generateMatch(path, matcher, delegate); @@ -31503,30 +32024,28 @@ return new Target(startingPath + path, matcher, delegate); } }; } - function addRoute(routeArray, path, handler, queryParams) { + function addRoute(routeArray, path, handler) { var len = 0; for (var i=0, l=routeArray.length; i<l; i++) { len += routeArray[i].path.length; } path = path.substr(len); var route = { path: path, handler: handler }; - if(queryParams) { route.queryParams = queryParams; } routeArray.push(route); } function eachRoute(baseRoute, matcher, callback, binding) { var routes = matcher.routes; - var queryParams = matcher.queryParams; for (var path in routes) { if (routes.hasOwnProperty(path)) { var routeArray = baseRoute.slice(); - addRoute(routeArray, path, routes[path], queryParams[path]); + addRoute(routeArray, path, routes[path]); if (matcher.children[path]) { eachRoute(routeArray, matcher.children[path], callback, binding); } else { callback.call(binding, routeArray); @@ -31543,219 +32062,254 @@ eachRoute([], matcher, function(route) { if (addRouteCallback) { addRouteCallback(this, route); } else { this.add(route); } }, this); }; - return RouteRecognizer; }); })(); (function() { -define("router", - ["route-recognizer","rsvp","exports"], +define("router/handler-info", + ["./utils","rsvp","exports"], function(__dependency1__, __dependency2__, __exports__) { "use strict"; - /** - @private + var bind = __dependency1__.bind; + var merge = __dependency1__.merge; + var oCreate = __dependency1__.oCreate; + var serialize = __dependency1__.serialize; + var resolve = __dependency2__.resolve; - This file references several internal structures: - - ## `RecognizedHandler` - - * `{String} handler`: A handler name - * `{Object} params`: A hash of recognized parameters - - ## `HandlerInfo` - - * `{Boolean} isDynamic`: whether a handler has any dynamic segments - * `{String} name`: the name of a handler - * `{Object} handler`: a handler object - * `{Object} context`: the active context for the handler - */ - - var RouteRecognizer = __dependency1__; - var RSVP = __dependency2__; - - var slice = Array.prototype.slice; - - - - /** - @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, promise) { - this.router = router; - this.promise = promise; - this.data = {}; - this.resolvedModels = {}; - this.providedModels = {}; - this.providedModelsArray = []; - this.sequence = ++Transition.currentSequence; - this.params = {}; + function HandlerInfo(props) { + if (props) { + merge(this, props); + } } - Transition.currentSequence = 0; - - Transition.prototype = { - targetName: null, - urlMethod: 'update', - providedModels: null, - resolvedModels: null, + HandlerInfo.prototype = { + name: null, + handler: null, params: null, - pivotHandler: null, - resolveIndex: 0, - handlerInfos: null, + context: null, - isActive: true, + log: function(payload, message) { + if (payload.log) { + payload.log(this.name + ': ' + message); + } + }, - /** - 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, + resolve: function(async, shouldContinue, payload) { + var checkForAbort = bind(this.checkForAbort, this, shouldContinue), + beforeModel = bind(this.runBeforeModelHook, this, async, payload), + model = bind(this.getModel, this, async, payload), + afterModel = bind(this.runAfterModelHook, this, async, payload), + becomeResolved = bind(this.becomeResolved, this, payload); - /** - 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, + return resolve().then(checkForAbort) + .then(beforeModel) + .then(checkForAbort) + .then(model) + .then(checkForAbort) + .then(afterModel) + .then(checkForAbort) + .then(becomeResolved); + }, - /** - A standard promise hook that resolves if the transition - succeeds and rejects if it fails/redirects/aborts. + runBeforeModelHook: function(async, payload) { + if (payload.trigger) { + payload.trigger(true, 'willResolveModel', payload, this.handler); + } + return this.runSharedModelHook(async, payload, 'beforeModel', []); + }, - 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. + runAfterModelHook: function(async, payload, resolvedModel) { + // Stash the resolved model on the payload. + // This makes it possible for users to swap out + // the resolved model in afterModel. + var name = this.name; + this.stashResolvedModel(payload, resolvedModel); - @param {Function} success - @param {Function} failure - */ - then: function(success, failure) { - return this.promise.then(success, failure); + return this.runSharedModelHook(async, payload, 'afterModel', [resolvedModel]) + .then(function() { + // Ignore the fulfilled value returned from afterModel. + // Return the value stashed in resolvedModels, which + // might have been swapped out in afterModel. + return payload.resolvedModels[name]; + }); }, - /** - 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; - }, + runSharedModelHook: function(async, payload, hookName, args) { + this.log(payload, "calling " + hookName + " hook"); - /** - 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() { - this.abort(); - var recogHandlers = this.router.recognizer.handlersFor(this.targetName), - handlerInfos = generateHandlerInfosWithQueryParams(this.router, recogHandlers, this.queryParams), - newTransition = performTransition(this.router, handlerInfos, this.providedModelsArray, this.params, this.queryParams, this.data); + if (this.queryParams) { + args.push(this.queryParams); + } + args.push(payload); - return newTransition; + var handler = this.handler; + return async(function() { + return handler[hookName] && handler[hookName].apply(handler, args); + }); }, - /** - 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). + getModel: function(payload) { + throw new Error("This should be overridden by a subclass of HandlerInfo"); + }, - @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). + checkForAbort: function(shouldContinue, promiseValue) { + return resolve(shouldContinue()).then(function() { + // We don't care about shouldContinue's resolve value; + // pass along the original value passed to this fn. + return promiseValue; + }); + }, - @return {Transition} this transition - */ - method: function(method) { - this.urlMethod = method; - return this; + stashResolvedModel: function(payload, resolvedModel) { + payload.resolvedModels = payload.resolvedModels || {}; + payload.resolvedModels[this.name] = resolvedModel; }, - /** - 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. + becomeResolved: function(payload, resolvedContext) { + var params = this.params || serialize(this.handler, resolvedContext, this.names); - @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; + if (payload) { + this.stashResolvedModel(payload, resolvedContext); + payload.params = payload.params || {}; + payload.params[this.name] = params; } - trigger(this.router, this.handlerInfos.slice(0, this.resolveIndex + 1), ignoreFailure, args); + + return new ResolvedHandlerInfo({ + context: resolvedContext, + name: this.name, + handler: this.handler, + params: params + }); }, - toString: function() { - return "Transition (sequence " + this.sequence + ")"; + shouldSupercede: function(other) { + // Prefer this newer handlerInfo over `other` if: + // 1) The other one doesn't exist + // 2) The names don't match + // 3) This handler has a context that doesn't match + // the other one (or the other one doesn't have one). + // 4) This handler has parameters that don't match the other. + if (!other) { return true; } + + var contextsMatch = (other.context === this.context); + return other.name !== this.name || + (this.hasOwnProperty('context') && !contextsMatch) || + (this.hasOwnProperty('params') && !paramsMatch(this.params, other.params)); } }; - function Router() { - this.recognizer = new RouteRecognizer(); + function ResolvedHandlerInfo(props) { + HandlerInfo.call(this, props); } - // TODO: separate into module? - Router.Transition = Transition; + ResolvedHandlerInfo.prototype = oCreate(HandlerInfo.prototype); + ResolvedHandlerInfo.prototype.resolve = function() { + // A ResolvedHandlerInfo just resolved with itself. + return resolve(this); + }; - __exports__["default"] = Router; + // These are generated by URL transitions and + // named transitions for non-dynamic route segments. + function UnresolvedHandlerInfoByParam(props) { + HandlerInfo.call(this, props); + this.params = this.params || {}; + } + UnresolvedHandlerInfoByParam.prototype = oCreate(HandlerInfo.prototype); + UnresolvedHandlerInfoByParam.prototype.getModel = function(async, payload) { + var fullParams = this.params; + if (payload && payload.queryParams) { + fullParams = {}; + merge(fullParams, this.params); + fullParams.queryParams = payload.queryParams; + } - /** - Promise reject reasons passed to promise rejection - handlers for failed transitions. - */ - Router.UnrecognizedURLError = function(message) { - this.message = (message || "UnrecognizedURLError"); - this.name = "UnrecognizedURLError"; + var hookName = typeof this.handler.deserialize === 'function' ? + 'deserialize' : 'model'; + + return this.runSharedModelHook(async, payload, hookName, [fullParams]); }; - Router.TransitionAborted = function(message) { - this.message = (message || "TransitionAborted"); - this.name = "TransitionAborted"; + + // These are generated only for named transitions + // with dynamic route segments. + function UnresolvedHandlerInfoByObject(props) { + HandlerInfo.call(this, props); + } + + UnresolvedHandlerInfoByObject.prototype = oCreate(HandlerInfo.prototype); + UnresolvedHandlerInfoByObject.prototype.getModel = function(async, payload) { + this.log(payload, this.name + ": resolving provided model"); + return resolve(this.context); }; - function errorTransition(router, reason) { - return new Transition(router, RSVP.reject(reason)); + function paramsMatch(a, b) { + if ((!a) ^ (!b)) { + // Only one is null. + return false; + } + + if (!a) { + // Both must be null. + return true; + } + + // Note: this assumes that both params have the same + // number of keys, but since we're comparing the + // same handlers, they should. + for (var k in a) { + if (a.hasOwnProperty(k) && a[k] !== b[k]) { + return false; + } + } + return true; } + __exports__.HandlerInfo = HandlerInfo; + __exports__.ResolvedHandlerInfo = ResolvedHandlerInfo; + __exports__.UnresolvedHandlerInfoByParam = UnresolvedHandlerInfoByParam; + __exports__.UnresolvedHandlerInfoByObject = UnresolvedHandlerInfoByObject; + }); +define("router/router", + ["route-recognizer","rsvp","./utils","./transition-state","./transition","./transition-intent/named-transition-intent","./transition-intent/url-transition-intent","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __exports__) { + "use strict"; + var RouteRecognizer = __dependency1__["default"]; + var resolve = __dependency2__.resolve; + var reject = __dependency2__.reject; + var async = __dependency2__.async; + var Promise = __dependency2__.Promise; + var trigger = __dependency3__.trigger; + var log = __dependency3__.log; + var slice = __dependency3__.slice; + var forEach = __dependency3__.forEach; + var merge = __dependency3__.merge; + var serialize = __dependency3__.serialize; + var extractQueryParams = __dependency3__.extractQueryParams; + var getChangelist = __dependency3__.getChangelist; + var TransitionState = __dependency4__.TransitionState; + var logAbort = __dependency5__.logAbort; + var Transition = __dependency5__.Transition; + var TransitionAborted = __dependency5__.TransitionAborted; + var NamedTransitionIntent = __dependency6__.NamedTransitionIntent; + var URLTransitionIntent = __dependency7__.URLTransitionIntent; + var pop = Array.prototype.pop; + + function Router() { + this.recognizer = new RouteRecognizer(); + this.reset(); + } + Router.prototype = { + /** The main entry point into the router. The API is essentially the same as the `map` method in `route-recognizer`. This method extracts the String handler at the last `.to()` @@ -31775,24 +32329,116 @@ hasRoute: function(route) { return this.recognizer.hasRoute(route); }, + // NOTE: this doesn't really belong here, but here + // it shall remain until our ES6 transpiler can + // handle cyclical deps. + transitionByIntent: function(intent, isIntermediate) { + + var wasTransitioning = !!this.activeTransition; + var oldState = wasTransitioning ? this.activeTransition.state : this.state; + var newTransition; + var router = this; + + try { + var newState = intent.applyToState(oldState, this.recognizer, this.getHandler, isIntermediate); + + if (handlerInfosEqual(newState.handlerInfos, oldState.handlerInfos)) { + + // This is a no-op transition. See if query params changed. + var queryParamChangelist = getChangelist(oldState.queryParams, newState.queryParams); + if (queryParamChangelist) { + + // This is a little hacky but we need some way of storing + // changed query params given that no activeTransition + // is guaranteed to have occurred. + this._changedQueryParams = queryParamChangelist.changed; + trigger(this, newState.handlerInfos, true, ['queryParamsDidChange', queryParamChangelist.changed, queryParamChangelist.all, queryParamChangelist.removed]); + this._changedQueryParams = null; + + if (!wasTransitioning && this.activeTransition) { + // One of the handlers in queryParamsDidChange + // caused a transition. Just return that transition. + return this.activeTransition; + } else { + // Running queryParamsDidChange didn't change anything. + // Just update query params and be on our way. + oldState.queryParams = finalizeQueryParamChange(this, newState.handlerInfos, newState.queryParams); + + // We have to return a noop transition that will + // perform a URL update at the end. This gives + // the user the ability to set the url update + // method (default is replaceState). + newTransition = new Transition(this); + newTransition.urlMethod = 'replace'; + newTransition.promise = newTransition.promise.then(function(result) { + updateURL(newTransition, oldState, true); + if (router.didTransition) { + router.didTransition(router.currentHandlerInfos); + } + return result; + }); + return newTransition; + } + } + + // No-op. No need to create a new transition. + return new Transition(this); + } + + if (isIntermediate) { + setupContexts(this, newState); + return; + } + + // Create a new transition to the destination route. + newTransition = new Transition(this, intent, newState); + + // Abort and usurp any previously active transition. + if (this.activeTransition) { + this.activeTransition.abort(); + } + this.activeTransition = newTransition; + + // Transition promises by default resolve with resolved state. + // For our purposes, swap out the promise to resolve + // after the transition has been finalized. + newTransition.promise = newTransition.promise.then(function(result) { + return router.async(function() { + return finalizeTransition(newTransition, result.state); + }); + }); + + if (!wasTransitioning) { + trigger(this, this.state.handlerInfos, true, ['willTransition', newTransition]); + } + + return newTransition; + } catch(e) { + return new Transition(this, intent, null, e); + } + }, + /** Clears the current and target route handlers and triggers exit on each of them starting at the leaf and traversing up through its ancestors. */ reset: function() { - eachHandler(this.currentHandlerInfos || [], function(handlerInfo) { - var handler = handlerInfo.handler; - if (handler.exit) { - handler.exit(); - } - }); + if (this.state) { + forEach(this.state.handlerInfos, function(handlerInfo) { + var handler = handlerInfo.handler; + if (handler.exit) { + handler.exit(); + } + }); + } + + this.state = new TransitionState(); this.currentHandlerInfos = null; - this.targetHandlerInfos = null; }, activeTransition: null, /** @@ -31810,11 +32456,12 @@ handleURL: function(url) { // Perform a URL-based transition, but don't change // the URL afterward, since it already happened. var args = slice.call(arguments); if (url.charAt(0) !== '/') { args[0] = '/' + url; } - return doTransition(this, args).method(null); + + return doTransition(this, args).method('replaceQuery'); }, /** Hook point for updating the URL. @@ -31849,10 +32496,32 @@ intermediateTransitionTo: function(name) { doTransition(this, arguments, true); }, + refresh: function(pivotHandler) { + + + var state = this.activeTransition ? this.activeTransition.state : this.state; + var handlerInfos = state.handlerInfos; + var params = {}; + for (var i = 0, len = handlerInfos.length; i < len; ++i) { + var handlerInfo = handlerInfos[i]; + params[handlerInfo.name] = handlerInfo.params || {}; + } + + log(this, "Starting a refresh transition"); + var intent = new NamedTransitionIntent({ + name: handlerInfos[handlerInfos.length - 1].name, + pivotHandler: pivotHandler || handlerInfos[0].handler, + contexts: [], // TODO collect contexts...? + queryParams: this._changedQueryParams || state.queryParams || {} + }); + + return this.transitionByIntent(intent, false); + }, + /** Identical to `transitionTo` except that the current URL will be replaced if possible. This method is intended primarily for use with `replaceState`. @@ -31862,428 +32531,117 @@ replaceWith: function(name) { return doTransition(this, arguments).method('replace'); }, /** - @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 {String} handlerName - @param {Array[Object]} contexts - @return {Object} a serialized parameter hash - */ - - paramsForHandler: function(handlerName, contexts) { - var partitionedArgs = extractQueryParams(slice.call(arguments, 1)); - return paramsForHandler(this, handlerName, partitionedArgs[0], partitionedArgs[1]); - }, - - /** - 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 {String} handlerName - @return {Array[String]} a list of query parameters - */ - queryParamsForHandler: function (handlerName) { - return queryParamsForHandler(this, handlerName); - }, - - /** Take a named route and context objects and generate a URL. @param {String} name the name of the route to generate a URL for @param {...Object} objects a list of objects to serialize @return {String} a URL */ generate: function(handlerName) { + var partitionedArgs = extractQueryParams(slice.call(arguments, 1)), suppliedParams = partitionedArgs[0], queryParams = partitionedArgs[1]; - var params = paramsForHandler(this, handlerName, suppliedParams, queryParams), - validQueryParams = queryParamsForHandler(this, handlerName); + // Construct a TransitionIntent with the provided params + // and apply it to the present state of the router. + var intent = new NamedTransitionIntent({ name: handlerName, contexts: suppliedParams }); + var state = intent.applyToState(this.state, this.recognizer, this.getHandler); + var params = {}; - var missingParams = []; - - for (var key in queryParams) { - if (queryParams.hasOwnProperty(key) && !~validQueryParams.indexOf(key)) { - missingParams.push(key); - } + 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; - if (missingParams.length > 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); - } - 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]; + var index = 0; + for (len = recogHandlers.length; index < len; ++index) { + handlerInfo = targetHandlerInfos[index]; + if (handlerInfo.name === handlerName) { break; } + } - merge(activeQueryParams, handlerInfo.queryParams); - if (queryParams !== false) { - merge(effectiveQueryParams, handlerInfo.queryParams); - mergeSomeKeys(effectiveQueryParams, queryParams, recogHandler.queryParams); - } + if (index === recogHandlers.length) { + // The provided route name isn't even in the route hierarchy. + return false; + } - if (handlerInfo.isDynamic && contexts.length > 0) { - object = contexts.pop(); + var state = new TransitionState(); + state.handlerInfos = targetHandlerInfos.slice(0, index + 1); + recogHandlers = recogHandlers.slice(0, index + 1); - 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 intent = new NamedTransitionIntent({ + name: targetHandler, + contexts: contexts + }); + var newState = intent.applyToHandlers(state, recogHandlers, this.getHandler, targetHandler, true, true); - return contexts.length === 0 && found && queryParamsEqual(activeQueryParams, effectiveQueryParams); + return handlerInfosEqual(newState.handlerInfos, state.handlerInfos) && + !getChangelist(activeQueryParams, queryParams); }, trigger: function(name) { var args = slice.call(arguments); 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. @param {String} message The message to log. */ 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<handlers.length; i++) { - handlerObj = handlers[i]; - handler = router.getHandler(handlerObj.handler); - names = handlerObj.names; - - // If it's a dynamic segment - if (names.length) { - // If we have objects, use them - if (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 - Takes an Array of `HandlerInfo`s, figures out which ones are exiting, entering, or changing contexts, and calls the proper handler hooks. For example, consider the following tree of handlers. Each handler is @@ -32315,108 +32673,74 @@ and `posts` 2. Triggers the `serialize` callback on `about` 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<l; i++) { - callback(handlerInfos[i]); - } - } - - /** - @private - - determines if two queryparam objects are the same or not - **/ - function queryParamsEqual(a, b) { - a = a || {}; - b = b || {}; - var checkedKeys = [], key; - for(key in a) { - if (!a.hasOwnProperty(key)) { continue; } - if(b[key] !== a[key]) { return false; } - checkedKeys.push(key); - } - for(key in b) { - if (!b.hasOwnProperty(key)) { continue; } - if (~checkedKeys.indexOf(key)) { continue; } - // b has a key not in a - return false; - } return true; } + /** @private This function is called when transitioning from one URL to another to determine which handlers are no longer active, @@ -32454,11 +32778,14 @@ @param {Array[HandlerInfo]} newHandlers a list of the handler information for the new URL @return {Partition} */ - function partitionHandlers(oldHandlers, newHandlers) { + function partitionHandlers(oldState, newState) { + var oldHandlers = oldState.handlerInfos; + var newHandlers = newState.handlerInfos; + var handlers = { updatedContext: [], exited: [], entered: [], unchanged: [] @@ -32469,12 +32796,10 @@ for (i=0, l=newHandlers.length; i<l; i++) { var oldHandler = oldHandlers[i], newHandler = newHandlers[i]; if (!oldHandler || oldHandler.handler !== newHandler.handler) { handlerChanged = true; - } else if (!queryParamsEqual(oldHandler.queryParams, newHandler.queryParams)) { - queryParamsChanged = true; } if (handlerChanged) { handlers.entered.push(newHandler); if (oldHandler) { handlers.exited.unshift(oldHandler); } @@ -32491,482 +32816,906 @@ } return handlers; } - function trigger(router, handlerInfos, ignoreFailure, args) { - if (router.triggerEvent) { - router.triggerEvent(handlerInfos, ignoreFailure, args); + function updateURL(transition, state, inputUrl) { + var urlMethod = transition.urlMethod; + + if (!urlMethod) { return; } - var name = args.shift(); + var router = transition.router, + handlerInfos = state.handlerInfos, + handlerName = handlerInfos[handlerInfos.length - 1].name, + params = {}; - if (!handlerInfos) { - if (ignoreFailure) { return; } - throw new Error("Could not trigger event '" + name + "'. There are no active handlers"); + for (var i = handlerInfos.length - 1; i >= 0; --i) { + var handlerInfo = handlerInfos[i]; + merge(params, handlerInfo.params); + if (handlerInfo.handler.inaccessibleByURL) { + urlMethod = null; + } } - var eventWasHandled = false; + if (urlMethod) { + params.queryParams = state.queryParams; + var url = router.recognizer.generate(handlerName, params); - for (var i=handlerInfos.length-1; i>=0; i--) { - var handlerInfo = handlerInfos[i], - handler = handlerInfo.handler; - - if (handler.events && handler.events[name]) { - if (handler.events[name].apply(handler, args) === true) { - eventWasHandled = true; - } else { - return; + if (urlMethod === 'replaceQuery') { + if (url !== inputUrl) { + router.replaceURL(url); } + } else if (urlMethod === 'replace') { + router.replaceURL(url); + } else { + router.updateURL(url); } } - - if (!eventWasHandled && !ignoreFailure) { - throw new Error("Nothing handled the event '" + name + "'."); - } } - function setContext(handler, context) { - handler.context = context; - if (handler.contextDidChange) { handler.contextDidChange(); } - } + /** + @private - function setQueryParams(handler, queryParams) { - handler.queryParams = queryParams; - if (handler.queryParamsDidChange) { handler.queryParamsDidChange(); } - } + 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."); - /** - @private + var router = transition.router, + handlerInfos = newState.handlerInfos, + seq = transition.sequence; - Extracts query params from the end of an array - **/ + // Run all the necessary enter/setup/exit hooks + setupContexts(router, newState, transition); - function extractQueryParams(array) { - var len = (array && array.length), head, queryParams; + // 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)); + } - 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]; - } - } + updateURL(transition, newState, transition.intent.url); - function performIntermediateTransition(router, recogHandlers, matchPointResults) { + transition.isActive = false; + router.activeTransition = null; - var handlerInfos = generateHandlerInfos(router, recogHandlers); - for (var i = 0; i < handlerInfos.length; ++i) { - var handlerInfo = handlerInfos[i]; - handlerInfo.context = matchPointResults.providedModels[handlerInfo.name]; - } + trigger(router, router.currentHandlerInfos, true, ['didTransition']); - var stubbedTransition = { - router: router, - isAborted: false - }; + if (router.didTransition) { + router.didTransition(router.currentHandlerInfos); + } - setupContexts(stubbedTransition, handlerInfos); + 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 - Creates, begins, and returns a Transition. - */ - function performTransition(router, recogHandlers, providedModelsArray, params, queryParams, data, isIntermediate) { + Begins and returns a Transition based on the provided + arguments. Accepts arguments in the form of both URL + transitions and named transitions. - var matchPointResults = getMatchPoint(router, recogHandlers, providedModelsArray, params, queryParams), - targetName = recogHandlers[recogHandlers.length - 1].handler, - wasTransitioning = false, - currentHandlerInfos = router.currentHandlerInfos; + @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 (isIntermediate) { - return performIntermediateTransition(router, recogHandlers, matchPointResults); + var lastArg = args[args.length-1]; + var queryParams = {}; + if (lastArg && lastArg.hasOwnProperty('queryParams')) { + queryParams = pop.call(args).queryParams; } - // Check if there's already a transition underway. - if (router.activeTransition) { - if (transitionsIdentical(router.activeTransition, targetName, providedModelsArray, queryParams)) { - return router.activeTransition; + 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; } - router.activeTransition.abort(); - wasTransitioning = true; } + return true; + } - var deferred = RSVP.defer(), - transition = new Transition(router, deferred.promise); + 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]); - 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 finalQueryParams = {}; + for (var i = 0, len = finalQueryParamsArray.length; i < len; ++i) { + var qp = finalQueryParamsArray[i]; + finalQueryParams[qp.key] = qp.value; + } + return finalQueryParams; + } - var handlerInfos = generateHandlerInfos(router, recogHandlers); - transition.handlerInfos = handlerInfos; + __exports__.Router = Router; + }); +define("router/transition-intent", + ["./utils","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var merge = __dependency1__.merge; - // 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]); + function TransitionIntent(props) { + if (props) { + merge(this, props); } + this.data = this.data || {}; + } - log(router, transition.sequence, "Beginning validation for transition to " + transition.targetName); - validateEntry(transition, matchPointResults.matchPoint, matchPointResults.handlerParams) - .then(transitionSuccess, transitionFailure); + TransitionIntent.prototype.applyToState = function(oldState) { + // Default TransitionIntent is a no-op. + return oldState; + }; - return transition; + __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 transitionSuccess() { - checkAbort(transition); + function NamedTransitionIntent(props) { + TransitionIntent.call(this, props); + } - try { - finalizeTransition(transition, handlerInfos); + NamedTransitionIntent.prototype = oCreate(TransitionIntent.prototype); + NamedTransitionIntent.prototype.applyToState = function(oldState, recognizer, getHandler, isIntermediate) { - // currentHandlerInfos was updated in finalizeTransition - trigger(router, router.currentHandlerInfos, true, ['didTransition']); + var partitionedArgs = extractQueryParams([this.name].concat(this.contexts)), + pureArgs = partitionedArgs[0], + queryParams = partitionedArgs[1], + handlers = recognizer.handlersFor(pureArgs[0]); - if (router.didTransition) { - router.didTransition(handlerInfos); - } + var targetRouteName = handlers[handlers.length-1].handler; - log(router, transition.sequence, "TRANSITION COMPLETE."); + return this.applyToHandlers(oldState, handlers, getHandler, targetRouteName, isIntermediate); + }; - // Resolve with the final handler. - transition.isActive = false; - deferred.resolve(handlerInfos[handlerInfos.length - 1].handler); - } catch(e) { - deferred.reject(e); - } + NamedTransitionIntent.prototype.applyToHandlers = function(oldState, handlers, getHandler, targetRouteName, isIntermediate, checkingIfActive) { - // Don't nullify if another transition is underway (meaning - // there was a transition initiated with enter/setup). - if (!transition.isAborted) { - router.activeTransition = null; + 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; + } } } - function transitionFailure(reason) { - deferred.reject(reason); - } - } + var pivotHandlerFound = !this.pivotHandler; - /** - @private + for (i = handlers.length - 1; i >= 0; --i) { + var result = handlers[i]; + var name = result.handler; + var handler = getHandler(name); - 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 oldHandlerInfo = oldState.handlerInfos[i]; + var newHandlerInfo = null; - var handlerInfo = { - isDynamic: !!isDynamic, - name: handlerObj.handler, - handler: router.getHandler(handlerObj.handler) - }; - if(handlerObj.queryParams) { - handlerInfo.queryParams = handlerObj.queryParams; + 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); } - handlerInfos.push(handlerInfo); - } - return handlerInfos; - } - /** - @private - */ - function transitionsIdentical(oldTransition, targetName, providedModelsArray, queryParams) { + 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; + } - if (oldTransition.targetName !== targetName) { return false; } + var handlerToUse = oldHandlerInfo; + if (i >= invalidateIndex || newHandlerInfo.shouldSupercede(oldHandlerInfo)) { + invalidateIndex = Math.min(i, invalidateIndex); + handlerToUse = newHandlerInfo; + } - var oldModels = oldTransition.providedModelsArray; - if (oldModels.length !== providedModelsArray.length) { return false; } + if (isIntermediate && !checkingIfActive) { + handlerToUse = handlerToUse.becomeResolved(null, handlerToUse.context); + } - for (var i = 0, len = oldModels.length; i < len; ++i) { - if (oldModels[i] !== providedModelsArray[i]) { return false; } + newState.handlerInfos.unshift(handlerToUse); } - if(!queryParamsEqual(oldTransition.queryParams, queryParams)) { - return false; + if (objects.length > 0) { + throw new Error("More context objects were passed than there are dynamic segments for the route: " + targetRouteName); } - return true; - } + if (!isIntermediate) { + this.invalidateNonDynamicHandlers(newState.handlerInfos, nonDynamicIndexes, invalidateIndex); + } - /** - @private + merge(newState.queryParams, oldState.queryParams); + merge(newState.queryParams, this.queryParams || {}); - Updates the URL (if necessary) and calls `setupContexts` - to update the router's array of `currentHandlerInfos`. - */ - function finalizeTransition(transition, handlerInfos) { + return newState; + }; - log(transition.router, transition.sequence, "Validation succeeded, finalizing transition;"); + 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: {} + }); + } + }); + }; - var router = transition.router, - seq = transition.sequence, - handlerName = handlerInfos[handlerInfos.length - 1].name, - urlMethod = transition.urlMethod, - i; + NamedTransitionIntent.prototype.getHandlerInfoForDynamicSegment = function(name, handler, names, objects, oldHandlerInfo, targetRouteName) { - // 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); + 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; + } - if (handlerInfo.handler.inaccessibleByURL) { - urlMethod = null; + 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); + } } } - var newQueryParams = {}; - for (i = handlerInfos.length - 1; i>=0; --i) { - merge(newQueryParams, handlerInfos[i].queryParams); + 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); } - router.currentQueryParams = newQueryParams; + var statesDiffer = false; - var params = paramsForHandler(router, handlerName, objects, transition.queryParams); + for (i = 0, len = results.length; i < len; ++i) { + var result = results[i]; + var name = result.handler; + var handler = getHandler(name); - router.currentParams = params; + if (handler.inaccessibleByURL) { + throw new UnrecognizedURLError(this.url); + } - if (urlMethod) { - var url = router.recognizer.generate(handlerName, params); + var newHandlerInfo = new UnresolvedHandlerInfoByParam({ + name: name, + handler: handler, + params: result.params + }); - if (urlMethod === 'replace') { - router.replaceURL(url); + var oldHandlerInfo = oldState.handlerInfos[i]; + if (statesDiffer || newHandlerInfo.shouldSupercede(oldHandlerInfo)) { + statesDiffer = true; + newState.handlerInfos[i] = newHandlerInfo; } else { - // Assume everything else is just a URL update for now. - router.updateURL(url); + newState.handlerInfos[i] = oldHandlerInfo; } } - setupContexts(transition, handlerInfos); - } + merge(newState.queryParams, results.queryParams); - /** - @private + return newState; + }; - 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. + /** + Promise reject reasons passed to promise rejection + handlers for failed transitions. */ - function validateEntry(transition, matchPoint, handlerParams) { + function UnrecognizedURLError(message) { + this.message = (message || "UnrecognizedURLError"); + this.name = "UnrecognizedURLError"; + } - var handlerInfos = transition.handlerInfos, - index = transition.resolveIndex; + __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; - if (index === handlerInfos.length) { - // No more contexts to resolve. - return RSVP.resolve(transition.resolvedModels); - } + function TransitionState(other) { + this.handlerInfos = []; + this.queryParams = {}; + this.params = {}; + } - var router = transition.router, - handlerInfo = handlerInfos[index], - handler = handlerInfo.handler, - handlerName = handlerInfo.name, - seq = transition.sequence; + TransitionState.prototype = { + handlerInfos: null, + queryParams: null, + params: null, - if (index < matchPoint) { - log(router, seq, handlerName + ": using context from already-active handler"); + resolve: function(async, shouldContinue, payload) { - // 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(); - } + // 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 || {}; + }); - transition.trigger(true, 'willResolveModel', transition, handler); + payload = payload || {}; + payload.resolveIndex = 0; - return RSVP.resolve().then(handleAbort) - .then(beforeModel) - .then(handleAbort) - .then(model) - .then(handleAbort) - .then(afterModel) - .then(handleAbort) - .then(null, handleError) - .then(proceed); + var currentState = this; + var wasAborted = false; - function handleAbort(result) { - if (transition.isAborted) { - log(transition.router, transition.sequence, "detected abort."); - return RSVP.reject(new Router.TransitionAborted()); + // 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; + }); } - return result; - } + 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 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); + 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); } - // otherwise, we're here because of a different error - transition.abort(); + function resolveOneHandlerInfo() { + if (payload.resolveIndex === currentState.handlerInfos.length) { + // This is is the only possible + // fulfill value of TransitionState#resolve + return { + error: null, + state: currentState + }; + } - log(router, seq, handlerName + ": handling error: " + reason); + var handlerInfo = currentState.handlerInfos[payload.resolveIndex]; - // 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); + return handlerInfo.resolve(async, innerShouldContinue, payload) + .then(proceed); + } + }, - // Propagate the original error. - return RSVP.reject(reason); + getResolvedHandlerInfos: function() { + var resolvedHandlerInfos = []; + var handlerInfos = this.handlerInfos; + for (var i = 0, len = handlerInfos.length; i < len; ++i) { + var handlerInfo = handlerInfos[i]; + if (!(handlerInfo instanceof ResolvedHandlerInfo)) { + break; + } + resolvedHandlerInfos.push(handlerInfo); + } + return resolvedHandlerInfos; } + }; - function beforeModel() { + __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; - log(router, seq, handlerName + ": calling beforeModel hook"); + /** + @private - var args; + 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 (handlerInfo.queryParams) { - args = [handlerInfo.queryParams, transition]; - } else { - args = [transition]; + 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; } - var p = handler.beforeModel && handler.beforeModel.apply(handler, args); - return (p instanceof Transition) ? null : p; + 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 model() { - log(router, seq, handlerName + ": resolving model"); - var p = getModel(handlerInfo, transition, handlerParams[handlerName], index >= matchPoint); - return (p instanceof Transition) ? null : p; + function checkForAbort() { + if (transition.isAborted) { + return reject(); + } } + } - function afterModel(context) { + Transition.currentSequence = 0; - log(router, seq, handlerName + ": calling afterModel hook"); + Transition.prototype = { + targetName: null, + urlMethod: 'update', + intent: null, + params: null, + pivotHandler: null, + resolveIndex: 0, + handlerInfos: null, + resolvedModels: null, + isActive: true, + state: null, - // 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. + /** + @public - transition.resolvedModels[handlerInfo.name] = context; + 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, - var args; + /** + @public - if (handlerInfo.queryParams) { - args = [context, handlerInfo.queryParams, transition]; + 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 { - args = [context, transition]; + // Throw errors on unhandled trigger events by default + ignoreFailure = false; } + trigger(this.router, this.state.handlerInfos.slice(0, this.resolveIndex + 1), ignoreFailure, args); + }, - var p = handler.afterModel && handler.afterModel.apply(handler, args); - return (p instanceof Transition) ? null : p; - } + /** + @public - function proceed() { - log(router, seq, handlerName + ": validation succeeded, proceeding"); + 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. - handlerInfo.context = transition.resolvedModels[handlerInfo.name]; - transition.resolveIndex++; - return validateEntry(transition, matchPoint, handlerParams); + @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 - Throws a TransitionAborted if the provided transition has been aborted. + Logs and returns a TransitionAborted error. */ - function checkAbort(transition) { - if (transition.isAborted) { - log(transition.router, transition.sequence, "detected abort."); - throw new Router.TransitionAborted(); - } + function logAbort(transition) { + log(transition.router, transition.sequence, "detected abort."); + return new TransitionAborted(); } - /** - @private + function TransitionAborted(message) { + this.message = (message || "TransitionAborted"); + this.name = "TransitionAborted"; + } - 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; + __exports__.Transition = Transition; + __exports__.logAbort = logAbort; + __exports__.TransitionAborted = TransitionAborted; + }); +define("router/utils", + ["exports"], + function(__exports__) { + "use strict"; + var slice = Array.prototype.slice; - if (!needsUpdate && handler.hasOwnProperty('context')) { - return handler.context; + function merge(hash, other) { + for (var prop in other) { + if (other.hasOwnProperty(prop)) { hash[prop] = other[prop]; } } + } - if (transition.providedModels.hasOwnProperty(handlerName)) { - var providedModel = transition.providedModels[handlerName]; - return typeof providedModel === 'function' ? providedModel() : providedModel; - } + var oCreate = Object.create || function(proto) { + function F() {} + F.prototype = proto; + return new F(); + }; - if (handlerInfo.queryParams) { - args = [handlerParams || {}, handlerInfo.queryParams, transition]; + /** + @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 { - args = [handlerParams || {}, transition, handlerInfo.queryParams]; + return [array, null]; } - - 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 + function bind(fn, context) { + var boundArgs = arguments; + return function(value) { + var args = slice.call(boundArgs, 2); + args.push(value); + return fn.apply(context, args); + }; + } - Begins and returns a Transition based on the provided - arguments. Accepts arguments in the form of both URL - transitions and named transitions. + function isParam(object) { + return (typeof object === "string" || object instanceof String || typeof object === "number" || object instanceof Number); + } - @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); - } + function forEach(array, callback) { + for (var i=0, l=array.length; i<l && false !== callback(array[i]); i++) { } } /** @private @@ -32978,11 +33727,10 @@ @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; } @@ -33001,12 +33749,100 @@ } else { object[name] = model; } return object; } + + function trigger(router, handlerInfos, ignoreFailure, args) { + if (router.triggerEvent) { + router.triggerEvent(handlerInfos, ignoreFailure, args); + return; + } + + var name = args.shift(); + + if (!handlerInfos) { + if (ignoreFailure) { return; } + throw new Error("Could not trigger event '" + name + "'. There are no active handlers"); + } + + var eventWasHandled = false; + + for (var i=handlerInfos.length-1; i>=0; i--) { + var handlerInfo = handlerInfos[i], + handler = handlerInfo.handler; + + if (handler.events && handler.events[name]) { + if (handler.events[name].apply(handler, args) === true) { + eventWasHandled = true; + } else { + return; + } + } + } + + if (!eventWasHandled && !ignoreFailure) { + throw new Error("Nothing handled the event '" + name + "'."); + } + } + + + function getChangelist(oldObject, newObject) { + var key; + var results = { + all: {}, + changed: {}, + removed: {} + }; + + merge(results.all, newObject); + + var didChange = false; + + // Calculate removals + for (key in oldObject) { + if (oldObject.hasOwnProperty(key)) { + if (!newObject.hasOwnProperty(key)) { + didChange = true; + results.removed[key] = oldObject[key]; + } + } + } + + // Calculate changes + for (key in newObject) { + if (newObject.hasOwnProperty(key)) { + if (oldObject[key] !== newObject[key]) { + results.changed[key] = newObject[key]; + didChange = true; + } + } + } + + return didChange && results; + } + + __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; + }); })(); (function() { @@ -33044,22 +33880,34 @@ } else { this.push(options.path, name, null, options.queryParams); } - }, + if (Ember.FEATURES.isEnabled("ember-routing-named-substates")) { + // For namespace-preserving nested resource (e.g. resource('foo.bar') within + // resource('foo')) we only want to use the last route name segment to determine + // the names of the error/loading substates (e.g. 'bar_loading') + name = name.split('.').pop(); + route(this, name + '_loading'); + route(this, name + '_error', { path: "/_unused_dummy_error_path_route_" + name + "/:error" }); + } + }, push: function(url, name, callback, queryParams) { var parts = name.split('.'); if (url === "" || url === "/" || parts[parts.length-1] === "index") { this.explicitIndex = true; } this.matches.push([url, name, callback, queryParams]); }, route: function(name, options) { route(this, name, options); - }, + if (Ember.FEATURES.isEnabled("ember-routing-named-substates")) { + route(this, name + '_loading'); + route(this, name + '_error', { path: "/_unused_dummy_error_path_route_" + name + "/:error" }); + } + }, generate: function() { var dslMatches = this.matches; if (!this.explicitIndex) { @@ -33068,11 +33916,11 @@ return function(match) { for (var i=0, l=dslMatches.length; i<l; i++) { var dslMatch = dslMatches[i]; var matchObj = match(dslMatch[0]).to(dslMatch[1], dslMatch[2]); - } + } }; } }; function route(dsl, name, options) { @@ -33195,14 +34043,17 @@ /** @module ember @submodule ember-routing */ -var Router = requireModule("router")['default']; -var get = Ember.get, set = Ember.set; +var routerJsModule = requireModule("router"); +var Router = routerJsModule.Router; +var Transition = routerJsModule.Transition; +var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; var defineProperty = Ember.defineProperty; var slice = Array.prototype.slice; +var forEach = Ember.EnumerableUtils.forEach; var DefaultView = Ember._MetamorphView; /** The `Ember.Router` class manages the application state and URLs. Refer to the [routing guide](http://emberjs.com/guides/routing/) for documentation. @@ -33365,17 +34216,10 @@ */ reset: function() { this.router.reset(); }, - willDestroy: function(){ - var location = get(this, 'location'); - location.destroy(); - - this._super.apply(this, arguments); - }, - _lookupActiveView: function(templateName) { var active = this._activeViews[templateName]; return active && active[0]; }, @@ -33394,20 +34238,27 @@ view.one('willDestroyElement', this, disconnect); }, _setupLocation: function() { var location = get(this, 'location'), - rootURL = get(this, 'rootURL'), - options = {}; + rootURL = get(this, 'rootURL'); - if (typeof rootURL === 'string') { - options.rootURL = rootURL; + if ('string' === typeof location && this.container) { + var resolvedLocation = this.container.lookup('location:' + location); + + if ('undefined' !== typeof resolvedLocation) { + location = set(this, 'location', resolvedLocation); + } else { + // Allow for deprecated registration of custom location API's + var options = {implementation: location}; + + location = set(this, 'location', Ember.Location.create(options)); + } } - if ('string' === typeof location) { - options.implementation = location; - location = set(this, 'location', Ember.Location.create(options)); + if (typeof rootURL === 'string') { + location.rootURL = rootURL; } // ensure that initState is called AFTER the rootURL is set on // the location instance if (typeof location.initState === 'function') { location.initState(); } @@ -33473,24 +34324,54 @@ // Normalize blank route to root URL. args = slice.call(args); args[0] = args[0] || '/'; var passedName = args[0], name, self = this, - isQueryParamsOnly = false; + isQueryParamsOnly = false, queryParams; - - if (!isQueryParamsOnly && passedName.charAt(0) === '/') { - name = passedName; - } else if (!isQueryParamsOnly) { + if (Ember.FEATURES.isEnabled("query-params-new")) { + if (args[args.length - 1].hasOwnProperty('queryParams')) { + if (args.length === 1) { + isQueryParamsOnly = true; + } + queryParams = args[args.length - 1].queryParams; + } + } + + if (!isQueryParamsOnly && passedName.charAt(0) !== '/') { if (!this.router.hasRoute(passedName)) { name = args[0] = passedName + '.index'; } else { name = passedName; } } + if (queryParams) { + // router.js expects queryParams to be passed in in + // their final serialized form, so we need to translate. + + if (!name) { + // Need to determine destination route name. + var handlerInfos = this.router.activeTransition ? + this.router.activeTransition.state.handlerInfos : + this.router.state.handlerInfos; + name = handlerInfos[handlerInfos.length - 1].name; + args.unshift(name); + } + + var qpMappings = this._queryParamNamesFor(name); + Ember.Router._translateQueryParams(queryParams, qpMappings.translations, name); + for (var key in queryParams) { + if (key in qpMappings.queryParams) { + var value = queryParams[key]; + delete queryParams[key]; + queryParams[qpMappings.queryParams[key]] = value; + } + } + } + var transitionPromise = this.router[method].apply(this.router, args); transitionPromise.then(null, function(error) { if (error.name === "UnrecognizedURLError") { } @@ -33520,24 +34401,107 @@ _cancelLoadingEvent: function () { if (this._loadingStateTimer) { Ember.run.cancel(this._loadingStateTimer); } this._loadingStateTimer = null; + }, + + _queryParamNamesFor: function(routeName) { + + // TODO: add caching + + routeName = this.router.hasRoute(routeName) ? routeName : routeName + '.index'; + + var handlerInfos = this.router.recognizer.handlersFor(routeName); + var result = { queryParams: Ember.create(null), translations: Ember.create(null) }; + var routerjs = this.router; + forEach(handlerInfos, function(recogHandler) { + var route = routerjs.getHandler(recogHandler.handler); + getQueryParamsForRoute(route, result); + }); + + return result; + }, + + _queryParamNamesForSingle: function(routeName) { + + // TODO: add caching + + var result = { queryParams: Ember.create(null), translations: Ember.create(null) }; + var route = this.router.getHandler(routeName); + + getQueryParamsForRoute(route, result); + + return result; + }, + + /** + @private + + Utility function for fetching all the current query params + values from a controller. + */ + _queryParamOverrides: function(results, queryParams, callback) { + for (var name in queryParams) { + var parts = name.split(':'); + var controller = this.container.lookup('controller:' + parts[0]); + + // Now assign the final URL-serialized key-value pair, + // e.g. "foo[propName]": "value" + results[queryParams[name]] = get(controller, parts[1]); + + if (callback) { + // Give callback a chance to override. + callback(name, queryParams[name], name); + } + } } }); /** + @private + */ +function getQueryParamsForRoute(route, result) { + var controllerName = route.controllerName || route.routeName, + controller = route.controllerFor(controllerName, true); + + if (controller && controller.queryParams) { + forEach(controller.queryParams, function(propName) { + + var parts = propName.split(':'); + + var urlKeyName; + if (parts.length > 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`. Returns true if the last callback fired requested to bubble upward. @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) { var handlerInfo = handlerInfos[i], route = handlerInfo.handler; @@ -33623,11 +34587,18 @@ var router = parentRoute.router, childName, targetChildRouteName = originatingChildRoute.routeName.split('.').pop(), namespace = parentRoute.routeName === 'application' ? '' : parentRoute.routeName + '.'; - + if (Ember.FEATURES.isEnabled("ember-routing-named-substates")) { + // First, try a named loading state, e.g. 'foo_loading' + childName = namespace + targetChildRouteName + '_' + name; + if (routeHasBeenDefined(router, childName)) { + return childName; + } + } + // Second, try general loading state, e.g. 'loading' childName = namespace + name; if (routeHasBeenDefined(router, childName)) { return childName; } @@ -33753,17 +34724,27 @@ path.push.apply(path, nameParts.slice(oldNameParts.length)); } 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; - })(); (function() { @@ -33795,10 +34776,13 @@ @private @method exit */ exit: function() { + if (Ember.FEATURES.isEnabled("query-params-new")) { + this.controller._deactivateQueryParamObservers(); + } this.deactivate(); this.teardownViews(); }, /** @@ -34017,11 +35001,63 @@ @type Hash @default null */ _actions: { finalizeQueryParamChange: function(params, finalParams) { + if (Ember.FEATURES.isEnabled("query-params-new")) { + // In this hook we receive a list of raw URL query + // param changes. We need to take any + + var controller = this.controller; + var changes = controller._queryParamChangesDuringSuspension; + var queryParams = get(controller, '_queryParamHash'); + + // Loop through all the query params that + // this controller knows about. + for (var k in queryParams) { + if (queryParams.hasOwnProperty(k)) { + + // Do a reverse lookup to see if the changed query + // param URL key corresponds to a QP property on + // this controller. + if (queryParams[k] in params) { + // Update this controller property in a way that + // won't fire observers. + controller._finalizingQueryParams = true; + if (!changes || !(k in changes)) { + // Only update the controller if the query param + // value wasn't overriden in setupController. + + // Arrays coming from router.js should be Emberized. + var newValue = params[queryParams[k]]; + newValue = Ember.isArray(newValue) ? Ember.A(newValue) : newValue; + set(controller, k, newValue); + } + controller._finalizingQueryParams = false; + + // Delete from params so that child routes + // don't also try to respond to changes to + // non-fully-qualified query param name changes. + delete params[queryParams[k]]; + } + + // Query params are ordered. This action bubbles up + // the route hierarchy so we unshift so that the final + // order of query params goes from root to leaf. + finalParams.unshift({ + key: queryParams[k], + value: get(controller, k) + }); } + } + + controller._queryParamChangesDuringSuspension = null; + + // Bubble so that parent routes can claim QPs. + return true; + } + } }, /** @deprecated @@ -34117,18 +35153,20 @@ @method transitionTo @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; return router.transitionTo.apply(router, arguments); }, /** - 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). This method is handy for performing intermediate transitions on the @@ -34144,10 +35182,34 @@ var router = this.router; 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. Beside that, it is identical to `transitionTo` in all other respects. See 'transitionTo' for additional information regarding multiple models. @@ -34170,10 +35232,12 @@ @method replaceWith @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; return router.replaceWith.apply(router, arguments); }, @@ -34220,54 +35284,55 @@ This hook is the entry point for router.js @private @method setup */ - setup: function(context, queryParams) { + setup: function(context, transition) { var controllerName = this.controllerName || this.routeName, controller = this.controllerFor(controllerName, true); if (!controller) { controller = this.generateController(controllerName, context); } // Assign the route's controller so that it can more easily be // referenced in action handlers this.controller = controller; - var args = [controller, context]; + if (Ember.FEATURES.isEnabled("query-params-new")) { + // TODO: configurable _queryParamScope + if (controllerName !== 'application') { + this.controller._queryParamScope = controllerName; + } + this.controller._activateQueryParamObservers(); + } - if (this.setupControllers) { this.setupControllers(controller, context); } else { - this.setupController.apply(this, args); + + if (Ember.FEATURES.isEnabled("query-params-new")) { + // Prevent updates to query params in setupController + // from firing another transition. Updating QPs in + // setupController will only affect the final + // generated URL. + controller._finalizingQueryParams = true; + controller._queryParamChangesDuringSuspension = {}; + this.setupController(controller, context, transition); + controller._finalizingQueryParams = false; + } else { + 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 or one of its children. It is called before `model` and `afterModel`, and is appropriate for cases when: @@ -34324,12 +35389,10 @@ // convert the reject into a resolve and the // transition would continue. To propagate the // error so that it'd be handled by the `error` // hook, you would have to either return Ember.RSVP.reject(e); - // or - throw e; }); } } }); ``` @@ -34374,15 +35437,37 @@ @return {Promise} if the value returned from this hook is a promise, the transition will pause until the transition 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. @private @method contextDidChange @@ -34443,10 +35528,12 @@ */ model: function(params, transition) { 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]; } sawParams = true; @@ -34457,11 +35544,24 @@ return this.findModel(name, value); }, /** + @private + Router.js hook. + */ + deserialize: function(params, transition) { + if (Ember.FEATURES.isEnabled("query-params-new")) { + return this.model(this.paramsFor(this.routeName), transition); + } else { + return this.model(params, transition); + } + }, + + /** + @method findModel @param {String} type the model type @param {Object} value the value passed to find */ findModel: function(){ @@ -34594,11 +35694,11 @@ @method setupController @param {Controller} controller instance @param {Object} model */ - setupController: function(controller, context) { + setupController: function(controller, context, transition) { if (controller && (context !== undefined)) { set(controller, 'model', context); } }, @@ -34909,12 +36009,49 @@ delete this.teardownOutletViews; delete this.lastRenderedTemplate; } }); + +if (Ember.FEATURES.isEnabled("query-params-new")) { + Ember.Route.reopen({ + paramsFor: function(name) { + var route = this.container.lookup('route:' + name); + + if (!route) { + return {}; + } + + var transition = this.router.router.activeTransition; + var queryParamsHash = this.router._queryParamNamesForSingle(route.routeName); + + var params, queryParams; + if (transition) { + params = transition.params[name] || {}; + queryParams = transition.queryParams; + } else { + var state = this.router.router.state; + params = state.params[name] || {}; + queryParams = state.queryParams; + } + + this.router._queryParamOverrides(params, queryParamsHash.queryParams, function(name, resultsName, colonized) { + // Replace the controller-supplied value with more up + // to date values (e.g. from an incoming transition). + var value = (resultsName in queryParams) ? + queryParams[resultsName] : params[resultsName]; + delete params[resultsName]; + params[colonized.split(':').pop()] = value; + }); + + return params; + } + }); +} + function parentRoute(route) { - var handlerInfos = route.router.router.targetHandlerInfos; + var handlerInfos = route.router.router.state.handlerInfos; if (!handlerInfos) { return; } var parent, current; @@ -34955,11 +36092,15 @@ } else { controller = route.controllerName || route.routeName; } 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; return options; @@ -35079,13 +36220,20 @@ @module ember @submodule ember-routing */ 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; function fullRouteName(router, name) { var nameWithIndex; @@ -35253,13 +36401,12 @@ this._super.apply(this, arguments); // 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 whenever the params change @private @@ -35295,28 +36442,34 @@ } 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(){ this._super.apply(this, arguments); 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}} @private @method concreteView @@ -35360,11 +36513,11 @@ currentWithIndex = currentWhen + '.index', isActive = router.isActive.apply(router, [currentWhen].concat(contexts)) || 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` CSS `class` to the element when the link is loading. @@ -35418,26 +36571,44 @@ router.transitionTo.apply(router, routeArgs); } }, /** - 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 @return {Array} */ resolvedParams: Ember.computed(function() { var parameters = this.parameters, options = parameters.options, 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 any dynamic segments. @@ -35461,43 +36632,63 @@ // If contexts aren't present, consider the linkView unloaded. return; } } - + if (Ember.FEATURES.isEnabled("query-params-new")) { + resolvedParams.push({ queryParams: get(this, 'queryParams') }); + } + 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]; - return queryParams; - }).property('_potentialQueryParams.[]'), + paramsForRecognizer[resultsName] = value; + }); + return paramsForRecognizer; + }).property('resolvedParams.[]'), + /** Sets the element's `href` attribute to the url for the `LinkView`'s targeted route. If the `LinkView`'s `tagName` is changed to a value other @@ -35788,24 +36979,28 @@ @param [options] {Object} Handlebars key/value pairs of options, you can override any property of Ember.LinkView @return {String} HTML string @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) { var linkTitle = params.shift(); var linkType = options.types.shift(); var context = this; if (linkType === 'ID') { options.linkTextPath = linkTitle; options.fn = function() { - return Ember.Handlebars.getEscaped(context, linkTitle, options); + return Ember.Handlebars.get(context, linkTitle, options); }; } else { options.fn = function() { return linkTitle; }; @@ -35819,10 +37014,21 @@ }; return Ember.Handlebars.helpers.view.call(this, LinkView, options); }); + + if (Ember.FEATURES.isEnabled("query-params-new")) { + Ember.Handlebars.registerHelper('query-params', function queryParamsHelper(options) { + + return QueryParams.create({ + values: options.hash, + types: options.hashTypes + }); + }); + } + /** See [link-to](/api/classes/Ember.Handlebars.helpers.html#method_link-to) @method linkTo @for Ember.Handlebars.helpers @@ -35994,11 +37200,11 @@ ``` ```handelbars <!-- application.hbs --> <h1>My great app</h1> - {{render navigation}} + {{render "navigation"}} ``` ```html <h1>My great app</h1> <div class='ember-view'> @@ -36039,11 +37245,12 @@ @param {Hash} options @return {String} HTML string */ 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; router = container.lookup('router:main'); @@ -36056,10 +37263,11 @@ context = Ember.Handlebars.get(options.contexts[1], contextString, options); } else { throw Ember.Error("You must pass a templateName to render"); } + // # legacy namespace name = name.replace(/\//g, '.'); // \ legacy slash as namespace support @@ -36443,12 +37651,15 @@ /** @module ember @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({ /** Transition the application into another route. The route may be either a single route or route path: @@ -36500,11 +37711,11 @@ return this.transitionToRoute.apply(this, arguments); }, /** 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 aController.replaceRoute('blogPosts'); aController.replaceRoute('blogPosts.recentEntries'); @@ -36550,10 +37761,99 @@ replaceWith: function() { return this.replaceRoute.apply(this, arguments); } }); +if (Ember.FEATURES.isEnabled("query-params-new")) { + Ember.ControllerMixin.reopen({ + + concatenatedProperties: ['queryParams'], + + queryParams: null, + + _queryParamScope: null, + + _finalizingQueryParams: false, + _queryParamHash: Ember.computed(function computeQueryParamHash() { + + // Given: queryParams: ['foo', 'bar:baz'] on controller:thing + // _queryParamHash should yield: { 'foo': 'thing[foo]' } + + var result = {}; + var queryParams = this.queryParams; + if (!queryParams) { + return result; + } + + for (var i = 0, len = queryParams.length; i < len; ++i) { + var full = queryParams[i]; + var parts = full.split(':'); + var key = parts[0]; + var urlKey = parts[1]; + if (!urlKey) { + if (this._queryParamScope) { + urlKey = this._queryParamScope + '[' + key + ']'; + } else { + urlKey = key; + } + } + result[key] = urlKey; + } + + return result; + }), + + _activateQueryParamObservers: function() { + var queryParams = get(this, '_queryParamHash'); + + for (var k in queryParams) { + if (queryParams.hasOwnProperty(k)) { + this.addObserver(k, this, this._queryParamChanged); + this.addObserver(k + '.[]', this, this._queryParamChanged); + } + } + }, + + _deactivateQueryParamObservers: function() { + var queryParams = get(this, '_queryParamHash'); + + for (var k in queryParams) { + if (queryParams.hasOwnProperty(k)) { + this.removeObserver(k, this, this._queryParamChanged); + this.removeObserver(k + '.[]', this, this._queryParamChanged); + } + } + }, + + _queryParamChanged: function(controller, key) { + // Normalize array observer firings. + if (key.substr(-3) === '.[]') { + key = key.substr(0, key.length-3); + } + + if (this._finalizingQueryParams) { + var changes = this._queryParamChangesDuringSuspension; + if (changes) { + changes[key] = true; + } + return; + } + + var queryParams = get(this, '_queryParamHash'); + queuedQueryParamChanges[queryParams[key]] = Ember.copy(get(this, key)); + Ember.run.once(this, this._fireQueryParamTransition); + }, + + _fireQueryParamTransition: function() { + this.transitionToRoute({ queryParams: queuedQueryParamChanges }); + queuedQueryParamChanges = {}; + }, + + _queryParamChangesDuringSuspension: null + }); +} + })(); (function() { @@ -36860,10 +38160,11 @@ @param {Object} implementation of the `location` API @deprecated Register your custom location implementation with the container directly. */ registerImplementation: function(name, implementation) { + this.implementations[name] = implementation; }, implementations: {} }; @@ -36889,10 +38190,11 @@ @class NoneLocation @namespace Ember @extends Ember.Object */ Ember.NoneLocation = Ember.Object.extend({ + implementation: 'none', path: '', /** Returns the current path. @@ -36959,12 +38261,10 @@ // helpers. return url; } }); -Ember.Location.registerImplementation('none', Ember.NoneLocation); - })(); (function() { @@ -36983,10 +38283,11 @@ @class HashLocation @namespace Ember @extends Ember.Object */ Ember.HashLocation = Ember.Object.extend({ + implementation: 'hash', init: function() { set(this, 'location', get(this, 'location') || window.location); }, @@ -36995,11 +38296,23 @@ @private @method getURL */ getURL: function() { - // Default implementation without feature flag enabled + if (Ember.FEATURES.isEnabled("query-params-new")) { + // location.hash is not used because it is inconsistently + // URL-decoded between browsers. + var href = get(this, 'location').href, + hashIndex = href.indexOf('#'); + + if ( hashIndex === -1 ) { + return ""; + } else { + return href.substr(hashIndex + 1); + } + } + // Default implementation without feature flag enabled return get(this, 'location').hash.substr(1); }, /** Set the `location.hash` and remembers what was set. This prevents @@ -37023,10 +38336,11 @@ @method replaceURL @param path {String} */ replaceURL: function(path) { get(this, 'location').replace('#' + path); + set(this, 'lastSetURL', path); }, /** Register a callback to be invoked when the hash changes. These callbacks will execute when the user presses the back or forward @@ -37078,12 +38392,10 @@ Ember.$(window).off('hashchange.ember-location-'+guid); } }); -Ember.Location.registerImplementation('hash', Ember.HashLocation); - })(); (function() { @@ -37103,13 +38415,15 @@ @class HistoryLocation @namespace Ember @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') || ''); }, /** Used to set state on first call to setURL @@ -37137,16 +38451,22 @@ @return url {String} */ 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, ''); - + if (Ember.FEATURES.isEnabled("query-params-new")) { + var search = location.search || ''; + url += search; + } + return url; }, /** Uses `history.pushState` to update the url without a page reload. @@ -37265,17 +38585,21 @@ @method formatURL @param url {String} @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; }, /** Cleans up the HistoryLocation event listener. @@ -37287,12 +38611,10 @@ Ember.$(window).off('popstate.ember-location-'+guid); } }); -Ember.Location.registerImplementation('history', Ember.HistoryLocation); - })(); (function() { @@ -38131,13 +39453,19 @@ Example: ```javascript App.inject(<full_name or type>, <property name>, <full_name>) - 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} @param property {String} @param injectionName {String} @@ -38457,9 +39785,13 @@ container.register('route:basic', Ember.Route, { instantiate: false }); container.register('event_dispatcher:main', Ember.EventDispatcher); 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'); container.injection('route', 'router', 'router:main');