From cd26fc103e93040f468a13f5d5ea7f75198784e5 Mon Sep 17 00:00:00 2001 From: Piotr Sarnacki Date: Mon, 30 Jul 2012 03:39:26 +0200 Subject: [PATCH] Update ember and ember-data --- assets/javascripts/vendor/ember-data.js | 42 +- assets/javascripts/vendor/ember.js | 779 +++++++++++++++------- public/javascripts/vendor.js | 821 ++++++++++++++++-------- 3 files changed, 1124 insertions(+), 518 deletions(-) diff --git a/assets/javascripts/vendor/ember-data.js b/assets/javascripts/vendor/ember-data.js index 2e2c06aa..d8dee4fa 100644 --- a/assets/javascripts/vendor/ember-data.js +++ b/assets/javascripts/vendor/ember-data.js @@ -248,7 +248,7 @@ DS.ManyArrayStateManager = Ember.StateManager.extend({ (function() { -var get = Ember.get, set = Ember.set, set = Ember.set; +var get = Ember.get, set = Ember.set; DS.ManyArray = DS.RecordArray.extend({ init: function() { @@ -3743,31 +3743,35 @@ DS.FixtureAdapter = DS.Adapter.extend({ find: function(store, type, id) { var fixtures = this.fixturesForType(type); + Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); + if (fixtures) { fixtures = fixtures.findProperty('id', id); } - Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); - - this.simulateRemoteCall(function() { - store.load(type, fixtures); - }, store, type); + if (fixtures) { + this.simulateRemoteCall(function() { + store.load(type, fixtures); + }, store, type); + } }, findMany: function(store, type, ids) { var fixtures = this.fixturesForType(type); + Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); + if (fixtures) { fixtures = fixtures.filter(function(item) { return ids.indexOf(item.id) !== -1; }); } - Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); - - this.simulateRemoteCall(function() { - store.loadMany(type, fixtures); - }, store, type); + if (fixtures) { + this.simulateRemoteCall(function() { + store.loadMany(type, fixtures); + }, store, type); + } }, findAll: function(store, type) { @@ -3783,13 +3787,15 @@ DS.FixtureAdapter = DS.Adapter.extend({ findQuery: function(store, type, query, array) { var fixtures = this.fixturesForType(type); - fixtures = this.queryFixtures(fixtures, query); - Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); - this.simulateRemoteCall(function() { - array.load(fixtures); - }, store, type); + fixtures = this.queryFixtures(fixtures, query); + + if (fixtures) { + this.simulateRemoteCall(function() { + array.load(fixtures); + }, store, type); + } }, createRecord: function(store, type, record) { @@ -4030,7 +4036,9 @@ DS.RESTAdapter = DS.Adapter.extend({ data: query, success: function(json) { this.sideload(store, type, json, plural); - recordArray.load(json[plural]); + setTimeout(function() { + recordArray.load(json[plural]); + }, 10); } }); }, diff --git a/assets/javascripts/vendor/ember.js b/assets/javascripts/vendor/ember.js index 6d633f4e..9a40c062 100644 --- a/assets/javascripts/vendor/ember.js +++ b/assets/javascripts/vendor/ember.js @@ -1,5 +1,5 @@ -// Version: v0.9.8.1-617-g2b213df -// Last commit: 2b213df (2012-07-14 16:44:53 -0700) +// Version: v0.9.8.1-672-gd7e24ac +// Last commit: d7e24ac (2012-07-26 09:35:30 -0700) (function() { @@ -142,8 +142,8 @@ window.ember_deprecateFunc = Ember.deprecateFunc("ember_deprecateFunc is deprec })(); -// Version: v0.9.8.1-658-gd4a9e46 -// Last commit: d4a9e46 (2012-07-21 19:16:48 -0700) +// Version: v0.9.8.1-672-gd7e24ac +// Last commit: d7e24ac (2012-07-26 09:35:30 -0700) (function() { @@ -1406,7 +1406,11 @@ set = function set(obj, keyName, value, tolerant) { if (value !== currentValue) { Ember.propertyWillChange(obj, keyName); if (MANDATORY_SETTER) { - meta.values[keyName] = value; + if (currentValue === undefined && !(keyName in obj)) { + Ember.defineProperty(obj, keyName, null, value); // setup mandatory setter + } else { + meta.values[keyName] = value; + } } else { obj[keyName] = value; } @@ -3481,7 +3485,7 @@ Ember.RunLoop = RunLoop; call. Ember.run(function(){ - // code to be execute within a RunLoop + // code to be execute within a RunLoop }); @name run @@ -3519,7 +3523,7 @@ var run = Ember.run; an lower-level way to use a RunLoop instead of using Ember.run(). Ember.run.begin(); - // code to be execute within a RunLoop + // code to be execute within a RunLoop Ember.run.end(); @@ -3535,7 +3539,7 @@ Ember.run.begin = function() { instead of using Ember.run(). Ember.run.begin(); - // code to be execute within a RunLoop + // code to be execute within a RunLoop Ember.run.end(); @returns {void} @@ -5268,7 +5272,7 @@ Ember.inspect = function(obj) { /** 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 + internal objects. For any other object that implements `isEqual()` it will respect that method. Ember.isEqual('hello', 'hello'); => true @@ -5451,7 +5455,7 @@ Ember.String = { > beta > gamma - @param {String} str + @param {String} str The string to split @returns {String} split string @@ -5460,7 +5464,7 @@ Ember.String = { /** Converts a camelized string into all lower case separated by underscores. - + 'innerHTML'.decamelize() => 'inner_html' 'action_name'.decamelize() => 'action_name' 'css-class-name'.decamelize() => 'css-class-name' @@ -5477,7 +5481,7 @@ Ember.String = { /** Replaces underscores or spaces with dashes. - + 'innerHTML'.dasherize() => 'inner-html' 'action_name'.dasherize() => 'action-name' 'css-class-name'.dasherize() => 'css-class-name' @@ -5644,7 +5648,7 @@ if (Ember.EXTEND_PROTOTYPES) { /** The `property` extension of Javascript's Function prototype is available - when Ember.EXTEND_PROTOTYPES is true, which is the default. + when Ember.EXTEND_PROTOTYPES is true, which is the default. Computed properties allow you to treat a function like a property: @@ -5699,7 +5703,7 @@ if (Ember.EXTEND_PROTOTYPES) { /** The `observes` extension of Javascript's Function prototype is available - when Ember.EXTEND_PROTOTYPES is true, which is the default. + when Ember.EXTEND_PROTOTYPES is true, which is the default. You can observe property changes simply by adding the `observes` call to the end of your method declarations in classes that you write. @@ -5710,7 +5714,7 @@ if (Ember.EXTEND_PROTOTYPES) { // Executes whenever the "value" property changes }.observes('value') }); - + @see Ember.Observable */ Function.prototype.observes = function() { @@ -5720,7 +5724,7 @@ if (Ember.EXTEND_PROTOTYPES) { /** The `observesBefore` extension of Javascript's Function prototype is - available when Ember.EXTEND_PROTOTYPES is true, which is the default. + available when Ember.EXTEND_PROTOTYPES is true, which is the default. You can get notified when a property changes is about to happen by by adding the `observesBefore` call to the end of your method @@ -5731,7 +5735,7 @@ if (Ember.EXTEND_PROTOTYPES) { // Executes whenever the "value" property is about to change }.observesBefore('value') }); - + @see Ember.Observable */ Function.prototype.observesBefore = function() { @@ -7325,7 +7329,7 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, colors.clear(); => [] colors.length(); => 0 - @returns {Ember.Array} An empty Array. + @returns {Ember.Array} An empty Array. */ clear: function () { var len = get(this, 'length'); @@ -7533,15 +7537,15 @@ var get = Ember.get, set = Ember.set, defineProperty = Ember.defineProperty; @class ## Overview - + This mixin provides properties and property observing functionality, core features of the Ember object model. - + Properties and observers allow one object to observe changes to a property on another object. This is one of the fundamental ways that models, controllers and views communicate with each other in an Ember application. - + Any object that has this mixin applied can be used in observer operations. That includes Ember.Object and most objects you will interact with as you write your Ember application. @@ -7549,16 +7553,16 @@ var get = Ember.get, set = Ember.set, defineProperty = Ember.defineProperty; Note that you will not generally apply this mixin to classes yourself, but you will use the features provided by this module frequently, so it is important to understand how to use it. - + ## Using get() and set() - + Because of Ember's support for bindings and observers, you will always access properties using the get method, and set properties using the set method. This allows the observing objects to be notified and computed properties to be handled properly. - + More documentation about `get` and `set` are below. - + ## Observing Property Changes You typically observe property changes simply by adding the `observes` @@ -7570,7 +7574,7 @@ var get = Ember.get, set = Ember.set, defineProperty = Ember.defineProperty; // Executes whenever the "value" property changes }.observes('value') }); - + Although this is the most common way to add an observer, this capability is actually built into the Ember.Object class on top of two methods defined in this mixin: `addObserver` and `removeObserver`. You can use @@ -7583,12 +7587,12 @@ var get = Ember.get, set = Ember.set, defineProperty = Ember.defineProperty; This will call the `targetAction` method on the `targetObject` to be called whenever the value of the `propertyKey` changes. - - Note that if `propertyKey` is a computed property, the observer will be - called when any of the property dependencies are changed, even if the + + Note that if `propertyKey` is a computed property, the observer will be + called when any of the property dependencies are changed, even if the resulting value of the computed property is unchanged. This is necessary because computed properties are not computed until `get` is called. - + @extends Ember.Mixin */ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { @@ -7602,7 +7606,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { This method is usually similar to using object[keyName] or object.keyName, however it supports both computed properties and the unknownProperty handler. - + Because `get` unifies the syntax for accessing all these kinds of properties, it can make many refactorings easier, such as replacing a simple property with a computed property, or vice versa. @@ -7798,11 +7802,11 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { Ember.propertyDidChange(this, keyName); return this; }, - + /** Convenience method to call `propertyWillChange` and `propertyDidChange` in succession. - + @param {String} keyName The property key to be notified about. @returns {Ember.Observable} */ @@ -7894,7 +7898,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { This method will be called when a client attempts to get the value of a property that has not been defined in one of the typical ways. Override this method to create "virtual" properties. - + @param {String} key The name of the unknown property that was requested. @returns {Object} The property value or undefined. Default is undefined. */ @@ -7906,7 +7910,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { This method will be called when a client attempts to set the value of a property that has not been defined in one of the typical ways. Override this method to create "virtual" properties. - + @param {String} key The name of the unknown property to be set. @param {Object} value The value the unknown property is to be set to. */ @@ -7939,9 +7943,9 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { /** Retrieves the value of a property, or a default value in the case that the property returns undefined. - + person.getWithDefault('lastName', 'Doe'); - + @param {String} keyName The name of the property to retrieve @param {Object} defaultValue The value to return if the property value is undefined @returns {Object} The property value or the defaultValue. @@ -7952,10 +7956,10 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { /** Set the value of a property to the current value plus some amount. - + person.incrementProperty('age'); team.incrementProperty('score', 2); - + @param {String} keyName The name of the property to increment @param {Object} increment The amount to increment by. Defaults to 1 @returns {Object} The new property value @@ -7965,13 +7969,13 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { set(this, keyName, (get(this, keyName) || 0)+increment); return get(this, keyName); }, - + /** Set the value of a property to the current value minus some amount. - + player.decrementProperty('lives'); orc.decrementProperty('health', 5); - + @param {String} keyName The name of the property to decrement @param {Object} increment The amount to decrement by. Defaults to 1 @returns {Object} The new property value @@ -7985,9 +7989,9 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { /** Set the value of a boolean property to the opposite of it's current value. - + starship.toggleProperty('warpDriveEnaged'); - + @param {String} keyName The name of the property to toggle @returns {Object} The new property value */ @@ -11377,6 +11381,28 @@ Ember.ControllerMixin.reopen({ set(this, outletName, view); return view; + }, + + /** + Convenience method to connect controllers. This method makes other controllers + available on the controller the method was invoked on. + + For example, to make the `personController` and the `postController` available + on the `overviewController`, you would call: + + overviewController.connectControllers('person', 'post'); + + @param {String...} controllerNames the controllers to make available + */ + connectControllers: function() { + var controllers = get(this, 'controllers'), + controllerNames = Array.prototype.slice.apply(arguments), + controllerName; + + for (var i=0, l=controllerNames.length; i `class` attribute values can also be set by providing a `classNameBindings` property - set to an array of properties names for the view. The return value of these properties + set to an array of properties names for the view. The return value of these properties will be added as part of the value for the view's `class` attribute. These properties can be computed properties: @@ -11508,7 +11534,7 @@ var invokeForState = {
- When using boolean class name bindings you can supply a string value other than the + When using boolean class name bindings you can supply a string value other than the property name for use as the `class` HTML attribute by appending the preferred value after a ":" character when defining the binding: @@ -11586,11 +11612,11 @@ var invokeForState = {
- Updates to the the value of a class name binding will result in automatic update + Updates to the the value of a class name binding will result in automatic update of the HTML `class` attribute in the view's rendered HTML representation. If the value becomes `false` or `undefined` the class name will be removed. - Both `classNames` and `classNameBindings` are concatenated properties. + Both `classNames` and `classNameBindings` are concatenated properties. See `Ember.Object` documentation for more information about concatenated properties. ## HTML Attributes @@ -11636,7 +11662,7 @@ var invokeForState = { }.property() }) - Updates to the the property of an attribute binding will result in automatic update + Updates to the the property of an attribute binding will result in automatic update of the HTML attribute in the view's rendered HTML representation. `attributeBindings` is a concatenated property. See `Ember.Object` documentation @@ -11727,7 +11753,7 @@ var invokeForState = { primary templates, layouts can be any function that accepts an optional context parameter and returns a string of HTML that will be inserted inside view's tag. Views whose HTML element is self closing (e.g. ``) cannot have a layout and this property will be ignored. - + Most typically in Ember a layout will be a compiled Ember.Handlebars template. A view's layout can be set directly with the `layout` property or reference an @@ -11752,7 +11778,7 @@ var invokeForState = { See `Handlebars.helpers.yield` for more information. ## Responding to Browser Events - Views can respond to user-initiated events in one of three ways: method implementation, + Views can respond to user-initiated events in one of three ways: method implementation, through an event manager, and through `{{action}}` helper use in their template or layout. ### Method Implementation @@ -11769,8 +11795,8 @@ var invokeForState = { ### Event Managers Views can define an object as their `eventManager` property. This object can then implement methods that match the desired event names. Matching events that occur - on the view's rendered HTML or the rendered HTML of any of its DOM descendants - will trigger this method. A `jQuery.Event` object will be passed as the first + on the view's rendered HTML or the rendered HTML of any of its DOM descendants + will trigger this method. A `jQuery.Event` object will be passed as the first argument to the method and an `Ember.View` object as the second. The `Ember.View` will be the view whose rendered HTML was interacted with. This may be the view with the `eventManager` property or one of its descendent views. @@ -11804,7 +11830,7 @@ var invokeForState = { Similarly a view's event manager will take precedence for events of any views rendered as a descendent. A method name that matches an event name will not be called - if the view instance was rendered inside the HTML representation of a view that has + if the view instance was rendered inside the HTML representation of a view that has an `eventManager` property defined that handles events of the name. Events not handled by the event manager will still trigger method calls on the descendent. @@ -11826,7 +11852,7 @@ var invokeForState = { // eventManager doesn't handle click events }, mouseEnter: function(event){ - // will never be called if rendered inside + // will never be called if rendered inside // an OuterView. } }) @@ -11847,7 +11873,7 @@ var invokeForState = { Form events: 'submit', 'change', 'focusIn', 'focusOut', 'input' HTML5 drag and drop events: 'dragStart', 'drag', 'dragEnter', 'dragLeave', 'drop', 'dragEnd' - + ## Handlebars `{{view}}` Helper Other `Ember.View` instances can be included as part of a view's template by using the `{{view}}` Handlebars helper. See `Handlebars.helpers.view` for additional information. @@ -14199,6 +14225,7 @@ Ember.ContainerView = Ember.View.extend({ if (currentView) { childViews.removeObject(currentView); + currentView.destroy(); } }, 'currentView'), @@ -14294,7 +14321,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; @class `Ember.CollectionView` is an `Ember.View` descendent responsible for managing a - collection (an array or array-like object) by maintaing a child view object and + collection (an array or array-like object) by maintaing a child view object and associated DOM representation for each item in the array and ensuring that child views and their associated rendered HTML are updated when items in the array are added, removed, or replaced. @@ -14338,7 +14365,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; ## Automatic matching of parent/child tagNames - Setting the `tagName` property of a `CollectionView` to any of + Setting the `tagName` property of a `CollectionView` to any of "ul", "ol", "table", "thead", "tbody", "tfoot", "tr", or "select" will result in the item views receiving an appropriately matched `tagName` property. @@ -14823,7 +14850,14 @@ Ember.State.reopenClass( transitionTo: function(target) { var event = function(stateManager, context) { if (Event && context instanceof Event) { - context = context.context; + if (context.hasOwnProperty('context')) { + context = context.context; + } else { + // If we received an event and it doesn't contain + // a context, don't pass along a superfluous + // context to the target of the event. + return stateManager.transitionTo(target); + } } stateManager.transitionTo(target, context); @@ -14842,6 +14876,167 @@ Ember.State.reopenClass( (function() { var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; var arrayForEach = Ember.ArrayPolyfills.forEach; +/** + @private + + A Transition takes the enter, exit and resolve states and normalizes + them: + + * takes any passed in contexts into consideration + * adds in `initialState`s +*/ +var Transition = function(raw) { + this.enterStates = raw.enterStates.slice(); + this.exitStates = raw.exitStates.slice(); + this.resolveState = raw.resolveState; + + this.finalState = raw.enterStates[raw.enterStates.length - 1] || raw.resolveState; +}; + +Transition.prototype = { + /** + @private + + Normalize the passed in enter, exit and resolve states. + + This process also adds `finalState` and `contexts` to the Transition object. + + @param {Ember.StateManager} manager the state manager running the transition + @param {Array} contexts a list of contexts passed into `transitionTo` + */ + normalize: function(manager, contexts) { + this.matchContextsToStates(contexts); + this.addInitialStates(); + this.removeUnchangedContexts(manager); + return this; + }, + + /** + @private + + Match each of the contexts passed to `transitionTo` to a state. + This process may also require adding additional enter and exit + states if there are more contexts than enter states. + + @param {Array} contexts a list of contexts passed into `transitionTo` + */ + matchContextsToStates: function(contexts) { + var stateIdx = this.enterStates.length - 1, + matchedContexts = [], + state, + context; + + // Next, we will match the passed in contexts to the states they + // represent. + // + // First, assign a context to each enter state in reverse order. If + // any contexts are left, add a parent state to the list of states + // to enter and exit, and assign a context to the parent state. + // + // If there are still contexts left when the state manager is + // reached, raise an exception. + // + // This allows the following: + // + // |- root + // | |- post + // | | |- comments + // | |- about (* current state) + // + // For `transitionTo('post.comments', post, post.get('comments')`, + // the first context (`post`) will be assigned to `root.post`, and + // the second context (`post.get('comments')`) will be assigned + // to `root.post.comments`. + // + // For the following: + // + // |- root + // | |- post + // | | |- index (* current state) + // | | |- comments + // + // For `transitionTo('post.comments', otherPost, otherPost.get('comments')`, + // the `` state will be added to the list of enter and exit + // states because its context has changed. + + while (contexts.length > 0) { + if (stateIdx >= 0) { + state = this.enterStates[stateIdx--]; + } else { + if (this.enterStates.length) { + state = get(this.enterStates[0], 'parentState'); + if (!state) { throw "Cannot match all contexts to states"; } + } else { + // If re-entering the current state with a context, the resolve + // state will be the current state. + state = this.resolveState; + } + + this.enterStates.unshift(state); + this.exitStates.unshift(state); + } + + // in routers, only states with dynamic segments have a context + if (get(state, 'hasContext')) { + context = contexts.pop(); + } else { + context = null; + } + + matchedContexts.unshift(context); + } + + this.contexts = matchedContexts; + }, + + /** + @private + + Add any `initialState`s to the list of enter states. + */ + addInitialStates: function() { + var finalState = this.finalState, initialState; + + while(true) { + initialState = get(finalState, 'initialState') || 'start'; + finalState = get(finalState, 'states.' + initialState); + + if (!finalState) { break; } + + this.finalState = finalState; + this.enterStates.push(finalState); + this.contexts.push(undefined); + } + }, + + /** + @private + + Remove any states that were added because the number of contexts + exceeded the number of explicit enter states, but the context has + not changed since the last time the state was entered. + + @param {Ember.StateManager} manager passed in to look up the last + context for a states + */ + removeUnchangedContexts: function(manager) { + // Start from the beginning of the enter states. If the state was added + // to the list during the context matching phase, make sure the context + // has actually changed since the last time the state was entered. + while (this.enterStates.length > 0) { + if (this.enterStates[0] !== this.exitStates[0]) { break; } + + if (this.enterStates.length === this.contexts.length) { + if (manager.getStateMeta(this.enterStates[0], 'context') !== this.contexts[0]) { break; } + this.contexts.shift(); + } + + this.resolveState = this.enterStates.shift(); + this.exitStates.shift(); + } + } +}; + /** @class @@ -15191,15 +15386,15 @@ var arrayForEach = Ember.ArrayPolyfills.forEach; robotManager.get('currentState.name') // 'rampaging' Transition actions can also be created using the `transitionTo` method of the Ember.State class. The - following example StateManagers are equivalent: - + following example StateManagers are equivalent: + aManager = Ember.StateManager.create({ stateOne: Ember.State.create({ changeToStateTwo: Ember.State.transitionTo('stateTwo') }), stateTwo: Ember.State.create({}) }) - + bManager = Ember.StateManager.create({ stateOne: Ember.State.create({ changeToStateTwo: function(manager, context){ @@ -15280,7 +15475,7 @@ Ember.StateManager = Ember.State.extend( @default true */ errorOnUnhandledEvent: true, - + send: function(event, context) { Ember.assert('Cannot send event "' + event + '" while currentState is ' + get(this, 'currentState'), get(this, 'currentState')); return this.sendRecursively(event, get(this, 'currentState'), context); @@ -15351,18 +15546,29 @@ Ember.StateManager = Ember.State.extend( return possible; }, - findStatesByPath: function(state, path) { + /** + @private + + A state stores its child states in its `states` hash. + This code takes a path like `posts.show` and looks + up `origin.states.posts.states.show`. + + It returns a list of all of the states from the + origin, which is the list of states to call `enter` + on. + */ + findStatesByPath: function(origin, path) { if (!path || path === "") { return undefined; } var r = path.split('.'), ret = []; for (var i=0, len = r.length; i < len; i++) { - var states = get(state, 'states'); + var states = get(origin, 'states'); if (!states) { return undefined; } var s = get(states, r[i]); - if (s) { state = s; ret.push(s); } + if (s) { origin = s; ret.push(s); } else { return undefined; } } @@ -15376,122 +15582,130 @@ Ember.StateManager = Ember.State.extend( }, transitionTo: function(path, context) { - // 1. Normalize arguments - // 2. Ensure that we are in the correct state - // 3. Map provided path to context objects and send - // appropriate transitionEvent events - + // XXX When is transitionTo called with no path if (Ember.empty(path)) { return; } + // The ES6 signature of this function is `path, ...contexts` var contexts = context ? Array.prototype.slice.call(arguments, 1) : [], - currentState = get(this, 'currentState') || this, - resolveState = currentState, - exitStates = [], - matchedContexts = [], - cachedPath, - enterStates, - state, - initialState, - stateIdx, - useContext; + currentState = get(this, 'currentState') || this; - if (!context && (cachedPath = currentState.pathsCacheNoContext[path])) { - // fast path + // First, get the enter, exit and resolve states for the current state + // and specified path. If possible, use an existing cache. + var hash = this.contextFreeTransition(currentState, path); - exitStates = cachedPath.exitStates; - enterStates = cachedPath.enterStates; - resolveState = cachedPath.resolveState; - } else { - // normal path + // Next, process the raw state information for the contexts passed in. + var transition = new Transition(hash).normalize(this, contexts); - if ((cachedPath = currentState.pathsCache[path])) { - // cache hit - - exitStates = cachedPath.exitStates; - enterStates = cachedPath.enterStates; - resolveState = cachedPath.resolveState; - } else { - // cache miss - - enterStates = this.findStatesByPath(currentState, path); - - while (resolveState && !enterStates) { - exitStates.unshift(resolveState); - - resolveState = get(resolveState, 'parentState'); - if (!resolveState) { - enterStates = this.findStatesByPath(this, path); - if (!enterStates) { - Ember.assert('Could not find state for path: "'+path+'"'); - return; - } - } - enterStates = this.findStatesByPath(resolveState, path); - } - - while (enterStates.length > 0 && enterStates[0] === exitStates[0]) { - resolveState = enterStates.shift(); - exitStates.shift(); - } - - currentState.pathsCache[path] = { - exitStates: exitStates, - enterStates: enterStates, - resolveState: resolveState - }; - } - - // Don't modify the cached versions - enterStates = enterStates.slice(); - exitStates = exitStates.slice(); - - stateIdx = enterStates.length-1; - while (contexts.length > 0) { - if (stateIdx >= 0) { - state = enterStates[stateIdx--]; - } else { - state = enterStates[0] ? get(enterStates[0], 'parentState') : resolveState; - if (!state) { throw "Cannot match all contexts to states"; } - enterStates.unshift(state); - exitStates.unshift(state); - } - - useContext = context && get(state, 'hasContext'); - matchedContexts.unshift(useContext ? contexts.pop() : null); - } - - state = enterStates[enterStates.length - 1] || resolveState; - while(true) { - initialState = get(state, 'initialState') || 'start'; - state = get(state, 'states.'+initialState); - if (!state) { break; } - enterStates.push(state); - matchedContexts.push(undefined); - } - - while (enterStates.length > 0) { - if (enterStates[0] !== exitStates[0]) { break; } - - if (enterStates.length === matchedContexts.length) { - if (this.getStateMeta(enterStates[0], 'context') !== matchedContexts[0]) { break; } - matchedContexts.shift(); - } - - resolveState = enterStates.shift(); - exitStates.shift(); - } - } - - this.enterState(exitStates, enterStates, enterStates[enterStates.length-1] || resolveState); - this.triggerSetupContext(enterStates, matchedContexts); + this.enterState(transition); + this.triggerSetupContext(transition); }, - triggerSetupContext: function(enterStates, contexts) { - var offset = enterStates.length - contexts.length; + contextFreeTransition: function(currentState, path) { + var cache = currentState.pathsCache[path]; + if (cache) { return cache; } + + var enterStates = this.findStatesByPath(currentState, path), + exitStates = [], + resolveState = currentState; + + // Walk up the states. For each state, check whether a state matching + // the `path` is nested underneath. This will find the closest + // parent state containing `path`. + // + // This allows the user to pass in a relative path. For example, for + // the following state hierarchy: + // + // | |root + // | |- posts + // | | |- show (* current) + // | |- comments + // | | |- show + // + // If the current state is ``, an attempt to + // transition to `comments.show` will match ``. + // + // First, this code will look for root.posts.show.comments.show. + // Next, it will look for root.posts.comments.show. Finally, + // it will look for `root.comments.show`, and find the state. + // + // After this process, the following variables will exist: + // + // * resolveState: a common parent state between the current + // and target state. In the above example, `` is the + // `resolveState`. + // * enterStates: a list of all of the states represented + // by the path from the `resolveState`. For example, for + // the path `root.comments.show`, `enterStates` would have + // `[, ]` + // * exitStates: a list of all of the states from the + // `resolveState` to the `currentState`. In the above + // example, `exitStates` would have + // `[`, `]`. + while (resolveState && !enterStates) { + exitStates.unshift(resolveState); + + resolveState = get(resolveState, 'parentState'); + if (!resolveState) { + enterStates = this.findStatesByPath(this, path); + if (!enterStates) { + Ember.assert('Could not find state for path: "'+path+'"'); + return; + } + } + enterStates = this.findStatesByPath(resolveState, path); + } + + // If the path contains some states that are parents of both the + // current state and the target state, remove them. + // + // For example, in the following hierarchy: + // + // |- root + // | |- post + // | | |- index (* current) + // | | |- show + // + // If the `path` is `root.post.show`, the three variables will + // be: + // + // * resolveState: `` + // * enterStates: `[, , ]` + // * exitStates: `[, , ]` + // + // The goal of this code is to remove the common states, so we + // have: + // + // * resolveState: `` + // * enterStates: `[]` + // * exitStates: `[]` + // + // This avoid unnecessary calls to the enter and exit transitions. + while (enterStates.length > 0 && enterStates[0] === exitStates[0]) { + resolveState = enterStates.shift(); + exitStates.shift(); + } + + // Cache the enterStates, exitStates, and resolveState for the + // current state and the `path`. + var transitions = currentState.pathsCache[path] = { + exitStates: exitStates, + enterStates: enterStates, + resolveState: resolveState + }; + + return transitions; + }, + + triggerSetupContext: function(transitions) { + var contexts = transitions.contexts, + offset = transitions.enterStates.length - contexts.length, + enterStates = transitions.enterStates, + transitionEvent = get(this, 'transitionEvent'); + Ember.assert("More contexts provided than states", offset >= 0); arrayForEach.call(enterStates, function(state, idx) { - state.trigger(get(this, 'transitionEvent'), this, contexts[idx-offset]); + state.trigger(transitionEvent, this, contexts[idx-offset]); }, this); }, @@ -15506,21 +15720,20 @@ Ember.StateManager = Ember.State.extend( } }, - enterState: function(exitStates, enterStates, state) { - var log = this.enableLogging, - stateManager = this; + enterState: function(transition) { + var log = this.enableLogging; - exitStates = exitStates.slice(0).reverse(); + var exitStates = transition.exitStates.slice(0).reverse(); arrayForEach.call(exitStates, function(state) { - state.trigger('exit', stateManager); - }); + state.trigger('exit', this); + }, this); - arrayForEach.call(enterStates, function(state) { + arrayForEach.call(transition.enterStates, function(state) { if (log) { Ember.Logger.log("STATEMANAGER: Entering " + get(state, 'path')); } - state.trigger('enter', stateManager); - }); + state.trigger('enter', this); + }, this); - set(this, 'currentState', state); + set(this, 'currentState', transition.finalState); } }); @@ -16109,9 +16322,9 @@ var get = Ember.get, set = Ember.set; ## Adding Routes to a Router The `initialState` property of Ember.Router instances is named `root`. The state stored in this - property should be a subclass of Ember.Route. The `root` route should itself have states that are - also subclasses of Ember.Route and have `route` properties describing the URL pattern you would - like to detect. + property must be a subclass of Ember.Route. The `root` route acts as the container for the + set of routable states but is not routable itself. It should have states that are also subclasses + of Ember.Route which each have a `route` property describing the URL pattern you would like to detect. App = Ember.Application.create({ Router: Ember.Router.extend({ @@ -16153,6 +16366,46 @@ var get = Ember.get, set = Ember.set; 'root.bRoute' ('/alphabeta') and transition the router first to the state named 'root' and then to the substate 'bRoute'. + ## Adding Nested Routes to a Router + Routes can contain nested subroutes each with their own `route` property describing the nested + portion of the URL they would like to detect and handle. Router, like all instances of StateManager, + cannot call `transitonTo` with an intermediary state. To avoid transitioning the Router into an + intermediary state when detecting URLs, a Route with nested routes must define both a base `route` + property for itself and a child Route with a `route` property of `'/'` which will be transitioned + to when the base route is detected in the URL: + + Given the following application code: + + App = Ember.Application.create({ + Router: Ember.Router.extend({ + root: Ember.Route.extend({ + aRoute: Ember.Route.extend({ + route: '/theBaseRouteForThisSet', + + indexSubRoute: Ember.Route.extend({ + route: '/', + }), + + subRouteOne: Ember.Route.extend({ + route: '/subroute1 + }), + + subRouteTwo: Ember.Route.extend({ + route: '/subRoute2' + }) + + }) + }) + }) + }); + App.initialize(); + + When the application is loaded at '/theBaseRouteForThisSet' the Router will transition to the route + at path 'root.aRoute' and then transition to state 'indexSubRoute'. + + When the application is loaded at '/theBaseRouteForThisSet/subRoute1' the Router will transition to + the route at path 'root.aRoute' and then transition to state 'subRouteOne'. + ## Route Transition Events Transitioning between Ember.Route instances (including the transition into the detected route when loading the application) triggers the same transition events as state transitions for @@ -16383,7 +16636,7 @@ var get = Ember.get, set = Ember.set; @@ -18528,7 +18781,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ Will result in HTML structure: - @@ -18550,7 +18803,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ }) aView.appendTo('body') - + Will result in HTML structure:
@@ -18624,7 +18877,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ Will result in the following HTML:
-
+
hi
@@ -18784,7 +19037,7 @@ var get = Ember.get, getPath = Ember.Handlebars.getPath, fmt = Ember.String.fmt;

Howdy Mary

Howdy Sara

- + @name Handlebars.helpers.collection @param {String} path @param {Hash} options @@ -19050,27 +19303,39 @@ Ember.Handlebars.registerHelper('template', function(name, options) { (function() { -var EmberHandlebars = Ember.Handlebars, getPath = EmberHandlebars.getPath, get = Ember.get; +var EmberHandlebars = Ember.Handlebars, + getPath = EmberHandlebars.getPath, + get = Ember.get, + a_slice = Array.prototype.slice; var ActionHelper = EmberHandlebars.ActionHelper = { registeredActions: {} }; -ActionHelper.registerAction = function(actionName, eventName, target, view, context, link) { +ActionHelper.registerAction = function(actionName, options) { var actionId = (++Ember.$.uuid).toString(); ActionHelper.registeredActions[actionId] = { - eventName: eventName, + eventName: options.eventName, handler: function(event) { - if (link && (event.button !== 0 || event.shiftKey || event.metaKey || event.altKey || event.ctrlKey)) { + var modifier = event.shiftKey || event.metaKey || event.altKey || event.ctrlKey, + secondaryClick = event.which > 1, // IE9 may return undefined + nonStandard = modifier || secondaryClick; + + if (options.link && nonStandard) { // Allow the browser to handle special link clicks normally return; } event.preventDefault(); - event.view = view; - event.context = context; + event.view = options.view; + + if (options.hasOwnProperty('context')) { + event.context = options.context; + } + + var target = options.target; // Check for StateManager (or compatible object) if (target.isState && typeof target.send === 'function') { @@ -19082,7 +19347,7 @@ ActionHelper.registerAction = function(actionName, eventName, target, view, cont } }; - view.on('willRerender', function() { + options.view.on('willRerender', function() { delete ActionHelper.registeredActions[actionId]; }); @@ -19091,14 +19356,16 @@ ActionHelper.registerAction = function(actionName, eventName, target, view, cont /** The `{{action}}` helper registers an HTML element within a template for - DOM event handling. User interaction with that element will call the method - on the template's associated `Ember.View` instance that has the same name - as the first provided argument to `{{action}}`: + DOM event handling and forwards that interaction to the Application's router, + the template's `Ember.View` instance, or supplied `target` option (see 'Specifiying a Target'). + + User interaction with that element will invoke the supplied action name on + the appropriate target. Given the following Handlebars template on the page @@ -19134,15 +19401,51 @@ ActionHelper.registerAction = function(actionName, eventName, target, view, cont handler. If you need the default handler to trigger you should either register your - own event handler, or use event methods on your view class. + own event handler, or use event methods on your view class. See Ember.View + 'Responding to Browser Events' for more information. - ### Specifying an Action Target + ### Specifying DOM event type - A `target` option can be provided to change which object will receive the - method call. This option must be a string representing a path to an object: + By default the `{{action}}` helper registers for DOM `click` events. You can + supply an `on` option to the helper to specify a different DOM event name: + + See Ember.View 'Responding to Browser Events' for a list of + acceptable DOM event names. + + Because `{{action}}` depends on Ember's event dispatch system it will only + function if an `Ember.EventDispatcher` instance is available. An + `Ember.EventDispatcher` instance will be created when a new + `Ember.Application` is created. Having an instance of `Ember.Application` + will satisfy this requirement. + + + ### Specifying a Target + There are several possible target objects for `{{action}}` helpers: + + In a typical `Ember.Router`-backed Application where views are managed + through use of the `{{outlet}}` helper, actions will be forwarded to the + current state of the Applications's Router. See Ember.Router 'Responding + to User-initiated Events' for more information. + + If you manaully set the `target` property on the controller of a template's + `Ember.View` instance, the specifed `controller.target` will become the target + for any actions. Likely custom values for a controller's `target` are the + controller itself or a StateManager other than the Application's Router. + + If the templates's view lacks a controller property the view itself is the target. + + Finally, a `target` option can be provided to the helper to change which object + will receive the method call. This option must be a string representing a + path to an object: + + @@ -19156,7 +19459,7 @@ ActionHelper.registerAction = function(actionName, eventName, target, view, cont a target: @@ -19173,7 +19476,7 @@ ActionHelper.registerAction = function(actionName, eventName, target, view, cont action name an error will be thrown. @@ -19192,34 +19495,15 @@ ActionHelper.registerAction = function(actionName, eventName, target, view, cont Will throw `Uncaught TypeError: Cannot call method 'call' of undefined` when "click me" is clicked. - ### Specifying DOM event type - - By default the `{{action}}` helper registers for DOM `click` events. You can - supply an `on` option to the helper to specify a different DOM event name: - - - - See `Ember.EventDispatcher` for a list of acceptable DOM event names. - - Because `{{action}}` depends on Ember's event dispatch system it will only - function if an `Ember.EventDispatcher` instance is available. An - `Ember.EventDispatcher` instance will be created when a new - `Ember.Application` is created. Having an instance of `Ember.Application` - will satisfy this requirement. - ### Specifying a context By default the `{{action}}` helper passes the current Handlebars context - along in the `jQuery.Event` object. You may specify an alternative object to + along in the `jQuery.Event` object. You may specify an alternate object to pass as the context by providing a property path: @@ -20482,7 +20735,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ Will result in HTML structure: - @@ -20504,7 +20757,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ }) aView.appendTo('body') - + Will result in HTML structure:
@@ -20578,7 +20831,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ Will result in the following HTML:
-
+
hi
@@ -20738,7 +20991,7 @@ var get = Ember.get, getPath = Ember.Handlebars.getPath, fmt = Ember.String.fmt;

Howdy Mary

Howdy Sara

- + @name Handlebars.helpers.collection @param {String} path @param {Hash} options @@ -21004,27 +21257,39 @@ Ember.Handlebars.registerHelper('template', function(name, options) { (function() { -var EmberHandlebars = Ember.Handlebars, getPath = EmberHandlebars.getPath, get = Ember.get; +var EmberHandlebars = Ember.Handlebars, + getPath = EmberHandlebars.getPath, + get = Ember.get, + a_slice = Array.prototype.slice; var ActionHelper = EmberHandlebars.ActionHelper = { registeredActions: {} }; -ActionHelper.registerAction = function(actionName, eventName, target, view, context, link) { +ActionHelper.registerAction = function(actionName, options) { var actionId = (++Ember.$.uuid).toString(); ActionHelper.registeredActions[actionId] = { - eventName: eventName, + eventName: options.eventName, handler: function(event) { - if (link && (event.button !== 0 || event.shiftKey || event.metaKey || event.altKey || event.ctrlKey)) { + var modifier = event.shiftKey || event.metaKey || event.altKey || event.ctrlKey, + secondaryClick = event.which > 1, // IE9 may return undefined + nonStandard = modifier || secondaryClick; + + if (options.link && nonStandard) { // Allow the browser to handle special link clicks normally return; } event.preventDefault(); - event.view = view; - event.context = context; + event.view = options.view; + + if (options.hasOwnProperty('context')) { + event.context = options.context; + } + + var target = options.target; // Check for StateManager (or compatible object) if (target.isState && typeof target.send === 'function') { @@ -21036,7 +21301,7 @@ ActionHelper.registerAction = function(actionName, eventName, target, view, cont } }; - view.on('willRerender', function() { + options.view.on('willRerender', function() { delete ActionHelper.registeredActions[actionId]; }); @@ -21045,14 +21310,16 @@ ActionHelper.registerAction = function(actionName, eventName, target, view, cont /** The `{{action}}` helper registers an HTML element within a template for - DOM event handling. User interaction with that element will call the method - on the template's associated `Ember.View` instance that has the same name - as the first provided argument to `{{action}}`: + DOM event handling and forwards that interaction to the Application's router, + the template's `Ember.View` instance, or supplied `target` option (see 'Specifiying a Target'). + + User interaction with that element will invoke the supplied action name on + the appropriate target. Given the following Handlebars template on the page @@ -21088,15 +21355,51 @@ ActionHelper.registerAction = function(actionName, eventName, target, view, cont handler. If you need the default handler to trigger you should either register your - own event handler, or use event methods on your view class. + own event handler, or use event methods on your view class. See Ember.View + 'Responding to Browser Events' for more information. - ### Specifying an Action Target + ### Specifying DOM event type - A `target` option can be provided to change which object will receive the - method call. This option must be a string representing a path to an object: + By default the `{{action}}` helper registers for DOM `click` events. You can + supply an `on` option to the helper to specify a different DOM event name: + + See Ember.View 'Responding to Browser Events' for a list of + acceptable DOM event names. + + Because `{{action}}` depends on Ember's event dispatch system it will only + function if an `Ember.EventDispatcher` instance is available. An + `Ember.EventDispatcher` instance will be created when a new + `Ember.Application` is created. Having an instance of `Ember.Application` + will satisfy this requirement. + + + ### Specifying a Target + There are several possible target objects for `{{action}}` helpers: + + In a typical `Ember.Router`-backed Application where views are managed + through use of the `{{outlet}}` helper, actions will be forwarded to the + current state of the Applications's Router. See Ember.Router 'Responding + to User-initiated Events' for more information. + + If you manaully set the `target` property on the controller of a template's + `Ember.View` instance, the specifed `controller.target` will become the target + for any actions. Likely custom values for a controller's `target` are the + controller itself or a StateManager other than the Application's Router. + + If the templates's view lacks a controller property the view itself is the target. + + Finally, a `target` option can be provided to the helper to change which object + will receive the method call. This option must be a string representing a + path to an object: + + @@ -21110,7 +21413,7 @@ ActionHelper.registerAction = function(actionName, eventName, target, view, cont a target: @@ -21127,7 +21430,7 @@ ActionHelper.registerAction = function(actionName, eventName, target, view, cont action name an error will be thrown. @@ -21146,34 +21449,15 @@ ActionHelper.registerAction = function(actionName, eventName, target, view, cont Will throw `Uncaught TypeError: Cannot call method 'call' of undefined` when "click me" is clicked. - ### Specifying DOM event type - - By default the `{{action}}` helper registers for DOM `click` events. You can - supply an `on` option to the helper to specify a different DOM event name: - - - - See `Ember.EventDispatcher` for a list of acceptable DOM event names. - - Because `{{action}}` depends on Ember's event dispatch system it will only - function if an `Ember.EventDispatcher` instance is available. An - `Ember.EventDispatcher` instance will be created when a new - `Ember.Application` is created. Having an instance of `Ember.Application` - will satisfy this requirement. - ### Specifying a context By default the `{{action}}` helper passes the current Handlebars context - along in the `jQuery.Event` object. You may specify an alternative object to + along in the `jQuery.Event` object. You may specify an alternate object to pass as the context by providing a property path: