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 ­
-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 ­
- 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');