dist/ember.js in ember-source-1.0.0.rc6.4 vs dist/ember.js in ember-source-1.0.0.rc7

- old
+ new

@@ -1,7 +1,7 @@ -// Version: v1.0.0-rc.6-221-g9d051c2 -// Last commit: 9d051c2 (2013-07-28 23:13:59 -0700) +// Version: v1.0.0-rc.7-2-ga4bfa51 +// Last commit: a4bfa51 (2013-08-14 00:39:16 -0500) (function() { /*global __fail__*/ @@ -154,12 +154,12 @@ }; }; })(); -// Version: v1.0.0-rc.6-221-g9d051c2 -// Last commit: 9d051c2 (2013-07-28 23:13:59 -0700) +// Version: v1.0.0-rc.7-2-ga4bfa51 +// Last commit: a4bfa51 (2013-08-14 00:39:16 -0500) (function() { var define, requireModule; @@ -222,11 +222,11 @@ The core Runtime framework is based on the jQuery API with a number of performance optimizations. @class Ember @static - @version 1.0.0-rc.6 + @version 1.0.0-rc.7 */ if ('undefined' === typeof Ember) { // Create core object. Make it act like an instance of Ember.Namespace so that // objects assigned to it are given a sane string representation. @@ -249,14 +249,14 @@ /** @property VERSION @type String - @default '1.0.0-rc.6.1' + @default '1.0.0-rc.7.1' @final */ -Ember.VERSION = '1.0.0-rc.6.1'; +Ember.VERSION = '1.0.0-rc.7.1'; /** Standard environmental variables. You can define these in a global `ENV` variable before loading Ember to control various configuration settings. @@ -6194,17 +6194,17 @@ } else { return mixin; // apply anonymous mixin properties } } -function concatenatedProperties(props, values, base) { +function concatenatedMixinProperties(concatProp, props, values, base) { var concats; // reset before adding each new mixin to pickup concats from previous - concats = values.concatenatedProperties || base.concatenatedProperties; - if (props.concatenatedProperties) { - concats = concats ? concats.concat(props.concatenatedProperties) : props.concatenatedProperties; + concats = values[concatProp] || base[concatProp]; + if (props[concatProp]) { + concats = concats ? concats.concat(props[concatProp]) : props[concatProp]; } return concats; } @@ -6267,11 +6267,32 @@ } else { return Ember.makeArray(value); } } -function addNormalizedProperty(base, key, value, meta, descs, values, concats) { +function applyMergedProperties(obj, key, value, values) { + var baseValue = values[key] || obj[key]; + + if (!baseValue) { return value; } + + var newBase = Ember.merge({}, baseValue); + for (var prop in value) { + if (!value.hasOwnProperty(prop)) { continue; } + + var propValue = value[prop]; + if (isMethod(propValue)) { + // TODO: support for Computed Properties, etc? + newBase[prop] = giveMethodSuper(obj, prop, propValue, baseValue, {}); + } else { + newBase[prop] = propValue; + } + } + + return newBase; +} + +function addNormalizedProperty(base, key, value, meta, descs, values, concats, mergings) { if (value instanceof Ember.Descriptor) { if (value === REQUIRED && descs[key]) { return CONTINUE; } // Wrap descriptor function to implement // _super() if needed @@ -6283,21 +6304,25 @@ values[key] = undefined; } else { // impl super if needed... if (isMethod(value)) { value = giveMethodSuper(base, key, value, values, descs); - } else if ((concats && a_indexOf.call(concats, key) >= 0) || key === 'concatenatedProperties') { + } else if ((concats && a_indexOf.call(concats, key) >= 0) || + key === 'concatenatedProperties' || + key === 'mergedProperties') { value = applyConcatenatedProperties(base, key, value, values); + } else if ((mergings && a_indexOf.call(mergings, key) >= 0)) { + value = applyMergedProperties(base, key, value, values); } descs[key] = undefined; values[key] = value; } } function mergeMixins(mixins, m, descs, values, base, keys) { - var mixin, props, key, concats, meta; + var mixin, props, key, concats, mergings, meta; function removeKeys(keyName) { delete descs[keyName]; delete values[keyName]; } @@ -6309,16 +6334,17 @@ props = mixinProperties(m, mixin); if (props === CONTINUE) { continue; } if (props) { meta = Ember.meta(base); - concats = concatenatedProperties(props, values, base); + concats = concatenatedMixinProperties('concatenatedProperties', props, values, base); + mergings = concatenatedMixinProperties('mergedProperties', props, values, base); for (key in props) { if (!props.hasOwnProperty(key)) { continue; } keys.push(key); - addNormalizedProperty(base, key, props[key], meta, descs, values, concats); + addNormalizedProperty(base, key, props[key], meta, descs, values, concats, mergings); } // manually copy toString() because some JS engines do not enumerate it if (props.hasOwnProperty('toString')) { base.toString = props.toString; } } else if (mixin.mixins) { @@ -6413,10 +6439,11 @@ key, value, desc, keys = []; // Go through all mixins and hashes passed in, and: // // * Handle concatenated properties + // * Handle merged properties // * Set up _super wrapping if necessary // * Set up computed property descriptors // * Copying `toString` in broken browsers mergeMixins(mixins, mixinsMeta(obj), descs, values, obj, keys); @@ -6752,30 +6779,36 @@ return Ember.observer.apply(this, arguments); }; /** - When observers fire, they are called with the arguments `obj`, `keyName` - and `value`. In a typical observer, value is the new, post-change value. + When observers fire, they are called with the arguments `obj`, `keyName`. - A `beforeObserver` fires before a property changes. The `value` argument contains - the pre-change value. + Note, `@each.property` observer is called per each add or replace of an element + and it's not called with a specific enumeration item. + A `beforeObserver` fires before a property changes. + A `beforeObserver` is an alternative form of `.observesBefore()`. ```javascript App.PersonView = Ember.View.extend({ - valueWillChange: function (obj, keyName, value) { - this.changingFrom = value; + friends: [{ name: 'Tom' }, { name: 'Stefan' }, { name: 'Kris' }], + valueWillChange: function (obj, keyName) { + this.changingFrom = obj.get(keyName); }.observesBefore('content.value'), - valueDidChange: function(obj, keyName, value) { + valueDidChange: function(obj, keyName) { // only run if updating a value already in the DOM if (this.get('state') === 'inDOM') { - var color = value > this.changingFrom ? 'green' : 'red'; + var color = obj.get(keyName) > this.changingFrom ? 'green' : 'red'; // logic } - }.observes('content.value') + }.observes('content.value'), + friendsDidChange: function(obj, keyName) { + // some logic + // obj.get(keyName) returns friends array + }.observes('friends.@each.name') }); ``` @method beforeObserver @for Ember @@ -8451,10 +8484,12 @@ (function() { /** Expose RSVP implementation + + Documentation can be found here: https://github.com/tildeio/rsvp.js/blob/master/README.md @class RSVP @namespace Ember @constructor */ @@ -11339,11 +11374,12 @@ @param {Number} increment The amount to increment by. Defaults to 1 @return {Number} The new property value */ incrementProperty: function(keyName, increment) { if (Ember.isNone(increment)) { increment = 1; } - set(this, keyName, (get(this, keyName) || 0)+increment); + Ember.assert("Must pass a numeric value to incrementProperty", (!isNaN(parseFloat(increment)) && isFinite(increment))); + 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. @@ -11358,11 +11394,12 @@ @param {Number} decrement The amount to decrement by. Defaults to 1 @return {Number} The new property value */ decrementProperty: function(keyName, decrement) { if (Ember.isNone(decrement)) { decrement = 1; } - set(this, keyName, (get(this, keyName) || 0)-decrement); + Ember.assert("Must pass a numeric value to decrementProperty", (!isNaN(parseFloat(decrement)) && isFinite(decrement))); + set(this, keyName, (get(this, keyName) || 0) - decrement); return get(this, keyName); }, /** Set the value of a boolean property to the opposite of it's @@ -11950,12 +11987,10 @@ reopen: function() { applyMixin(this, arguments, true); return this; }, - isInstance: true, - /** An overridable method called when objects are instantiated. By default, does nothing unless it is overridden during class definition. Example: @@ -12094,10 +12129,12 @@ return this; }, /** Override to implement teardown. + + @method willDestroy */ willDestroy: Ember.K, /** @private @@ -14548,14 +14585,11 @@ @module ember @submodule ember-runtime */ /** - `Ember.ObjectController` is part of Ember's Controller layer. A single shared - instance of each `Ember.ObjectController` subclass in your application's - namespace will be created at application initialization and be stored on your - application's `Ember.Router` instance. + `Ember.ObjectController` is part of Ember's Controller layer. `Ember.ObjectController` derives its functionality from its superclass `Ember.ObjectProxy` and the `Ember.ControllerMixin` mixin. @class ObjectController @@ -15478,11 +15512,13 @@ _dispatchEvent: function(object, evt, eventName, view) { var result = true; var handler = object[eventName]; if (Ember.typeOf(handler) === 'function') { - result = handler.call(object, evt, view); + result = Ember.run(function() { + return handler.call(object, evt, view); + }); // Do not preventDefault in eventManagers. evt.stopPropagation(); } else { result = this._bubbleEvent(view, evt, eventName); @@ -15662,10 +15698,14 @@ } else { return parent; } }).property('_parentView'), + _viewForYield: Ember.computed(function(){ + return this; + }).property(), + state: null, _parentView: null, // return the current view, not including virtual views @@ -15976,11 +16016,11 @@ and a different class name if it evaluates to false, you can pass a binding like this: ```javascript // Applies 'enabled' class when isEnabled is true and 'disabled' when isEnabled is false - Ember.View.create({ + Ember.View.extend({ classNameBindings: ['isEnabled:enabled:disabled'] isEnabled: true }); ``` @@ -15999,11 +16039,11 @@ This syntax offers the convenience to add a class if a property is `false`: ```javascript // Applies no class when isEnabled is true and class 'disabled' when isEnabled is false - Ember.View.create({ + Ember.View.extend({ classNameBindings: ['isEnabled::disabled'] isEnabled: true }); ``` @@ -16293,11 +16333,11 @@ mouseEnter: function(event) { // will never trigger. }, eventManager: Ember.Object.create({ mouseEnter: function(event, view) { - // takes presedence over AView#mouseEnter + // takes precedence over AView#mouseEnter } }) }); ``` @@ -16536,10 +16576,13 @@ } }).volatile(), /** The parent context for this template. + + @method parentContext + @return {Ember.View} */ parentContext: function() { var parentView = get(this, '_parentView'); return parentView && get(parentView, '_context'); }, @@ -17046,10 +17089,11 @@ */ appendTo: function(target) { // Schedule the DOM element to be created and appended to the given // element after bindings have synchronized. this._insertElementLater(function() { + Ember.assert("You tried to append to (" + target + ") but that isn't in the DOM", Ember.$(target).length > 0); Ember.assert("You cannot append to an existing Ember.View. Consider using Ember.ContainerView instead.", !Ember.$(target).is('.ember-view') && !Ember.$(target).parents().is('.ember-view')); this.$().appendTo(target); }); return this; @@ -17067,10 +17111,11 @@ @method replaceIn @param {String|DOMElement|jQuery} A selector, element, HTML string, or jQuery object @return {Ember.View} received */ replaceIn: function(target) { + Ember.assert("You tried to replace in (" + target + ") but that isn't in the DOM", Ember.$(target).length > 0); Ember.assert("You cannot replace an existing Ember.View. Consider using Ember.ContainerView instead.", !Ember.$(target).is('.ember-view') && !Ember.$(target).parents().is('.ember-view')); this._insertElementLater(function() { Ember.$(target).empty(); this.$().appendTo(target); @@ -17457,33 +17502,33 @@ is a string value, the value of that string will be applied as a class name. ```javascript // Applies the 'high' class to the view element - Ember.View.create({ + Ember.View.extend({ classNameBindings: ['priority'] priority: 'high' }); ``` If the value of the property is a Boolean, the name of that property is added as a dasherized class name. ```javascript // Applies the 'is-urgent' class to the view element - Ember.View.create({ + Ember.View.extend({ classNameBindings: ['isUrgent'] isUrgent: true }); ``` If you would prefer to use a custom value instead of the dasherized property name, you can pass a binding like this: ```javascript // Applies the 'urgent' class to the view element - Ember.View.create({ + Ember.View.extend({ classNameBindings: ['isUrgent:urgent'] isUrgent: true }); ``` @@ -17500,22 +17545,22 @@ a string value, the value of that string will be applied as the attribute. ```javascript // Applies the type attribute to the element // with the value "button", like <div type="button"> - Ember.View.create({ + Ember.View.extend({ attributeBindings: ['type'], type: 'button' }); ``` If the value of the property is a Boolean, the name of that property is added as an attribute. ```javascript // Renders something like <div enabled="enabled"> - Ember.View.create({ + Ember.View.extend({ attributeBindings: ['enabled'], enabled: true }); ``` @@ -18744,12 +18789,15 @@ }, initializeViews: function(views, parentView, templateData) { forEach(views, function(view) { set(view, '_parentView', parentView); - set(view, 'container', parentView && parentView.container); + if (!view.container && parentView) { + set(view, 'container', parentView.container); + } + if (!get(view, 'templateData')) { set(view, 'templateData', templateData); } }); }, @@ -19305,47 +19353,83 @@ Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { init: function() { this._super(); set(this, 'context', this); set(this, 'controller', this); - set(this, 'templateData', {keywords: {}}); }, + // during render, isolate keywords + cloneKeywords: function() { + return { + view: this, + controller: this + }; + }, + targetObject: Ember.computed(function(key) { var parentView = get(this, '_parentView'); return parentView ? get(parentView, 'controller') : null; }).property('_parentView'), + _viewForYield: Ember.computed(function(){ + return get(this, '_parentView') || this; + }).property('_parentView'), + /** - Sends an action to component's controller. A component inherits its - controller from the context in which it is used. + Sends an action to component's controller. A component inherits its + controller from the context in which it is used. - By default, calling `sendAction()` will send an action with the name - of the component's `action` property. + By default, calling `sendAction()` will send an action with the name + of the component's `action` property. - For example, if the component had a property `action` with the value - `"addItem"`, calling `sendAction()` would send the `addItem` action - to the component's controller. + For example, if the component had a property `action` with the value + `"addItem"`, calling `sendAction()` would send the `addItem` action + to the component's controller. - If you provide an argument to `sendAction()`, that key will be used to look - up the action name. + If you provide the `action` argument to `sendAction()`, that key will + be used to look up the action name. - For example, if the component had a property `playing` with the value - `didStartPlaying`, calling `sendAction('playing')` would send the - `didStartPlaying` action to the component's controller. + For example, if the component had a property `playing` with the value + `didStartPlaying`, calling `sendAction('playing')` would send the + `didStartPlaying` action to the component's controller. - Whether or not you are using the default action or a named action, if - the action name is not defined on the component, calling `sendAction()` - does not have any effect. + Whether or not you are using the default action or a named action, if + the action name is not defined on the component, calling `sendAction()` + does not have any effect. - For example, if you call `sendAction()` on a component that does not have - an `action` property defined, no action will be sent to the controller, - nor will an exception be raised. + For example, if you call `sendAction()` on a component that does not have + an `action` property defined, no action will be sent to the controller, + nor will an exception be raised. - @param [action] {String} the action to trigger + You can send a context object with the action by supplying the `context` + argument. The context will be supplied as the first argument in the + target's action method. Example: + + ```javascript + App.MyTree = Ember.Component.extend({ + click: function() { + this.sendAction('didClickTreeNode', this.get('node')); + } + }); + + App.CategoriesController = Ember.Controller.extend({ + didClickCategory: function(category) { + //Do something with the node/category that was clicked + } + }); + ``` + + ```handlebars + {{! categories.hbs}} + {{my-tree didClickTreeNode='didClickCategory'}} + ``` + + @method sendAction + @param [action] {String} the action to trigger + @param [context] {*} a context to send with the action */ - sendAction: function(action) { + sendAction: function(action, context) { var actionName; // Send the default action if (action === undefined) { actionName = get(this, 'action'); @@ -19353,16 +19437,16 @@ } else { actionName = get(this, action); Ember.assert("The " + action + " action was triggered on the component " + this.toString() + ", but the action name (" + actionName + ") was not a string.", isNone(actionName) || typeof actionName === 'string'); } - // If no action name for that action could be found, just abort. if (actionName === undefined) { return; } this.triggerAction({ - action: actionName + action: actionName, + actionContext: context }); } }); })(); @@ -19986,11 +20070,11 @@ For more examples of bound helpers, see documentation for `Ember.Handlebars.registerBoundHelper`. ## Custom view helper example - Assuming a view subclass named `App.CalenderView` were defined, a helper + Assuming a view subclass named `App.CalendarView` were defined, a helper for rendering instances of this view could be registered as follows: ```javascript Ember.Handlebars.helper('calendar', App.CalendarView): ``` @@ -20179,11 +20263,14 @@ var ast = Handlebars.parse(string); var options = { data: true, stringParams: true }; var environment = new Ember.Handlebars.Compiler().compile(ast, options); var templateSpec = new Ember.Handlebars.JavaScriptCompiler().compile(environment, options, undefined, true); - return Ember.Handlebars.template(templateSpec); + var template = Ember.Handlebars.template(templateSpec); + template.isMethod = false; //Make sure we don't wrap templates with ._super + + return template; }; } })(); @@ -22412,11 +22499,11 @@ */ var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.normalizePath; /** - `log` allows you to output the value of a value in the current rendering + `log` allows you to output the value of a variable in the current rendering context. ```handlebars {{log myVariable}} ``` @@ -22972,28 +23059,29 @@ @for Ember.Handlebars.helpers @param {Hash} options @return {String} HTML string */ Ember.Handlebars.registerHelper('yield', function(options) { - var currentView = options.data.view, view = currentView, template; + var currentView = options.data.view, view = currentView; while (view && !get(view, 'layout')) { view = get(view, 'parentView'); } Ember.assert("You called yield in a template that was not a layout", !!view); - template = get(view, 'template'); + var template = get(view, 'template'), + contextView = get(view, '_viewForYield'), + keywords = contextView.cloneKeywords(); - var keywords = view._parentView.cloneKeywords(); - currentView.appendChild(Ember.View, { - isVirtual: true, - tagName: '', - template: template, - context: get(view._parentView, 'context'), - controller: get(view._parentView, 'controller'), + isVirtual: true, + isYield: true, + tagName: '', + template: template, + context: get(contextView, 'context'), + controller: get(contextView, 'controller'), templateData: {keywords: keywords} }); }); })(); @@ -24352,25 +24440,20 @@ There's no harm to running this twice, since we remove the templates from the DOM after processing. */ Ember.onLoad('Ember.Application', function(Application) { - if (Application.initializer) { - Application.initializer({ - name: 'domTemplates', - initialize: bootstrap - }); + Application.initializer({ + name: 'domTemplates', + initialize: bootstrap + }); - Application.initializer({ - name: 'registerComponents', - after: 'domTemplates', - initialize: registerComponents - }); - } else { - // for ember-old-router - Ember.onLoad('application', bootstrap); - } + Application.initializer({ + name: 'registerComponents', + after: 'domTemplates', + initialize: registerComponents + }); }); })(); @@ -24933,16 +25016,16 @@ /** @private A Transition is a thennable (a promise-like object) that represents an attempt to transition to another route. It can be aborted, either - explicitly via `abort` or by attempting another transition while a + explicitly via `abort` or by attempting another transition while a previous one is still underway. An aborted transition can also - be `retry()`d later. + be `retry()`d later. */ - function Transition(router, promise) { + function Transition(router, promise) { this.router = router; this.promise = promise; this.data = {}; this.resolvedModels = {}; this.providedModels = {}; @@ -24962,13 +25045,13 @@ /** The Transition's internal promise. Calling `.then` on this property is that same as calling `.then` on the Transition object itself, but this property is exposed for when you want to pass around a - Transition's promise, but not the Transition object itself, since + Transition's promise, but not the Transition object itself, since Transition object can be externally `abort`ed, while the promise - cannot. + cannot. */ promise: null, /** Custom state can be stored on a Transition's `data` object. @@ -24978,38 +25061,38 @@ transition. */ data: null, /** - A standard promise hook that resolves if the transition + A standard promise hook that resolves if the transition succeeds and rejects if it fails/redirects/aborts. Forwards to the internal `promise` property which you can use in situations where you want to pass around a thennable, - but not the Transition itself. + but not the Transition itself. @param {Function} success @param {Function} failure */ then: function(success, failure) { return this.promise.then(success, failure); }, /** Aborts the Transition. Note you can also implicitly abort a transition - by initiating another transition while a previous one is underway. + by initiating another transition while a previous one is underway. */ abort: function() { if (this.isAborted) { return this; } log(this.router, this.sequence, this.targetName + ": transition was aborted"); this.isAborted = true; this.router.activeTransition = null; - return this; + return this; }, /** - Retries a previously-aborted transition (making sure to abort the + Retries a previously-aborted transition (making sure to abort the transition if it's still active). Returns a new transition that represents the new attempt to transition. */ retry: function() { this.abort(); @@ -25019,11 +25102,11 @@ return newTransition; }, /** - Sets the URL-changing method to be employed at the end of a + Sets the URL-changing method to be employed at the end of a successful transition. By default, a new Transition will just use `updateURL`, but passing 'replace' to this method will cause the URL to update using 'replaceWith' instead. Omitting a parameter will disable the URL change, allowing for transitions that don't update the URL at completion (this is also used for @@ -25052,16 +25135,16 @@ /** Promise reject reasons passed to promise rejection handlers for failed transitions. */ Router.UnrecognizedURLError = function(message) { - this.message = (message || "UnrecognizedURLError"); + this.message = (message || "UnrecognizedURLError"); this.name = "UnrecognizedURLError"; }; Router.TransitionAborted = function(message) { - this.message = (message || "TransitionAborted"); + this.message = (message || "TransitionAborted"); this.name = "TransitionAborted"; }; function errorTransition(router, reason) { return new Transition(router, RSVP.reject(reason)); @@ -25224,12 +25307,12 @@ object = contexts.pop(); if (isParam(object)) { var recogHandler = recogHandlers[i], name = recogHandler.names[0]; if (object.toString() !== this.currentParams[name]) { return false; } - } else if (handlerInfo.context !== object) { - return false; + } else if (handlerInfo.context !== object) { + return false; } } } } @@ -25256,24 +25339,24 @@ a shared pivot parent route and other data necessary to perform a transition. */ function getMatchPoint(router, handlers, objects, inputParams) { - var matchPoint = handlers.length, + var matchPoint = handlers.length, providedModels = {}, i, currentHandlerInfos = router.currentHandlerInfos || [], params = {}, oldParams = router.currentParams || {}, activeTransition = router.activeTransition, handlerParams = {}, obj; objects = slice.call(objects); merge(params, inputParams); - + for (i = handlers.length - 1; i >= 0; i--) { - var handlerObj = handlers[i], + var handlerObj = handlers[i], handlerName = handlerObj.handler, oldHandlerInfo = currentHandlerInfos[i], hasChanged = false; // Check if handler names have changed. @@ -25307,11 +25390,11 @@ for (var j = 0, len = names.length; j < len; ++j) { var name = names[j]; handlerParams[handlerName][name] = params[name] = params[name] || oldParams[name]; } } - } + } if (hasChanged) { matchPoint = i; } } if (objects.length > 0) { @@ -25333,17 +25416,17 @@ } else { return object; } } else if (activeTransition) { // Use model from previous transition attempt, preferably the resolved one. - return (paramName && activeTransition.providedModels[handlerName]) || - activeTransition.resolvedModels[handlerName]; - } + return activeTransition.resolvedModels[handlerName] || + (paramName && activeTransition.providedModels[handlerName]); + } } function isParam(object) { - return object && (typeof object === "string" || object instanceof String || !isNaN(object)); + return (typeof object === "string" || object instanceof String || !isNaN(object)); } /** @private @@ -25480,14 +25563,10 @@ }); eachHandler(partition.entered, function(handlerInfo) { handlerEnteredOrUpdated(transition, currentHandlerInfos, handlerInfo, true); }); - - if (router.didTransition) { - router.didTransition(handlerInfos); - } } /** @private @@ -25505,11 +25584,11 @@ setContext(handler, context); if (handler.setup) { handler.setup(context); } checkAbort(transition); } catch(e) { - if (!(e instanceof Router.TransitionAborted)) { + if (!(e instanceof Router.TransitionAborted)) { // Trigger the `error` event starting from this failed handler. trigger(currentHandlerInfos.concat(handlerInfo), true, ['error', e, transition]); } // Propagate the error so that the transition promise will reject. @@ -25652,18 +25731,19 @@ */ function performTransition(router, recogHandlers, providedModelsArray, params, data) { var matchPointResults = getMatchPoint(router, recogHandlers, providedModelsArray, params), targetName = recogHandlers[recogHandlers.length - 1].handler, - wasTransitioning = false; + wasTransitioning = false, + currentHandlerInfos = router.currentHandlerInfos; // Check if there's already a transition underway. - if (router.activeTransition) { + if (router.activeTransition) { if (transitionsIdentical(router.activeTransition, targetName, providedModelsArray)) { return router.activeTransition; } - router.activeTransition.abort(); + router.activeTransition.abort(); wasTransitioning = true; } var deferred = RSVP.defer(), transition = new Transition(router, deferred.promise); @@ -25678,11 +25758,11 @@ var handlerInfos = generateHandlerInfos(router, recogHandlers); // Fire 'willTransition' event on current handlers, but don't fire it // if a transition was already underway. if (!wasTransitioning) { - trigger(router.currentHandlerInfos, true, ['willTransition', transition]); + trigger(currentHandlerInfos, true, ['willTransition', transition]); } log(router, transition.sequence, "Beginning validation for transition to " + transition.targetName); validateEntry(transition, handlerInfos, 0, matchPointResults.matchPoint, matchPointResults.handlerParams) .then(transitionSuccess, transitionFailure); @@ -25691,12 +25771,24 @@ function transitionSuccess() { checkAbort(transition); try { - finalizeTransition(transition, handlerInfos); + log(router, transition.sequence, "Validation succeeded, finalizing transition;"); + // Don't overwrite contexts / update URL if this was a noop transition. + if (!currentHandlerInfos || !currentHandlerInfos.length || + currentHandlerInfos.length !== matchPointResults.matchPoint) { + finalizeTransition(transition, handlerInfos); + } + + if (router.didTransition) { + router.didTransition(handlerInfos); + } + + log(router, transition.sequence, "TRANSITION COMPLETE."); + // Resolve with the final handler. deferred.resolve(handlerInfos[handlerInfos.length - 1].handler); } catch(e) { deferred.reject(e); } @@ -25715,12 +25807,12 @@ /** @private Accepts handlers in Recognizer format, either returned from - recognize() or handlersFor(), and returns unified - `HandlerInfo`s. + recognize() or handlersFor(), and returns unified + `HandlerInfo`s. */ function generateHandlerInfos(router, recogHandlers) { var handlerInfos = []; for (var i = 0, len = recogHandlers.length; i < len; ++i) { var handlerObj = recogHandlers[i], @@ -25761,12 +25853,10 @@ var router = transition.router, seq = transition.sequence, handlerName = handlerInfos[handlerInfos.length - 1].name; - log(router, seq, "Validation succeeded, finalizing transition;"); - // Collect params for URL. var objects = [], providedModels = transition.providedModelsArray.slice(); for (var i = handlerInfos.length - 1; i>=0; --i) { var handlerInfo = handlerInfos[i]; if (handlerInfo.isDynamic) { @@ -25780,11 +25870,11 @@ transition.providedModelsArray = []; transition.providedContexts = {}; router.currentParams = params; var urlMethod = transition.urlMethod; - if (urlMethod) { + if (urlMethod) { var url = router.recognizer.generate(handlerName, params); if (urlMethod === 'replace') { router.replaceURL(url); } else { @@ -25792,11 +25882,10 @@ router.updateURL(url); } } setupContexts(transition, handlerInfos); - log(router, seq, "TRANSITION COMPLETE."); } /** @private @@ -25821,11 +25910,13 @@ if (index < matchPoint) { log(router, seq, handlerName + ": using context from already-active handler"); // We're before the match point, so don't run any hooks, // just use the already resolved context from the handler. - transition.resolvedModels[handlerInfo.name] = handlerInfo.handler.context; + transition.resolvedModels[handlerInfo.name] = + transition.providedModels[handlerInfo.name] || + handlerInfo.handler.context; return proceed(); } return RSVP.resolve().then(handleAbort) .then(beforeModel) @@ -25856,16 +25947,16 @@ // otherwise, we're here because of a different error transition.abort(); log(router, seq, handlerName + ": handling error: " + reason); - // An error was thrown / promise rejected, so fire an + // An error was thrown / promise rejected, so fire an // `error` event from this handler info up to root. trigger(handlerInfos.slice(0, index + 1), true, ['error', reason, transition]); - if (handler.error) { - handler.error(reason, transition); + if (handler.error) { + handler.error(reason, transition); } // Propagate the original error. return RSVP.reject(reason); } @@ -25911,11 +26002,11 @@ @private Throws a TransitionAborted if the provided transition has been aborted. */ function checkAbort(transition) { - if (transition.isAborted) { + if (transition.isAborted) { log(transition.router, transition.sequence, "detected abort."); throw new Router.TransitionAborted(); } } @@ -25941,11 +26032,11 @@ return handler.model && handler.model(handlerParams || {}, transition); } /** - @private + @private */ function log(router, sequence, msg) { if (!router.log) { return; } @@ -26000,11 +26091,11 @@ } // Use custom serialize if it exists. if (handler.serialize) { return handler.serialize(model, names); - } + } if (names.length !== 1) { return; } var name = names[0]; @@ -26178,26 +26269,10 @@ var Router = requireModule("router"); var get = Ember.get, set = Ember.set; var defineProperty = Ember.defineProperty; var DefaultView = Ember._MetamorphView; -function setupLocation(router) { - var location = get(router, 'location'), - rootURL = get(router, 'rootURL'), - options = {}; - - if (typeof rootURL === 'string') { - options.rootURL = rootURL; - } - - if ('string' === typeof location) { - options.implementation = location; - location = set(router, 'location', Ember.Location.create(options)); - - } -} - /** The `Ember.Router` class manages the application state and URLs. Refer to the [routing guide](http://emberjs.com/guides/routing/) for documentation. @class Router @@ -26208,11 +26283,11 @@ location: 'hash', init: function() { this.router = this.constructor.router || this.constructor.map(Ember.K); this._activeViews = {}; - setupLocation(this); + this._setupLocation(); }, url: Ember.computed(function() { return get(this, 'location').getURL(); }), @@ -26223,11 +26298,11 @@ var router = this.router, location = get(this, 'location'), container = this.container, self = this; - setupRouter(this, router, location); + this._setupRouter(router, location); container.register('view:default', DefaultView); container.register('view:toplevel', Ember.View.extend()); location.onUpdateURL(function(url) { @@ -26237,11 +26312,11 @@ this.handleURL(location.getURL()); }, didTransition: function(infos) { var appController = this.container.lookup('controller:application'), - path = routePath(infos); + path = Ember.Router._routePath(infos); if (!('currentPath' in appController)) { defineProperty(appController, 'currentPath'); } set(appController, 'currentPath', path); @@ -26251,21 +26326,19 @@ Ember.Logger.log("Transitioned into '" + path + "'"); } }, handleURL: function(url) { - scheduleLoadingStateEntry(this); - - return this.router.handleURL(url).then(transitionCompleted); + return this._doTransition('handleURL', [url]); }, transitionTo: function() { - return doTransition(this, 'transitionTo', arguments); + return this._doTransition('transitionTo', arguments); }, replaceWith: function() { - return doTransition(this, 'replaceWith', arguments); + return this._doTransition('replaceWith', arguments); }, generate: function() { var url = this.router.generate.apply(this.router, arguments); return this.location.formatURL(url); @@ -26312,159 +26385,159 @@ delete this._activeViews[templateName]; }; this._activeViews[templateName] = [view, disconnect]; view.one('willDestroyElement', this, disconnect); - } -}); + }, -function getHandlerFunction(router) { - var seen = {}, container = router.container, - DefaultRoute = container.resolve('route:basic'); + _setupLocation: function() { + var location = get(this, 'location'), + rootURL = get(this, 'rootURL'), + options = {}; - return function(name) { - var routeName = 'route:' + name, - handler = container.lookup(routeName); - - if (seen[name]) { return handler; } - - seen[name] = true; - - if (!handler) { - if (name === 'loading') { return {}; } - - container.register(routeName, DefaultRoute.extend()); - handler = container.lookup(routeName); - - if (get(router, 'namespace.LOG_ACTIVE_GENERATION')) { - Ember.Logger.info("generated -> " + routeName, { fullName: routeName }); - } + if (typeof rootURL === 'string') { + options.rootURL = rootURL; } - if (name === 'application') { - // Inject default `error` handler. - handler.events = handler.events || {}; - handler.events.error = handler.events.error || defaultErrorHandler; + if ('string' === typeof location) { + options.implementation = location; + location = set(this, 'location', Ember.Location.create(options)); } + }, - handler.routeName = name; - return handler; - }; -} + _getHandlerFunction: function() { + var seen = {}, container = this.container, + DefaultRoute = container.resolve('route:basic'), + self = this; -function defaultErrorHandler(error, transition) { - Ember.Logger.error('Error while loading route:', error); + return function(name) { + var routeName = 'route:' + name, + handler = container.lookup(routeName); - // Using setTimeout allows us to escape from the Promise's try/catch block - setTimeout(function() { throw error; }); -} + if (seen[name]) { return handler; } + seen[name] = true; -function routePath(handlerInfos) { - var path = []; + if (!handler) { + if (name === 'loading') { return {}; } - for (var i=1, l=handlerInfos.length; i<l; i++) { - var name = handlerInfos[i].name, - nameParts = name.split("."); + container.register(routeName, DefaultRoute.extend()); + handler = container.lookup(routeName); - path.push(nameParts[nameParts.length - 1]); - } + if (get(self, 'namespace.LOG_ACTIVE_GENERATION')) { + Ember.Logger.info("generated -> " + routeName, { fullName: routeName }); + } + } - return path.join("."); -} + if (name === 'application') { + // Inject default `error` handler. + handler.events = handler.events || {}; + handler.events.error = handler.events.error || Ember.Router._defaultErrorHandler; + } -function setupRouter(emberRouter, router, location) { - var lastURL; + handler.routeName = name; + return handler; + }; + }, - router.getHandler = getHandlerFunction(emberRouter); + _setupRouter: function(router, location) { + var lastURL, emberRouter = this; - var doUpdateURL = function() { - location.setURL(lastURL); - }; + router.getHandler = this._getHandlerFunction(); - router.updateURL = function(path) { - lastURL = path; - Ember.run.once(doUpdateURL); - }; - - if (location.replaceURL) { - var doReplaceURL = function() { - location.replaceURL(lastURL); + var doUpdateURL = function() { + location.setURL(lastURL); }; - router.replaceURL = function(path) { + router.updateURL = function(path) { lastURL = path; - Ember.run.once(doReplaceURL); + Ember.run.once(doUpdateURL); }; - } - router.didTransition = function(infos) { - emberRouter.didTransition(infos); - }; -} + if (location.replaceURL) { + var doReplaceURL = function() { + location.replaceURL(lastURL); + }; -function doTransition(router, method, args) { - // Normalize blank route to root URL. - args = [].slice.call(args); - args[0] = args[0] || '/'; + router.replaceURL = function(path) { + lastURL = path; + Ember.run.once(doReplaceURL); + }; + } - var passedName = args[0], name; + router.didTransition = function(infos) { + emberRouter.didTransition(infos); + }; + }, - if (passedName.charAt(0) === '/') { - name = passedName; - } else { - if (!router.router.hasRoute(passedName)) { - name = args[0] = passedName + '.index'; - } else { + _doTransition: function(method, args) { + // Normalize blank route to root URL. + args = [].slice.call(args); + args[0] = args[0] || '/'; + + var passedName = args[0], name, self = this; + + if (passedName.charAt(0) === '/') { name = passedName; + } else { + if (!this.router.hasRoute(passedName)) { + name = args[0] = passedName + '.index'; + } else { + name = passedName; + } + + Ember.assert("The route " + passedName + " was not found", this.router.hasRoute(name)); } - Ember.assert("The route " + passedName + " was not found", router.router.hasRoute(name)); - } + var transitionPromise = this.router[method].apply(this.router, args); - scheduleLoadingStateEntry(router); + // Don't schedule loading state entry if user has already aborted the transition. + if (this.router.activeTransition) { + this._scheduleLoadingStateEntry(); + } - var transitionPromise = router.router[method].apply(router.router, args); - transitionPromise.then(transitionCompleted); + transitionPromise.then(function(route) { + self._transitionCompleted(route); + }); - // We want to return the configurable promise object - // so that callers of this function can use `.method()` on it, - // which obviously doesn't exist for normal RSVP promises. - return transitionPromise; -} + // We want to return the configurable promise object + // so that callers of this function can use `.method()` on it, + // which obviously doesn't exist for normal RSVP promises. + return transitionPromise; + }, -function scheduleLoadingStateEntry(router) { - if (router._loadingStateActive) { return; } - router._shouldEnterLoadingState = true; - Ember.run.scheduleOnce('routerTransitions', null, enterLoadingState, router); -} + _scheduleLoadingStateEntry: function() { + if (this._loadingStateActive) { return; } + this._shouldEnterLoadingState = true; + Ember.run.scheduleOnce('routerTransitions', this, this._enterLoadingState); + }, -function enterLoadingState(router) { - if (router._loadingStateActive || !router._shouldEnterLoadingState) { return; } + _enterLoadingState: function() { + if (this._loadingStateActive || !this._shouldEnterLoadingState) { return; } - var loadingRoute = router.router.getHandler('loading'); - if (loadingRoute) { - if (loadingRoute.enter) { loadingRoute.enter(); } - if (loadingRoute.setup) { loadingRoute.setup(); } - router._loadingStateActive = true; - } -} + var loadingRoute = this.router.getHandler('loading'); + if (loadingRoute) { + if (loadingRoute.enter) { loadingRoute.enter(); } + if (loadingRoute.setup) { loadingRoute.setup(); } + this._loadingStateActive = true; + } + }, -function exitLoadingState(router) { - router._shouldEnterLoadingState = false; - if (!router._loadingStateActive) { return; } + _exitLoadingState: function () { + this._shouldEnterLoadingState = false; + if (!this._loadingStateActive) { return; } - var loadingRoute = router.router.getHandler('loading'); - if (loadingRoute && loadingRoute.exit) { loadingRoute.exit(); } - router._loadingStateActive = false; -} + var loadingRoute = this.router.getHandler('loading'); + if (loadingRoute && loadingRoute.exit) { loadingRoute.exit(); } + this._loadingStateActive = false; + }, -function transitionCompleted(route) { - var router = route.router; - router.notifyPropertyChange('url'); - exitLoadingState(router); -} + _transitionCompleted: function(route) { + this.notifyPropertyChange('url'); + this._exitLoadingState(); + } +}); Ember.Router.reopenClass({ map: function(callback) { var router = this.router; if (!router) { @@ -26486,10 +26559,30 @@ }); router.callbacks.push(callback); router.map(dsl.generate()); return router; + }, + + _defaultErrorHandler: function(error, transition) { + Ember.Logger.error('Error while loading route:', error); + + // Using setTimeout allows us to escape from the Promise's try/catch block + setTimeout(function() { throw error; }); + }, + + _routePath: function(handlerInfos) { + var path = []; + + for (var i=1, l=handlerInfos.length; i<l; i++) { + var name = handlerInfos[i].name, + nameParts = name.split("."); + + path.push(nameParts[nameParts.length - 1]); + } + + return path.join("."); } }); })(); @@ -26542,14 +26635,47 @@ These functions will be invoked when a matching `{{action}}` is triggered from within a template and the application's current route is this route. Events can also be invoked from other parts of your application via `Route#send` - or `Controller#send`. + or `Controller#send`. - The context of the event will be this route. + The `events` hash will inherit event handlers from + the `events` hash defined on extended Route parent classes + or mixins rather than just replace the entire hash, e.g.: + ```js + App.CanDisplayBanner = Ember.Mixin.create({ + events: { + displayBanner: function(msg) { + // ... + } + } + }); + + App.WelcomeRoute = Ember.Route.extend(App.CanDisplayBanner, { + events: { + playMusic: function() { + // ... + } + } + }); + + // `WelcomeRoute`, when active, will be able to respond + // to both events, since the events hash is merged rather + // then replaced when extending mixins / parent classes. + this.send('displayBanner'); + this.send('playMusic'); + ``` + + It is also possible to call `this._super()` from within an + event handler if it overrides a handle defined on a parent + class or mixin. + + Within a route's event handler, the value of the `this` context + is the Route object. + ## Bubbling By default, an event will stop bubbling once a handler defined on the `events` hash handles it. To continue bubbling the event, you must return `true` from the handler. @@ -26653,10 +26779,12 @@ @type Hash @default null */ events: null, + mergedProperties: ['events'], + /** This hook is executed when the router completely exits this route. It is not executed when the model for the route changes. @method deactivate @@ -26853,10 +26981,12 @@ Refer to documentation for `beforeModel` for a description of transition-pausing semantics when a promise is returned from this hook. @method afterModel + @param {Object} resolvedModel the value returned from `model`, + or its resolved value if it was a promise @param {Transition} transition @return {Promise} if the value returned from this hook is a promise, the transition will pause until the transition resolves. Otherwise, non-promise return values are not utilized in any way. @@ -26908,12 +27038,17 @@ if a promise returned from `model` fails, the error will be handled by the `error` hook on `Ember.Route`. @method model @param {Object} params the parameters extracted from the URL + @param {Transition} transition + @return {Object|Promise} the model for this route. If + a promise is returned, the transition will pause until + the promise resolves, and the resolved value of the promise + will be used as the model for this route. */ - model: function(params, resolvedParentModels) { + model: function(params, transition) { var match, name, sawParams, value; for (var prop in params) { if (match = prop.match(/^(.*)_id$/)) { name = match[1]; @@ -27043,11 +27178,11 @@ controller = container.lookup('controller:' + name); // NOTE: We're specifically checking that skipAssert is true, because according // to the old API the second parameter was model. We do not want people who // passed a model to skip the assertion. - Ember.assert("The controller "+name+" could not be found. Make sure the controller has been generated first. This will happen the first time the associated route is entered.", controller || _skipAssert === true); + Ember.assert("The controller for route '"+name+"'' could not be found. Make sure that this route exists and has already been entered at least once. If you must intialize the controller without entering a route, use `generateController`.", controller || _skipAssert === true); return controller; }, /** @@ -27063,12 +27198,10 @@ generateController: function(name, model) { var container = this.router.container; model = model || this.modelFor(name); - Ember.assert("You are trying to look up a controller that you did not define, and for which Ember does not know the model.\n\nThis is not a controller for a route, so you must explicitly define the controller ("+this.router.namespace.toString() + "." + Ember.String.capitalize(Ember.String.camelize(name))+"Controller) or pass a model as the second parameter to `controllerFor`, so that Ember knows which type of controller to create for you.", model || this.container.lookup('route:' + name)); - return Ember.generateController(container, name, model); }, /** Returns the current model for a given route. @@ -27466,10 +27599,18 @@ @default null **/ title: null, /** + Sets the `rel` attribute of the `LinkView`'s HTML element. + + @property rel + @default null + **/ + rel: null, + + /** The CSS class to apply to `LinkView`'s element when its `active` property is `true`. @property activeClass @type String @@ -27505,11 +27646,11 @@ @property replace @type Boolean @default false **/ replace: false, - attributeBindings: ['href', 'title'], + attributeBindings: ['href', 'title', 'rel'], classNameBindings: ['active', 'loading', 'disabled'], /** By default the `{{linkTo}}` helper responds to the `click` event. You can override this globally by setting this property to your custom @@ -27554,19 +27695,11 @@ for(i=0; i < length; i++) { paths.pushObject(createPath(params[i])); } var observer = function(object, path) { - var notify = true, i; - for(i=0; i < paths.length; i++) { - if (!get(this, paths[i])) { - notify = false; - } - } - if (notify) { - this.notifyPropertyChange('routeArgs'); - } + this.notifyPropertyChange('routeArgs'); }; for(i=0; i < length; i++) { this.registerObserver(this, paths[i], this, observer); } @@ -27653,11 +27786,11 @@ if (this.bubbles === false) { event.stopPropagation(); } if (get(this, '_isDisabled')) { return false; } if (get(this, 'loading')) { - Ember.Logger.warn("This linkTo's parameters are either not yet loaded or point to an invalid route."); + Ember.Logger.warn("This linkTo is in an inactive loading state because at least one of its parameters' presently has a null/undefined value, or the provided route name is invalid."); return false; } var router = get(this, 'router'), routeArgs = get(this, 'routeArgs'); @@ -28009,11 +28142,11 @@ options = property; property = 'main'; } outletSource = options.data.view; - while (!(outletSource.get('template.isTop'))) { + while (!outletSource.get('template.isTop') || outletSource.isYield) { outletSource = outletSource.get('_parentView'); } outletContainerClass = options.hash.viewClass || Handlebars.OutletView; @@ -28062,11 +28195,12 @@ @param {Object?} contextString @param {Hash} options */ Ember.Handlebars.registerHelper('render', function(name, contextString, options) { Ember.assert("You must pass a template to render", arguments.length >= 2); - var container, router, controller, view, context, lookupOptions; + var contextProvided = arguments.length === 3, + container, router, controller, view, context, lookupOptions; if (arguments.length === 2) { options = contextString; contextString = undefined; } @@ -28078,11 +28212,11 @@ name = name.replace(/\//g, '.'); container = options.data.keywords.controller.container; router = container.lookup('router:main'); - Ember.assert("You can only use the {{render}} helper once without a model object as its second argument, as in {{render \"post\" post}}.", context || !router || !router._lookupActiveView(name)); + Ember.assert("You can only use the {{render}} helper once without a model object as its second argument, as in {{render \"post\" post}}.", contextProvided || !router || !router._lookupActiveView(name)); view = container.lookup('view:' + name) || container.lookup('view:default'); var controllerName = options.hash.controller; @@ -28093,11 +28227,11 @@ } else { controller = container.lookup('controller:' + name, lookupOptions) || Ember.generateController(container, name, context); } - if (controller && context) { + if (controller && contextProvided) { controller.set('model', context); } var root = options.contexts[1]; @@ -29827,19 +29961,21 @@ to use the router for this purpose. @method deferReadiness */ deferReadiness: function() { + Ember.assert("You must call deferReadiness on an instance of Ember.Application", this instanceof Ember.Application); Ember.assert("You cannot defer readiness since the `ready()` hook has already been called.", this._readinessDeferrals > 0); this._readinessDeferrals++; }, /** @method advanceReadiness @see {Ember.Application#deferReadiness} */ advanceReadiness: function() { + Ember.assert("You must call advanceReadiness on an instance of Ember.Application", this instanceof Ember.Application); this._readinessDeferrals--; if (this._readinessDeferrals === 0) { Ember.run.once(this, this.didBecomeReady); } @@ -30176,10 +30312,11 @@ container.set = Ember.set; container.normalize = normalize; container.resolver = resolverFor(namespace); container.describe = container.resolver.describe; + container.optionsForType('component', { singleton: false }); container.optionsForType('view', { singleton: false }); container.optionsForType('template', { instantiate: false }); container.register('application:main', namespace, { instantiate: false }); container.register('controller:basic', Ember.Controller, { instantiate: false }); @@ -31644,9 +31781,447 @@ Ember States @module ember @submodule ember-states @requires ember-runtime +*/ + +})(); + +(function() { +/** +@module ember +@submodule ember-extension-support +*/ +/** + The `DataAdapter` helps a data persistence library + interface with tools that debug Ember such + as the Chrome Ember Extension. + + This class will be extended by a persistence library + which will override some of the methods with + library-specific code. + + The methods likely to be overriden are + `getFilters`, `detect`, `columnsForType`, + `getRecords`, `getRecordColumnValues`, + `getRecordKeywords`, `getRecordFilterValues`, + `getRecordColor`, `observeRecord` + + The adapter will need to be registered + in the application's container as `dataAdapter:main` + + Example: + ```javascript + Application.initializer({ + name: "dataAdapter", + + initialize: function(container, application) { + application.register('dataAdapter:main', DS.DataAdapter); + } + }); + ``` + + @class DataAdapter + @namespace Ember + @extends Ember.Object +*/ +Ember.DataAdapter = Ember.Object.extend({ + init: function() { + this._super(); + this.releaseMethods = Ember.A(); + }, + + /** + The container of the application being debugged. + This property will be injected + on creation. + */ + container: null, + + /** + @private + + Number of attributes to send + as columns. (Enough to make the record + identifiable). + */ + attributeLimit: 3, + + /** + @private + + Stores all methods that clear observers. + These methods will be called on destruction. + */ + releaseMethods: Ember.A(), + + /** + @public + + Specifies how records can be filtered. + Records returned will need to have a `filterValues` + property with a key for every name in the returned array. + + @method getFilters + @return {Array} List of objects defining filters. + The object should have a `name` and `desc` property. + */ + getFilters: function() { + return Ember.A(); + }, + + /** + @public + + Fetch the model types and observe them for changes. + + @method watchModelTypes + + @param {Function} typesAdded Callback to call to add types. + Takes an array of objects containing wrapped types (returned from `wrapModelType`). + + @param {Function} typesUpdated Callback to call when a type has changed. + Takes an array of objects containing wrapped types. + + @return {Function} Method to call to remove all observers + */ + watchModelTypes: function(typesAdded, typesUpdated) { + var modelTypes = this.getModelTypes(), + self = this, typesToSend, releaseMethods = Ember.A(); + + typesToSend = modelTypes.map(function(type) { + var wrapped = self.wrapModelType(type); + releaseMethods.push(self.observeModelType(type, typesUpdated)); + return wrapped; + }); + + typesAdded(typesToSend); + + var release = function() { + releaseMethods.forEach(function(fn) { fn(); }); + self.releaseMethods.removeObject(release); + }; + this.releaseMethods.pushObject(release); + return release; + }, + + /** + @public + + Fetch the records of a given type and observe them for changes. + + @method watchRecords + + @param {Function} recordsAdded Callback to call to add records. + Takes an array of objects containing wrapped records. + The object should have the following properties: + columnValues: {Object} key and value of a table cell + object: {Object} the actual record object + + @param {Function} recordsUpdated Callback to call when a record has changed. + Takes an array of objects containing wrapped records. + + @param {Function} recordsRemoved Callback to call when a record has removed. + Takes the following parameters: + index: the array index where the records were removed + count: the number of records removed + + @return {Function} Method to call to remove all observers + */ + watchRecords: function(type, recordsAdded, recordsUpdated, recordsRemoved) { + var self = this, releaseMethods = Ember.A(), records = this.getRecords(type), release; + + var recordUpdated = function(updatedRecord) { + recordsUpdated([updatedRecord]); + }; + + var recordsToSend = records.map(function(record) { + releaseMethods.push(self.observeRecord(record, recordUpdated)); + return self.wrapRecord(record); + }); + + + var contentDidChange = function(array, idx, removedCount, addedCount) { + for (var i = idx; i < idx + addedCount; i++) { + var record = array.objectAt(i); + var wrapped = self.wrapRecord(record); + releaseMethods.push(self.observeRecord(record, recordUpdated)); + recordsAdded([wrapped]); + } + + if (removedCount) { + recordsRemoved(idx, removedCount); + } + }; + + var observer = { didChange: contentDidChange, willChange: Ember.K }; + records.addArrayObserver(self, observer); + + release = function() { + releaseMethods.forEach(function(fn) { fn(); }); + records.removeArrayObserver(self, observer); + self.releaseMethods.removeObject(release); + }; + + recordsAdded(recordsToSend); + + this.releaseMethods.pushObject(release); + return release; + }, + + /** + @private + + Clear all observers before destruction + */ + willDestroy: function() { + this._super(); + this.releaseMethods.forEach(function(fn) { + fn(); + }); + }, + + /** + @private + + Detect whether a class is a model. + + Test that against the model class + of your persistence library + + @method detect + @param {Class} klass The class to test + @return boolean Whether the class is a model class or not + */ + detect: function(klass) { + return false; + }, + + /** + @private + + Get the columns for a given model type. + + @method columnsForType + @param {Class} type The model type + @return {Array} An array of columns of the following format: + name: {String} name of the column + desc: {String} Humanized description (what would show in a table column name) + */ + columnsForType: function(type) { + return Ember.A(); + }, + + /** + @private + + Adds observers to a model type class. + + @method observeModelType + @param {Class} type The model type class + @param {Function} typesUpdated Called when a type is modified. + @return {Function} The function to call to remove observers + */ + + observeModelType: function(type, typesUpdated) { + var self = this, records = this.getRecords(type); + + var onChange = function() { + typesUpdated([self.wrapModelType(type)]); + }; + var observer = { + didChange: function() { + Ember.run.scheduleOnce('actions', this, onChange); + }, + willChange: Ember.K + }; + + records.addArrayObserver(this, observer); + + var release = function() { + records.removeArrayObserver(self, observer); + }; + + return release; + }, + + + /** + @private + + Wraps a given model type and observes changes to it. + + @method wrapModelType + @param {Class} type A model class + @param {Function} typesUpdated callback to call when the type changes + @return {Object} contains the wrapped type and the function to remove observers + Format: + type: {Object} the wrapped type + The wrapped type has the following format: + name: {String} name of the type + count: {Integer} number of records available + columns: {Columns} array of columns to describe the record + object: {Class} the actual Model type class + release: {Function} The function to remove observers + */ + wrapModelType: function(type, typesUpdated) { + var release, records = this.getRecords(type), + typeToSend, self = this; + + typeToSend = { + name: type.toString(), + count: Ember.get(records, 'length'), + columns: this.columnsForType(type), + object: type + }; + + + return typeToSend; + }, + + + /** + @private + + Fetches all models defined in the application. + TODO: Use the resolver instead of looping over namespaces. + + @method getModelTypes + @return {Array} Array of model types + */ + getModelTypes: function() { + var namespaces = Ember.A(Ember.Namespace.NAMESPACES), types = Ember.A(), self = this; + + namespaces.forEach(function(namespace) { + for (var key in namespace) { + if (!namespace.hasOwnProperty(key)) { continue; } + var klass = namespace[key]; + if (self.detect(klass)) { + types.push(klass); + } + } + }); + return types; + }, + + /** + @private + + Fetches all loaded records for a given type. + + @method getRecords + @return {Array} array of records. + This array will be observed for changes, + so it should update when new records are added/removed. + */ + getRecords: function(type) { + return Ember.A(); + }, + + /** + @private + + Wraps a record and observers changes to it + + @method wrapRecord + @param {Object} record The record instance + @return {Object} the wrapped record. Format: + columnValues: {Array} + searchKeywords: {Array} + */ + wrapRecord: function(record) { + var recordToSend = { object: record }, columnValues = {}, self = this; + + recordToSend.columnValues = this.getRecordColumnValues(record); + recordToSend.searchKeywords = this.getRecordKeywords(record); + recordToSend.filterValues = this.getRecordFilterValues(record); + recordToSend.color = this.getRecordColor(record); + + return recordToSend; + }, + + /** + @private + + Gets the values for each column. + + @method getRecordColumnValues + @return {Object} Keys should match column names defined + by the model type. + */ + getRecordColumnValues: function(record) { + return {}; + }, + + /** + @private + + Returns keywords to match when searching records. + + @method getRecordKeywords + @return {Array} Relevant keywords for search. + */ + getRecordKeywords: function(record) { + return Ember.A(); + }, + + /** + @private + + Returns the values of filters defined by `getFilters`. + + @method getRecordFilterValues + @param {Object} record The record instance + @return {Object} The filter values + */ + getRecordFilterValues: function(record) { + return {}; + }, + + /** + @private + + Each record can have a color that represents its state. + + @method getRecordColor + @param {Object} record The record instance + @return {String} The record's color + Possible options: black, red, blue, green + */ + getRecordColor: function(record) { + return null; + }, + + /** + @private + + Observes all relevant properties and re-sends the wrapped record + when a change occurs. + + @method observerRecord + @param {Object} record The record instance + @param {Function} recordUpdated The callback to call when a record is updated. + @return {Function} The function to call to remove all observers. + */ + observeRecord: function(record, recordUpdated) { + return function(){}; + } + +}); + + +})(); + + + +(function() { +/** +Ember Extension Support + +@module ember +@submodule ember-extension-support +@requires ember-application */ })(); (function() {