Comparing version 0.9.8 to 1.0.0-pre
@@ -7,2 +7,3 @@ // | ||
require("./states"); | ||
require("./routing"); | ||
@@ -96,21 +97,21 @@ | ||
Instantiate all controllers currently available on the namespace | ||
and inject them onto a state manager. | ||
and inject them onto a router. | ||
Example: | ||
App.PostsController = Ember.ArrayController.extend(); | ||
App.CommentsController = Ember.ArrayController.extend(); | ||
App.PostsController = Ember.ArrayController.extend(); | ||
App.CommentsController = Ember.ArrayController.extend(); | ||
var stateManager = Ember.StateManager.create({ | ||
... | ||
}); | ||
var router = Ember.Router.create({ | ||
... | ||
}); | ||
App.initialize(stateManager); | ||
App.initialize(router); | ||
stateManager.get('postsController') // <App.PostsController:ember1234> | ||
stateManager.get('commentsController') // <App.CommentsController:ember1235> | ||
router.get('postsController') // <App.PostsController:ember1234> | ||
router.get('commentsController') // <App.CommentsController:ember1235> | ||
stateManager.getPath('postsController.stateManager') // stateManager | ||
router.get('postsController.router') // router | ||
*/ | ||
initialize: function(stateManager) { | ||
initialize: function(router) { | ||
var properties = Ember.A(Ember.keys(this)), | ||
@@ -120,9 +121,30 @@ injections = get(this.constructor, 'injections'), | ||
if (!router && Ember.Router.detect(namespace['Router'])) { | ||
router = namespace['Router'].create(); | ||
this._createdRouter = router; | ||
} | ||
if (router) { | ||
set(this, 'router', router); | ||
// By default, the router's namespace is the current application. | ||
// | ||
// This allows it to find model classes when a state has a | ||
// route like `/posts/:post_id`. In that case, it would first | ||
// convert `post_id` into `Post`, and then look it up on its | ||
// namespace. | ||
set(router, 'namespace', this); | ||
} | ||
Ember.runLoadHooks('application', this); | ||
properties.forEach(function(property) { | ||
injections.forEach(function(injection) { | ||
injection(namespace, stateManager, property); | ||
injections.forEach(function(injection) { | ||
properties.forEach(function(property) { | ||
injection[1](namespace, router, property); | ||
}); | ||
}); | ||
if (router && router instanceof Ember.Router) { | ||
this.startRouting(router); | ||
} | ||
}, | ||
@@ -133,3 +155,2 @@ | ||
var eventDispatcher = get(this, 'eventDispatcher'), | ||
stateManager = get(this, 'stateManager'), | ||
customEvents = get(this, 'customEvents'); | ||
@@ -140,6 +161,2 @@ | ||
this.ready(); | ||
if (stateManager) { | ||
this.setupStateManager(stateManager); | ||
} | ||
}, | ||
@@ -150,12 +167,22 @@ | ||
If the application has a state manager, use it to route | ||
to the current URL, and trigger a new call to `route` | ||
whenever the URL changes. | ||
If the application has a router, use it to route to the current URL, and | ||
trigger a new call to `route` whenever the URL changes. | ||
*/ | ||
setupStateManager: function(stateManager) { | ||
var location = get(stateManager, 'location'); | ||
startRouting: function(router) { | ||
var location = get(router, 'location'), | ||
rootElement = get(this, 'rootElement'), | ||
applicationController = get(router, 'applicationController'); | ||
stateManager.route(location.getURL()); | ||
Ember.assert("ApplicationView and ApplicationController must be defined on your application", (this.ApplicationView && applicationController) ); | ||
var applicationView = this.ApplicationView.create({ | ||
controller: applicationController | ||
}); | ||
this._createdApplicationView = applicationView; | ||
applicationView.appendTo(rootElement); | ||
router.route(location.getURL()); | ||
location.onUpdateURL(function(url) { | ||
stateManager.route(url); | ||
router.route(url); | ||
}); | ||
@@ -171,9 +198,10 @@ }, | ||
/** @private */ | ||
destroy: function() { | ||
willDestroy: function() { | ||
get(this, 'eventDispatcher').destroy(); | ||
return this._super(); | ||
if (this._createdRouter) { this._createdRouter.destroy(); } | ||
if (this._createdApplicationView) { this._createdApplicationView.destroy(); } | ||
}, | ||
registerInjection: function(callback) { | ||
this.constructor.registerInjection(callback); | ||
registerInjection: function(options) { | ||
this.constructor.registerInjection(options); | ||
} | ||
@@ -185,15 +213,38 @@ }); | ||
injections: Ember.A(), | ||
registerInjection: function(callback) { | ||
get(this, 'injections').pushObject(callback); | ||
registerInjection: function(options) { | ||
var injections = get(this, 'injections'), | ||
before = options.before, | ||
name = options.name, | ||
injection = options.injection, | ||
location; | ||
if (before) { | ||
location = injections.find(function(item) { | ||
if (item[0] === before) { return true; } | ||
}); | ||
location = injections.indexOf(location); | ||
} else { | ||
location = get(injections, 'length'); | ||
} | ||
injections.splice(location, 0, [name, injection]); | ||
} | ||
}); | ||
Ember.Application.registerInjection(function(app, stateManager, property) { | ||
if (!/^[A-Z].*Controller$/.test(property)) { return; } | ||
Ember.Application.registerInjection({ | ||
name: 'controllers', | ||
injection: function(app, router, property) { | ||
if (!/^[A-Z].*Controller$/.test(property)) { return; } | ||
var name = property[0].toLowerCase() + property.substr(1), | ||
controller = app[property].create(); | ||
var name = property.charAt(0).toLowerCase() + property.substr(1), | ||
controller = app[property].create(); | ||
stateManager.set(name, controller); | ||
controller.set('target', stateManager); | ||
router.set(name, controller); | ||
controller.setProperties({ | ||
target: router, | ||
controllers: router, | ||
namespace: app | ||
}); | ||
} | ||
}); | ||
@@ -216,15 +267,58 @@ | ||
onUpdateURL(callback): triggers the callback when the URL changes | ||
formatURL(url): formats `url` to be placed into `href` attribute | ||
Calling setURL will not trigger onUpdateURL callbacks. | ||
TODO: This, as well as the Ember.Location documentation below, should | ||
perhaps be moved so that it's visible in the JsDoc output. | ||
*/ | ||
/** | ||
@class | ||
Ember.Location returns an instance of the correct implementation of | ||
the `location` API. | ||
You can pass it a `implementation` ('hash', 'history', 'none') to force a | ||
particular implementation. | ||
*/ | ||
Ember.Location = { | ||
create: function(options) { | ||
var implementation = options && options.implementation; | ||
Ember.assert("Ember.Location.create: you must specify a 'implementation' option", !!implementation); | ||
var implementationClass = this.implementations[implementation]; | ||
Ember.assert("Ember.Location.create: " + implementation + " is not a valid implementation", !!implementationClass); | ||
return implementationClass.create.apply(implementationClass, arguments); | ||
}, | ||
registerImplementation: function(name, implementation) { | ||
this.implementations[name] = implementation; | ||
}, | ||
implementations: {} | ||
}; | ||
})(); | ||
(function() { | ||
var get = Ember.get, set = Ember.set; | ||
/** | ||
@class | ||
Ember.HashLocation implements the location API using the browser's | ||
hash. At present, it relies on a hashchange event existing in the | ||
browser. | ||
@extends Ember.Object | ||
*/ | ||
Ember.HashLocation = Ember.Object.extend({ | ||
Ember.HashLocation = Ember.Object.extend( | ||
/** @scope Ember.HashLocation.prototype */ { | ||
/** @private */ | ||
init: function() { | ||
set(this, 'location', get(this, 'location') || window.location); | ||
set(this, 'callbacks', Ember.A()); | ||
}, | ||
@@ -262,4 +356,5 @@ | ||
var self = this; | ||
var guid = Ember.guidFor(this); | ||
var hashchange = function() { | ||
Ember.$(window).bind('hashchange.ember-location-'+guid, function() { | ||
var path = location.hash.substr(1); | ||
@@ -271,34 +366,138 @@ if (get(self, 'lastSetURL') === path) { return; } | ||
callback(location.hash.substr(1)); | ||
}; | ||
}); | ||
}, | ||
get(this, 'callbacks').pushObject(hashchange); | ||
window.addEventListener('hashchange', hashchange); | ||
/** | ||
@private | ||
Given a URL, formats it to be placed into the page as part | ||
of an element's `href` attribute. | ||
This is used, for example, when using the {{action}} helper | ||
to generate a URL based on an event. | ||
*/ | ||
formatURL: function(url) { | ||
return '#'+url; | ||
}, | ||
/** @private */ | ||
willDestroy: function() { | ||
get(this, 'callbacks').forEach(function(callback) { | ||
window.removeEventListener('hashchange', callback); | ||
}); | ||
set(this, 'callbacks', null); | ||
var guid = Ember.guidFor(this); | ||
Ember.$(window).unbind('hashchange.ember-location-'+guid); | ||
} | ||
}); | ||
Ember.Location.registerImplementation('hash', Ember.HashLocation); | ||
})(); | ||
(function() { | ||
var get = Ember.get, set = Ember.set; | ||
/** | ||
Ember.Location returns an instance of the correct implementation of | ||
the `location` API. | ||
@class | ||
You can pass it a `style` ('hash', 'html5', 'none') to force a | ||
particular implementation. | ||
Ember.HistoryLocation implements the location API using the browser's | ||
history.pushState API. | ||
@extends Ember.Object | ||
*/ | ||
Ember.Location = { | ||
create: function(options) { | ||
var style = options && options.style; | ||
Ember.assert("you must provide a style to Ember.Location.create", !!style); | ||
Ember.HistoryLocation = Ember.Object.extend( | ||
/** @scope Ember.HistoryLocation.prototype */ { | ||
if (style === "hash") { | ||
return Ember.HashLocation.create.apply(Ember.HashLocation, arguments); | ||
/** @private */ | ||
init: function() { | ||
set(this, 'location', get(this, 'location') || window.location); | ||
set(this, '_initialURL', get(this, 'location').pathname); | ||
}, | ||
/** | ||
Will be pre-pended to path upon state change | ||
*/ | ||
rootURL: '/', | ||
/** | ||
@private | ||
Used to give history a starting reference | ||
*/ | ||
_initialURL: null, | ||
/** | ||
@private | ||
Returns the current `location.pathname`. | ||
*/ | ||
getURL: function() { | ||
return get(this, 'location').pathname; | ||
}, | ||
/** | ||
@private | ||
Uses `history.pushState` to update the url without a page reload. | ||
*/ | ||
setURL: function(path) { | ||
var state = window.history.state, | ||
initialURL = get(this, '_initialURL'); | ||
path = this.formatPath(path); | ||
if ((initialURL !== path && !state) || (state && state.path !== path)) { | ||
window.history.pushState({ path: path }, null, path); | ||
} | ||
}, | ||
/** | ||
@private | ||
Register a callback to be invoked whenever the browser | ||
history changes, including using forward and back buttons. | ||
*/ | ||
onUpdateURL: function(callback) { | ||
var guid = Ember.guidFor(this); | ||
Ember.$(window).bind('popstate.ember-location-'+guid, function(e) { | ||
callback(location.pathname); | ||
}); | ||
}, | ||
/** | ||
@private | ||
returns the given path appended to rootURL | ||
*/ | ||
formatPath: function(path) { | ||
var rootURL = get(this, 'rootURL'); | ||
if (path !== '') { | ||
rootURL = rootURL.replace(/\/$/, ''); | ||
} | ||
return rootURL + path; | ||
}, | ||
/** | ||
@private | ||
Used when using {{action}} helper. Since no formatting | ||
is required we just return the url given. | ||
*/ | ||
formatURL: function(url) { | ||
return url; | ||
}, | ||
/** @private */ | ||
willDestroy: function() { | ||
var guid = Ember.guidFor(this); | ||
Ember.$(window).unbind('popstate.ember-location-'+guid); | ||
} | ||
}; | ||
}); | ||
Ember.Location.registerImplementation('history', Ember.HistoryLocation); | ||
})(); | ||
@@ -309,3 +508,40 @@ | ||
(function() { | ||
var get = Ember.get, set = Ember.set; | ||
/** | ||
@class | ||
Ember.NoneLocation does not interact with the browser. It is useful for | ||
testing, or when you need to manage state with your Router, but temporarily | ||
don't want it to muck with the URL (for example when you embed your | ||
application in a larger page). | ||
@extends Ember.Object | ||
*/ | ||
Ember.NoneLocation = Ember.Object.extend( | ||
/** @scope Ember.NoneLocation.prototype */ { | ||
path: '', | ||
getURL: function() { | ||
return get(this, 'path'); | ||
}, | ||
setURL: function(path) { | ||
set(this, 'path', path); | ||
}, | ||
onUpdateURL: function(callback) { | ||
// We are not wired up to the browser, so we'll never trigger the callback. | ||
}, | ||
formatURL: function(url) { | ||
// The return value is not overly meaningful, but we do not want to throw | ||
// errors when test code renders templates containing {{action href=true}} | ||
// helpers. | ||
return url; | ||
} | ||
}); | ||
Ember.Location.registerImplementation('none', Ember.NoneLocation); | ||
})(); | ||
@@ -319,1 +555,7 @@ | ||
(function() { | ||
})(); | ||
54
debug.js
@@ -19,22 +19,18 @@ // | ||
Ember.ENV = 'undefined' === typeof ENV ? {} : ENV; | ||
if (!('MANDATORY_SETTER' in Ember.ENV)) { | ||
Ember.ENV.MANDATORY_SETTER = true; // default to true for debug dist | ||
} | ||
/** | ||
Define an assertion that will throw an exception if the condition is not | ||
met. Ember build tools will remove any calls to Ember.assert() when | ||
doing a production build. | ||
doing a production build. Example: | ||
## Examples | ||
// Test for truthiness | ||
Ember.assert('Must pass a valid object', obj); | ||
// Fail unconditionally | ||
Ember.assert('This code path should never be run') | ||
#js: | ||
// pass a simple Boolean value | ||
Ember.assert('must pass a valid object', !!obj); | ||
// pass a function. If the function returns false the assertion fails | ||
// any other return value (including void) will pass. | ||
Ember.assert('a passed record must have a firstName', function() { | ||
if (obj instanceof Ember.Record) { | ||
return !Ember.empty(obj.firstName); | ||
} | ||
}); | ||
@static | ||
@@ -47,8 +43,6 @@ @function | ||
@param {Boolean} test | ||
Must return true for the assertion to pass. If you pass a function it | ||
will be executed. If the function returns false an exception will be | ||
Must be truthy for the assertion to pass. If falsy, an exception will be | ||
thrown. | ||
*/ | ||
Ember.assert = function(desc, test) { | ||
if ('function' === typeof test) test = test()!==false; | ||
if (!test) throw new Error("assertion failed: "+desc); | ||
@@ -68,9 +62,9 @@ }; | ||
@param {Boolean} test | ||
An optional boolean or function. If the test returns false, the warning | ||
will be displayed. | ||
An optional boolean. If falsy, the warning will be displayed. | ||
*/ | ||
Ember.warn = function(message, test) { | ||
if (arguments.length === 1) { test = false; } | ||
if ('function' === typeof test) test = test()!==false; | ||
if (!test) Ember.Logger.warn("WARNING: "+message); | ||
if (!test) { | ||
Ember.Logger.warn("WARNING: "+message); | ||
if ('trace' in Ember.Logger) Ember.Logger.trace(); | ||
} | ||
}; | ||
@@ -89,4 +83,3 @@ | ||
@param {Boolean} test | ||
An optional boolean or function. If the test returns false, the deprecation | ||
will be displayed. | ||
An optional boolean. If falsy, the deprecation will be displayed. | ||
*/ | ||
@@ -97,3 +90,2 @@ Ember.deprecate = function(message, test) { | ||
if (arguments.length === 1) { test = false; } | ||
if ('function' === typeof test) { test = test()!==false; } | ||
if (test) { return; } | ||
@@ -103,3 +95,3 @@ | ||
var error, stackStr = ''; | ||
var error; | ||
@@ -109,5 +101,4 @@ // When using new Error, we can't do the arguments check for Chrome. Alternatives are welcome | ||
if (error.stack) { | ||
var stack; | ||
if (Ember.LOG_STACKTRACE_ON_DEPRECATION && error.stack) { | ||
var stack, stackStr = ''; | ||
if (error['arguments']) { | ||
@@ -126,5 +117,6 @@ // Chrome | ||
stackStr = "\n " + stack.slice(2).join("\n "); | ||
message = message + stackStr; | ||
} | ||
Ember.Logger.warn("DEPRECATION: "+message+stackStr); | ||
Ember.Logger.warn("DEPRECATION: "+message); | ||
}; | ||
@@ -131,0 +123,0 @@ |
@@ -8,2 +8,3 @@ // | ||
require("./states"); | ||
require("./routing"); | ||
require("./handlebars"); | ||
@@ -10,0 +11,0 @@ require("./application"); |
@@ -7,7 +7,7 @@ { | ||
"author": "Charles Jolley", | ||
"version": "v0.9.8", | ||
"version": "v1.0.0-pre", | ||
"dependencies": { | ||
"handlebars": "~> 1.0.0-beta.6", | ||
"window": "git://github.com/charlesjolley/node-window.git#master", | ||
"jquery": "~> 1.7", | ||
"jquery": "~> 1.7.0", | ||
"convoy": "~0.3" | ||
@@ -14,0 +14,0 @@ }, |
1199
states.js
@@ -9,11 +9,36 @@ // | ||
(function() { | ||
var get = Ember.get, set = Ember.set, getPath = Ember.getPath; | ||
var get = Ember.get, set = Ember.set; | ||
Ember.State = Ember.Object.extend(Ember.Evented, { | ||
/** | ||
@class | ||
@extends Ember.Object | ||
*/ | ||
Ember.State = Ember.Object.extend(Ember.Evented, | ||
/** @scope Ember.State.prototype */{ | ||
isState: true, | ||
/** | ||
A reference to the parent state. | ||
@type Ember.State | ||
*/ | ||
parentState: null, | ||
start: null, | ||
/** | ||
The name of this state. | ||
@type String | ||
*/ | ||
name: null, | ||
/** | ||
The full path to this state. | ||
@type String | ||
@readOnly | ||
*/ | ||
path: Ember.computed(function() { | ||
var parentPath = getPath(this, 'parentState.path'), | ||
var parentPath = get(this, 'parentState.path'), | ||
path = get(this, 'name'); | ||
@@ -34,3 +59,3 @@ | ||
*/ | ||
fire: function(name) { | ||
trigger: function(name) { | ||
if (this[name]) { | ||
@@ -42,7 +67,9 @@ this[name].apply(this, [].slice.call(arguments, 1)); | ||
/** @private */ | ||
init: function() { | ||
var states = get(this, 'states'), foundStates; | ||
set(this, 'childStates', Ember.A()); | ||
set(this, 'eventTransitions', get(this, 'eventTransitions') || {}); | ||
var name; | ||
var name, value, transitionTarget; | ||
@@ -60,3 +87,10 @@ // As a convenience, loop over the properties | ||
if (name === "constructor") { continue; } | ||
this.setupChild(states, name, this[name]); | ||
if (value = this[name]) { | ||
if (transitionTarget = value.transitionTarget) { | ||
this.eventTransitions[name] = transitionTarget; | ||
} | ||
this.setupChild(states, name, value); | ||
} | ||
} | ||
@@ -71,14 +105,16 @@ | ||
set(this, 'routes', {}); | ||
set(this, 'pathsCache', {}); | ||
set(this, 'pathsCacheNoContext', {}); | ||
}, | ||
/** @private */ | ||
setupChild: function(states, name, value) { | ||
if (!value) { return false; } | ||
if (Ember.State.detect(value)) { | ||
if (value.isState) { | ||
set(value, 'name', name); | ||
} else if (Ember.State.detect(value)) { | ||
value = value.create({ | ||
name: name | ||
}); | ||
} else if (value.isState) { | ||
set(value, 'name', name); | ||
} | ||
@@ -93,2 +129,13 @@ | ||
lookupEventTransition: function(name) { | ||
var path, state = this; | ||
while(state && !path) { | ||
path = state.eventTransitions[name]; | ||
state = state.get('parentState'); | ||
} | ||
return path; | ||
}, | ||
/** | ||
@@ -99,3 +146,3 @@ A Boolean value indicating whether the state is a leaf state | ||
@property {Boolean} | ||
@type Boolean | ||
*/ | ||
@@ -106,7 +153,87 @@ isLeaf: Ember.computed(function() { | ||
setupControllers: Ember.K, | ||
/** | ||
A boolean value indicating whether the state takes a context. | ||
By default we assume all states take contexts. | ||
*/ | ||
hasContext: true, | ||
/** | ||
This is the default transition event. | ||
@event | ||
@param {Ember.StateManager} manager | ||
@param context | ||
@see Ember.StateManager#transitionEvent | ||
*/ | ||
setup: Ember.K, | ||
/** | ||
This event fires when the state is entered. | ||
@event | ||
@param {Ember.StateManager} manager | ||
*/ | ||
enter: Ember.K, | ||
/** | ||
This event fires when the state is exited. | ||
@event | ||
@param {Ember.StateManager} manager | ||
*/ | ||
exit: Ember.K | ||
}); | ||
var Event = Ember.$ && Ember.$.Event; | ||
Ember.State.reopenClass( | ||
/** @scope Ember.State */{ | ||
/** | ||
@static | ||
Creates an action function for transitioning to the named state while preserving context. | ||
The 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){ | ||
manager.transitionTo('stateTwo', context) | ||
} | ||
}), | ||
stateTwo: Ember.State.create({}) | ||
}) | ||
@param {String} target | ||
*/ | ||
transitionTo: function(target) { | ||
var event = function(stateManager, context) { | ||
if (Event && context instanceof Event) { | ||
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); | ||
}; | ||
event.transitionTarget = target; | ||
return event; | ||
} | ||
}); | ||
})(); | ||
@@ -117,6 +244,168 @@ | ||
(function() { | ||
var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt; | ||
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 `<root.post>` 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 | ||
StateManager is part of Ember's implementation of a finite state machine. A StateManager | ||
@@ -160,3 +449,3 @@ instance manages a number of properties that are instances of `Ember.State`, | ||
When created a StateManager instance will immediately enter into the state | ||
defined as its `start` property or the state referenced by name in its | ||
defined as its `start` property or the state referenced by name in its | ||
`initialState` property: | ||
@@ -168,3 +457,3 @@ | ||
managerA.getPath('currentState.name') // 'start' | ||
managerA.get('currentState.name') // 'start' | ||
@@ -176,5 +465,5 @@ managerB = Ember.StateManager.create({ | ||
managerB.getPath('currentState.name') // 'beginHere' | ||
managerB.get('currentState.name') // 'beginHere' | ||
Because it is a property you may also provided a computed function if you wish to derive | ||
Because it is a property you may also provide a computed function if you wish to derive | ||
an `initialState` programmatically: | ||
@@ -198,20 +487,20 @@ | ||
Calling `goToState` transitions between states: | ||
Calling `transitionTo` transitions between states: | ||
robotManager = Ember.StateManager.create({ | ||
initialState: 'poweredDown', | ||
poweredDown: Ember.State.create({}), | ||
poweredUp: Ember.State.create({}) | ||
}) | ||
robotManager = Ember.StateManager.create({ | ||
initialState: 'poweredDown', | ||
poweredDown: Ember.State.create({}), | ||
poweredUp: Ember.State.create({}) | ||
}) | ||
robotManager.getPath('currentState.name') // 'poweredDown' | ||
robotManager.goToState('poweredUp') | ||
robotManager.getPath('currentState.name') // 'poweredUp' | ||
robotManager.get('currentState.name') // 'poweredDown' | ||
robotManager.transitionTo('poweredUp') | ||
robotManager.get('currentState.name') // 'poweredUp' | ||
Before transitioning into a new state the existing `currentState` will have its | ||
`exit` method called with the StateManager instance as its first argument and | ||
`exit` method called with the StateManager instance as its first argument and | ||
an object representing the transition as its second argument. | ||
After transitioning into a new state the new `currentState` will have its | ||
`enter` method called with the StateManager instance as its first argument and | ||
`enter` method called with the StateManager instance as its first argument and | ||
an object representing the transition as its second argument. | ||
@@ -222,3 +511,3 @@ | ||
poweredDown: Ember.State.create({ | ||
exit: function(stateManager, transition){ | ||
exit: function(stateManager){ | ||
console.log("exiting the poweredDown state") | ||
@@ -228,3 +517,3 @@ } | ||
poweredUp: Ember.State.create({ | ||
enter: function(stateManager, transition){ | ||
enter: function(stateManager){ | ||
console.log("entering the poweredUp state. Destroy all humans.") | ||
@@ -235,4 +524,4 @@ } | ||
robotManager.getPath('currentState.name') // 'poweredDown' | ||
robotManager.goToState('poweredUp') | ||
robotManager.get('currentState.name') // 'poweredDown' | ||
robotManager.transitionTo('poweredUp') | ||
// will log | ||
@@ -244,3 +533,3 @@ // 'exiting the poweredDown state' | ||
Once a StateManager is already in a state, subsequent attempts to enter that state will | ||
not trigger enter or exit method calls. Attempts to transition into a state that the | ||
not trigger enter or exit method calls. Attempts to transition into a state that the | ||
manager does not have will result in no changes in the StateManager's current state: | ||
@@ -251,3 +540,3 @@ | ||
poweredDown: Ember.State.create({ | ||
exit: function(stateManager, transition){ | ||
exit: function(stateManager){ | ||
console.log("exiting the poweredDown state") | ||
@@ -257,3 +546,3 @@ } | ||
poweredUp: Ember.State.create({ | ||
enter: function(stateManager, transition){ | ||
enter: function(stateManager){ | ||
console.log("entering the poweredUp state. Destroy all humans.") | ||
@@ -264,103 +553,102 @@ } | ||
robotManager.getPath('currentState.name') // 'poweredDown' | ||
robotManager.goToState('poweredUp') | ||
robotManager.get('currentState.name') // 'poweredDown' | ||
robotManager.transitionTo('poweredUp') | ||
// will log | ||
// 'exiting the poweredDown state' | ||
// 'entering the poweredUp state. Destroy all humans.' | ||
robotManager.goToState('poweredUp') // no logging, no state change | ||
robotManager.transitionTo('poweredUp') // no logging, no state change | ||
robotManager.goToState('someUnknownState') // silently fails | ||
robotManager.getPath('currentState.name') // 'poweredUp' | ||
robotManager.transitionTo('someUnknownState') // silently fails | ||
robotManager.get('currentState.name') // 'poweredUp' | ||
Each state property may itself contain properties that are instances of Ember.State. | ||
The StateManager can transition to specific sub-states in a series of goToState method calls or | ||
via a single goToState with the full path to the specific state. The StateManager will also | ||
Each state property may itself contain properties that are instances of Ember.State. | ||
The StateManager can transition to specific sub-states in a series of transitionTo method calls or | ||
via a single transitionTo with the full path to the specific state. The StateManager will also | ||
keep track of the full path to its currentState | ||
robotManager = Ember.StateManager.create({ | ||
initialState: 'poweredDown', | ||
poweredDown: Ember.State.create({ | ||
charging: Ember.State.create(), | ||
charged: Ember.State.create() | ||
}), | ||
poweredUp: Ember.State.create({ | ||
mobile: Ember.State.create(), | ||
stationary: Ember.State.create() | ||
}) | ||
}) | ||
initialState: 'poweredDown', | ||
poweredDown: Ember.State.create({ | ||
charging: Ember.State.create(), | ||
charged: Ember.State.create() | ||
}), | ||
poweredUp: Ember.State.create({ | ||
mobile: Ember.State.create(), | ||
stationary: Ember.State.create() | ||
}) | ||
}) | ||
robotManager.getPath('currentState.name') // 'poweredDown' | ||
robotManager.get('currentState.name') // 'poweredDown' | ||
robotManager.goToState('poweredUp') | ||
robotManager.getPath('currentState.name') // 'poweredUp' | ||
robotManager.transitionTo('poweredUp') | ||
robotManager.get('currentState.name') // 'poweredUp' | ||
robotManager.goToState('mobile') | ||
robotManager.getPath('currentState.name') // 'mobile' | ||
robotManager.transitionTo('mobile') | ||
robotManager.get('currentState.name') // 'mobile' | ||
// transition via a state path | ||
robotManager.goToState('poweredDown.charging') | ||
robotManager.getPath('currentState.name') // 'charging' | ||
// transition via a state path | ||
robotManager.transitionTo('poweredDown.charging') | ||
robotManager.get('currentState.name') // 'charging' | ||
robotManager.getPath('currentState.get.path') // 'poweredDown.charging' | ||
robotManager.get('currentState.path') // 'poweredDown.charging' | ||
Enter transition methods will be called for each state and nested child state in their | ||
hierarchical order. Exit methods will be called for each state and its nested states in | ||
reverse hierarchical order. | ||
Enter transition methods will be called for each state and nested child state in their | ||
hierarchical order. Exit methods will be called for each state and its nested states in | ||
reverse hierarchical order. | ||
Exit transitions for a parent state are not called when entering into one of its child states, | ||
only when transitioning to a new section of possible states in the hierarchy. | ||
Exit transitions for a parent state are not called when entering into one of its child states, | ||
only when transitioning to a new section of possible states in the hierarchy. | ||
robotManager = Ember.StateManager.create({ | ||
initialState: 'poweredDown', | ||
poweredDown: Ember.State.create({ | ||
robotManager = Ember.StateManager.create({ | ||
initialState: 'poweredDown', | ||
poweredDown: Ember.State.create({ | ||
enter: function(){}, | ||
exit: function(){ | ||
console.log("exited poweredDown state") | ||
}, | ||
charging: Ember.State.create({ | ||
enter: function(){}, | ||
exit: function(){ | ||
console.log("exited poweredDown state") | ||
}, | ||
charging: Ember.State.create({ | ||
enter: function(){}, | ||
exit: function(){} | ||
}), | ||
charged: Ember.State.create({ | ||
enter: function(){ | ||
console.log("entered charged state") | ||
}, | ||
exit: function(){ | ||
console.log("exited charged state") | ||
} | ||
}) | ||
exit: function(){} | ||
}), | ||
poweredUp: Ember.State.create({ | ||
charged: Ember.State.create({ | ||
enter: function(){ | ||
console.log("entered poweredUp state") | ||
console.log("entered charged state") | ||
}, | ||
exit: function(){}, | ||
mobile: Ember.State.create({ | ||
enter: function(){ | ||
console.log("entered mobile state") | ||
}, | ||
exit: function(){} | ||
}), | ||
stationary: Ember.State.create({ | ||
enter: function(){}, | ||
exit: function(){} | ||
}) | ||
exit: function(){ | ||
console.log("exited charged state") | ||
} | ||
}) | ||
}), | ||
poweredUp: Ember.State.create({ | ||
enter: function(){ | ||
console.log("entered poweredUp state") | ||
}, | ||
exit: function(){}, | ||
mobile: Ember.State.create({ | ||
enter: function(){ | ||
console.log("entered mobile state") | ||
}, | ||
exit: function(){} | ||
}), | ||
stationary: Ember.State.create({ | ||
enter: function(){}, | ||
exit: function(){} | ||
}) | ||
}) | ||
}) | ||
robotManager.get('currentState.get.path') // 'poweredDown' | ||
robotManager.goToState('charged') | ||
// logs 'entered charged state' | ||
// but does *not* log 'exited poweredDown state' | ||
robotManager.getPath('currentState.name') // 'charged | ||
robotManager.get('currentState.path') // 'poweredDown' | ||
robotManager.transitionTo('charged') | ||
// logs 'entered charged state' | ||
// but does *not* log 'exited poweredDown state' | ||
robotManager.get('currentState.name') // 'charged | ||
robotManager.goToState('poweredUp.mobile') | ||
// logs | ||
// 'exited charged state' | ||
// 'exited poweredDown state' | ||
// 'entered poweredUp state' | ||
// 'entered mobile state' | ||
robotManager.transitionTo('poweredUp.mobile') | ||
// logs | ||
// 'exited charged state' | ||
// 'exited poweredDown state' | ||
// 'entered poweredUp state' | ||
// 'entered mobile state' | ||
@@ -370,5 +658,5 @@ During development you can set a StateManager's `enableLogging` property to `true` to | ||
robotManager = Ember.StateManager.create({ | ||
enableLogging: true | ||
}) | ||
robotManager = Ember.StateManager.create({ | ||
enableLogging: true | ||
}) | ||
@@ -380,3 +668,3 @@ ## Managing currentState with Actions | ||
and moving up through the parent states in a state hierarchy until an appropriate method is found | ||
or the StateManager instance itself is reached. | ||
or the StateManager instance itself is reached. | ||
@@ -398,3 +686,3 @@ If an appropriately named method is found it will be called with the state manager as the first | ||
managerA.getPath('currentState.name') // 'subsubstateOne' | ||
managerA.get('currentState.name') // 'subsubstateOne' | ||
managerA.send('anAction') | ||
@@ -407,7 +695,7 @@ // 'stateOne.substateOne.subsubstateOne' has no anAction method | ||
someObject = {} | ||
managerA.send('anAction', someObject) | ||
// the 'anAction' method of 'stateOne.substateOne' is called again | ||
// with managerA as the first argument and | ||
// someObject as the second argument. | ||
someObject = {} | ||
managerA.send('anAction', someObject) | ||
// the 'anAction' method of 'stateOne.substateOne' is called again | ||
// with managerA as the first argument and | ||
// someObject as the second argument. | ||
@@ -420,65 +708,83 @@ | ||
managerB = Ember.StateManager.create({ | ||
initialState: 'stateOne.substateOne.subsubstateOne', | ||
stateOne: Ember.State.create({ | ||
substateOne: Ember.State.create({ | ||
subsubstateOne: Ember.State.create({}) | ||
}) | ||
}), | ||
stateTwo: Ember.State.create({ | ||
anAction: function(manager, context){ | ||
// will not be called below because it is | ||
// not a parent of the current state | ||
} | ||
managerB = Ember.StateManager.create({ | ||
initialState: 'stateOne.substateOne.subsubstateOne', | ||
stateOne: Ember.State.create({ | ||
substateOne: Ember.State.create({ | ||
subsubstateOne: Ember.State.create({}) | ||
}) | ||
}), | ||
stateTwo: Ember.State.create({ | ||
anAction: function(manager, context){ | ||
// will not be called below because it is | ||
// not a parent of the current state | ||
} | ||
}) | ||
}) | ||
managerB.getPath('currentState.name') // 'subsubstateOne' | ||
managerB.send('anAction') | ||
// Error: <Ember.StateManager:ember132> could not | ||
// respond to event anAction in state stateOne.substateOne.subsubstateOne. | ||
managerB.get('currentState.name') // 'subsubstateOne' | ||
managerB.send('anAction') | ||
// Error: <Ember.StateManager:ember132> could not | ||
// respond to event anAction in state stateOne.substateOne.subsubstateOne. | ||
Inside of an action method the given state should delegate `goToState` calls on its | ||
Inside of an action method the given state should delegate `transitionTo` calls on its | ||
StateManager. | ||
robotManager = Ember.StateManager.create({ | ||
initialState: 'poweredDown.charging', | ||
poweredDown: Ember.State.create({ | ||
charging: Ember.State.create({ | ||
chargeComplete: function(manager, context){ | ||
manager.goToState('charged') | ||
} | ||
}), | ||
charged: Ember.State.create({ | ||
boot: function(manager, context){ | ||
manager.goToState('poweredUp') | ||
} | ||
}) | ||
}), | ||
poweredUp: Ember.State.create({ | ||
beginExtermination: function(manager, context){ | ||
manager.goToState('rampaging') | ||
}, | ||
rampaging: Ember.State.create() | ||
}) | ||
}) | ||
robotManager = Ember.StateManager.create({ | ||
initialState: 'poweredDown.charging', | ||
poweredDown: Ember.State.create({ | ||
charging: Ember.State.create({ | ||
chargeComplete: function(manager, context){ | ||
manager.transitionTo('charged') | ||
} | ||
}), | ||
charged: Ember.State.create({ | ||
boot: function(manager, context){ | ||
manager.transitionTo('poweredUp') | ||
} | ||
}) | ||
}), | ||
poweredUp: Ember.State.create({ | ||
beginExtermination: function(manager, context){ | ||
manager.transitionTo('rampaging') | ||
}, | ||
rampaging: Ember.State.create() | ||
}) | ||
}) | ||
robotManager.getPath('currentState.name') // 'charging' | ||
robotManager.send('boot') // throws error, no boot action | ||
// in current hierarchy | ||
robotManager.getPath('currentState.name') // remains 'charging' | ||
robotManager.get('currentState.name') // 'charging' | ||
robotManager.send('boot') // throws error, no boot action | ||
// in current hierarchy | ||
robotManager.get('currentState.name') // remains 'charging' | ||
robotManager.send('beginExtermination') // throws error, no beginExtermination | ||
// action in current hierarchy | ||
robotManager.getPath('currentState.name') // remains 'charging' | ||
robotManager.send('beginExtermination') // throws error, no beginExtermination | ||
// action in current hierarchy | ||
robotManager.get('currentState.name') // remains 'charging' | ||
robotManager.send('chargeComplete') | ||
robotManager.getPath('currentState.name') // 'charged' | ||
robotManager.send('chargeComplete') | ||
robotManager.get('currentState.name') // 'charged' | ||
robotManager.send('boot') | ||
robotManager.getPath('currentState.name') // 'poweredUp' | ||
robotManager.send('boot') | ||
robotManager.get('currentState.name') // 'poweredUp' | ||
robotManager.send('beginExtermination', allHumans) | ||
robotManager.getPath('currentState.name') // 'rampaging' | ||
robotManager.send('beginExtermination', allHumans) | ||
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: | ||
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){ | ||
manager.transitionTo('stateTwo', context) | ||
} | ||
}), | ||
stateTwo: Ember.State.create({}) | ||
}) | ||
**/ | ||
@@ -500,3 +806,3 @@ Ember.StateManager = Ember.State.extend( | ||
if (!initialState && getPath(this, 'states.start')) { | ||
if (!initialState && get(this, 'states.start')) { | ||
initialState = 'start'; | ||
@@ -506,9 +812,45 @@ } | ||
if (initialState) { | ||
this.goToState(initialState); | ||
this.transitionTo(initialState); | ||
Ember.assert('Failed to transition to initial state "' + initialState + '"', !!get(this, 'currentState')); | ||
} | ||
}, | ||
stateMetaFor: function(state) { | ||
var meta = get(this, 'stateMeta'), | ||
stateMeta = meta.get(state); | ||
if (!stateMeta) { | ||
stateMeta = {}; | ||
meta.set(state, stateMeta); | ||
} | ||
return stateMeta; | ||
}, | ||
setStateMeta: function(state, key, value) { | ||
return set(this.stateMetaFor(state), key, value); | ||
}, | ||
getStateMeta: function(state, key) { | ||
return get(this.stateMetaFor(state), key); | ||
}, | ||
/** | ||
The current state from among the manager's possible states. This property should | ||
not be set directly. Use `transitionTo` to move between states by name. | ||
@type Ember.State | ||
@readOnly | ||
*/ | ||
currentState: null, | ||
/** | ||
The name of transitionEvent that this stateManager will dispatch | ||
@property {String} | ||
@default 'setup' | ||
*/ | ||
transitionEvent: 'setup', | ||
/** | ||
If set to true, `errorOnUnhandledEvents` will cause an exception to be | ||
@@ -518,3 +860,4 @@ raised if you attempt to send an event to a state manager that is not | ||
@property {Boolean} | ||
@type Boolean | ||
@default true | ||
*/ | ||
@@ -524,10 +867,10 @@ errorOnUnhandledEvent: true, | ||
send: function(event, context) { | ||
this.sendRecursively(event, get(this, 'currentState'), context); | ||
Ember.assert('Cannot send event "' + event + '" while currentState is ' + get(this, 'currentState'), get(this, 'currentState')); | ||
return this.sendRecursively(event, get(this, 'currentState'), context); | ||
}, | ||
sendRecursively: function(event, currentState, context) { | ||
var log = this.enableLogging; | ||
var log = this.enableLogging, | ||
action = currentState[event]; | ||
var action = currentState[event]; | ||
// Test to see if the action is a method that | ||
@@ -541,9 +884,9 @@ // can be invoked. Don't blindly check just for | ||
if (log) { Ember.Logger.log(fmt("STATEMANAGER: Sending event '%@' to state %@.", [event, get(currentState, 'path')])); } | ||
action.call(currentState, this, context); | ||
return action.call(currentState, this, context); | ||
} else { | ||
var parentState = get(currentState, 'parentState'); | ||
if (parentState) { | ||
this.sendRecursively(event, parentState, context); | ||
return this.sendRecursively(event, parentState, context); | ||
} else if (get(this, 'errorOnUnhandledEvent')) { | ||
throw new Ember.Error(this.toString() + " could not respond to event " + event + " in state " + getPath(this, 'currentState.path') + "."); | ||
throw new Ember.Error(this.toString() + " could not respond to event " + event + " in state " + get(this, 'currentState.path') + "."); | ||
} | ||
@@ -553,118 +896,205 @@ } | ||
findStatesByRoute: function(state, route) { | ||
if (!route || route === "") { return undefined; } | ||
var r = route.split('.'), ret = []; | ||
/** | ||
Finds a state by its state path. | ||
for (var i=0, len = r.length; i < len; i += 1) { | ||
var states = get(state, 'states') ; | ||
Example: | ||
if (!states) { return undefined; } | ||
manager = Ember.StateManager.create({ | ||
root: Ember.State.create({ | ||
dashboard: Ember.State.create() | ||
}) | ||
}); | ||
var s = get(states, r[i]); | ||
if (s) { state = s; ret.push(s); } | ||
else { return undefined; } | ||
manager.getStateByPath(manager, "root.dashboard") | ||
// returns the dashboard state | ||
@param {Ember.State} root the state to start searching from | ||
@param {String} path the state path to follow | ||
@returns {Ember.State} the state at the end of the path | ||
*/ | ||
getStateByPath: function(root, path) { | ||
var parts = path.split('.'), | ||
state = root; | ||
for (var i=0, l=parts.length; i<l; i++) { | ||
state = get(get(state, 'states'), parts[i]); | ||
if (!state) { break; } | ||
} | ||
return ret; | ||
return state; | ||
}, | ||
goToState: function() { | ||
// not deprecating this yet so people don't constantly need to | ||
// make trivial changes for little reason. | ||
return this.transitionTo.apply(this, arguments); | ||
}, | ||
findStateByPath: function(state, path) { | ||
var possible; | ||
pathForSegments: function(array) { | ||
return Ember.ArrayUtils.map(array, function(tuple) { | ||
Ember.assert("A segment passed to transitionTo must be an Array", Ember.typeOf(tuple) === "array"); | ||
return tuple[0]; | ||
}).join("."); | ||
while (!possible && state) { | ||
possible = this.getStateByPath(state, path); | ||
state = get(state, 'parentState'); | ||
} | ||
return possible; | ||
}, | ||
transitionTo: function(name, context) { | ||
// 1. Normalize arguments | ||
// 2. Ensure that we are in the correct state | ||
// 3. Map provided path to context objects and send | ||
// appropriate setupControllers events | ||
/** | ||
@private | ||
if (Ember.empty(name)) { return; } | ||
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`. | ||
var segments; | ||
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 = []; | ||
if (Ember.typeOf(name) === "array") { | ||
segments = Array.prototype.slice.call(arguments); | ||
} else { | ||
segments = [[name, context]]; | ||
} | ||
for (var i=0, len = r.length; i < len; i++) { | ||
var states = get(origin, 'states'); | ||
var path = this.pathForSegments(segments); | ||
if (!states) { return undefined; } | ||
var currentState = get(this, 'currentState') || this, state, newState; | ||
var s = get(states, r[i]); | ||
if (s) { origin = s; ret.push(s); } | ||
else { return undefined; } | ||
} | ||
var exitStates = [], enterStates, resolveState; | ||
return ret; | ||
}, | ||
state = currentState; | ||
goToState: function() { | ||
// not deprecating this yet so people don't constantly need to | ||
// make trivial changes for little reason. | ||
return this.transitionTo.apply(this, arguments); | ||
}, | ||
if (state.routes[path]) { | ||
// cache hit | ||
transitionTo: function(path, context) { | ||
// XXX When is transitionTo called with no path | ||
if (Ember.empty(path)) { return; } | ||
var route = state.routes[path]; | ||
exitStates = route.exitStates; | ||
enterStates = route.enterStates; | ||
state = route.futureState; | ||
resolveState = route.resolveState; | ||
} else { | ||
// cache miss | ||
// The ES6 signature of this function is `path, ...contexts` | ||
var contexts = context ? Array.prototype.slice.call(arguments, 1) : [], | ||
currentState = get(this, 'currentState') || this; | ||
newState = this.findStatesByRoute(currentState, 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); | ||
while (state && !newState) { | ||
exitStates.unshift(state); | ||
// Next, process the raw state information for the contexts passed in. | ||
var transition = new Transition(hash).normalize(this, contexts); | ||
state = get(state, 'parentState'); | ||
if (!state) { | ||
newState = this.findStatesByRoute(this, path); | ||
if (!newState) { return; } | ||
} | ||
newState = this.findStatesByRoute(state, path); | ||
} | ||
this.enterState(transition); | ||
this.triggerSetupContext(transition); | ||
}, | ||
resolveState = state; | ||
contextFreeTransition: function(currentState, path) { | ||
var cache = currentState.pathsCache[path]; | ||
if (cache) { return cache; } | ||
enterStates = newState.slice(0); | ||
exitStates = exitStates.slice(0); | ||
var enterStates = this.findStatesByPath(currentState, path), | ||
exitStates = [], | ||
resolveState = currentState; | ||
if (enterStates.length > 0) { | ||
state = enterStates[enterStates.length - 1]; | ||
// 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 `<root.posts.show>`, an attempt to | ||
// transition to `comments.show` will match `<root.comments.show>`. | ||
// | ||
// 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, `<root>` 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 | ||
// `[<root.comments>, <root.comments.show>]` | ||
// * exitStates: a list of all of the states from the | ||
// `resolveState` to the `currentState`. In the above | ||
// example, `exitStates` would have | ||
// `[<root.posts>`, `<root.posts.show>]`. | ||
while (resolveState && !enterStates) { | ||
exitStates.unshift(resolveState); | ||
while (enterStates.length > 0 && enterStates[0] === exitStates[0]) { | ||
enterStates.shift(); | ||
exitStates.shift(); | ||
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); | ||
} | ||
currentState.routes[path] = { | ||
exitStates: exitStates, | ||
enterStates: enterStates, | ||
futureState: state, | ||
resolveState: resolveState | ||
}; | ||
// 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: `<state manager>` | ||
// * enterStates: `[<root>, <root.post>, <root.post.show>]` | ||
// * exitStates: `[<root>, <root.post>, <root.post.index>]` | ||
// | ||
// The goal of this code is to remove the common states, so we | ||
// have: | ||
// | ||
// * resolveState: `<root.post>` | ||
// * enterStates: `[<root.post.show>]` | ||
// * exitStates: `[<root.post.index>]` | ||
// | ||
// This avoid unnecessary calls to the enter and exit transitions. | ||
while (enterStates.length > 0 && enterStates[0] === exitStates[0]) { | ||
resolveState = enterStates.shift(); | ||
exitStates.shift(); | ||
} | ||
this.enterState(exitStates, enterStates, state); | ||
this.triggerSetupContext(resolveState, segments); | ||
// 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(root, segments) { | ||
var state = root; | ||
triggerSetupContext: function(transitions) { | ||
var contexts = transitions.contexts, | ||
offset = transitions.enterStates.length - contexts.length, | ||
enterStates = transitions.enterStates, | ||
transitionEvent = get(this, 'transitionEvent'); | ||
Ember.ArrayUtils.forEach(segments, function(tuple) { | ||
var path = tuple[0], context = tuple[1]; | ||
Ember.assert("More contexts provided than states", offset >= 0); | ||
state = this.findStatesByRoute(state, path); | ||
state = state[state.length-1]; | ||
state.fire('setupControllers', this, context); | ||
arrayForEach.call(enterStates, function(state, idx) { | ||
state.trigger(transitionEvent, this, contexts[idx-offset]); | ||
}, this); | ||
//getPath(root, path).setupControllers(this, context); | ||
}, | ||
@@ -683,63 +1113,16 @@ | ||
asyncEach: function(list, callback, doneCallback) { | ||
var async = false, self = this; | ||
if (!list.length) { | ||
if (doneCallback) { doneCallback.call(this); } | ||
return; | ||
} | ||
var head = list[0]; | ||
var tail = list.slice(1); | ||
var transition = { | ||
async: function() { async = true; }, | ||
resume: function() { | ||
self.asyncEach(tail, callback, doneCallback); | ||
} | ||
}; | ||
callback.call(this, head, transition); | ||
if (!async) { transition.resume(); } | ||
}, | ||
enterState: function(exitStates, enterStates, state) { | ||
enterState: function(transition) { | ||
var log = this.enableLogging; | ||
var stateManager = this; | ||
var exitStates = transition.exitStates.slice(0).reverse(); | ||
arrayForEach.call(exitStates, function(state) { | ||
state.trigger('exit', this); | ||
}, this); | ||
exitStates = exitStates.slice(0).reverse(); | ||
this.asyncEach(exitStates, function(state, transition) { | ||
state.fire('exit', stateManager, transition); | ||
}, function() { | ||
this.asyncEach(enterStates, function(state, transition) { | ||
if (log) { Ember.Logger.log("STATEMANAGER: Entering " + get(state, 'path')); } | ||
state.fire('enter', stateManager, transition); | ||
}, function() { | ||
var startState = state, enteredState, initialState; | ||
arrayForEach.call(transition.enterStates, function(state) { | ||
if (log) { Ember.Logger.log("STATEMANAGER: Entering " + get(state, 'path')); } | ||
state.trigger('enter', this); | ||
}, this); | ||
initialState = get(startState, 'initialState'); | ||
if (!initialState) { | ||
initialState = 'start'; | ||
} | ||
// right now, start states cannot be entered asynchronously | ||
while (startState = get(get(startState, 'states'), initialState)) { | ||
enteredState = startState; | ||
if (log) { Ember.Logger.log("STATEMANAGER: Entering " + get(startState, 'path')); } | ||
startState.fire('enter', stateManager); | ||
initialState = get(startState, 'initialState'); | ||
if (!initialState) { | ||
initialState = 'start'; | ||
} | ||
} | ||
set(this, 'currentState', enteredState || state); | ||
}); | ||
}); | ||
set(this, 'currentState', transition.finalState); | ||
} | ||
@@ -753,180 +1136,2 @@ }); | ||
(function() { | ||
var escapeForRegex = function(text) { | ||
return text.replace(/[\-\[\]{}()*+?.,\\\^\$|#\s]/g, "\\$&"); | ||
}; | ||
Ember._RouteMatcher = Ember.Object.extend({ | ||
state: null, | ||
init: function() { | ||
var route = this.route, | ||
identifiers = [], | ||
count = 1, | ||
escaped; | ||
// Strip off leading slash if present | ||
if (route.charAt(0) === '/') { | ||
route = this.route = route.substr(1); | ||
} | ||
escaped = escapeForRegex(route); | ||
var regex = escaped.replace(/:([a-z_]+)(?=$|\/)/gi, function(match, id) { | ||
identifiers[count++] = id; | ||
return "([^/]+)"; | ||
}); | ||
this.identifiers = identifiers; | ||
this.regex = new RegExp("^/?" + regex); | ||
}, | ||
match: function(path) { | ||
var match = path.match(this.regex); | ||
if (match) { | ||
var identifiers = this.identifiers, | ||
hash = {}; | ||
for (var i=1, l=identifiers.length; i<l; i++) { | ||
hash[identifiers[i]] = match[i]; | ||
} | ||
return { | ||
remaining: path.substr(match[0].length), | ||
hash: hash | ||
}; | ||
} | ||
}, | ||
generate: function(hash) { | ||
var identifiers = this.identifiers, route = this.route, id; | ||
for (var i=1, l=identifiers.length; i<l; i++) { | ||
id = identifiers[i]; | ||
route = route.replace(new RegExp(":" + id), hash[id]); | ||
} | ||
return route; | ||
} | ||
}); | ||
})(); | ||
(function() { | ||
var get = Ember.get, getPath = Ember.getPath; | ||
// The Ember Routable mixin assumes the existance of a simple | ||
// routing shim that supports the following three behaviors: | ||
// | ||
// * .getURL() - this is called when the page loads | ||
// * .setURL(newURL) - this is called from within the state | ||
// manager when the state changes to a routable state | ||
// * .onURLChange(callback) - this happens when the user presses | ||
// the back or forward button | ||
Ember.Routable = Ember.Mixin.create({ | ||
init: function() { | ||
this.on('setupControllers', this, this.stashContext); | ||
this._super(); | ||
}, | ||
stashContext: function(manager, context) { | ||
var meta = get(manager, 'stateMeta'), | ||
serialized = this.serialize(manager, context); | ||
meta.set(this, serialized); | ||
if (get(this, 'isRoutable')) { | ||
this.updateRoute(manager, get(manager, 'location')); | ||
} | ||
}, | ||
updateRoute: function(manager, location) { | ||
if (location && get(this, 'isLeaf')) { | ||
var path = this.absoluteRoute(manager); | ||
location.setURL(path); | ||
} | ||
}, | ||
absoluteRoute: function(manager) { | ||
var parentState = get(this, 'parentState'); | ||
var path = ''; | ||
if (get(parentState, 'isRoutable')) { | ||
path = parentState.absoluteRoute(manager); | ||
} | ||
var matcher = get(this, 'routeMatcher'), | ||
hash = get(manager, 'stateMeta').get(this); | ||
var generated = matcher.generate(hash); | ||
if (generated !== "") { | ||
return path + '/' + matcher.generate(hash); | ||
} else { | ||
return path; | ||
} | ||
}, | ||
isRoutable: Ember.computed(function() { | ||
return typeof this.route === "string"; | ||
}).cacheable(), | ||
routeMatcher: Ember.computed(function() { | ||
return Ember._RouteMatcher.create({ route: get(this, 'route') }); | ||
}).cacheable(), | ||
deserialize: function(manager, context) { | ||
return context; | ||
}, | ||
serialize: function(manager, context) { | ||
return context; | ||
}, | ||
routePath: function(manager, path) { | ||
if (get(this, 'isLeaf')) { return; } | ||
var childStates = get(this, 'childStates'), match; | ||
childStates = childStates.sort(function(a, b) { | ||
return getPath(b, 'route.length') - getPath(a, 'route.length'); | ||
}); | ||
var state = childStates.find(function(state) { | ||
var matcher = get(state, 'routeMatcher'); | ||
if (match = matcher.match(path)) { return true; } | ||
}); | ||
Ember.assert("Could not find state for path " + path, !!state); | ||
var object = state.deserialize(manager, match.hash) || {}; | ||
manager.transitionTo(get(state, 'path'), object); | ||
manager.send('routePath', match.remaining); | ||
} | ||
}); | ||
Ember.State.reopen(Ember.Routable); | ||
})(); | ||
(function() { | ||
Ember.Router = Ember.StateManager.extend({ | ||
route: function(path) { | ||
if (path.charAt(0) === '/') { | ||
path = path.substr(1); | ||
} | ||
this.send('routePath', path); | ||
} | ||
}); | ||
})(); | ||
(function() { | ||
// ========================================================================== | ||
@@ -933,0 +1138,0 @@ // Project: Ember Statecharts |
@@ -12,14 +12,53 @@ // | ||
(function() { | ||
var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt; | ||
var get = Ember.get; | ||
Ember.StateManager.reopen( | ||
/** @scope Ember.StateManager.prototype */ { | ||
/** | ||
If the current state is a view state or the descendent of a view state, | ||
this property will be the view associated with it. If there is no | ||
view state active in this state manager, this value will be null. | ||
@type Ember.View | ||
*/ | ||
currentView: Ember.computed(function() { | ||
var currentState = get(this, 'currentState'), | ||
view; | ||
while (currentState) { | ||
// TODO: Remove this when view state is removed | ||
if (get(currentState, 'isViewState')) { | ||
view = get(currentState, 'view'); | ||
if (view) { return view; } | ||
} | ||
currentState = get(currentState, 'parentState'); | ||
} | ||
return null; | ||
}).property('currentState').cacheable() | ||
}); | ||
})(); | ||
(function() { | ||
var get = Ember.get, set = Ember.set; | ||
/** | ||
@class | ||
@deprecated | ||
Ember.ViewState extends Ember.State to control the presence of a childView within a | ||
container based on the current state of the ViewState's StateManager. | ||
## Interactions with Ember's View System. | ||
When combined with instances of `Ember.ViewState`, StateManager is designed to | ||
interact with Ember's view system to control which views are added to | ||
When combined with instances of `Ember.StateManager`, ViewState is designed to | ||
interact with Ember's view system to control which views are added to | ||
and removed from the DOM based on the manager's current state. | ||
By default, a StateManager will manage views inside the 'body' element. This can be | ||
customized by setting the `rootElement` property to a CSS selector of an existing | ||
customized by setting the `rootElement` property to a CSS selector of an existing | ||
HTML element you would prefer to receive view rendering. | ||
@@ -39,3 +78,3 @@ | ||
// make sure this view instance is added to the browser | ||
aLayoutView.appendTo('body') | ||
aLayoutView.appendTo('body') | ||
@@ -73,3 +112,3 @@ App.viewStates = Ember.StateManager.create({ | ||
viewStates.goToState('showingPeople') | ||
viewStates.transitionTo('showingPeople') | ||
@@ -88,6 +127,6 @@ The above code will change the rendered HTML from | ||
Changing the current state via `goToState` from `showingPeople` to | ||
Changing the current state via `transitionTo` from `showingPeople` to | ||
`showingPhotos` will remove the `showingPeople` view and add the `showingPhotos` view: | ||
viewStates.goToState('showingPhotos') | ||
viewStates.transitionTo('showingPhotos') | ||
@@ -128,3 +167,3 @@ will change the rendered HTML to | ||
viewStates.goToState('showingPeople.withEditingPanel') | ||
viewStates.transitionTo('showingPeople.withEditingPanel') | ||
@@ -152,6 +191,6 @@ | ||
view: Ember.View.extend({}), | ||
enter: function(manager, transition){ | ||
enter: function(manager){ | ||
// calling _super ensures this view will be | ||
// properly inserted | ||
this._super(manager, transition); | ||
this._super(manager); | ||
@@ -198,4 +237,4 @@ // now you can do other things | ||
If you prefer to start with an empty body and manage state programmatically you | ||
can also take advantage of StateManager's `rootView` property and the ability of | ||
`Ember.ContainerView`s to manually manage their child views. | ||
can also take advantage of StateManager's `rootView` property and the ability of | ||
`Ember.ContainerView`s to manually manage their child views. | ||
@@ -232,3 +271,3 @@ | ||
## User Manipulation of State via `{{action}}` Helpers | ||
The Handlebars `{{action}}` helper is StateManager-aware and will use StateManager action sending | ||
The Handlebars `{{action}}` helper is StateManager-aware and will use StateManager action sending | ||
to connect user interaction to action-based state transitions. | ||
@@ -259,42 +298,13 @@ | ||
`view` that references the `Ember.View` object that was interacted with. | ||
**/ | ||
Ember.StateManager.reopen( | ||
/** @scope Ember.StateManager.prototype */ { | ||
Ember.ViewState = Ember.State.extend( | ||
/** @scope Ember.ViewState.prototype */ { | ||
isViewState: true, | ||
/** | ||
If the current state is a view state or the descendent of a view state, | ||
this property will be the view associated with it. If there is no | ||
view state active in this state manager, this value will be null. | ||
init: function() { | ||
Ember.deprecate("Ember.ViewState is deprecated and will be removed from future releases. Consider using the outlet pattern to display nested views instead. For more information, see http://emberjs.com/guides/outlets/."); | ||
return this._super(); | ||
}, | ||
@property | ||
*/ | ||
currentView: Ember.computed(function() { | ||
var currentState = get(this, 'currentState'), | ||
view; | ||
while (currentState) { | ||
if (get(currentState, 'isViewState')) { | ||
view = get(currentState, 'view'); | ||
if (view) { return view; } | ||
} | ||
currentState = get(currentState, 'parentState'); | ||
} | ||
return null; | ||
}).property('currentState').cacheable() | ||
}); | ||
})(); | ||
(function() { | ||
var get = Ember.get, set = Ember.set; | ||
Ember.ViewState = Ember.State.extend({ | ||
isViewState: true, | ||
enter: function(stateManager) { | ||
@@ -301,0 +311,0 @@ var view = get(this, 'view'), root, childViews; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
746398
26
19587
Updatedjquery@~> 1.7.0